<?php
/**

 * @file  multiotp.class.php
 * @brief Main file of the multiOTP PHP class.
 *
 * @mainpage
 *
 * multiOTP PHP class - strong two-factor authentication PHP class
 * multiOTP is OATH certified for TOTP/HOTP
 *
 * https://www.multiOTP.net/
 *
 * Visit http://forum.multiotp.net/ for additional support.
 *
 * The multiOTP package is the lightest package available that provides so many
 * strong authentication functionalities and goodies, and best of all, for anyone
 * that is interested about security issues, it's a fully open source solution!
 *
 * This package is the result of a *bunch* of work. If you are happy using this
 * package, [Donation] are always welcome to support this project.
 * Please check https://www.multiOTP.net/ and you will find the magic button ;-)
 * https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=PRS3VDNYL58HJ
 *
 * If you need some specific features in the open source edition of multiOTP,
 * please contact us in order to discuss about a sponsorship in order to
 * prioritize your needs.
 *
 * The multiOTP class is a strong authentication class in pure PHP
 * that supports the following algorithms and RFC's:
 *  - RFC1994 CHAP (Challenge Handshake Authentication Protocol)
 *  - RFC2433 MS-CHAP (Microsoft PPP CHAP Extensions)
 *  - RFC2487 SMTP Service Extension for Secure SMTP over TLS
 *  - RFC2759 MS-CHAPv2 (Microsoft PPP CHAP Extensions, Version 2)
 *  - RFC2821 SMTP (Simple Mail Transfer Protocol)
 *  - RFC4226 OATH/HOTP (HOTP: An HMAC-Based One-Time Password Algorithm)
 *  - RFC5424 Syslog Protocol (client)
 *  - RFC6030 PSKC (Additional Portable Symmetric Key Container Algorithm Profiles)
 *  - RFC6238 OATH/TOTP (TOTP: Time-Based One-Time Password Algorithm)
 *  - Yubico OTP (http://yubico.com/yubikey)
 *  - mOTP (http://motp.sourceforge.net)
 *  - OATH/HOTP or OATH/TOTP, base32/hex/raw seed, QRcode provisioning
 *    (FreeOTP, Google Authenticator, ...)
 *  - SMS tokens (using Afilnet, aspsms, Clickatell, eCall, IntelliSMS, Nexmo, NowSMS, 
 *      SMSEagle, SMSGateway, Spryng, Swisscom LA REST, Telnyx, any custom provider, your own script)
 *  - TAN (emergency scratch passwords)
 *
 * This class can be used as is in your own PHP project, but it can also be
 * used easily as an external authentication provider with at least the
 * following RADIUS servers (using the multiotp command line script):
 *  - FreeRADIUS, a free RADIUS server implementation for Linux and
 *    and *nix environments (http://freeradius.org/)
 *  - WinRADIUS, the FreeRADIUS implementation ported for Windows
 *    (http://winradius.eu/)
 *  - TekRADIUS LT, a free RADIUS server for Windows with SQLite backend
 *    (http:/www.tekradius.com/)
 *  - TekRADIUS, a free RADIUS server for Windows with MS-SQL backend
 *    (http:/www.tekradius.com/)
 *
 * This class is also used as the central component in various commercial
 * products and services developed by SysCo systemes de communication sa:
 *  - multiOTP Pro, available as a virtual appliance or a device in order
 *    to provide a complete strong authentication solution with a simple
 *    to use web based interface (https://www.multiOTP.com/)
 *  - multiOTP Enterprise, an HA master-slave virtual appliance to
 *    provide a complete strong authentication solution
 *  - secuPASS.net, a simple service to centralize provisioning and SMS
 *    authentication for (free) Wifi hotspot (http://www.secupass.net/)
 *
 * The Readme file contains additional information.
 *
 * PHP 7.4 or higher is supported.
 *
 * @author    Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
 * @version   5.10.2.1
 * @date      2026-03-23
 * @since     2010-06-08
 * @copyright (c) 2010-2026 SysCo systemes de communication sa
 * @copyright GNU Lesser General Public License
 *
 *//*
 *
 * LICENCE
 *
 *   Copyright (c) 2010-2026 SysCo systemes de communication sa
 *   SysCo (tm) is a trademark of SysCo systemes de communication sa
 *   (http://www.sysco.ch/)
 *   All rights reserved.
 * 
 *   This file is part of the multiOTP project.
 *
 *   multiOTP PHP class is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Lesser General Public License as
 *   published by the Free Software Foundation, either version 3 of the License,
 *   or (at your option) any later version.
 * 
 *   multiOTP PHP class is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Lesser General Public License for more details.
 * 
 *   You should have received a copy of the GNU Lesser General Public
 *   License along with multiOTP PHP class.
 *   If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Usage
 *
 *   require_once('multiotp.class.php');
 *   $multiotp = new Multiotp('MyPersonalEncryptionKey');
 *   // The use of $multiotp->SetEncryptionKey('MyPersonalEncryptionKey')
 *   // after creating the class without argument is DEPRECATED
 *   $multiotp->SetUser('user');
 *   $result = $multiotp->CheckToken('token');
 *
 *
 * Examples
 *
 *  Create a new user
 *    require_once('multiotp.class.php');
 *    $multiotp = new Multiotp('MyPersonalEncryptionKey');
 *    // The use of $multiotp->SetEncryptionKey('MyPersonalEncryptionKey')
 *    // after creating the class without argument is DEPRECATED
 *    $multiotp->EnableVerboseLog(); // Could be helpful at the beginning
 *    $multiotp->SetUser('username');
 *    $multiotp->SetUserPrefixPin(0); // We don’t want the prefix PIN feature for this example
 *    $multiotp->SetUserAlgorithm('TOTP');
 *    $multiotp->SetUserTokenSeed('D6F9DF7C0110C85D6F9D');
 *    $multiotp->SetUserPin('1111'); // Useless for TOTP in this case without prefix PIN feature
 *    $multiotp->SetUserTokenNumberOfDigits(6);
 *    $multiotp->SetUserTokenTimeInterval(30);
 *    $multiotp->WriteUserData();
 *  
 *  
 *  Verify a token
 *    require_once('multiotp.class.php');
 *    $multiotp = new Multiotp('MyPersonalEncryptionKey');
 *    // The use of $multiotp->SetEncryptionKey('MyPersonalEncryptionKey')
 *    // after creating the class without argument is DEPRECATED
 *    $multiotp->EnableVerboseLog(); // Could be helpful at the beginning
 *    $multiotp->SetUser('username');
 *    if (0 == $multiotp->CheckToken('token')) {
 *        // Authentication accepted
 *    } else {
 *        // Authentication rejected
 *    }
 *  
 *  
 *  Resync a user (normally only useful for HOTP, but useful too if TOTP/mOTP device or server is not well synchronized)
 *    require_once('multiotp.class.php');
 *    $multiotp = new Multiotp('MyPersonalEncryptionKey');
 *    // The use of $multiotp->SetEncryptionKey('MyPersonalEncryptionKey')
 *    // after creating the class without argument is DEPRECATED
 *    $multiotp->EnableVerboseLog(); // Could be helpful at the beginning
 *    $multiotp->SetUser('username');
 *    if (0 == $multiotp->CheckToken('token1','token2')) // it must two consecutive tokens {
 *        // Synchronization successful
 *    } else {
 *        // Synchronization failed
 *    }
 *
 *
 *  Verify a token and be sure to encrypt some more data in the flat file
 *    require_once('multiotp.class.php');
 *    $multiotp = new Multiotp('MyPersonalEncryptionKey');
 *    // The use of $multiotp->SetEncryptionKey('MyPersonalEncryptionKey')
 *    // after creating the class without argument is DEPRECATED
 *    $multiotp->EnableVerboseLog(); // Could be helpful at the beginning
 *    $multiotp->SetAttributesToEncrypt('*user_pin*token_seed*token_serial*seed_password*');
 *    $multiotp->SetUser('username');
 *    if (0 == $multiotp->CheckToken('token')) {
 *        // Authentication accepted
 *    } else {
 *        // Authentication rejected
 *    }
 *  
 *
 *   For examples on how to integrate it with radius servers, please have a look
 *   to the readme.txt file or read the header of the multiotp.cli.header.php file.
 *
 *
 * External files created
 *
 *   Users database files in the subfolder called users (or anywhere else if defined)
 *   Tokens database files in the subfolder called tokens (or anywhere else if defined)
 *   Log file in the subfolder called log (or anywhere else if defined)
 *   Configuration file in the subfolder called config (or anywhere else if defined)
 *
 *
 * External files needed
 *
 *   Users database files in the subfolder called users
 *   Tokens database files in the subfolder called tokens
 *
 *
 * External packages used
 *
 *   barcode (MIT License)
 *   Kreative Software
 *   https://github.com/kreativekorp/barcode
 *
 *   NuSOAP - PHP Web Services Toolkit (LGPLv2.1)
 *   NuSphere Corporation
 *   http://sourceforge.net/projects/nusoap/
 *
 *   phpseclib (MIT License)
 *   MMVI Jim Wigginton
 *   http://phpseclib.sourceforge.net/
 *
 *   PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY (LGPLv2.1)
 *   Scott Barnett
 *   http://adldap.sourceforge.net/
 *
 *   PHP radius class (LGPLv3)
 *   André Liechti
 *   http://developer.sysco.ch/php/
 *
 *   PHP Syslog class (FREE "AS IS")
 *   André Liechti
 *   http://developer.sysco.ch/php/
 *
 *   status_bar.php (2010) (FREE "AS IS")
 *   dealnews.com, Inc.
 *   http://brian.moonspot.net/status_bar.php.txt
 *
 *   TCPDF (LGPLv3)
 *   Nicola Asuni
 *   http://www.tcpdf.org/
 *
 *   XML Parser Class (LGPLv3)
 *   Adam A. Flynn
 *   http://www.criticaldevelopment.net/xml/
 *
 *   XPertMailer package (LGPLv2.1)
 *   Tanase Laurentiu Iulian
 *   http://xpertmailer.sourceforge.net/
 *
 *
 * Special issues
 *
 *   If you need specific developements concerning strong authentication,
 *   do not hesistate to contact us per email at info@multiotp.net.
 *
 *
 * Other related ressources
 *
 *   Mobile-OTP: Strong Two-Factor Authentication with Mobile Phones:
 *     http://motp.sourceforge.net/
 *
 *   The Initiative for Open Authentication:
 *     http://www.openauthentication.org/
 *
 *   TekRADIUS, a free RADIUS server for windows, available in two versions (MS-SQL and SQLite):
 *     http://www.tekradius.com/
 *
 *   FreeRADIUS, a free Radius server implementation for Linux and *nix environments:
 *     http://www.freeradius.org/
 *
 *   WinRADIUS, the FreeRADIUS implementation ported for Windows
 *     (http://winradius.eu/)
 *
 *   Additional Portable Symmetric Key Container (PSKC) Algorithm Profiles
 *     RFC 6030 (http://tools.ietf.org/html/rfc6030)
 *
 *   Google Authenticator (based on OATH/TOTP)
 *     https://github.com/google/google-authenticator
 *
 *
 * Change Log
 *
 *   Please check the readme file for the whole change log since 2010
 *********************************************************************/

define("_MULTIOTP_DEFAULT_PUSH_TIMEOUT_", 20);
define("_MULTIOTP_DEFAULT_TOTP_INTERVAL_", 30);
define("_MULTIOTP_DEFAULT_GUI_TIMEOUT_", 10 * 60);
define("_MULTIOTP_PUSH_TIMEOUT_CODE_", "PUSH_TIMEOUT");
define("_MULTIOTP_PUSH_DENIED_CODE_", "PUSH_DENIED");
define("_MULTIOTP_CREDENTIAL_PROVIDER_", "Credential Provider");
define("_MULTIOTP_DEFAULT_PASSWORD_", "1234");
define("_MULTIOTP_DEFAULT_SYSLOG_FACILITY_", 18);// 18 = local2
define("_MULTIOTP_DEFAULT_SYSLOG_SEVERITY_LEVEL_", 5); // 5 = notice
define("_MULTIOTP_DEFAULT_SYSLOG_TIMEOUT_", 3);
define("_MULTIOTP_DEFAULT_HASH_SALT_", "AjaxH@shS@lt");

define("_MULTIOTP_AUTH_OK_", 0);
define("_MULTIOTP_INFO__1_", 1);
define("_MULTIOTP_INFO__2_", 2);
define("_MULTIOTP_INFO__3_", 3);
define("_MULTIOTP_INFO__4_", 4);
define("_MULTIOTP_INFO__5_", 5);
define("_MULTIOTP_USER_WITH_PUSH_", 6);
define("_MULTIOTP_USER_WITH_TOKEN_", 7);
define("_MULTIOTP_USER_WITHOUT2FA_", 8);
define("_MULTIOTP_USER_CHALLENGE_", 9);
define("_MULTIOTP_USER_CHALLENGE_RADIUS_", 10);
define("_MULTIOTP_USER_UPDATED_", 11);
define("_MULTIOTP_USER_DELETED_", 12);
define("_MULTIOTP_USER_PIN_UPDATED_", 13);
define("_MULTIOTP_TOKEN_RESYNCED_", 14);
define("_MULTIOTP_TOKEN_IMPORTED_", 15);
define("_MULTIOTP_USER_QRCODE_CREATED_", 16);
define("_MULTIOTP_USER_URLINK_CREATED_", 17);
define("_MULTIOTP_USER_STATIC_CODE_REQUESTED_", 18);
define("_MULTIOTP_OPERATION_DONE_", 19);
define("_MULTIOTP_USER_BLACKLISTED_", 20);
define("_MULTIOTP_USER_MISSING_", 21);
define("_MULTIOTP_USER_EXISTS_", 22);
define("_MULTIOTP_BAD_ALGORITHM_", 23);
define("_MULTIOTP_USER_LOCKED_", 24);
define("_MULTIOTP_USER_DELAYED_", 25);
define("_MULTIOTP_TOKEN_REUSED_", 26);
define("_MULTIOTP_TOKEN_RESYNC_FAILED_", 27);
define("_MULTIOTP_DATA_UPDATE_ERROR_", 28);
define("_MULTIOTP_TOKEN_MISSING_", 29);
define("_MULTIOTP_PARAM_MISSING_", 30);
define("_MULTIOTP_TOKEN_MISSING_FILE_", 31);
define("_MULTIOTP_TOKEN_IMPORT_ERROR_", 32);
define("_MULTIOTP_ENCRYPTION_ERROR_", 33);
define("_MULTIOTP_USER_LINKED_MISSING_", 34);
define("_MULTIOTP_USER_CREATION_ERROR_", 35);
define("_MULTIOTP_TOKEN_EXISTS_", 36);
define("_MULTIOTP_TOKEN_ALREADY_ATTRIBUTED_", 37);
define("_MULTIOTP_USER_DISABLED_", 38);
define("_MULTIOTP_OPERATION_CANCELLED_", 39);
define("_MULTIOTP_SYSTEM_SQL_QUERY_ERROR_", 40);
define("_MULTIOTP_SYSTEM_SQL_ERROR_", 41);
define("_MULTIOTP_SYSTEM_KEY_NOT_IN_SCHEMA_ERROR_", 42);
define("_MULTIOTP_SYSTEM_SQL_UPDATE_ERROR_", 43);
define("_MULTIOTP_ERROR__44_", 44);
define("_MULTIOTP_ERROR__45_", 45);
define("_MULTIOTP_ERROR__46_", 46);
define("_MULTIOTP_ERROR__47_", 47);
define("_MULTIOTP_ERROR__48_", 48);
define("_MULTIOTP_GROUP_MISSING_", 49);
define("_MULTIOTP_USER_QRCODE_CREATION_ERROR_", 50);
define("_MULTIOTP_USER_URLINK_CREATION_ERROR_", 51);
define("_MULTIOTP_USER_HTML_CREATION_ERROR_", 52);
define("_MULTIOTP_ERROR__53_", 53);
define("_MULTIOTP_ERROR__54_", 54);
define("_MULTIOTP_ERROR__55_", 55);
define("_MULTIOTP_ERROR__56_", 56);
define("_MULTIOTP_ERROR__57_", 57);
define("_MULTIOTP_MISSING_FILE_", 58);
define("_MULTIOTP_BAD_RESTORE_PASSWORD_", 59);
define("_MULTIOTP_SMS_NO_DESTINATION_ERROR_", 60);
define("_MULTIOTP_SMS_CODE_TRANSMISSION_ERROR_", 61);
define("_MULTIOTP_SMS_PROVIDER_ERROR_", 62);
define("_MULTIOTP_SMS_CODE_EXPIRED_", 63);
define("_MULTIOTP_SMS_RESENT_DELAYED_", 64);
define("_MULTIOTP_SMS_REQUEST_UNAUTHORIZED_", 65);
define("_MULTIOTP_EMAIL_REQUEST_UNAUTHORIZED_", 66);
define("_MULTIOTP_EMAIL_NO_DESTINATION_ERROR_", 67);
define("_MULTIOTP_EMAIL_CODE_TRANSMISSION_ERROR_", 68);
define("_MULTIOTP_EMAIL_SEND_ERROR_", 69);
define("_MULTIOTP_SERVER_AUTH_FAILED_", 70);
define("_MULTIOTP_SERVER_REQUEST_FORMAT_ERROR_", 71);
define("_MULTIOTP_SERVER_ANSWER_FORMAT_ERROR_", 72);
define("_MULTIOTP_SMTP_UNDEFINED_", 73);
define("_MULTIOTP_PUSH_ERROR_", 74);
define("_MULTIOTP_ERROR__75_", 75);
define("_MULTIOTP_ERROR__76_", 76);
define("_MULTIOTP_ERROR__77_", 77);
define("_MULTIOTP_ERROR__78_", 78);
define("_MULTIOTP_LDAP_CONNECTION_ERROR_", 79);
define("_MULTIOTP_CACHE_SERVER_ERROR_", 80);
define("_MULTIOTP_CACHE_TOO_OLD_ERROR_", 81);
define("_MULTIOTP_USER_UNAUTHORIZED_", 82);
define("_MULTIOTP_ERROR__83_", 83);
define("_MULTIOTP_ERROR__84_", 84);
define("_MULTIOTP_ERROR__85_", 85);
define("_MULTIOTP_ERROR__86_", 86);
define("_MULTIOTP_DEVICE_MISSING_", 87);
define("_MULTIOTP_DEVICE_IS_NOT_HA_SLAVE_", 88);
define("_MULTIOTP_DEVICE_IS_NOT_HA_MASTER_", 89);
define("_MULTIOTP_LDAP_AUTH_ERROR_", 90);
define("_MULTIOTP_WITHOUT2FA_TOKEN_UNAUTHORIZED_", 91);
define("_MULTIOTP_AUTH_PASSWORD_FAILED_", 92);
define("_MULTIOTP_AUTH_TOTP_OUTOFSYNC_ERROR_", 93);
define("_MULTIOTP_API_REQUEST_ERROR_", 94);
define("_MULTIOTP_API_AUTH_ERROR_", 95);
define("_MULTIOTP_PUSH_TIMEOUT_ERROR_", 96);
define("_MULTIOTP_PUSH_DENIED_ERROR_", 97);
define("_MULTIOTP_AUTH_TOKEN_LENGTH_ERROR_", 98);
define("_MULTIOTP_ERROR_", 99);

define("_MULTIOTP_DEFAULT_BOOT_CACHE_DELAY_", 600);
define("_MULTIOTP_DEFAULT_UPTIME_DELAY_", 30);

class Multiotp
/**
 * @class     Multiotp
 * @brief     Main class definition of the multiOTP project.
 *
 * @author    Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
 * @version   5.10.2.1
 * @date      2026-03-23
 * @since     2010-07-18
 */
{
  
  var $_hardware_array;           // Hardware info (to only detect it once)
  var $_class;                    // Name of the class
  var $_version;                  // Current version of the library
  var $_date;                     // Current date of the library
  var $_copyright;                // Copyright message of the library, don't change it !
  var $_website;                  // Website dedicated to this LGPL library, please don't change it !

  var $_base_dir;                 // Specific base directory
  var $_valid_algorithms;         // String containing valid algorithms to be used, separated by *, like *mOTP*HOTP*TOTP*YubicoOTP*without2FA*
  var $_attributes_to_encrypt;    // Attributes of the data to encrypt
  var $_encryption_key;           // Symetric data encryption key
  var $_source_tag;               // Source tag of the request (for a shared installation for example)
  var $_source_ip;                // Source IP of the request (for a RADIUS request for example, Packet-Src-IP-Address)
  var $_source_mac;               // Source MAC of the request (for a RADIUS request for example, Called-Station-Id)
  var $_calling_ip;               // Source IP of the request (for a RADIUS request for example, Framed-IP-Address)
  var $_calling_mac;              // Source MAC of the request (for a RADIUS request for example, Calling-Station-Id)
  var $_chap_challenge;           // CHAP-Challenge (instead of traditional PAP password)
  var $_chap_id;                  // CHAP-Id (instead of traditional PAP password)
  var $_chap_password;            // CHAP-Password (instead of traditional PAP password)
  var $_ms_chap_challenge;        // MS-CHAP challenge
  var $_ms_chap_response;         // MS-CHAP response
  var $_ms_chap2_response;        // MS-CHAP2 response
  var $_ms_nt_key;                // NTLM NT key
  var $_errors_text;              // An array containing errors text description
  var $_translation_array;        // An array containing translated texts
  var $_config_data;              // An array with all the general config related info
  var $_config_data_read;         // An array with the last config related info read
  var $_stat_data;                // An array with all the general stat related info
  var $_stat_data_read;           // An array with the last stat related info read
  var $_config_folder;            // Folder where the general config file is written
  var $_ddns;                     // Current dynamic DNS
  var $_ddns_data;                // An array with all the dynamic DNS related info
  var $_default_time_interval;    // Default time interval
  var $_device;                   // Current device
  var $_device_data;              // An array with all the device related info
  var $_group;                    // Current group
  var $_group_data;               // An array with all the group related info
  var $_user;                     // Current user, case insensitive
  var $_rawUser;                  // Original user name passed by authentication request
  var $_user_data;                // An array with all the user related info
  var $_user_data_read_flag;      // Indicate if the user data has been read from the database file
  var $_current_user_sid;         // Current User SID (not saved in the user data)
  var $_users_folder;             // Folder where users definition files are stored
  var $_credusers_folder;         // Folder where users definition files are stored in SID named files
  var $_templates_folder;         // Folder where template files are stored
  var $_devices_folder;           // Folder where devices definition files are stored
  var $_ddns_folder;              // Foler where dynamic DNS information are stored
  var $_groups_folder;            // Folder where groups definition files are stored
  var $_token;                    // Current token, case insensitive
  var $_token_data;               // An array with all the token related info
  var $_token_data_read_flag;     // Indicate if the token data has been read from the database file
  var $_tokens_folder;            // Folder where tokens definition files are stored
  var $_log_folder;               // Folder where log file is written
  var $_log_file_name;            // Name of the log file
  var $_log_flag;                 // Enable or disable the log
  var $_log_header_written;       // Internal flag to know if the header was already written or not in the log file
  var $_log_verbose_flag;         // Enable or disable the verbose mode for the log
  var $_log_display_flag;         // Log will also be displayed on the console
  var $_last_imported_tokens;     // An array containing the names (which are mostly the serials) of the last imported tokens
  var $_reply_array_for_radius;   // Specific reply message(s) for the radius (to be displayed in all cases by the command line tool)
  var $_initialize_backend;       // Initialize backend flag
  var $_debug_via_html;           // Set the debug output to HTML standard
  var $_linux_file_mode;          // File mode of the created linux files in octal (for example '0644')
  var $_server_challenge;         // Server challenge for client-server mutual authentication
  var $_xml_dump_in_log;          // For internal debugging only
  var $_servers_temp_bad_list;    // Temporary list of servers that are not currently responding well
  var $_test_server_secret;       // Temporary server secret for tests
  var $_last_clear_otp_value;     // Last clear OTP value (including the prefix if typed)
  var $_last_ldap_error;          // Last LDAP/AD error (boolean)
  var $_parser_pointers;          // An array of pointers used for iterative parsing
  var $_cache_folder;             // Folder where the cache file is written
  var $_ldap_server_reachable;    // Flag to know if the LDAP server is reachable
  var $_default_ssl_context;      // Default SSL context
  var $_sql_tables;               // Array of alias names for SQL tables
  var $_anonymous_stat_interval;  // Anonymous stat interval in seconds
  var $_no_display_log;           // No log on display (if runing as a web server for example)
  var $_cli_mode;                 // Flag to know if we are in CLI mode
  var $_cli_proxy_mode;           // Flag to know if we are in CLI proxy mode
  var $_cp_mode;                  // Flag to indicate that we are in Credential Provider mode
  var $_cp_request;               // Flag to indicate that we received a request from a Credential Provider
  var $_touch_folder;             // Touch folder (to detect changing elements)
  var $_touch_suffix_array;       // Touch suffix
  var $_lock_folder;              // Lock folder (to handle semaphore files)
  var $_lock_time;                // Valid time for a semaphore lock file, in seconds
  var $_ldap_sync_lock_file_name; // AD/LDAP synchronization lock file name
  var $_ldap_sync_stop_file_name; // AD/LDAP synchronization stop file name
  var $_last_http_status;         // Last HTTP status
  var $_bad_syslog_server;        // The Syslog server is temporarly bad
  var $_state;                    // State provided by the radius client

  // No more dynamic properties with PHP >= 8.2
  var $_sql_tables_schema;
  var $_sql_tables_index;
  var $_sql_tables_ignore;
  var $_sql_tables_not_in_schema;
  var $_hash_salt;
  var $_random_salt;
  var $_encryption_check;
  var $_mysql_database_link;
  var $_mysqli;
  var $_pgsql_database_link;
  var $_migration_from_file;
  var $_keep_local;
  var $_sms_providers_array;
  var $_linux_folder_mode;
  var $_cache_data;

  /**
   * @brief   Class constructor.
   *
   * @param   string  $encryption_key      A specific encryption key to encrypt stored data instead of the default one.
   * @param   boolean $initialize_backend  If we initialize the backend, we don't want to write in the database before the end of the initialization.
   * @param   boolean $base_dir            Define the base directory, which is always better than automatic detection.
   * @param   boolean $config_dir          Define the config directory, which is always better than automatic detection.
   * @retval  void
   *
   * @author    Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version   5.10.2.1
   * @date      2026-03-23
   * @since     2010-07-18
   */
  function __construct(
      $encryption_key = "",
      $initialize_backend = false,
      $base_dir = "",
      $config_dir = "",
      $no_init = false
  ) {
      
      // Internal encoding set to UTF-8
      mb_internal_encoding("UTF-8");
      
      // destructor definition (for PHP 4 compatibility)
      if (!version_compare(phpversion(), '5', '>=')) {
          register_shutdown_function(array(&$this, '__destruct'));
      }

      // 3 seconds timeout (for Linux only) for gethostbyname() which can be used by third party modules
      putenv('RES_OPTIONS=retrans:1 retry:1 timeout:3 attempts:1');

      if (!isset($this->_class)) { $this->_class = base64_decode('bXVsdGlPVFA='); }
      if (!isset($this->_version)) {
        $temp_version = '@version   5.10.2.1'; // You should add a suffix for your changes (for example 5.0.3.2-andy-2016-10-XX)
        $this->_version = nullable_trim(mb_substr($temp_version, 8));
      }
      if (!isset($this->_date)) {
        $temp_date = '@date      2026-03-23'; // You should update the date with the date of your changes
        $this->_date = nullable_trim(mb_substr($temp_date, 8));
      }
      if (!isset($this->_copyright)) { $this->_copyright = base64_decode('KGMpIDIwMTAtMjAyNiBTeXNDbyBzeXN0ZW1lcyBkZSBjb21tdW5pY2F0aW9uIHNh'); }
      if (!isset($this->_website)) { $this->_website = base64_decode('aHR0cDovL3d3dy5tdWx0aU9UUC5uZXQ='); }
      
      $this->_anonymous_stat_interval = 604800; // Stat interval: 7 * 24 * 60 * 60 = 604800 = 1 week
      
      $this->_log_header_written    = FALSE; // Flag indicating if the header has already been written in the log file or not
      $this->_valid_algorithms      = '*mOTP*HOTP*TOTP*YubicoOTP*without2FA*'; // Supported algorithms, don't change it (unless you have added the handling of a new algorithm ;-)
      $this->_attributes_to_encrypt = '*admin_password_hash*challenge*device_secret*email_otp*ldap_hash_cache*ldap_server_password*scratch_passwords*seed_password*server_secret*sms_api_id*sms_otp*sms_password*sms_userkey*smtp_password*sql_password*token_seed*user_pin*'; // This default list of attributes can be changed using SetAttributesToEncrypt(). Each attribute must be between "*".
      
      $this->_no_display_log = false; // No log on display (if runing as a web server for example)

      $this->_cli_mode = false; // No CLI mode
      $this->_cli_proxy_mode = false; // No CLI proxy mode
      $this->_cp_mode = false; // By default, not on CP mode
      $this->_cp_request = false; // By default, not a CP request

      // BEGIN some specific files and folders initialization

      clearstatcache();

      $this->_touch_folder = "";
      $this->_touch_suffix_array = [];

      $this->SetLockTime(300);

      // Search for a fast temporary directory that flush at reboot
      $lock_folder = '/dev/shm/';
      if (!file_exists($lock_folder)) {
          // If /dev/shm/ is not available, we take the regular temporary directory and
          //   we clean the lock files which are older than the allowed lock time
          $lock_folder = sys_get_temp_dir()."/";
          $actual_folder = $lock_folder;
          $actual_filter = "multiotp-*.lock";
          if (($actual_dir = opendir($actual_folder)) !== FALSE) {
            while(($actual_file_name = readdir($actual_dir)) !== FALSE) {
              if (fnmatch($actual_filter, $actual_file_name)) {
                $actual_file = $actual_folder.$actual_file_name;
                if (filemtime($actual_file) <= (time() - $this->GetLockTime())) {
                  unlink($actual_file);
                }
              }
            }
          }
      }
      $this->SetLockFolder($lock_folder);

      $this->SetLdapSyncLockFileName("multiotp-ldap.lock");
      $this->SetLdapSyncStopFileName("multiotp-ldap.stop");

      // END some specific files and folders initialization

      $this->_ldap_server_reachable = FALSE;

      // http://phpsecurity.readthedocs.io/en/latest/Transport-Layer-Security-(HTTPS-SSL-and-TLS).html
      $this->_default_ssl_context = array(
          'ssl' => array(
              'verify_peer'         => false,
              'verify_peer_name'    => false,
              'disable_compression' => true,
              'ciphers'             => 'ALL!EXPORT!EXPORT40!EXPORT56!aNULL!LOW!RC4'
          )
      );
      if (function_exists("stream_context_set_default")) {
          $default_context = stream_context_set_default($this->_default_ssl_context);
      }

      $this->_sql_tables = array('cache',
                                 'config',
                                 'ddns',
                                 'devices',
                                 'groups',
                                 'log',
                                 'stat',
                                 'tokens',
                                 'users'
                                );

      // int() is for mysql and replaced by numeric() for pgSQL
      // datetime is for MySQL and replaced by timestamp for pgSQL

      $this->_sql_tables_schema['cache']   = array(
          'active_users_count'      => "int(10) DEFAULT -1",
          'create_host'             => "TEXT DEFAULT ''",
          'create_time'             => "int(10) DEFAULT 0",
          'devices_count'           => "int(10) DEFAULT -1",
          'last_sync_update'        => "int(10) DEFAULT 0",
          'last_sync_update_host'   => "TEXT DEFAULT ''",
          'last_update'             => "int(10) DEFAULT 0",
          'last_update_host'        => "TEXT DEFAULT ''",
          'locked_users_count'      => "int(10) DEFAULT -1",
          'locked_users_list'       => "int(10) DEFAULT -1",
          'delayed_users_count'     => "int(10) DEFAULT -1",
          'delayed_users_list'      => "int(10) DEFAULT -1",
          'tokens_count'            => "int(10) DEFAULT -1",
          'users_count'             => "int(10) DEFAULT -1");
      $this->_sql_tables_index['cache']    = '**';
      $this->_sql_tables_ignore['cache']   = "**";
      
      $this->_sql_tables_schema['config']  = array(
          'actual_version'              => "TEXT DEFAULT ''",
          'admin_password_hash'         => "TEXT DEFAULT ''",
          'anonymous_stat'              => "int(1) DEFAULT 1",
          'anonymous_stat_timeout'      => "int(10) DEFAULT 5",
          'anonymous_stat_last_update'  => "int(10) DEFAULT 0",
          'anonymous_stat_random_id'    => "TEXT DEFAULT ''",
          'attributes_to_encrypt'       => "TEXT DEFAULT ''",
          'auto_resync'                 => "int(1) DEFAULT 1",
          // Backend encoding (UTF-8 or others)
          'backend_encoding'            => "TEXT DEFAULT 'UTF-8'",
          // Backend storage type (files / mysql / pqsql)
          'backend_type'                => "TEXT DEFAULT 'files'",
          // By default, backend_type is not validated
          'backend_type_validated'      => "int(1) DEFAULT 0",
          'cache_data'                  => "int(1) DEFAULT 0",
          'cache_ldap_hash'             => "int(1) DEFAULT 1",
          'case_sensitive_users'        => "int(1) DEFAULT 0",
          'challenge_response_enabled'  => "int(1) DEFAULT 0",
          'clear_otp_attribute'         => "TEXT DEFAULT ''",
          // No console authentication by default
          'console_authentication'      => "int(1) DEFAULT 0",
          'create_host'                 => "TEXT DEFAULT ''",
          'create_time'                 => "int(10) DEFAULT 0",
          // Debug mode (to enable it permanently)
          'debug'                       => "int(1) DEFAULT 0",
          'default_2fa_digits'          => "int(10) DEFAULT 6",
          'default_algorithm'           => "TEXT DEFAULT 'totp'",
          'default_dialin_ip_mask'      => "TEXT DEFAULT ''",
          'default_pin_digits'          => "int(10) DEFAULT 4",
          'default_push_enabled'        => "int(1) DEFAULT 0",
          'default_user_group'          => "TEXT DEFAULT ''",
          'default_request_ldap_pwd'    => "int(1) DEFAULT 1",
          'default_request_prefix_pin'  => "int(1) DEFAULT 1",
          'demo_mode'                   => "int(1) DEFAULT 0",
          'developer_mode'              => "int(1) DEFAULT 0",
          // Display log mode (to enable it permanently)
          'display_log'                 => "int(1) DEFAULT 0",
          'domain_name'                 => "TEXT DEFAULT ''",
          'email_admin_address'         => "TEXT DEFAULT ''",
          'email_code_allowed'          => "int(1) DEFAULT 0",
          'email_code_timeout'          => "int(10) DEFAULT 600",
          'email_digits'                => "int(10) DEFAULT 6",
          'encode_file_id'              => "int(1) DEFAULT 0",
          'encryption_key_full_path'    => "TEXT DEFAULT ''",
          // Locking delay in seconds between two trials after "max_delayed_failures" failures
          'failure_delayed_time'        => "int(10) DEFAULT 300",
          'group_attribute'             => "TEXT DEFAULT 'Filter-Id'",
          'hash_salt_full_path'         => "TEXT DEFAULT ''",
          'ignore_no_prefix_cp'         => "int(1) DEFAULT 0",
          'issuer'                      => "TEXT DEFAULT 'multiOTP'",
          'language'                    => "TEXT DEFAULT 'en'",
          'last_failed_white_delay'     => "int(10) DEFAULT 60",
          'last_sync_update'            => "int(10) DEFAULT 0",
          'last_sync_update_host'       => "TEXT DEFAULT ''",
          'last_update'                 => "int(10) DEFAULT 0",
          'last_update_host'            => "TEXT DEFAULT ''",
          'ldap_expired_password_valid' => "int(1) DEFAULT 1",
          'ldap_account_suffix'         => "TEXT DEFAULT ''",
          'ldap_activated'              => "int(1) DEFAULT 0",
          'ldap_base_dn'                => "TEXT DEFAULT ''",
          'ldap_bind_dn'                => "TEXT DEFAULT ''",
          // If ldap_cache_folder is empty, the default value used is "tempdir"
          'ldap_cache_folder'           => "TEXT DEFAULT ''",
          'ldap_cache_on'               => "int(1) DEFAULT 1",
          // If ldap_cn_identifier is empty, the default value used is "sAMAccountName"
          'ldap_cn_identifier'          => "TEXT DEFAULT ''",
          'ldap_default_algorithm'      => "TEXT DEFAULT 'totp'",
          'ldap_domain_controllers'     => "TEXT DEFAULT ''",
          'ldap_dont_auto_disable'      => "int(1) DEFAULT 0",
          'ldap_dont_auto_enable'       => "int(1) DEFAULT 0",
          // If ldap_group_attribute is empty, the default value used is "memberOf"
          'ldap_filter'                 => "TEXT DEFAULT ''",
          'ldap_group_attribute'        => "TEXT DEFAULT ''",
          // If ldap_group_cn_identifier is empty, the default value used is "cn"
          'ldap_group_cn_identifier'    => "TEXT DEFAULT ''",
          'ldap_users_dn'               => "TEXT DEFAULT ''",
          // Hash cache time: 7 * 24 * 60 * 60 = 604800 = 1 week
          'ldap_hash_cache_time'        => "int(10) DEFAULT 604800",
          'ldap_in_group'               => "TEXT DEFAULT ''",
          'ldap_language_attribute'     => "TEXT DEFAULT 'preferredLanguage'",
          'ldap_network_timeout'        => "int(10) DEFAULT 10",
          'ldap_port'                   => "TEXT DEFAULT '389'",
          'ldap_recursive_cache_only'   => "int(1) DEFAULT 0",
          'ldap_recursive_groups'       => "int(1) DEFAULT 1",
          'ldap_server_password'        => "TEXT DEFAULT ''",
          // Default type 1 is Active Directory, 2 for Generic LDAP, 3 for legacy Active Directory, 4 for eDirectory, 5 for Microsoft Entra ID (Azure AD)
          'ldap_server_type'            => "int(10) DEFAULT 1",
          'ldap_ssl'                    => "int(1) DEFAULT 0",
          'ldap_synced_user_attribute'  => "TEXT DEFAULT ''",
          'ldap_time_limit'             => "int(10) DEFAULT 30",
          'ldap_without2fa_in_group'    => "TEXT DEFAULT ''",
          // If ldaptls_reqcert is empty, the default value used is "auto"
          'ldaptls_reqcert'             => "TEXT DEFAULT ''",
          // If ldaptls_cipher_suite is empty, the default value used is "auto"
          'ldaptls_cipher_suite'        => "TEXT DEFAULT ''",
          'log'                         => "int(1) DEFAULT 0",
          'log_forced_in_file'          => "int(1) DEFAULT 0",
          'max_block_failures'          => "int(10) DEFAULT 6",
          'max_delayed_failures'        => "int(10) DEFAULT 3",
          'max_event_resync_window'     => "int(10) DEFAULT 10000",
          'max_event_window'            => "int(10) DEFAULT 100",
          'max_time_resync_window'      => "int(10) DEFAULT 90000",
          // Maximum time window to be accepted, in seconds (+/-)
          // Initialized to a little bit more than +/- 10 minutes
          // (was 8000 seconds in version 3.x, and Stefan Kügler suggested to put a lower default value)
          'max_time_window'             => "int(10) DEFAULT 600",
          'multiple_groups'             => "int(1) DEFAULT 0",
          'ntp_server'                  => "TEXT DEFAULT 'pool.ntp.org'",
          // Overwrite request_ldap_pwd value for synced users
          'overwrite_push_enabled'      => "int(1) DEFAULT 0",
          'overwrite_request_ldap_pwd'  => "int(1) DEFAULT 1",
          'push_activated'              => "int(1) DEFAULT 0",
          'push_gateway'                => "TEXT DEFAULT ''",
          'push_subscription_timeout'   => "int(10) DEFAULT 5",
          'push_timeout'                => "int(10) DEFAULT " . _MULTIOTP_DEFAULT_PUSH_TIMEOUT_,
          'radius_error_reply_message'  => "int(1) DEFAULT 1",
          'radius_reply_attributor'     => "TEXT DEFAULT ' += '",
          'radius_reply_separator_hex'  => "TEXT DEFAULT '".nullable_bin2hex(',')."'",
          'radius_tag_prefix'           => "TEXT DEFAULT ''",
          'scratch_passwords_digits'    => "int(10) DEFAULT 6",
          'scratch_passwords_amount'    => "int(10) DEFAULT 10",
          'self_registration'           => "int(1) DEFAULT 1",
          // Client-server configuration
          'server_cache_level'          => "int(10) DEFAULT 1",
          // 15552000 = 6 monthes
          'server_cache_lifetime'       => "int(10) DEFAULT 15552000",
          // If server_secret is empty, the default value used is "ClientServerSecret"
          'server_secret'               => "TEXT DEFAULT ''",
          'server_timeout'              => "int(10) DEFAULT 5",
          // If server_type is empty, the default value used is "xml"
          'server_type'                 => "TEXT DEFAULT ''",
          // Server URL can contain multiple servers, they must be separated by ;
          'server_url'                  => "TEXT DEFAULT ''",
          'session_timeout'             => "int(10) DEFAULT " . _MULTIOTP_DEFAULT_GUI_TIMEOUT_,
          'sms_api_id'                  => "TEXT DEFAULT ''",
          'sms_basic_auth'              => "int(1) DEFAULT 0",
          'sms_code_allowed'            => "int(1) DEFAULT 1",
          'sms_content_encoding'        => "TEXT DEFAULT ''",
          'sms_content_success'         => "TEXT DEFAULT ''",
          'sms_digits'                  => "int(10) DEFAULT 6",
          'sms_encoding'                => "TEXT DEFAULT ''",
          'sms_header'                  => "TEXT DEFAULT ''",
          'sms_international_format'    => "int(1) DEFAULT 0",
          'sms_ip'                      => "TEXT DEFAULT ''",
          // If sms_message_prefix is empty, the default value used is "%s is your SMS-Code"
          'sms_message_prefix'          => "TEXT DEFAULT ''",
          'sms_method'                  => "TEXT DEFAULT ''",
          'sms_no_double_zero'          => "int(1) DEFAULT 0",
          'sms_originator'              => "TEXT DEFAULT 'multiOTP'",
          'sms_password'                => "TEXT DEFAULT ''",
          'sms_port'                    => "TEXT DEFAULT ''",
          'sms_provider'                => "TEXT DEFAULT ''",
          'sms_send_template'           => "TEXT DEFAULT ''",
          'sms_status_success'          => "TEXT DEFAULT ''",
          // SMS timeout before authenticating (in seconds)
          'sms_timeout'                 => "int(10) DEFAULT 180",
          'sms_url'                     => "TEXT DEFAULT ''",
          'sms_userkey'                 => "TEXT DEFAULT ''",
          'smtp_auth'                   => "int(1) DEFAULT 0",
          'smtp_password'               => "TEXT DEFAULT ''",
          'smtp_port'                   => "int(10) DEFAULT 25",
          'smtp_sender'                 => "TEXT DEFAULT ''",
          'smtp_sender_name'            => "TEXT DEFAULT ''",
          'smtp_server'                 => "TEXT DEFAULT ''",
          'smtp_ssl'                    => "int(1) DEFAULT 0",
          'smtp_username'               => "TEXT DEFAULT ''",
          'sql_server'                  => "TEXT DEFAULT ''",
          'sql_username'                => "TEXT DEFAULT ''",
          'sql_password'                => "TEXT DEFAULT ''",
          'sql_database'                => "TEXT DEFAULT ''",
          'sql_schema'                  => "TEXT DEFAULT ''",
          // Default SQL table names. If empty, the related data will be written to a file.
          'sql_config_table'            => "TEXT DEFAULT 'multiotp_config'",
          'sql_cache_table'             => "TEXT DEFAULT 'multiotp_cache'",
          'sql_ddns_table'              => "TEXT DEFAULT 'multiotp_ddns'",
          'sql_devices_table'           => "TEXT DEFAULT 'multiotp_devices'",
          'sql_groups_table'            => "TEXT DEFAULT 'multiotp_groups'",
          'sql_log_table'               => "TEXT DEFAULT 'multiotp_log'",
          'sql_stat_table'              => "TEXT DEFAULT 'multiotp_stat'",
          'sql_tokens_table'            => "TEXT DEFAULT 'multiotp_tokens'",
          'sql_users_table'             => "TEXT DEFAULT 'multiotp_users'",
          'sync_delete_retention_days'  => "int(10) DEFAULT 30",
          'syslog_facility'             => "int(10) DEFAULT " . _MULTIOTP_DEFAULT_SYSLOG_FACILITY_,
          'syslog_level'                => "int(10) DEFAULT " . _MULTIOTP_DEFAULT_SYSLOG_SEVERITY_LEVEL_,
          'syslog_port'                 => "int(10) DEFAULT 514",
          'syslog_server'               => "TEXT DEFAULT ''",
          'syslog_timeout'              => "int(10) DEFAULT " . _MULTIOTP_DEFAULT_SYSLOG_TIMEOUT_,
          'tel_default_country_code'    => "TEXT DEFAULT ''",
          'timezone'                    => "TEXT DEFAULT 'Europe/Zurich'",
          'token_serial_number_length'  => "TEXT DEFAULT '12'",
          'token_otp_list_of_length'    => "TEXT DEFAULT '6'",
          'verbose_log_prefix'          => "TEXT DEFAULT ''",
          
          'sms_challenge_enabled'      => "int(1) DEFAULT 0",
          // If text_sms_challenge is empty, the default value used is "Please enter the code received on your mobile phone"
          'text_sms_challenge'         => "TEXT DEFAULT ''",
          // If text_token_challenge is empty, the default value used is "Please enter the code displayed on the token"
          'text_token_challenge'       => "TEXT DEFAULT ''",
          'encryption_hash'             => "TEXT DEFAULT ''");
      $this->_sql_tables_index['config']   = '**';
      $this->_sql_tables_ignore['config']  = '*backend_type*backend_type_validated*sql_server*sql_username*sql_password*sql_database*sql_schema*sql_config_table*';
     
      $this->_sql_tables_schema['ddns'] = array(
          'ddns_id'                    => "TEXT DEFAULT ''",
          'alternate_password'         => "TEXT DEFAULT ''",
          'create_host'                => "TEXT DEFAULT ''",
          'create_time'                => "int(10) DEFAULT 0",
          'ip'                         => "TEXT DEFAULT ''",
          'password'                   => "TEXT DEFAULT ''",
          'tag'                        => "TEXT DEFAULT ''",
          'username'                   => "TEXT DEFAULT ''",
          'last_sync_update'           => "int(10) DEFAULT 0",
          'last_sync_update_host'      => "TEXT DEFAULT ''",
          'last_update'                => "int(10) DEFAULT 0",
          'last_update_host'           => "TEXT DEFAULT ''",
          'encryption_hash'            => "TEXT DEFAULT ''");
      $this->_sql_tables_index['ddns']  = '*ddns_id*';
      $this->_sql_tables_ignore['ddns'] = "**";

      $this->_sql_tables_schema['devices'] = array(
          'device_id'                  => "TEXT DEFAULT ''",
          'cache_result_enabled'       => "int(1) DEFAULT 0",
          'cache_timeout'              => "int(10) DEFAULT 3600",
          'challenge_response_enabled' => "int(1) DEFAULT 0",
          'create_host'                => "TEXT DEFAULT ''",
          'create_time'                => "int(10) DEFAULT 0",
          'description'                => "TEXT DEFAULT ''",
          'device_secret'              => "TEXT DEFAULT ''",
          'dont_send_groups'           => "int(1) DEFAULT 0",
          'force_no_prefix_pin'        => "int(1) DEFAULT 0",
          'ip_or_fqdn'                 => "TEXT DEFAULT ''",
          'last_sync_update'           => "int(10) DEFAULT 0",
          'last_sync_update_host'      => "TEXT DEFAULT ''",
          'last_update'                => "int(10) DEFAULT 0",
          'last_update_host'           => "TEXT DEFAULT ''",
          'shortname'                  => "TEXT DEFAULT ''",
          'sms_challenge_enabled'      => "int(1) DEFAULT 0",
          'subnet'                     => "TEXT DEFAULT ''",
          'text_sms_challenge'         => "TEXT DEFAULT 'Please enter the code received on your mobile phone'",
          'text_token_challenge'       => "TEXT DEFAULT 'Please enter the code displayed on the token'",
          'encryption_hash'            => "TEXT DEFAULT ''");
      $this->_sql_tables_index['devices']  = '*device_id*ip_or_fqdn*shortname*';
      $this->_sql_tables_ignore['devices'] = "**";

      $this->_sql_tables_schema['groups']  = array(
          'group_id'                => "TEXT DEFAULT ''",
          'create_host'             => "TEXT DEFAULT ''",
          'create_time'             => "int(10) DEFAULT 0",
          'description'             => "TEXT DEFAULT ''",
          'name'                    => "TEXT DEFAULT ''",
          'last_sync_update'        => "int(10) DEFAULT 0",
          'last_sync_update_host'   => "TEXT DEFAULT ''",
          'last_update'             => "int(10) DEFAULT 0",
          'last_update_host'        => "TEXT DEFAULT ''",
          'encryption_hash'         => "TEXT DEFAULT ''");
      $this->_sql_tables_index['groups']   = '*group_id*name*';
      $this->_sql_tables_ignore['groups'] = "**";

      $this->_sql_tables_schema['log']     = array(
          'log_id'                  => "TEXT DEFAULT ''",
          'category'                => "TEXT DEFAULT ''",
          'create_host'             => "TEXT DEFAULT ''",
          'create_time'             => "int(10) DEFAULT 0",
          'datetime'                => "datetime DEFAULT NULL",
          'destination'             => "TEXT DEFAULT ''",
          'last_sync_update'        => "int(10) DEFAULT 0",
          'last_sync_update_host'   => "TEXT DEFAULT ''",
          'last_update'             => "int(10) DEFAULT 0",
          'last_update_host'        => "TEXT DEFAULT ''",
          'local_only'              => "int(1) DEFAULT 0",
          'logentry'                => "TEXT DEFAULT ''",
          'note'                    => "TEXT DEFAULT ''",
          'severity'                => "TEXT DEFAULT ''",
          'source'                  => "TEXT DEFAULT ''",
          'user'                    => "TEXT DEFAULT ''");
      $this->_sql_tables_index['log']      = '*datetime*';
      $this->_sql_tables_ignore['log']     = "**";

      $this->_sql_tables_schema['stat']  = array(
          'anonymous_stat_last_update'  => "int(10) DEFAULT 0",
          'create_host'                 => "TEXT DEFAULT ''",
          'create_time'                 => "int(10) DEFAULT 0",
          'last_sync_update'            => "int(10) DEFAULT 0",
          'last_sync_update_host'       => "TEXT DEFAULT ''",
          'last_update'                 => "int(10) DEFAULT 0",
          'last_update_host'            => "TEXT DEFAULT ''",
          'encryption_hash'             => "TEXT DEFAULT ''");
      $this->_sql_tables_index['stat']   = '**';
      $this->_sql_tables_ignore['stat']  = '**';

      $this->_sql_tables_schema['tokens']  = array(
          'algorithm'               => "TEXT DEFAULT ''",
          'attributed_users'        => "TEXT DEFAULT ''",
          'create_host'             => "TEXT DEFAULT ''",
          'create_time'             => "int(10) DEFAULT 0",
          'delta_time'              => "int(10) DEFAULT 0",
          'description'             => "TEXT DEFAULT ''",
          'error_counter'           => "int(10) DEFAULT 0",
          'format'                  => "TEXT DEFAULT ''",
          'key_id'                  => "TEXT DEFAULT ''",
          'key_usage'               => "TEXT DEFAULT ''",
          'issue_no'                => "TEXT DEFAULT ''",
          'issuer'                  => "TEXT DEFAULT ''",
          'key_algorithm'           => "TEXT DEFAULT ''",
          'last_error'              => "int(10) DEFAULT 0",
          'last_event'              => "int(10) DEFAULT -1",
          'last_login'              => "int(10) DEFAULT 0",
          'last_sync_update'        => "int(10) DEFAULT 0",
          'last_sync_update_host'   => "TEXT DEFAULT ''",
          'last_update'             => "int(10) DEFAULT 0",
          'last_update_host'        => "TEXT DEFAULT ''",
          'locked'                  => "int(1) DEFAULT 0",
          'manufacturer'            => "TEXT DEFAULT 'multiOTP'",
          'model'                   => "TEXT DEFAULT ''",
          'number_of_digits'        => "int(10) DEFAULT 6",
          'otp'                     => "TEXT DEFAULT ''",
          'private_id'              => "TEXT DEFAULT ''",
          'serial_no'               => "TEXT DEFAULT ''",
          'time_interval'           => "int(10) DEFAULT 0",
          'token_algo_suite'        => "TEXT DEFAULT ''",
          'token_id'                => "TEXT DEFAULT ''",
          // Token seed, default set to the RFC test seed, hexadecimal coded
          'token_seed'              => "TEXT DEFAULT '3132333435363738393031323334353637383930'",
          'token_serial'            => "TEXT DEFAULT ''",
          'encryption_hash'         => "TEXT DEFAULT ''");
      $this->_sql_tables_index['tokens']   = '*attributed_users*token_id*token_serial*';
      $this->_sql_tables_ignore['tokens']  = "**";

      $this->_sql_tables_schema['users']   = array(
          'admin_with_otp'          => "int(1) DEFAULT 0",
          'algorithm'               => "TEXT DEFAULT ''",
          'attributed_tokens'       => "TEXT DEFAULT ''",
          // Autolock time (for cached data)
          'autolock_time'           => "int(10) DEFAULT 0",
          'cache_level'             => "int(10) DEFAULT 1",
          'cache_lifetime'          => "int(10) DEFAULT 15552000",
          // Challenge initialization
          'challenge'               => "TEXT DEFAULT ''",
          'challenge_validity'      => "int(10) DEFAULT 0",
          'create_host'             => "TEXT DEFAULT ''",
          'create_time'             => "int(10) DEFAULT 0",
          // Delta time in seconds for a time based token
          'delta_time'              => "int(10) DEFAULT 0",
          // Desactivated user info
          'desactivated'            => "int(1) DEFAULT 0",
          'description'             => "TEXT DEFAULT ''",
          'dialin_ip_address'       => "TEXT DEFAULT ''",
          'dialin_ip_mask'          => "TEXT DEFAULT ''",
          'display_name'            => "TEXT DEFAULT ''",
          'email'                   => "TEXT DEFAULT ''",
          'email_otp'               => "TEXT DEFAULT ''",
          // User email otp validity
          'email_validity'          => "int(10) DEFAULT 0",
          // Login error counter
          'error_counter'           => "int(10) DEFAULT 0",
          'group'                   => "TEXT DEFAULT ''",
          'key_id'                  => "TEXT DEFAULT ''",
          'language'                => "TEXT DEFAULT ''",
          'last_cached_credential'  => "TEXT DEFAULT ''",
          // Last error login
          'last_error'              => "int(10) DEFAULT 0",
          // Last successful event
          'last_event'              => "int(10) DEFAULT -1",
          // Last successful login
          'last_failed_credential'  => "TEXT DEFAULT ''",
          'last_failed_time'        => "int(10) DEFAULT 0",
          'last_login'              => "int(10) DEFAULT 0",
          'last_login_for_cache'    => "int(10) DEFAULT 0",
          'last_success_credential' => "TEXT DEFAULT ''",
          'last_sync_update'        => "int(10) DEFAULT 0",
          'last_sync_update_host'   => "TEXT DEFAULT ''",
          'last_update'             => "int(10) DEFAULT 0",
          'last_update_host'        => "TEXT DEFAULT ''",
          // LDAP password hash caching mechanism
          'ldap_hash_cache'         => "TEXT DEFAULT ''",
          'ldap_hash_validity'      => "int(10) DEFAULT 0",
          // Token locked
          'locked'                  => "int(1) DEFAULT 0",
          'mosid'                   => "TEXT DEFAULT ''",
          'mouid'                   => "TEXT DEFAULT ''",
          // User is a special multi-account user (the real user is in the token, like this: "user[space]token"
          'multi_account'           => "int(1) DEFAULT 0",
          // Number of digits returned by the token
          'number_of_digits'        => "int(10) DEFAULT 6",
          'private_id'              => "TEXT DEFAULT ''",
          // Request the LDAP password as a prefix of the returned token value
          'push_enable_date'        => "int(10) DEFAULT 0",
          'push_enabled'            => "int(1) DEFAULT 0",
          'push_gateway'            => "TEXT DEFAULT ''",
          'request_ldap_pwd'        => "int(1) DEFAULT 0",
          'request_prefix_pin'      => "int(1) DEFAULT 0",
          'scratch_passwords'       => "TEXT DEFAULT ''",
          'seed_password'           => "TEXT DEFAULT ''",
          'sms'                     => "TEXT DEFAULT ''",
          'sms_otp'                 => "TEXT DEFAULT ''",
          // User sms otp validity
          'sms_validity'            => "int(10) DEFAULT 0",
          // Synchronized user info
          'synchronized'            => "int(1) DEFAULT 0",
          'synchronized_channel'    => "TEXT DEFAULT ''",
          'synchronized_dn'         => "TEXT DEFAULT ''",
          'synchronized_server'     => "TEXT DEFAULT ''",
          'synchronized_time'       => "int(10) DEFAULT 0",
          // Time interval in seconds for a time based token
          'time_interval'           => "int(10) DEFAULT 0",
          'token_algo_suite'        => "TEXT DEFAULT ''",
          // Token seed, default set to the RFC test seed, hexadecimal coded
          'token_seed'              => "TEXT DEFAULT '3132333435363738393031323334353637383930'",
          'token_serial'            => "TEXT DEFAULT ''",
          'user'                    => "TEXT DEFAULT ''",
          'user_last_login'         => "int(10) DEFAULT 0",
          'user_pin'                => "TEXT DEFAULT ''",
          'user_principal_name'     => "TEXT DEFAULT ''",
          'encryption_hash'         => "TEXT DEFAULT ''");
      $this->_sql_tables_index['users']    = '*attributed_tokens*desactivated*locked*user*';
      $this->_sql_tables_ignore['users']   = "**";
      $this->_sql_tables_not_in_schema['users'] = array(
          'delayed_account',
          'delayed_time',
          'delayed_finished');

      if ("" == $encryption_key) {
          $this->_encryption_key = 'MuLtIoTpEnCrYpTiOn'; // This default value should be changed for each project using SetEncryptionKey()
      } else {
          $this->_encryption_key = $encryption_key;
      }

      $current_dir = $base_dir;
      if ("" == $current_dir) {
          $env_folder_path = getenv('MULTIOTP_PATH');
          if (false !== $env_folder_path) {
              $current_dir = $env_folder_path;
          }
      }
      $this->SetBaseDir($current_dir);

      if ("" != nullable_trim($config_dir)) {
          $this->SetConfigFolder($config_dir, true, false);
      }
      
      $this->_parser_pointers         = [];
      
      $this->_hash_salt               = 'MySalt'; // Can be Set in your application using SetHashSalt()
      $this->_random_salt             = "Random"; // Updated regulary with SetRandomSalt

      $this->_source_tag              = "";

      $this->_source_ip               = "";
      $this->_source_mac              = "";
      
      $this->_calling_ip              = "";
      $this->_calling_mac             = "";

      $this->_chap_challenge          = "";
      $this->_chap_id                 = "";
      $this->_chap_password           = "";
      
      $this->_ms_nt_key               = "";
      
      $this->_encryption_check        = true; // Check if the encryption hash is valid, default is true

      $this->_user                    = ""; // Name of the current user to authenticate (case insensitive)
      $this->_rawUser                 = ""; // Name of the current user to authenticate (passed by authentication request)
      $this->_user_data_read_flag     = false; // Flag to know if the data concerning the current user has been read
      $this->_users_folder            = ""; // Folders which contain the users flat files
      $this->_credusers_folder        = ""; // Folders which contain the users flat files in SID named files
      
      $this->_current_user_sid        = ""; // Current User SID (not saved in the user data)

      $this->_last_ldap_error         = false;
      $this->_log_file_name           = 'multiotp.log';
      $this->_log_flag                = false;
      $this->_log_folder              = ""; // Folder which contains the log file
      $this->_log_verbose_flag        = false;
      $this->_log_display_flag        = false;
      
      $this->_mysql_database_link     = NULL;
      $this->_mysqli                  = NULL;
      $this->_pgsql_database_link     = NULL;

      $this->_migration_from_file     = false; // To allow an automatic migration of users profiles,
                                               // enable a database backend and set the migration option ;-) !

      $this->_reply_array_for_radius = [];
      
      $this->_servers_temp_bad_list  = [];

      $this->_initialize_backend = $initialize_backend;
      
      $this->_debug_via_html = false;
      
      $this->_linux_file_mode = "";

      $this->_last_http_status = 0;

      $this->_bad_syslog_server = false;
      
      $this->_state = "";

      $this->ReadConfigData(true); // Read the configuration data, for the encryption information only
      if (("" == $encryption_key) || ('MuLtIoTpEnCrYpTiOn' == $encryption_key) || ('DefaultCliEncryptionKey' == $encryption_key)) {
          if (("" != $this->GetEncryptionKeyFullPath()) && file_exists($this->GetEncryptionKeyFullPath())) {
              if ($encryption_key_file_handler = @fopen($this->GetEncryptionKeyFullPath(), "rt")) {
                  flock($encryption_key_file_handler, LOCK_SH);
                  $temp_encryption_key = nullable_trim(fgets($encryption_key_file_handler));
                  if ("" != $temp_encryption_key) {
                      $this->SetEncryptionKey($temp_encryption_key, false);
                  }
                  fclose($encryption_key_file_handler);
              }
          }
      }

      $this->_server_challenge = $this->GetEncryptionKey();

      $this->_keep_local = false;
      
      $this->_xml_dump_in_log = false; // For debugging purpose only
      
      $this->_sms_providers_array = array(array("afilnet", "Afilnet (HTTPS)", "https://www.afilnet.com/", "username,password"),
                                          array("aspsms", "aspsms.com (XML)", "https://www.aspsms.com/", "username,password"),
                                          array("clickatell", "Clickatell (legacy XML)", "https://archive.clickatell.com/developers/2015/10/08/xml/", "api_id,username,password"),
                                          array("clickatell2", "Clickatell (HTTPS)", "https://www.clickatell.com/", "api_id"),
                                          array("ecall", "eCall.ch (HTTPS)", "https://www.ecall.ch/", "username,password"),
                                          array("intellisms", "IntelliSMS.co.uk (HTTPS)", "https://www.intellisms.co.uk/", "username,password"),
                                          array("nexmo", "Nexmo (HTTPS)", "https://www.nexmo.com/", "api_id,password"),
                                          array("nowsms", "NowSMS.com (on-premises gateway)", "https://www.nowsms.com/", "ip,port,username,password"),
                                          array("smseagle", "SMSEagle (hardware gateway)", "https://www.smseagle.eu/", "ip,port,username,password"),
                                          array("smsgateway", "SMSGateway (open source gateway)", "https://github.com/multiOTP/SMSGateway", "ip,port,api_id,password"),
                                          array("spryng", "Spryng (REST API)", "https://www.spryng.be", "api_id,ip"),
                                          array("spryngsimple", "Spryng (Simple API)", "https://www.spryng.be", "username,password,ip"),
                                          array("swisscom", "Swisscom LA (REST-JSON)", "https://messagingproxy.swisscom.ch:4300/rest/1.0.0/", "api_id,username,password"),
                                          array("telnyx", "Telnyx", "https://developers.telnyx.com/docs/api/v2/messaging", "api_id"),
                                          array("custom", "Custom provider", "")
                                         );

      // As various accounts are using the same files
      $this->SetLinuxFileMode('0666');
      $this->SetLinuxFolderMode('0777');
      
      // Reset/initialize the errors text array, should be the first reset method to call
      $this->ResetErrorsArray();

      // Reset/initialize the translation text array
      $this->ResetTranslationArray();

      // Reset/initialize the config array, should be the second reset method to call
      $this->ResetConfigArray();
      
      $this->ResetStatArray();
      
      // Reset/initialize the ddns array
      $this->ResetDdnsArray();

      // Reset/initialize the device array
      $this->ResetDeviceArray();

      // Reset/initialize the group array
      $this->ResetGroupArray();

      // Reset/initialize the user array
      $this->ResetUserArray();

      // Reset/initialize the token array
      $this->ResetTokenArray();

      if (!$no_init) {
          
          // In case of initialization, we will disable the backend validation
          $this->ReadConfigData();

          // Reset/initialize the cache array
          $this->ResetCacheArray();
          if ($this->IsCacheData()) {
              $this->ReadCacheData();
          }

          // Set the Ldap TLS options
          $this->SetLdapTlsOptions();
      }
  }

  // Class destructor, also called in PHP4
  function __destruct()
  {
      /*
      if ($this->IsCacheData()) {
          $this->WriteCacheData();
      }
      */
  }


  // Reset the errors array
  function ResetErrorsArray()
  {
    $this->_errors_text[0] = "OK: Token accepted";

    $this->_errors_text[7] = "INFO: User requires a token";
    $this->_errors_text[8] = "INFO: User can be authenticated without a token (WITHOUT2FA)";
    $this->_errors_text[9] = "INFO: Access Challenge returned back to the client";
    $this->_errors_text[10] = "INFO: Access Challenge returned back to the client";

    $this->_errors_text[11] = "INFO: User successfully created or updated";
    $this->_errors_text[12] = "INFO: User successfully deleted";
    $this->_errors_text[13] = "INFO: User PIN code successfully changed";
    $this->_errors_text[14] = "INFO: Token has been resynchronized successfully";
    $this->_errors_text[15] = "INFO: Tokens definition file successfully imported";
    $this->_errors_text[16] = "INFO: QRcode successfully created";
    $this->_errors_text[17] = "INFO: UrlLink successfully created";
    $this->_errors_text[18] = "INFO: Static code request received";
    $this->_errors_text[19] = "INFO: Requested operation successfully done";

    $this->_errors_text[20] = "ERROR: User blacklisted";
    $this->_errors_text[21] = "ERROR: User doesn't exist";
    $this->_errors_text[22] = "ERROR: User already exists";
    $this->_errors_text[23] = "ERROR: Invalid algorithm";
    $this->_errors_text[24] = "ERROR: User locked (too many tries)";
    $this->_errors_text[25] = "ERROR: User delayed (too many tries, but still a hope in a few minutes)";
    $this->_errors_text[26] = "ERROR: This token has already been used";
    $this->_errors_text[27] = "ERROR: Resynchronization of the token has failed";
    $this->_errors_text[28] = "ERROR: Unable to write the changes in the file";
    $this->_errors_text[29] = "ERROR: Token doesn't exist";

    $this->_errors_text[30] = "ERROR: At least one parameter is missing";
    $this->_errors_text[31] = "ERROR: Tokens definition file doesn't exist";
    $this->_errors_text[32] = "ERROR: Tokens definition file not successfully imported";
    $this->_errors_text[33] = "ERROR: Encryption hash error, encryption key is not matching";
    $this->_errors_text[34] = "ERROR: Linked user doesn't exist";
    $this->_errors_text[35] = "ERROR: User not created";
    $this->_errors_text[36] = "ERROR: Token doesn't exist";
    $this->_errors_text[37] = "ERROR: Token already attributed";
    $this->_errors_text[38] = "ERROR: User is desactivated";
    $this->_errors_text[39] = "ERROR: Requested operation aborted";
   
    $this->_errors_text[40] = "ERROR: SQL query error";
    $this->_errors_text[41] = "ERROR: SQL error";
    $this->_errors_text[42] = "ERROR: They key is not in the table schema";
    $this->_errors_text[43] = "ERROR: SQL entry cannot be updated";

    $this->_errors_text[50] = "ERROR: QRcode not created";
    $this->_errors_text[51] = "ERROR: UrlLink not created (no provisionable client for this protocol)";
    $this->_errors_text[52] = "ERROR: HTML info not created";
    $this->_errors_text[58] = "ERROR: File is missing";
    $this->_errors_text[59] = "ERROR: Bad restore configuration password";

    $this->_errors_text[60] = "ERROR: No information on where to send SMS code";
    $this->_errors_text[61] = "ERROR: SMS code request received, but an error occurred during transmission";
    $this->_errors_text[62] = "ERROR: SMS provider not supported";
    $this->_errors_text[63] = "ERROR: This SMS code has expired";
    $this->_errors_text[64] = "ERROR: Cannot resent an SMS code right now";
    $this->_errors_text[65] = "ERROR: SMS code request not allowed";
    $this->_errors_text[66] = "ERROR: Email code request not allowed";
    $this->_errors_text[67] = "ERROR: No information on where to send Email code";
    $this->_errors_text[68] = "ERROR: Email code request received, but an error occurred during transmission";
    $this->_errors_text[69] = "ERROR: Failed to send email";
    
    $this->_errors_text[70] = "ERROR: Server authentication error";
    $this->_errors_text[71] = "ERROR: Server request is not correctly formatted";
    $this->_errors_text[72] = "ERROR: Server answer is not correctly formatted";
    $this->_errors_text[73] = "ERROR: Email SMTP server not defined";
    $this->_errors_text[79] = "ERROR: AD/LDAP connection error";
    
    $this->_errors_text[80] = "ERROR: Server cache error";
    $this->_errors_text[81] = "ERROR: Cache too old for this user, account autolocked";
    $this->_errors_text[82] = "ERROR: User not allowed for this device";
    $this->_errors_text[88] = "ERROR: Device is not defined as a HA slave";
    $this->_errors_text[89] = "ERROR: Device is not defined as a HA master";
    $this->_errors_text[90] = "ERROR: AD/LDAP authentication failed";

    $this->_errors_text[91] = "ERROR: Authentication failed (without2fa token not authorized here)";
    $this->_errors_text[92] = "ERROR: Authentication failed (bad password)";
    $this->_errors_text[93] = "ERROR: Authentication failed (time based token probably out of sync)";
    $this->_errors_text[94] = "ERROR: API request error";
    $this->_errors_text[95] = "ERROR: API authentication failed";
    $this->_errors_text[96] = "ERROR: Push authentication timeout";
    $this->_errors_text[97] = "ERROR: Push authentication denied";
    $this->_errors_text[98] = "ERROR: Authentication failed (wrong token length)";
    $this->_errors_text[99] = "ERROR: Authentication failed (and other possible unknown errors)";
  }


  function ResetTranslationArray()
  {
    $this->_translation_array["push_title"] = array(
      "en" => "Approve Sign-in ?",
      "fr" => "Valider la connexion ?",
      "de" => "Anmeldung genehmigen ?",
      "it" => "Approvare l'accesso ?"
    );
  }


  function Translate(
    $label,
    $lang = ""
  ) {
    if (isset($this->_translation_array[$label])) {
      $result_array = $this->_translation_array[$label];
      if (isset($result_array[strtolower($lang)])) {
        $result = $result_array[strtolower($lang)];
      } else {
        $result = $result_array[strtolower($lang)];
      }
    } else {
      $result = $label;
    }
    return $result;
  }


  function GetShortModelName(
    $default_type = '',
    $default_name = ''
  ) {
    $type = ('' != $default_type) ? $default_type : $this->GetHardwareType();
    $name = ('' != $default_name) ? $default_name : 'open source';
    return $name;
  }


  function SetPushActivated(
      $value
  ) {
      $this->_config_data['push_activated'] = ((intval($value) > 0)?1:0);
  }


  function GetPushActivated()
  {
      return intval($this->_config_data['push_activated']);
  }


  function IsPushActivated() {
    return (1 == intval($this->_config_data['push_activated']));
  }


  function GetPushId() {
    $push_id = "";
    if (mb_strtolower(mb_substr(PHP_OS, 0, 3),'UTF-8') === 'win') { // Windows
      $push_id = $this->ReadBootCacheValue("push_id");
      if (is_null($push_id)) {
        $output = [];
        if (empty($push_id)) {
          $cmd = 'powershell -Command "(Get-CimInstance Win32_ComputerSystemProduct).UUID"';
          exec($cmd, $output);
          $push_id = nullable_trim($output[0]);
        }
        $this->WriteBootCacheValue("push_id", $push_id);
      }
    } else {
      $push_id = nullable_trim(file_get_contents('/etc/machine-id'));
    }

    $push_id = str_replace('-','',$push_id);
    $push_id = str_replace(':','',$push_id);
    
    return nullable_trim($push_id);
  }


  function GetPushGateway(
    $force_url = false
  ) {
    return nullable_trim(isset($this->_config_data['push_gateway']) ? $this->_config_data['push_gateway'] : '');
  }


  function SetPushSubscriptionTimeout(
      $value
  ) {
      $this->_config_data['push_subscription_timeout'] = intval($value);
  }


  function GetPushSubscriptionTimeout() {
    $value = intval(isset($this->_config_data['push_subscription_timeout']) ? $this->_config_data['push_subscription_timeout'] : 5);
    if (0 == $value) {
      $value = 5;
    }
    return $value;
  }


  function SetPushTimeout(
      $value
  ) {
      $this->_config_data['push_timeout'] = intval($value);
  }


  function GetPushTimeout() {
    $value = intval(isset($this->_config_data['push_timeout']) ? $this->_config_data['push_timeout'] : _MULTIOTP_DEFAULT_PUSH_TIMEOUT_);
    if ($value <= 0) {
      $value = _MULTIOTP_DEFAULT_PUSH_TIMEOUT_;
    } elseif ($value >= 120) {
      // Previous default value of 120 seconds was really too long
      $value = _MULTIOTP_DEFAULT_PUSH_TIMEOUT_;
    }
    return $value;
  }


  function GetPushApiDeviceRegister() {
    $push_api_device_register = "";
    return $push_api_device_register;
  }


  function GetPushUniqueId(
    $hash_array = []
  ) {
    return uniqid();
  }


  function GetPushWsFqdn() {
    $push_gateway = $this->GetUserPushGateway();
    return ( (!empty($push_gateway)) ? ("wss://" . $push_gateway . "/wss/") : "" );
  }


  function PushRequest(
    $push_request_array = []
  ) {
    $user = isset($push_request_array['user']) ? $push_request_array['user'] : "";
    $title = isset($push_request_array['title']) ? $push_request_array['title'] : "";
    $subtitle = isset($push_request_array['subtitle']) ? $push_request_array['subtitle'] : "";
    $mouid = $this->GetUserMouid($user);
    $mosid = $this->GetUserMosid();
    $hash = $this->GetPushUniqueId();
    $answer = "";
    
    $actual_user = $this->GetUser();
    if (!empty($mouid)) {
      $request = "EHLO§" . $hash . "§" . $mouid . "§" . $mosid . "§" . $title . "§" . $subtitle;

      $talk = new multiotpwebsocket($this->GetPushWsFqdn(), $this->GetPushTimeout());
      $talk->writeSocket($request);
      $answer = $talk->readSocket();

      if ($this->GetVerboseFlag()) {
          $this->WriteLog("Debug: push request, FQDN: " . $this->GetPushWsFqdn() . ", request: " . $request .", answer: " . $answer, FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Debug', '');
      }
    } else {
      $this->WriteLog("Error: Push request failed, push token not initialized for $actual_user", FALSE, FALSE, _MULTIOTP_PUSH_ERROR_, 'User', $actual_user);
    }
    
    $array_answer = explode(";", $answer);
    $answer = nullable_trim(end($array_answer));
    $status = strtolower(nullable_trim(reset($array_answer)));
    if ("ko" == $status) {
      $status_detail = strtolower($answer);
      $status_info = $this->Translate('push_status_' . $status_detail, $this->GetUserLanguage());
      $this->WriteLog("Error: Push request failed for $actual_user, $status_info", FALSE, FALSE, _MULTIOTP_PUSH_ERROR_, 'User', $actual_user);
    } elseif (empty($status) && empty($answer)) {
      $answer = _MULTIOTP_PUSH_TIMEOUT_CODE_;
      $this->WriteLog("Error: Push authentication timeout for " . $actual_user, FALSE, FALSE, _MULTIOTP_PUSH_ERROR_, 'User', $actual_user);
    } elseif (("ok" == $status) && ("0" == $answer)) {
      $answer = _MULTIOTP_PUSH_DENIED_CODE_;
      $this->WriteLog("Error: Push authentication denied for " . $actual_user, FALSE, FALSE, _MULTIOTP_PUSH_ERROR_, 'User', $actual_user);
    }
    return $answer;
  }


  function GetMouidPrefix() {
    return "";
  }


  function GetUserMosid(
    $user_mosid_array = []
  ) {
    $user = nullable_trim(isset($user_mosid_array['user']) ? $user_mosid_array['user'] : "");
    $stored = nullable_trim(isset($user_mosid_array['stored']) ? $user_mosid_array['stored'] : false);
    if (!empty($user)) {
      if ($this->CheckUserExists($user)) {
        $this->SetUser($user);
      }
    }
    $user_mosid = nullable_trim(isset($this->_user_data['mosid']) ? $this->_user_data['mosid'] : "");
    if ((!$stored) && empty($user_mosid)) {
      $user_mosid = $this->GetPushId();
    }
    return $user_mosid;
  }


  function GetUserMouid(
    $user = ""
  ) {
    if (!empty($user)) {
      if ($this->CheckUserExists($user)) {
        $this->SetUser($user);
      }
    }
    if ($this->GetUserPushEnableDate() > 0) {
    $mouid = nullable_trim(isset($this->_user_data['mouid']) ? $this->_user_data['mouid'] : "");
    } else {
      $mouid = "";
    }
    return $mouid;
  }


  function UserPushSubscription(
    $user_push_subscription = []
  ) {
    $user = isset($user_push_subscription['user']) ? $user_push_subscription['user'] : '';
    $mouid = isset($user_push_subscription['mouid']) ? $user_push_subscription['mouid'] : '';
    $mosid = isset($user_push_subscription['mosid']) ? $user_push_subscription['mosid'] : '';
    $push_gateway = isset($user_push_subscription['push_gateway']) ? $user_push_subscription['push_gateway'] : '';
    $subscription_result = $this->PostHttpDataXmlRequest("mouid=" . $mouid . "&mosid=" . $mosid, "https://" . $push_gateway . "/api/device/user", $this->GetPushSubscriptionTimeout());
    $status = $this->GetLastHttpStatus();
    if ($this->GetVerboseFlag()) {
      $this->WriteLog("Info: Push subscription sent for " . $user . ": " . $status . " " . $subscription_result, FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'User', $user);
    }
    return $status;
  }


  function SetCurrentUserSid($value) {
    $this->_current_user_sid = nullable_trim($value);
    if ($this->GetVerboseFlag()) {
      $this->WriteLog("Info: *Current User SID set to '" . $this->GetCurrentUserSid() . "'", FALSE, FALSE, 8888, 'System', "");
    }
    return nullable_trim($value);
  }


  function GetCurrentUserSid() {
      return nullable_trim($this->_current_user_sid);
  }
  
  
  function SetRadiusTagPrefix($value) {
      $this->_config_data['radius_tag_prefix'] = nullable_trim($value);
      return nullable_trim($value);
  }


  function GetRadiusTagPrefix() {
      return nullable_trim($this->_config_data['radius_tag_prefix']);
  }
  
  
  // Get the HTTP/Ajax session timeout in seconds
  function GetSessionTimeout()
  {
    return intval(($this->_config_data['session_timeout'] > 0) ? $this->_config_data['session_timeout'] : _MULTIOTP_DEFAULT_GUI_TIMEOUT_);
  }


  function SetLastFailedWhiteDelay(
    $delay
  ) {
    $this->_config_data['last_failed_white_delay'] = $delay;
  }


  function GetLastFailedWhiteDelay()
  {
    return intval(isset($this->_config_data['smtp_username']) ? $this->_config_data['smtp_username'] : 60);
  }


  function SetLdapRecursiveGroups($value) {
      $this->_config_data['ldap_recursive_groups'] = ((intval($value) > 0)?1:0);
  }


  function SetLdapCacheOn($value) {
      $this->_config_data['ldap_cache_on'] = ((intval($value) > 0)?1:0);
  }


  function SetLdapRecursiveCacheOnly($value) {
      $this->_config_data['ldap_recursive_cache_only'] = ((intval($value) > 0)?1:0);
  }


  function SetLdapCacheFolder($value) {
      $folder = $value;
      if ($this->ConvertToWindowsPathIfNeeded(sys_get_temp_dir()."/") == $value) {
          $folder = "tempdir";
      }
      $this->_config_data['ldap_cache_folder'] = nullable_trim($folder);
  }


  function IsLdapRecursiveGroups() {
      return (1 == $this->_config_data['ldap_recursive_groups']);
  }


  function IsLdapCacheOn() {
      return (1 == $this->_config_data['ldap_cache_on']);
  }


  function IsLdapRecursiveCacheOnly() {
      return (1 == $this->_config_data['ldap_recursive_cache_only']);
  }


  function GetLdapCacheFolder() {
      $folder = nullable_trim($this->_config_data['ldap_cache_folder']);
      if (("tempdir" == $folder) || ("" == nullable_trim($folder))) {
          $folder = $this->ConvertToWindowsPathIfNeeded(sys_get_temp_dir()."/");
      }
      if (file_exists($folder) && touch($folder."test.cache")) {
          unlink($folder."test.cache");
          if (!file_exists($folder.".ldap_cache/")) {

              if (@mkdir(
                      $folder.".ldap_cache/",
                      ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
                      true //recursive
              )) {
                  if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Debug: *LDAP cache folder created (".$folder.".ldap_cache/".")", FALSE, FALSE, 8888, 'System', "");
                  }
              }
          }
          if (file_exists($folder.".ldap_cache/") && touch($folder.".ldap_cache/test.cache")) {
              unlink($folder.".ldap_cache/test.cache");
              $folder = $folder.".ldap_cache/";
          }
      } else {
          $folder = "";
      }
      if ($this->GetVerboseFlag()) {
        $this->WriteLog("Debug: *LDAP cache folder value: $folder", FALSE, FALSE, 8888, 'System', "");
      }
      return $folder;
  }


  function EncodeForBackend($value) {
    $encoding = mb_strtolower($this->GetBackendEncoding(),'UTF-8');
    if (("utf-8" == $encoding) || ("utf8" == $encoding)) {
      $result = encode_utf8_if_needed($value);
    } else {
      $result = decode_utf8_if_needed($value);
    }
    return $result;
  }


  function SetBackendEncoding(
    $encoding
  ) {
    $this->_config_data['backend_encoding'] = $encoding;
  }


  function GetBackendEncoding()
  {
    return (isset($this->_config_data['backend_encoding']) ? $this->_config_data['backend_encoding'] : 'UTF-8');
  }


  function SetMultipleGroups(
      $value
  ) {
      $this->_config_data['multiple_groups'] = ((intval($value) > 0)?1:0);
  }


  function EnableMultipleGroups()
  {
      $this->_config_data['multiple_groups'] = 1;
  }


  function DisableMultipleGroups()
  {
      $this->_config_data['multiple_groups'] = 0;
  }


  function IsMultipleGroupsEnabled()
  {
      return (1 == ($this->_config_data['multiple_groups']));
  }


  function SetUserCacheLevel(
      $value
  ) {
      $this->_user_data['cache_level'] = intval($value);
  }


  function GetUserCacheLevel()
  {
      return intval($this->_user_data['cache_level']);
  }


  function SetUserCacheLifetime(
      $value
  ) {
      $this->_user_data['cache_lifetime'] = intval($value);
  }


  function GetUserCacheLifetime()
  {
      return intval($this->_user_data['cache_lifetime']);
  }


  function SetUserDialinIpAddress(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = TRUE;
      $value = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $result = $this->SetUser($first_param);
          $value = $second_param;
      }
      $this->_user_data['dialin_ip_address'] = $value;

      return $result;
  }


  function GetUserDialinIpAddress(
      $user = ""
  ) {
      if("" != $user) {
          $this->SetUser($user);
      }
      return (isset($this->_user_data['dialin_ip_address']) ? $this->_user_data['dialin_ip_address'] : "");
  }


  function SetUserDialinIpMask(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = TRUE;
      $value = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $result = $this->SetUser($first_param);
          $value = $second_param;
      }
      $this->_user_data['dialin_ip_mask'] = $value;

      return $result;
  }


  function GetUserDialinIpMask(
      $user = ""
  ) {
      if("" != $user) {
          $this->SetUser($user);
      }
      return (isset($this->_user_data['dialin_ip_mask']) ? $this->_user_data['dialin_ip_mask'] : "");
  }


  function SetUserChallenge(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = TRUE;
      $value = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $result = $this->SetUser($first_param);
          $value = $second_param;
      }
      $this->_user_data['challenge'] = $value;

      return $result;
  }


  function GetUserChallenge(
      $user = ""
  ) {
      if("" != $user) {
          $this->SetUser($user);
      }
      return $this->_user_data['challenge'];
  }

  
  function SetUserChallengeValidity(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = TRUE;
      $value = 0;
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $result = $this->SetUser($first_param);
          $value = $second_param;
      }
      $this->_user_data['challenge_validity'] = intval($value);

      return $result;
  }


  function GetUserChallengeValidity(
      $user = ""
  ) {
      if("" != $user) {
          $this->SetUser($user);
      }
      return intval($this->_user_data['challenge_validity']);
  }


  function SetDefaultDialinIpMask($ip_mask)
  {
      $this->_config_data['default_dialin_ip_mask'] = $ip_mask;
      return TRUE;
  }


  function GetDefaultDialinIpMask()
  {
    return (isset($this->_user_data['default_dialin_ip_mask']) ? $this->_user_data['default_dialin_ip_mask'] : "");
  }


  function SetLastHttpStatus($status = 200)
  {
      $this->_last_http_status = intval($status);
  }


  function GetLastHttpStatus()
  {
      return intval($this->_last_http_status);
  }


  function SetLockTime($value)
  {
    $this->_lock_time = nullable_trim($value);
  }


  function GetLockTime()
  {
    return nullable_trim($this->_lock_time);
  }


  function SetLdapSyncStopFileName($value)
  {
    $this->_ldap_sync_stop_file_name = nullable_trim($value);
  }


  function GetLdapSyncStopFileName()
  {
    return nullable_trim($this->_ldap_sync_stop_file_name);
  }


  function SetLdapSyncLockFileName($value)
  {
    $this->_ldap_sync_lock_file_name = nullable_trim($value);
  }


  function GetLdapSyncLockFileName()
  {
    return nullable_trim($this->_ldap_sync_lock_file_name);
  }


  function SetLockFolder($value)
  {
    $this->_lock_folder = nullable_trim($value);
  }


  function GetLockFolder()
  {
    return nullable_trim($this->_lock_folder);
  }


  function PurgeLockFolder()
  {
    $actual_folder = $this->GetLockFolder();
    $actual_filter = "multiotp-*.lock";
    if (($actual_dir = opendir($actual_folder)) !== FALSE) {
      while(($actual_file_name = readdir($actual_dir)) !== FALSE) {
        if (fnmatch($actual_filter, $actual_file_name)) {
          $actual_file = $actual_folder.$actual_file_name;
          unlink($actual_file);
        }
      }
    }
    return TRUE;
  }


  function PurgeLdapCacheFolder()
  {
    $actual_filter = "ldap_*.cache";
    $actual_folder = $this->GetLdapCacheFolder();
    if (($actual_dir = opendir($actual_folder)) !== FALSE) {
      while(($actual_file_name = readdir($actual_dir)) !== FALSE) {
        if (fnmatch($actual_filter, $actual_file_name)) {
          $actual_file = $actual_folder.$actual_file_name;
          unlink($actual_file);
        }
      }
    }
    $actual_folder = $this->ConvertToWindowsPathIfNeeded(sys_get_temp_dir()."/");
    if (($actual_dir = opendir($actual_folder)) !== FALSE) {
      while(($actual_file_name = readdir($actual_dir)) !== FALSE) {
        if (fnmatch($actual_filter, $actual_file_name)) {
          $actual_file = $actual_folder.$actual_file_name;
          unlink($actual_file);
        }
      }
    }
    return TRUE;
  }


  /**
   * @brief   Set an array of suffix(es) to use when folder is touched
   *
   * @param   array $touch_suffix_array   Array of suffix(es) to use when folder is touched
   *
   * @retval  n/a
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.0.3.6
   * @date    2017-02-12
   * @since   2017-02-10
   */
  function SetTouchSuffixArray($touch_suffix_array)
  {
    $this->_touch_suffix_array = $touch_suffix_array;
    return TRUE;
  }


  function GetTouchSuffixArray()
  {
    return ($this->_touch_suffix_array);
  }


  function SetTouchFolder($value)
  {
    $this->_touch_folder = nullable_trim($value);
  }


  function GetTouchFolder()
  {
    return nullable_trim($this->_touch_folder);
  }


  function PurgeTouchFolder()
  {
    $actual_folder = $this->GetTouchFolder();
    if ("" != $actual_folder) {
      $actual_filter = "*.sync";
      if (($actual_dir = opendir($actual_folder)) !== FALSE) {
        while(($actual_file_name = readdir($actual_dir)) !== FALSE) {
          if (fnmatch($actual_filter, $actual_file_name)) {
            $actual_file = $actual_folder.$actual_file_name;
            unlink($actual_file);
          }
        }
      }
    }
    return TRUE;
  }


  /**
   * @brief   Touch special file(s) for each modified element, like a "dirty flag" (except for data/stat)
   *          The file name is based on a suffix, the suffix(es) are contained in an array
   *
   * @param   string $type_fn          Type of the data ('data', 'file', ...)
   * @param   string  $item_fn         Item category ('Configuration', 'User', 'Token', ...)
   * @param   string  $id_fn           Id of the item
   * @param   boolean $folder_touched  At the end of the process, call the FolderTouched() method
   * @param   string  $touch_info      Specific touch information, can be used by FolderTouched() method
   *
   * @retval  void
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.0.2.5
   * @date    2018-08-21
   * @since   2016-10-16
   */
  function TouchFolder(
    $type_fn = "",
    $item_fn = "",
    $id_fn = "",
    $folder_touched = TRUE,
    $touch_info = ""
  ) {
    $touch_suffix_array = $this->GetTouchSuffixArray();
    if (("" != $this->GetTouchFolder()) && (0 < count($touch_suffix_array)) && (!(('data' == mb_strtolower($type_fn,'UTF-8')) && ('stat' == mb_strtolower($item_fn,'UTF-8'))))) {
      if (('slavelog' == mb_strtolower($type_fn,'UTF-8')) && ('unique_id' == mb_strtolower($item_fn,'UTF-8'))) {
          $touch_suffix_array = array($id_fn);
      }
      if ($this->GetVerboseFlag()) {
        $this->WriteLog("Debug: *Touch element $type_fn $item_fn $id_fn", FALSE, FALSE, 8888, 'System', "");
      }
      foreach ($touch_suffix_array as $one_touch_suffix) {
        $filename_path = $this->GetTouchFolder().nullable_bin2hex($type_fn."\t".$item_fn."\t".$id_fn.(("" != $one_touch_suffix) ? ("\t".$one_touch_suffix) : "")).".sync";
        $result = touch($filename_path);
        if ($result && ("" != $this->GetLinuxFileMode())) {
          @chmod($filename_path, octdec($this->GetLinuxFileMode()));
        }
      }

      if ($folder_touched) {
        $this->FolderTouched($type_fn, $item_fn, $id_fn, $touch_info);
      }
    }
  }


  function FolderTouched(
      $type_fn = "",
      $item_fn = "",
      $id_fn = "",
      $touch_info = "") {
      // Stub for your own extension
  }


  function UserRestoreBeforeWrite()
  {
      // Stub for your own extension
  }


  function SetUserLanguage($value) {
    $this->_user_data['language'] = mb_strtolower(nullable_trim($value),'UTF-8');
  }


  function GetUserLanguage($ignore_main_language = FALSE) {
    $language = nullable_trim(isset($this->_user_data['language']) ? $this->_user_data['language'] : "");
    if (("" == $language) && (TRUE != $ignore_main_language)) {
      $language = $this->GetLanguage();
    }
    return mb_strtolower($language,'UTF-8');
  }


  function SetLanguage($value) {
    $this->_config_data['language'] = mb_strtolower(nullable_trim($value),'UTF-8');
  }


  function GetLanguage() {
    return mb_strtolower(nullable_trim($this->_config_data['language']),'UTF-8');
  }


  // Customized information (to be overcharged if needed)
  function GetCustomInfo() {
    return "";
  }


  function GetLibraryHash(
    $param1 = "",
    $param2 = "",
    $param3 = ""
  ) {
    if (file_exists(__FILE__)) {
      if ($me_handler = @fopen(__FILE__, "rt")) {
        flock($me_handler, LOCK_SH);
        $content = "";
        while (!feof($me_handler)) {
          $content.= fgets($me_handler);
        }
        fclose($me_handler);
        $hash = md5($content);
      }
    } else {
      $hash = '00000000000000000000000000000000';
    }

    $this->SendWeeklyAnonymousStat();

    return ($hash);
  }

  function UpgradeSchemaIfNeeded()
  {
    if ($this->GetActualVersion() != $this->GetVersion()) {
      if ($this->InitializeBackend() < 20) {
        $this->SetActualVersion($this->GetVersion());
        $this->WriteConfigData();
      }
    }
  }


  function GetErrorText(
    $error_number = 99
  ) {
    $text = "";
    if (isset($this->_errors_text[$error_number])) {
      $text = $this->_errors_text[$error_number];
    } elseif (intval($error_number) > 0) {
      $text = $this->_errors_text[99];
    }
    return $text;
  }


  function GetStatusCodesArray()
  {
    return $this->_errors_text;
  }


  // Reset the cache array
  function ResetCacheArray()
  {
    // First, we reset all values (we know the key based on the schema)
    foreach ($this->_sql_tables_schema['cache'] as $valid_key => $valid_format) {
      $pos = mb_strpos(mb_strtoupper($valid_format,'UTF-8'), 'DEFAULT');
      $value = "";
      if ($pos !== FALSE) {
        $value = nullable_trim(mb_substr($valid_format, $pos + mb_strlen("DEFAULT")));
        if (("'" == mb_substr($value,0,1)) && ("'" == mb_substr($value,-1))) {
          $value = mb_substr($value,1,-1);
        }
      }
      $this->_cache_data[$valid_key] = $value;
    }
  }

  
  /**
   * @brief   Write specific data in the backend (SQL table or file), generic method
   *
   * @param   array/string $item           New unique array, or Item family to be handled (Cache, Configuration, Token, etc.)
   * @param   string  $table               Name of the table if the backend is handling tables
   * @param   string  $folder              Name of the folder if the backend is handling folders
   * @param   string  $data_array          Data array of the item to be written
   * @param   boolean $force_file          File backend must also always be used
   * @param   string  $id_field            Index field of the item if the backend is handling tables
   * @param   string  $id_value            Value of the indexed item
   * @param   string  $id_case_sensitive   We want to be case sensitive for the backend storage
   * @param   boolean $automatically       The process is done automatically (for long content only)
   * @param   boolean $update_last_change  Update the last_update field (true by default)
   * @param   boolean $no_encryption_hash  No encryption hash, and no field are encrypted (false by default)
   *
   * New unique array parameter
   *   string  item                Item family to be handled (Cache, Configuration, Token, etc.) or new array
   *   string  table               Name of the table if the backend is handling tables
   *   string  folder              Name of the folder if the backend is handling folders
   *   string  data_array          Data array of the item to be written
   *   boolean force_file          File backend must also always be used
   *   string  id_field            Index field of the item if the backend is handling tables
   *   string  id_value            Value of the indexed item
   *   string  id_case_sensitive   We want to be case sensitive for the backend storage
   *   boolean automatically       The process is done automatically (for long content only)
   *   string  automatic_host      The process is automatically initiated from this host
   *   boolean sync_process        The process is automatically initiated for a synchronization
   *   boolean update_last_change  Update the last_update field (true by default)
   *   boolean no_encryption_hash  No encryption hash, and no field are encrypted (false by default)
   *   boolean encrypt_all         Encrypt all lines (if used with no_encryption_hash, no encryption hash are generated)
   *   string  encryption_key      Specific encryption key (if not defined, the default one is used)
   *   string  backup_file         Name of the backup file (if defined, everything is written in a backup file)
   *   string  raw_folder          Folder of a raw file to be backed up in configuration file
   *   string  raw_file            Raw file to be backed up
   *   boolean return_content      Return the content as the result 
   *   boolean flush_attributes    Table of attributes to flush before writing
   *
   * @retval  boolean              Result of the operation
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.3.4.3
   * @date    2016-06-11
   * @since   2014-12-30
   */
  function WriteData(
      $write_data_array         = "",
      $table_param              = "",
      $folder_param             = "",
      $data_array_param         = [],
      $force_file_param         = false,
      $id_field_param           = "",
      $id_value_param           = "",
      $id_case_sensitive_param  = false,
      $automatically_param      = false,
      $update_last_change_param = true,
      $no_encryption_hash_param = false
  ) {
      if (is_array($write_data_array)) {
        $item               = isset($write_data_array['item'])?$write_data_array['item']:"";
        $table              = isset($write_data_array['table'])?$write_data_array['table']:"";
        $folder             = isset($write_data_array['folder'])?$write_data_array['folder']:"";
        $data_array         = isset($write_data_array['data_array'])?$write_data_array['data_array']:[];
        $force_file         = isset($write_data_array['force_file'])?$write_data_array['force_file']:false;
        $id_field           = isset($write_data_array['id_field'])?$write_data_array['id_field']:"";
        $id_value           = isset($write_data_array['id_value'])?$write_data_array['id_value']:"";
        $id_case_sensitive  = isset($write_data_array['id_case_sensitive'])?$write_data_array['id_case_sensitive']:false;
        $automatically      = isset($write_data_array['automatically'])?$write_data_array['automatically']:false;
        $automatic_host     = isset($write_data_array['automatic_host'])?$write_data_array['automatic_host']:"";
        $sync_process       = isset($write_data_array['sync_process'])?$write_data_array['sync_process']:false;
        $update_last_change = isset($write_data_array['update_last_change'])?$write_data_array['update_last_change']:true;
        $no_encryption_hash = isset($write_data_array['no_encryption_hash'])?$write_data_array['no_encryption_hash']:false;

        $encrypt_all        = isset($write_data_array['encrypt_all'])?$write_data_array['encrypt_all']:false;
        $encryption_key     = isset($write_data_array['encryption_key'])?$write_data_array['encryption_key']:"";
        $backup_file        = isset($write_data_array['backup_file'])?$write_data_array['backup_file']:"";
        $raw_folder         = isset($write_data_array['raw_folder'])?$write_data_array['raw_folder']:"";
        $raw_file           = isset($write_data_array['raw_file'])?$write_data_array['raw_file']:"";
        $return_content     = isset($write_data_array['return_content'])?$write_data_array['return_content']:false;
        $flush_attributes   = isset($write_data_array['flush_attributes'])?$write_data_array['flush_attributes']:[];
        $encode_file_id     = isset($write_data_array['encode_file_id'])?$write_data_array['encode_file_id']:false;

        $file_only          = isset($write_data_array['file_only'])?$write_data_array['file_only']:false;
        $copy_file          = isset($write_data_array['copy_file'])?$write_data_array['copy_file']:"";
      } else {
        // Backward compatibility
        $item               = $write_data_array;
        $table              = $table_param;
        $folder             = $folder_param;
        $data_array         = $data_array_param;
        $force_file         = $force_file_param;
        $id_field           = $id_field_param;
        $id_value           = $id_value_param;
        $id_case_sensitive  = $id_case_sensitive_param;
        $automatically      = $automatically_param;
        $update_last_change = $update_last_change_param;
        $no_encryption_hash = $no_encryption_hash_param;
        $encrypt_all        = false;
        $encryption_key     = "";
        $backup_file        = "";
        $raw_folder         = "";
        $raw_file           = "";
        $return_content     = false;
        $flush_attributes   = [];
        $sync_process       = false;
        $automatic_host     = "";
        $file_only          = false;
        $copy_file          = "";
      }
      
      $backup_format = ("" != $backup_file);
      if ($backup_format) {
        $force_file = true;
        // if ((false !== mb_strpos($backup_file,"/")) || (false !== mb_strpos($backup_file,"\\"))) {
          $folder = "";
        // }
      }

      foreach ($flush_attributes as $one_flush_attribute) {
        if (isset($data_array[$one_flush_attribute])) {
          $data_array[$one_flush_attribute] = "";
        }
      }

      if ("" != $raw_file) {
        if (!file_exists($raw_folder.$raw_file)) {
          if ("" == $raw_folder) {
            $raw_folder = $folder;
          }
        }
        if (!file_exists($raw_folder.$raw_file)) {
          return false;
        }
      }
      $clean_raw_folder = str_replace($this->GetConfigFolder(), "--config-@-folder--", $raw_folder);

      if (('*CLEAR*' == $encryption_key) || ('*UNENC*' == $encryption_key)) {
          $encryption_key = "";
      } elseif ("" == $encryption_key) {
          $encryption_key = $this->GetEncryptionKey();
      }

      $item_info = nullable_trim($item." ".$id_value);
      if ('group' == mb_strtolower($item,'UTF-8')) {
          $item_info = nullable_trim($item." ".(isset($data_array['name'])?$data_array['name']:$id_value));
      } elseif ('device' == mb_strtolower($item,'UTF-8')) {
          $item_info = nullable_trim($item." ".(isset($data_array['description'])?$data_array['description']:(isset($data_array['ip_or_fqdn'])?$data_array['ip_or_fqdn']:$id_value)));
      }

      if ('configuration' == mb_strtolower($item,'UTF-8')) {
          $filename = 'multiotp.ini';
          $force_file = true;
      } elseif ('stat' == mb_strtolower($item,'UTF-8')) {
          $filename = 'stat.ini';
      } elseif ('cache' == mb_strtolower($item,'UTF-8')) {
          $filename = 'cache.ini';
      } else {
          if ($encode_file_id) {
              $filename = $this->EncodeFileId($id_value, $this->IsCaseSensitiveUsers()).'.db';
          } else {
              $filename = $id_value.'.db';
              if (!$this->IsCaseSensitiveUsers()) {
                  $filename = mb_strtolower($filename,'UTF-8');
              }
          }
      }

      if ($backup_format) {
        $filename = $backup_file;
        $item_info = "backup process";
      }

      $now_epoch = time();
      if (0 >= (isset($data_array['create_time']) ? $data_array['create_time'] : 0)) {
          $data_array['create_time'] = $now_epoch;
      }

      if ("" == (isset($data_array['create_host']) ? $data_array['create_host'] : "")) {
          $data_array['create_host'] = $this->GetCreateHost();
      }
      
      if (($update_last_change) && (!$automatically)) {
          $data_array['last_update'] = $now_epoch;
          $data_array['last_update_host'] = $this->GetCreateHost();
      }

      if ($sync_process) {
          $data_array['last_sync_update'] = $now_epoch;
          $data_array['last_sync_update_host'] = $automatic_host;
      }

      $result = false;

      $item_created = FALSE;
      
      $data_array['encryption_hash'] = $this->CalculateControlHash($encryption_key);

      if($no_encryption_hash) {
        unset($data_array['encryption_hash']);
      }

      if ((($this->GetBackendTypeValidated()) &&
           ((isset($this->_config_data['sql_'.$table.'_table'])) && ("" != $this->_config_data['sql_'.$table.'_table']))
          ) || 
          ('files' == $this->GetBackendType()) ||
          $force_file ||
          $backup_format
         ) {
          if (('files' == $this->GetBackendType()) || $force_file || $backup_format) {
              $file_time = $now_epoch;
              if (!$id_case_sensitive) {
                  $filename = mb_strtolower($filename,'UTF-8');
              }
              $file_created = false;

              $legacy_filename = "";
              $legacy_folder = "";
              $creduser = "";
              if (("User" == $item) && ("" != $this->GetCurrentUserSid())) {
                $creduser = $this->GetCurrentUserSid();
                $legacy_filename = $filename;
                $legacy_folder = $folder;
                $filename = $creduser.'.db';
                $folder = $this->GetCredUsersFolder();
              }

              // We open a handler only if it's not a return content request
              if ('@' != mb_strtolower($filename,'UTF-8')) {
                if (!file_exists($folder.$filename)) {
                    $item_created = true;
                    $file_created = true;
                } elseif ((!$update_last_change) && (!$file_created)) {
                    $file_time = filemtime($folder.$filename);
                }

                if ($backup_format) {
                  if ($file_handler = @fopen($folder.$filename,"ab+")) {
                    flock($file_handler, LOCK_EX);
                  }
                } else {
                  if ($file_handler = @fopen($folder.$filename, "ct")) { // was wt, need ftruncate after flock now
                    flock($file_handler, LOCK_EX);
                    ftruncate($file_handler, 0);
                  }
                }
              } else {
                $file_handler = TRUE;
                $return_content = TRUE;
              }
              $line = "";

              if (FALSE === $file_handler) {
                  $this->WriteLog("Error: database file for ".$item_info." cannot be written", FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "", 3);
              } else {
                if ($backup_format) {
                  if ($file_created) {
                    $config_time = date("YmdHis");
                    $line.= "# CONFIGURATION /".$config_time."/".mb_substr(md5($config_time.$encryption_key.$config_time),0,14)."/\n";
                    $line.= "; #!#multiotp-database-format-v3\n";
                    $line.= "; #!#timestamp=".time()."\n";
                    $line.= "; #!#encryption_hash=".$this->CalculateControlHash($encryption_key)."\n";
                  }
                  $line.= "; #!#element-start#!#\n";
                  $line.= "; #!#element-timestamp=".time()."\n";
                  if (($backup_format) && ("" != $raw_file)) {
                    $line.= "; #!#type=file\n";
                    $line.= "; #!#item=$clean_raw_folder\n";
                    $line.= "; #!#id_value=$raw_file\n";
                  } else {
                    $line.= "; #!#type=data\n";
                    $line.= "; #!#item=$item\n";
                    $line.= "; #!#id_value=$id_value\n";
                  }
                } else {
                  $line.= "multiotp-database-format-v3\n";
                  if ('configuration' == mb_strtolower($item,'UTF-8')) {
                    $line.= "; If backend is set to something different than files,\n";
                    $line.= "; and backend_type_validated is set to 1,\n";
                    $line.= "; only the specific information needed for the backend\n";
                    $line.= "; is used from this config file.\n";
                    $line.= "\n";
                  }
                }
                if (($backup_format) && ("" != $raw_file)) {
                  $key = "raw_data";
                  if ($raw_fn = @fopen($raw_folder.$raw_file, "rb")) {
                    flock($raw_fn, LOCK_SH);
                    while(!feof($raw_fn))
                    {
                      $line.= mb_strtolower($key,'UTF-8');
                      $value = nullable_bin2hex(fread($raw_fn, 40));
                      if ($encrypt_all ||
                          ((!$no_encryption_hash) &&
                           ((FALSE !== mb_strpos(mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'), mb_strtolower('*'.$key.'*','UTF-8'))) || ("*all*" == mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8')))
                          )
                         ) {
                        $value = $this->Encrypt($key,$value,$encryption_key);
                        $line.= ":";
                      }
                      $line.= "=".$value."\n";
                    }
                    fclose($raw_fn);
                  }
                } else {
                  foreach ($data_array as $key => $value) {
                    $value = str_replace(chr(13).chr(10),"<<CRLF>>",$value);
                    $value = str_replace(chr(10),"<<CRLF>>",$value);
                    $value = str_replace(chr(13),"<<CRLF>>",$value);
                    if ("" != nullable_trim($key)) {
                      $line.= mb_strtolower($key,'UTF-8');
                      if ($encrypt_all ||
                          ((!$no_encryption_hash) &&
                           ((FALSE !== mb_strpos(mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'), mb_strtolower('*'.$key.'*','UTF-8'))) || ("*all*" == mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8')))
                          )
                         ) {
                        $value = $this->Encrypt($key,$value,$encryption_key);
                        $line.= ":";
                      }
                      $line.= "=".$value."\n";
                    }
                  }
                } // (not a raw file)
                if ($backup_format) {
                  $line.= "; #!#element-stop#!#\n";
                  $line.= "; ##############################\n";
                }
                if ('@' != mb_strtolower($filename,'UTF-8')) {
                  // foreach (explode("\n", $line) as $one_line) {
                  //   fwrite($file_handler, encode_utf8_if_needed($one_line)."\n");
                  // }
                  fwrite($file_handler, $line);
                  fclose($file_handler);
                  if ("" != $copy_file) {
                      if (@copy($folder.$filename, $copy_file)) {
                          @chmod($copy_file, octdec($this->GetLinuxFileMode()));
                      }
                  }
                }
                $result = $return_content ? $line : true;
                if ((!$update_last_change) && (!$file_created) && (!$backup_format)) {
                  touch($folder.$filename, $file_time);
                }
                if ($file_created && ("" != $this->GetLinuxFileMode())) {
                  @chmod($folder.$filename, octdec($this->GetLinuxFileMode()));
                }
                
                if ("" != $creduser) {
                  protect_file($folder.$filename, $creduser);
                  if (!copy($folder.$filename, $legacy_folder.$legacy_filename)) {
                    if ($this->GetVerboseFlag()) {
                      $this->WriteLog(array('text'       => "Warning: *SID file $folder.$filename not copied successfully to $legacy_folder.$legacy_filename",
                                            'error_code' => 8888,
                                            'category'   => 'System',
                                            'user'       => '$id_value',
                                            'local_only' => 1)
                                     );
                    }
                  }
                }
              }
              if ($this->GetVerboseFlag()) {
                if ($file_created) {
                  $this->WriteLog(array('text'       => "Info: *File created: ".$folder.$filename,
                                        'error_code' => 8888,
                                        'category'   => 'System',
                                        'user'       => "",
                                        'local_only' => 1)
                                 );
                }
              }
          }
          if ((!$backup_format) && ('mysql' == $this->GetBackendType()) && ($this->GetBackendTypeValidated()) && (!$file_only)) {
              $esc_id_value = escape_mysql_string($id_value);
              if ($this->OpenMysqlDatabase()) {
                  $result = TRUE;
                  $sQi_Columns = "";
                  $sQi_Values  = "";
                  $sQu_Data    = "";
                  foreach ($data_array as $key => $value) {
                      $value = str_replace(chr(13).chr(10),"<<CRLF>>",$value);
                      $value = str_replace(chr(10),"<<CRLF>>",$value);
                      $value = str_replace(chr(13),"<<CRLF>>",$value);
                      $in_the_schema = FALSE;
                      $row_type = "";
                      foreach ($this->_sql_tables_schema[$table] as $valid_key => $valid_format) {
                          $row_type = "";
                          if ((mb_strtolower(mb_substr($valid_format, 0, 4),'UTF-8') == "int(") || (mb_strtolower(mb_substr($valid_format, 0, 8),'UTF-8') == "numeric(")) {
                            $row_type = "int";
                          } elseif ((mb_strtolower(mb_substr($valid_format, 0, 8),'UTF-8') == "datetime") || (mb_strtolower(mb_substr($valid_format, 0, 9),'UTF-8') == "timestamp")) {
                            $row_type = "datetime";
                          }
                          if ($valid_key == $key) {
                              $in_the_schema = TRUE;
                              if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                $value = "";
                              }
                              break;
                          }
                      }
                      $not_in_the_schema = FALSE;
                      if (isset($this->_sql_tables_not_in_schema[$table])) {
                          foreach ($this->_sql_tables_not_in_schema[$table] as $ignore_key => $ignore_format) {
                              if ($ignore_key == $key) {
                                  $not_in_the_schema = TRUE;
                                  break;
                              }
                          }
                      }

                      if (($in_the_schema) && ($key != $id_field)) {
                          if (("" == nullable_trim($value)) && ("int" == $row_type)) {
                              $value = 0;
                          }
                          if (($encrypt_all ||
                               ((!$no_encryption_hash) &&
                                ((FALSE !== mb_strpos(mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'), mb_strtolower('*'.$key.'*','UTF-8'))) || ("*all*" == mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8')))
                               )
                              ) &&
                              ("" != $value)
                             ) {
                            $value = 'ENC:'.$this->Encrypt($key,$value,$encryption_key).':ENC';
                          }
                          $value = escape_mysql_string($value);
                          $sQu_Data    .= "`{$key}`='{$value}',"; // Data for UPDATE query
                          $sQi_Columns .= "`{$key}`,"; // Columns for INSERT query
                          $sQi_Values  .= "'{$value}',"; // Values for INSERT query
                      } elseif ((!$in_the_schema) && (!$not_in_the_schema) && ('unique_id' != $key)  && $this->GetVerboseFlag()) {
                          $this->WriteLog(array('text'       => "Warning: *The key ".$key." is not in the $table table schema",
                                                'error_code' => 8888,
                                                'category'   => 'System',
                                                'user'       => "",
                                                'local_only' => 1)
                                         );
                      }
                  }
                  $num_rows = 0;
                  $sQuery = "SELECT * FROM `".$this->_config_data['sql_'.$table.'_table']."`";
                  if ("" != $id_field) {
                      $sQuery.= " WHERE `$id_field`='".$esc_id_value."'";
                  }
                  
                  if (is_object($this->_mysqli)) {
                      if (!($result = @$this->_mysqli->query($sQuery))) {
                          $this->WriteLog(array('text'       => "Error: SQL query error ".nullable_trim($this->_mysqli->error)." ".$sQuery,
                                                'file_only'  => TRUE,
                                                'error_code' => _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_,
                                                'category'   => 'System',
                                                'user'       => "",
                                                'overwrite_severity' => 3)
                                         );
                      } else {
                          $num_rows = $result->num_rows;                                    
                      }
                  } elseif ((NULL != $this->_mysql_database_link) && (!($result = @mysql_query($sQuery, $this->_mysql_database_link)))) {
                      $this->WriteLog(array('text'       => "Error: SQL query error ($sQuery) : ".mysql_error(),
                                            'file_only'  => TRUE,
                                            'error_code' => _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_,
                                            'category'   => 'System',
                                            'user'       => "",
                                            'overwrite_severity' => 3)
                                     );
                  } else {
                      $num_rows = mysql_num_rows($result);
                  }

                  if ($num_rows > 0) {
                      $sQuery = "UPDATE `".$this->_config_data['sql_'.$table.'_table']."` SET ".mb_substr($sQu_Data,0,-1);
                      if ("" != $id_field) {
                          $sQuery.= " WHERE `$id_field`='".$esc_id_value."'";
                      }
                      if (is_object($this->_mysqli)) {
                        try {
                          $rResult = @$this->_mysqli->query($sQuery);
                          // Backward compatibility
                          if (!$rResult) {
                            $this->WriteLog(array('text'       => "Error: SQL query error ".nullable_trim($this->_mysqli->error)." ".$sQuery,
                                                  'file_only'  => TRUE,
                                                  'error_code' => _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_,
                                                  'category'   => 'System',
                                                  'user'       => "",
                                                  'overwrite_severity' => 3)
                                           );
                            $result = FALSE;
                          }
                        }
                        catch(Exception $e) {
                          $this->WriteLog(array('text'       => "Error: SQL query error ".nullable_trim($this->_mysqli->error)." ".$sQuery,
                                                'file_only'  => TRUE,
                                                'error_code' => _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_,
                                                'category'   => 'System',
                                                'user'       => "",
                                                'overwrite_severity' => 3)
                                         );
                          $result = FALSE;
                        }
                      } else {
                        try {
                          $rResult = @mysql_query($sQuery, $this->_mysql_database_link);
                          // Backward compatibility
                          if (!$rResult) {
                            $this->WriteLog(array('text'       => "Error: SQL query error ($sQuery) : ".mysql_error(),
                                                  'file_only'  => TRUE,
                                                  'error_code' => _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_,
                                                  'category'   => 'System',
                                                  'user'       => "",
                                                  'overwrite_severity' => 3)
                                           );
                            $result = FALSE;
                          }
                        }
                        catch(Exception $e) {
                          $this->WriteLog(array('text'       => "Error: SQL query error ($sQuery) : ".mysql_error(),
                                                'file_only'  => TRUE,
                                                'error_code' => _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_,
                                                'category'   => 'System',
                                                'user'       => "",
                                                'overwrite_severity' => 3)
                                         );
                          $result = FALSE;
                        }
                      }
                  } else {
                      if ("" != $id_field) {
                          $sQuery = "INSERT INTO `".$this->_config_data['sql_'.$table.'_table']."` (`$id_field`,".mb_substr($sQi_Columns,0,-1).") VALUES ('".$esc_id_value."',".mb_substr($sQi_Values,0,-1).")";
                      } else {
                          $sQuery = "INSERT INTO `".$this->_config_data['sql_'.$table.'_table']."` (".mb_substr($sQi_Columns,0,-1).") VALUES (".mb_substr($sQi_Values,0,-1).")";
                      }
                      if (is_object($this->_mysqli)) {
                          if (!($rResult = @$this->_mysqli->query($sQuery))) {
                              $this->WriteLog("Error: SQL query error ".nullable_trim($this->_mysqli->error)." ".$sQuery, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                          } elseif (0 == $this->_mysqli->affected_rows) {
                              $this->WriteLog("Error: SQL entry for ".$item_info." cannot be created or changed", FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_UPDATE_ERROR_, 'System', "", 3);
                              $result = FALSE;
                          } else {
                              $item_created = TRUE;
                          }
                      } elseif (!($rResult = @mysql_query($sQuery, $this->_mysql_database_link))) {
                          $this->WriteLog("Error: SQL query error ($sQuery) : ".mysql_error(), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                          $result = FALSE;
                      } elseif (0 == mysql_affected_rows($this->_mysql_database_link)) {
                          $this->WriteLog("Error: SQL entry for ".$item_info." cannot be created or changed", FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_UPDATE_ERROR_, 'System', "", 3);
                          $result = FALSE;
                      } else {
                          $item_created = TRUE;
                      }
                  }
              }
          } elseif ((!$backup_format) && ('pgsql' == $this->GetBackendType()) && ($this->GetBackendTypeValidated()) && (!$file_only)) {
              if ($this->OpenPGSQLDatabase()) {
                  $esc_id_value = pg_escape_string($this->_pgsql_database_link, $id_value);
                  $result = TRUE;
                  $sQi_Columns = "";
                  $sQi_Values  = "";
                  $sQu_Data    = "";
                  foreach ($data_array as $key => $value) {
                      $value = str_replace(chr(13).chr(10),"<<CRLF>>",$value);
                      $value = str_replace(chr(10),"<<CRLF>>",$value);
                      $value = str_replace(chr(13),"<<CRLF>>",$value);
                      $in_the_schema = FALSE;
                      $row_type = "";
                      foreach ($this->_sql_tables_schema[$table] as $valid_key => $valid_format) {
                          $row_type = "";
                          if ((mb_strtolower(mb_substr($valid_format, 0, 4),'UTF-8') == "int(") || (mb_strtolower(mb_substr($valid_format, 0, 8),'UTF-8') == "numeric(")) {
                            $row_type = "int";
                          } elseif ((mb_strtolower(mb_substr($valid_format, 0, 8),'UTF-8') == "datetime") || (mb_strtolower(mb_substr($valid_format, 0, 9),'UTF-8') == "timestamp")) {
                            $row_type = "datetime";
                          }
                          if ($valid_key == $key) {
                              $in_the_schema = TRUE;
                              if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                $value = "";
                              }
                              break;
                          }
                      }
                      $not_in_the_schema = FALSE;
                      if (isset($this->_sql_tables_not_in_schema[$table])) {
                          foreach ($this->_sql_tables_not_in_schema[$table] as $ignore_key => $ignore_format) {
                              if ($ignore_key == $key) {
                                  $not_in_the_schema = TRUE;
                                  break;
                              }
                          }
                      }

                      if (($in_the_schema) && ($key != $id_field)) {
                          if (("" == nullable_trim($value)) && ("int" == $row_type)) {
                              $value = 0;
                          }
                          if (($encrypt_all ||
                               ((!$no_encryption_hash) &&
                                ((FALSE !== mb_strpos(mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'), mb_strtolower('*'.$key.'*','UTF-8'))) || ("*all*" == mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8')))
                               )
                              ) &&
                              ("" != $value)
                             ) {
                            $value = 'ENC:'.$this->Encrypt($key,$value,$encryption_key).':ENC';
                          }
                          $value = pg_escape_string($this->_pgsql_database_link, $value);
                          $sQu_Data    .= "\"{$key}\" = '{$value}',"; // Data for UPDATE query
                          $sQi_Columns .= "\"{$key}\","; // Columns for INSERT query
                          $sQi_Values  .= "'{$value}',"; // Values for INSERT query
                      } elseif ((!$in_the_schema) && (!$not_in_the_schema) && ('unique_id' != $key)  && $this->GetVerboseFlag()) {
                          $this->WriteLog("Warning: *The key ".$key." is not in the $table table schema", FALSE, FALSE, 8888, 'System', "");
                      }
                  }
                  $num_rows = 0;
                  $sQuery = "SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_'.$table.'_table']."\"";
                  if ("" != $id_field) {
                      $sQuery.= " WHERE \"".$id_field."\" = '".$esc_id_value."'";
                  }
                  
                  if (!($result = @pg_query($this->_pgsql_database_link, $sQuery))) {
                      $this->WriteLog("Error: SQL query error ($sQuery) : ".pg_last_error($this->_pgsql_database_link), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                  } else {
                      $num_rows = pg_num_rows($result);
                  }

                  if ($num_rows > 0) {
                      $sQuery = "UPDATE \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_'.$table.'_table']."\" SET ".mb_substr($sQu_Data,0,-1);
                      if ("" != $id_field) {
                          $sQuery.= " WHERE \"".$id_field."\" = '".$esc_id_value."'";
                      }
                      if (!($rResult = @pg_query($this->_pgsql_database_link, $sQuery))) {
                          $this->WriteLog("Error: SQL query error ($sQuery) : ".pg_last_error($this->_pgsql_database_link), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                          $result = FALSE;
                      }
                  } else {
                      if ("" != $id_field) {
                          $sQuery = "INSERT INTO \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_'.$table.'_table']."\" (\"".$id_field."\",".mb_substr($sQi_Columns,0,-1).") VALUES ('".$esc_id_value."',".mb_substr($sQi_Values,0,-1).")";
                      } else {
                          $sQuery = "INSERT INTO \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_'.$table.'_table']."\" (".mb_substr($sQi_Columns,0,-1).") VALUES (".mb_substr($sQi_Values,0,-1).")";
                      }
                      if (!($rResult = @pg_query($this->_pgsql_database_link, $sQuery))) {
                          $this->WriteLog("Error: SQL query error ($sQuery) : ".pg_last_error($this->_pgsql_database_link), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                          $result = FALSE;
                      } elseif (0 == pg_affected_rows($rResult)) {
                          $this->WriteLog("Error: SQL entry for ".$item_info." cannot be created or changed", FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_UPDATE_ERROR_, 'System', "", 3);
                          $result = FALSE;
                      } else {
                          $item_created = TRUE;
                      }
                  }
              }
          }
      }
      if (!$backup_format) {
          if ($item_created && $result) {
              if ($automatically) {
                  $this->WriteLog(array('text'       => "Info: ".$item_info." automatically created",
                                        'error_code' => _MULTIOTP_OPERATION_DONE_,
                                        'category'   => 'System',
                                        'user'       => "",
                                        'local_only' => 1)
                                 );
              }
              else {
                  $this->WriteLog(array('text'       => "Info: ".$item_info." manually created",
                                        'error_code' => _MULTIOTP_OPERATION_DONE_,
                                        'category'   => 'System',
                                        'user'       => "",
                                        'local_only' => 1)
                                 );
              }
          }
      }

      if ((!$backup_format) && ($update_last_change) && ('cache' != mb_strtolower($item,'UTF-8'))) {
        $this->TouchFolder(("" != $raw_file) ? 'file' : 'data',
                           ("" != $raw_file) ? $clean_raw_folder : $item,
                           ("" != $raw_file) ? $raw_file : $id_value,
                           TRUE,
                           "WriteData");
      }

      return $result;
  }


  function ReadCacheValue(
      $key
  ) {
      return ((isset($this->_cache_data[$key]))?$this->_cache_data[$key]:"");
  }


  function WriteCacheValue(
      $key,
      $value
  ) {
      $this->_cache_data[$key] = $value;
  }


  function ReadCacheData()
  {
      $this->ResetCacheArray();
      $result = false;
      
      // First, we read the cache file if the backend is files or when migration is enabled
      if (('files' == $this->GetBackendType()) || ($this->GetMigrationFromFile())) {
          $cache_filename = 'cache.ini'; // File exists in v3 format only, we don't need any conversion
          if (file_exists($this->GetCacheFolder().$cache_filename)) {
              if ($file_handler = @fopen($this->GetCacheFolder().$cache_filename, "rt")) {
                  flock($file_handler, LOCK_SH);
                  $first_line = nullable_trim(fgets($file_handler));
                  
                  while (!feof($file_handler)) {
                      $line = str_replace(chr(10), "", str_replace(chr(13), "", fgets($file_handler)));
                      $line_array = explode("=",$line,2);
                      if (('#' != mb_substr($line, 0, 1)) && (';' != mb_substr($line, 0, 1)) && ("" != nullable_trim($line)) && (isset($line_array[1]))) {
                          if ("" != $line_array[0]) {
                              $this->_cache_data[mb_strtolower($line_array[0],'UTF-8')] = $line_array[1];
                          }
                      }
                  }
                  fclose($file_handler);
                  $result = TRUE;
              }
          }
      }
      
      // And now, we override the values if another backend type is defined
      if ($this->GetBackendTypeValidated()) {
          switch ($this->GetBackendType()) {
              case 'mysql':
                  if ($this->OpenMysqlDatabase()) {
                      if ("" != $this->_config_data['sql_cache_table']) {
                          $sQuery  = "SELECT * FROM `".$this->_config_data['sql_cache_table']."` ";
                          
                          $aRow = NULL;

                          if (is_object($this->_mysqli)) {
                              if (!($result = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: ".nullable_trim($this->_mysqli->error)." ".$sQuery, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  $aRow = $result->fetch_assoc();
                              }
                          } else {
                              if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                                  $this->WriteLog("Error: ".mysql_error()." ".$sQuery, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  $aRow = mysql_fetch_assoc($rResult);
                              }
                          }

                          if (NULL != $aRow) {
                              $result = TRUE;
                              foreach ($aRow as $key => $value) {
                                  $in_the_schema = FALSE;
                                  foreach ($this->_sql_tables_schema['cache'] as $valid_key => $valid_format) {
                                      if ($valid_key == $key) {
                                          $in_the_schema = TRUE;
                                          if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                            $value = "";
                                          }
                                          break;
                                      }
                                  }
                                  if ($in_the_schema) {
                                      $this->_cache_data[$key] = $value;
                                  } elseif (('unique_id' != $key) && $this->GetVerboseFlag()) {
                                      $this->WriteLog("Warning: *the key ".$key." is not in the cache database schema", FALSE, FALSE, 8888, 'System', "");
                                  }
                              }
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      if ("" != $this->_config_data['sql_cache_table']) {
                          $sQuery  = "SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_cache_table']."\" ";
                          
                          $aRow = NULL;

                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: ".pg_last_error($this->_pgsql_database_link)." ".$sQuery, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              $result = FALSE;
                          } else {
                              $aRow = pg_fetch_assoc($rResult);
                          }

                          if (NULL != $aRow) {
                              $result = TRUE;
                              foreach ($aRow as $key => $value) {
                                  $in_the_schema = FALSE;
                                  foreach ($this->_sql_tables_schema['cache'] as $valid_key => $valid_format) {
                                      if ($valid_key == $key) {
                                          $in_the_schema = TRUE;
                                          if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                            $value = "";
                                          }
                                          break;
                                      }
                                  }
                                  if ($in_the_schema) {
                                      $this->_cache_data[$key] = $value;
                                  } elseif (('unique_id' != $key) && $this->GetVerboseFlag()) {
                                      $this->WriteLog("Warning: *the key ".$key." is not in the cache database schema", FALSE, FALSE, 8888, 'System', "");
                                  }
                              }
                          }
                      }
                  }
                  break;
              default:
              // Nothing to do if the backend type is unknown
                  break;
          }
      }
      
      // If cache is too old (more than one day), we reset the cache and we save it
      if (((24*60*60) + intval($this->_cache_data['last_update'])) < time()) {
          $this->ResetCacheArray();
          $this->WriteCacheData();
      }
      return $result;
  }


  function WriteCacheData(
      $write_cache_data_array = []
  ) {
      $result = $this->WriteData(array_merge(array('item'               => 'Cache',
                                                   'table'              => 'cache',
                                                   'folder'             => $this->GetCacheFolder(),
                                                   'data_array'         => $this->_cache_data
                                                  ), $write_cache_data_array));
      return $result;
  }


  function ReadBootCacheValue(
    $key = "",
    $timeout = _MULTIOTP_DEFAULT_BOOT_CACHE_DELAY_
  ) {
    $value = null;
    if (!empty($key)) {
      $tmpfs = '/dev/shm';
      if (!file_exists($tmpfs)) {
        // Probably not under Linux
        $tmpfs = $this->ConvertToWindowsPathIfNeeded($this->GetScriptFolder()."../_temp");
        $boot_cache_file = "$tmpfs/multiotp_cache_$key";
        if (file_exists($boot_cache_file)) {
          if (filemtime($boot_cache_file) < (time() - $timeout)) {
            unlink($boot_cache_file);
          } else {
            if ($file_handler = @fopen($boot_cache_file, "rt")) {
              flock($file_handler, LOCK_SH);
              $value = nullable_trim(fgets($file_handler));
              fclose($file_handler);
            }
          }
        }
      }
    }
    return $value;
  }


  function WriteBootCacheValue(
    $key = "",
    $value = ""
  ) {
    $result = false;
    if (!empty($key)) {
      $tmpfs = '/dev/shm';
      if (!file_exists($tmpfs)) {
        // Probably not under Linux
        $tmpfs = $this->ConvertToWindowsPathIfNeeded($this->GetScriptFolder()."../_temp");
      }
      if (file_exists($tmpfs)) {
        $boot_cache_file = "$tmpfs/multiotp_cache_$key";
        if ($file_handler = @fopen($boot_cache_file, "ct")) {
          flock($file_handler, LOCK_EX);
          ftruncate($file_handler, 0);
          fwrite($file_handler,$value);
          fclose($file_handler);
          $result = true;
          if ('' != $this->GetLinuxFileMode()) {
            @chmod($boot_cache_file, octdec($this->GetLinuxFileMode()));
          }
        }
      }
    }
    return $result;
  }


  // Reset the config array
  function ResetConfigArray($array_to_reset = "")
  {
      if (!is_array($array_to_reset)) {
        $array_to_reset = $this->_sql_tables_schema['config'];
      }
      // First, we reset all values (we know the key based on the schema)
      foreach ($array_to_reset as $valid_key => $valid_format) {
          $pos = mb_strpos(mb_strtoupper($valid_format,'UTF-8'), 'DEFAULT');
          $value = "";
          if ($pos !== FALSE) {
              $value = nullable_trim(mb_substr($valid_format, $pos + mb_strlen("DEFAULT")));
              if (("'" == mb_substr($value,0,1)) && ("'" == mb_substr($value,-1))) {
                  $value = mb_substr($value,1,-1);
              }
          }
          $this->_config_data[$valid_key] = $value;
      }
  }


  // Reset the stat array
  function ResetStatArray($array_to_reset = "")
  {
      if (!is_array($array_to_reset)) {
        $array_to_reset = $this->_sql_tables_schema['stat'];
      }
    // First, we reset all values (we know the key based on the schema)
    foreach ($array_to_reset as $valid_key => $valid_format) {
      $pos = mb_strpos(mb_strtoupper($valid_format,'UTF-8'), 'DEFAULT');
      $value = "";
      if ($pos !== FALSE) {
        $value = nullable_trim(mb_substr($valid_format, $pos + mb_strlen("DEFAULT")));
        if (("'" == mb_substr($value,0,1)) && ("'" == mb_substr($value,-1))) {
          $value = mb_substr($value,1,-1);
        }
      }
      $this->_stat_data[$valid_key] = $value;
    }
  }

  
  function SetAnonymousStat(
      $value
  ) {
      $this->_config_data['anonymous_stat'] = ((intval($value) > 0)?1:0);
  }


  function EnableAnonymousStat()
  {
      $this->_config_data['anonymous_stat'] = 1;
  }


  function DisableAnonymousStat()
  {
      $this->_config_data['anonymous_stat'] = 0;
  }


  function IsAnonymousStat()
  {
      return (1 == ($this->_config_data['anonymous_stat']));
  }


  function SetLdapExpiredPasswordValid(
      $value
  ) {
      $this->_config_data['ldap_expired_password_valid'] = ((intval($value) > 0)?1:0);
  }


  function EnableLdapExpiredPasswordValid()
  {
      $this->_config_data['ldap_expired_password_valid'] = 1;
  }


  function DisableLdapExpiredPasswordValid()
  {
      $this->_config_data['ldap_expired_password_valid'] = 0;
  }


  function IsLdapExpiredPasswordValid()
  {
      return (1 == ($this->_config_data['ldap_expired_password_valid']));
  }
  
  
  function IsAnonymousStatTime()
  {
      return ($this->IsAnonymousStat() && (0 != $this->GetAnonymousStatInterval()) && (time() > ($this->GetAnonymousStatLastUpdate() + $this->GetAnonymousStatInterval())));
  }


  function GetAnonymousStatRandomId()
  {
      $result = nullable_trim($this->_config_data['anonymous_stat_random_id']);
      if ("" == $result) {
          $result = mb_substr(md5(date("YmdHis").mt_rand(100000,999999)),0,20).mb_substr(sha1(mt_rand(100000,999999).date("YmdHis")),0,20);
          $this->_config_data['anonymous_stat_random_id'] = $result;
      }
      return ($result);
  }
  
  
  function GetAnonymousStatLastUpdate()
  {
      return intval($this->_config_data['anonymous_stat_last_update']);
  }


  function UpdateAnonymousStatLastUpdate()
  {
      $this->_config_data['anonymous_stat_last_update'] = time();
      $this->WriteConfigData(array("force_write_needed" => TRUE));
  }


  function GetAnonymousStatInterval()
  {
    return ($this->_anonymous_stat_interval);
  }


  function SetAnonymousStatTimeout(
      $value
  ) {
      $this->_config_data['anonymous_stat_timeout'] = intval($value);
  }


  function GetAnonymousStatTimeout() {
    $timeout = intval(isset($this->_config_data['anonymous_stat_timeout']) ? $this->_config_data['anonymous_stat_timeout'] : 5);
    if (0 == $timeout) {
      $timeout = 5;
    }
    return $timeout;
  }


  function SendWeeklyAnonymousStat()
  {
    if ($this->IsAnonymousStatTime()) {
      $this->UpdateAnonymousStatLastUpdate();
      $this->WriteLog("Info: Send anonymous weekly stats (stats can be disabled)", FALSE, FALSE, 19, 'System', "");
      $result_stats = FALSE;
      $stats_array = [];
      $stats_array['id'] = sha1($this->GetAnonymousStatRandomId().$this->GetClassName());
      $stats_array['backend_type'] = $this->GetBackendType();
      $stats_array['class_name'] = $this->GetClassName();
      $stats_array['ldap_cn_identifier'] = $this->GetLdapCnIdentifier();
      $stats_array['ldap_enabled'] = ("" != $this->GetLdapDomainControllers());
      $stats_array['ldap_sync_user_attribute'] = $this->GetLdapSyncedUserAttribute();
      $stats_array['os'] = php_uname();
      if (file_exists("/usr/local/bin/docker.flag")) {
        $stats_array['os'].= " [DOCKER]";
      } elseif ($this->GetCliProxyMode()) {
        $stats_array['os'].= " [CLI PROXY]";
      } elseif ($this->GetCliMode()) {
        $stats_array['os'].= " [CLI]";
      }
      if ($this->GetCredentialProviderMode()) {
        $stats_array['os'].= " [CP]";
      }
      $stats_array['php'] = phpversion();
      $stats_array['tokens'] = intval($this->GetTokensCount());
      $stats_array['users'] = intval($this->GetUsersCount());
      $stats_array['version_date'] = $this->GetVersionDate();
      $stats_public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtfZPCfqhemvRagng961LMbBHKVb2B3uSUsWlkcOqu5EuXfRIyIqurJR7vigJ0GMu4zMvrBilQQYegCjXtceo03mGSthAJ+6rTZ9Qvlu7GY0CUgUCranZ8Ckw8EXdEiUNTdgK1pKm6kef+wK4wc3V/sU+XYo8gbMbH9C5YsG/XUon4hPx+FSuNNU1IX/GhTcHo7Tmc5+kZZw4ImCGAsrXO/N4qYcn9Y11HceKiRyglAdRoBhM/pbhzl1rgSVxnfUu6R0NBDWRVW8l3NMkp1He8ugzP5dca2cdBYdIgslNKQwzGccWsxDEAkK1Q6htjmQ85g+qv2hiShEpOI/EiWw3uwIDAQAB";
      $rsa = new Crypt_RSA();
      $rsa->loadKey($stats_public_key);
      $rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_OAEP);
      $encoded_stats_value = urlencode(base64_encode($rsa->encrypt(json_encode($stats_array))));
      $result_stats = $this->PostHttpDataXmlRequest($encoded_stats_value, "http://stats.multiotp.net/", $this->GetAnonymousStatTimeout());
      // if (FALSE !== mb_strpos($result_stats, "OK")) {
      // We have to upgrade the anonymous last update even if the answer is not correct, because we could be offline

      if ((FALSE !== mb_strpos($result_stats, "<infoweb>")) && (FALSE !== mb_strpos($result_stats, "</infoweb>"))) {
          $infoweb_start = mb_strpos($result_stats, "<infoweb>") + mb_strlen("<infoweb>");
          $infoweb_stop = mb_strpos($result_stats, "</infoweb>");
          if ($infoweb_stop > $infoweb_start) {
              $infoweb = mb_substr($result_stats, $infoweb_start, ($infoweb_stop - $infoweb_start));
              $infoweb_filename = "infoweb.html";
              if ($infoweb_handler = @fopen($multiotp->GetConfigFolder().$infoweb_filename, "ct")) { // was wt, need ftruncate after flock now
                  flock($infoweb_handler, LOCK_EX);
                  ftruncate($infoweb_handler, 0);
                  fwrite($infoweb_handler, $infoweb);
                  fclose($infoweb_handler);
                  if ("" != $this->GetLinuxFileMode()) {
                      @chmod($multiotp->GetConfigFolder().$infoweb_filename, octdec($this->GetLinuxFileMode()));
                  }
              }
          }
      }

      /*
      if ($this->GetVerboseFlag()) {
        $stats_info = "";
        foreach ($stats_array as $stats_key => $stats_value) {
          $stats_info.= (("" != $stats_info) ? "; " : "") . "$stats_key=$stats_value";
        }
        $this->WriteLog("Debug: *Stats info: $stats_info", FALSE, FALSE, 8888, 'System', "");
      }
      */

    }    
  }


  /**
   * @brief   Backup the whole configuration in a unified format
   *            The regular functions are used with specific parameters:
   *            - WriteConfigData
   *            - WriteDeviceData
   *            - WriteGroupData
   *            - WriteTokenData
   *            - WriteUserData
   *
   * @param   array $bc_array string  'backup_file'    : name of the backup file to create
   *                          boolean 'return_content' : return content instead of saving in the file
   *                          string  'encryption_key' : encryption key
   *                          boolean 'encrypt_all'    : encrypt all attributes
   * @retval  string Content of the file to backup
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.0.2.6
   * @date    2016-10-25
   * @since   2016-05-03
   */
  function BackupConfiguration(
    $bc_array = []
  ) {
    @set_time_limit(0); // It can take a lot of time...
    clearstatcache();
    $bc_array['backup_file']       = isset($bc_array['backup_file'])       ? $bc_array['backup_file']:"@";
    $bc_array['encryption_key']    = isset($bc_array['encryption_key'])    ? $bc_array['encryption_key']:"";
    $bc_array['return_content']    = isset($bc_array['return_content'])    ? $bc_array['return_content']:FALSE;
    $bc_array['encrypt_all']       = isset($bc_array['encrypt_all'])       ? $bc_array['encrypt_all']:TRUE;
    // $bc_array['ignore_attributes'] = isset($bc_array['ignore_attributes']) ? $bc_array['ignore_attributes']:[];
    $bc_array['config_only']       = isset($bc_array['config_only'])       ? $bc_array['config_only']:FALSE;

    // Do not create an encryption hash entry in the items of the backup
    $bc_array['no_encryption_hash'] = TRUE;

    // Do not update the last change date in the items of the backup
    $bc_array['update_last_change'] = FALSE;
    if (("" == $bc_array['backup_file']) || ('@' == $bc_array['backup_file'])) {
      $bc_array['backup_file'] = "@";
      $bc_array['return_content'] = TRUE;
    } else {
      if (file_exists($bc_array['backup_file'])) {
        unlink($bc_array['backup_file']);
      }
    }

    $backup_time = time();
    $backup_content = "";
    $result = TRUE;

    // Configuration
    $config_bc_array = $bc_array;
    $config_bc_array["return_content"] = TRUE; // Force returning the result
    $content = $this->WriteConfigData($config_bc_array);
    $result = $result && ($content !== FALSE);
    $backup_content.= (is_bool($content)?"":$content);

    if (!$bc_array['config_only']) {
      // Ddns
      foreach (explode("\t", $this->GetDdnsList()) as $one_ddns) {
        if ("" != nullable_trim($one_ddns)) {
          $ddns_array = $this->ReadDdnsData($one_ddns);
          if ($ddns_array["exists"]) {
            $content = $this->WriteDdnsData($ddns_array, $one_ddns, $bc_array);
            $result = $result && ($content !== FALSE);
            $backup_content.= (is_bool($content)?"":$content);
          }
        }
      }
      // Devices
      foreach (explode("\t", $this->GetDevicesList()) as $one_device) {
        if ("" != nullable_trim($one_device)) {
          if ($this->ReadDeviceData($one_device)) {
            $content = $this->WriteDeviceData(array_merge($bc_array,
                                                          array("with_radius_update" => FALSE)));
            $result = $result && ($content !== FALSE);
            $backup_content.= (is_bool($content)?"":$content);
          }
        }
      }
      // Groups
      foreach (explode("\t", $this->GetGroupsList()) as $one_group) {
        if ("" != nullable_trim($one_group)) {
          if ($this->ReadGroupData($one_group)) {
            $content = $this->WriteGroupData($bc_array);
            $result = $result && ($content !== FALSE);
            $backup_content.= (is_bool($content)?"":$content);
          }
        }
      }
      // Tokens
      foreach (explode("\t", $this->GetTokensList()) as $one_token) {
        if ("" != nullable_trim($one_token)) {
          if ($this->ReadTokenData($one_token)) {
            $content = $this->WriteTokenData($bc_array);
            $result = $result && ($content !== FALSE);
            $backup_content.= (is_bool($content)?"":$content);
          }
        }
      }
      // Users
      $user_array = $this->GetNextUserArray(TRUE);
      while (FALSE !== $user_array) {
        if (isset($user_array['user'])) {
          if ($this->ReadUserData($user_array['user'], FALSE, TRUE)) {
            $content = $this->WriteUserData($bc_array);
            $result = $result && ($content !== FALSE);
            $backup_content.= (is_bool($content)?"":$content);
          }
        }
        $user_array = $this->GetNextUserArray();
      }
    } // if (!$bc_array['config_only']) {

    return ($bc_array['return_content']?($result?$backup_content:FALSE):$result);
  }


  /**
   * @brief   Restore the whole configuration from the unified format
   *
   * @param   array $rc_array string  'backup_file'        name of the backup file to read
   *                          string  'restore_key'        encryption key
   *                          array   'ignore_attributes'  array of string of ignored attributes (can be a part of the start or the end of the name)
   *                          array   'ignore_files'       array of string of ignored files
   *                          array   'rename_files'       array of a renaming array(original => "", renamed => "")
   *                          boolean 'update_config'      Update the config file instead of replacing it
   * @retval  string Content of the file to backup
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.0.2.6
   * @date    2016-10-25
   * @since   2016-05-03
   */
  function RestoreConfiguration(
    $rc_array = []
  ) {
    $backup_file       = isset($rc_array['backup_file'])       ? $rc_array['backup_file']              : "";
    $restore_key       = isset($rc_array['restore_key'])       ? $rc_array['restore_key']              : "";
    $ignore_attributes = isset($rc_array['ignore_attributes']) ? $rc_array['ignore_attributes']        : [];
    $ignore_files      = isset($rc_array['ignore_files'])      ? $rc_array['ignore_files']             : [];
    $rename_files      = isset($rc_array['rename_files'])      ? $rc_array['rename_files']             : [];
    $update_config     = isset($rc_array['update_config'])     ? (TRUE === $rc_array['update_config']) : FALSE;
    $ignore_config     = isset($rc_array['ignore_config'])     ? (TRUE === $rc_array['ignore_config']) : FALSE;
    $automatically     = isset($rc_array['automatically'])     ? (TRUE === $rc_array['automatically']) : FALSE;
    $sync_process      = isset($rc_array['sync_process'])      ? (TRUE === $rc_array['sync_process'])  : FALSE;

    if (!is_array($ignore_attributes)) {
      $ignore_attributes = array('multiotp-database-format', 'actual_version', 'anonymous_');
    } else {
      $ignore_attributes = array_merge($ignore_attributes,
                                       array('multiotp-database-format',
                                             'actual_version',
                                             'anonymous_'
                                            ));
    }
    if (!is_array($rename_files)) {
      $rename_files = [];
    }

    if (('*CLEAR*' == $restore_key) || ('*UNENC*' == $restore_key)) {
      $restore_key = "";
    } elseif ("" == $restore_key) {
      $restore_key = $this->GetEncryptionKey();
    }

    $type = "";
    $item = "";
    $id_value = "";
    $deleted = FALSE;
    $file_handler = FALSE;
    $data_array = [];
    $validity = TRUE;
    $result = TRUE;

    if (file_exists($backup_file)) {

      if ($backup_handler = @fopen($backup_file, "rt")) {
        flock($backup_handler, LOCK_SH);
        $first_line = TRUE;
        $line = str_replace(chr(10), "", str_replace(chr(13), "", fgets($backup_handler)));

        if (0 === mb_strpos($line, '# CONFIGURATION')) {
          $first_line = FALSE;
          $validity_array = explode("/", $line."///");
          $check_validity = mb_substr(md5($validity_array[1].$restore_key.$validity_array[1]),0,14);
          if ($validity_array[2] != $check_validity) {
            $this->WriteLog("Error: Bad restore configuration password", FALSE, FALSE, _MULTIOTP_BAD_RESTORE_PASSWORD_, 'System', "", 3);
            $result = FALSE;
          }
        }

        while ((!feof($backup_handler)) && $result) {
          if ($first_line) {
            $first_line = FALSE;
          } else {
            $line = str_replace(chr(10), "", str_replace(chr(13), "", fgets($backup_handler)));
          }
          if (0 === mb_strpos($line, '; #!#')) {
            // Headers and meta data
            $config_line = mb_substr($line, 5);
            $config_line_array = explode("=",$config_line,2);
            $config_command = isset($config_line_array[0]) ? $config_line_array[0] : "";
            $config_parameter = isset($config_line_array[1]) ? $config_line_array[1] : "";
            if (0 === mb_strpos($config_command, 'element-start')) {
              $type = "";
              $item = "";
              $id_value = "";
              $deleted = FALSE;
              $file_handler = FALSE;
              $data_array = [];
            } elseif (0 === mb_strpos($config_command, 'type')) {
              $type = $config_parameter;
            } elseif (0 === mb_strpos($config_command, 'item')) {
              $item = $config_parameter;
              $item = str_replace("--config-@-folder--", $this->GetConfigFolder(), $item);
            } elseif (0 === mb_strpos($config_command, 'id_value')) {
              $id_value = $config_parameter;
              switch ($type) {
                case 'data':
                  switch ($item) {
                    case 'Configuration':
                      if ($ignore_config) {
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *System configuration data ignored", FALSE, FALSE, 8888, 'System', "");
                        }
                      } else {
                        if ($update_config) {
                          if ($this->GetVerboseFlag()) {
                            $this->WriteLog("Info: *Configuration update_config", FALSE, FALSE, 8888, 'System', "");
                          }
                          $this->ReadConfigData();
                        } else {
                          if ($this->GetVerboseFlag()) {
                            $this->WriteLog("Info: *Reset (update) the configuration array", FALSE, FALSE, 8888, 'System', "");
                          }
                          $this->ReadConfigData(); // No reset, please, always update
                          // $this->ResetConfigArray();
                        }
                      }
                      break;
                    case 'Ddns':
                      $this->ResetDdnsArray();
                      $this->_ddns = mb_strtolower($id_value,'UTF-8');
                      $this->_ddns_data['id_value'] = $this->_ddns;
                      break;
                    case 'Device':
                      $this->ResetDeviceArray();
                      $this->_device = mb_strtolower($id_value,'UTF-8');
                      break;
                    case 'Group':
                      $this->ResetGroupArray();
                      $this->_group = mb_strtolower($id_value,'UTF-8');
                      break;
                    case 'Token':
                      $this->ResetTokenArray();
                      $this->_token = mb_strtolower($id_value,'UTF-8');
                      break;
                    case 'User':
                      $this->ResetUserArray();
                      $this->_user = str_replace("\\", "", ($this->IsCaseSensitiveUsers()) ? $id_value : mb_strtolower($id_value,'UTF-8'));
                      break;
                  }
                  break;

                case 'file':
                  $ignore = FALSE;
                  foreach ($ignore_files as $one_ignore_file) {
                    if ($one_ignore_file == $id_value) {
                      $ignore = TRUE;
                      break;
                    }
                  }
                  if ((!$ignore) && ("" != $id_value)) {
                    if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Info: *File $id_value to restore", FALSE, FALSE, 8888, 'System', "");
                    }
                    foreach ($rename_files as $one_file) {
                      if ($id_value == isset($one_file['original'])?$one_file['original']:"") {
                        if ("" != (isset($one_file['original'])?$one_file['original']:"")) {
                          $id_value = $one_file['renamed'];
                          break;
                        }
                      }
                    }
                    if (file_exists($item)) {
                      if ($file_handler = @fopen($item.$id_value, "cb")) { // was wb, need ftruncate after flock now
                        flock($file_handler, LOCK_EX);
                        ftruncate($file_handler, 0);
                      } else {
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *File ".$id_value." cannot be created", FALSE, FALSE, 8888, 'System', "");
                        }
                      }
                    } elseif ($this->GetVerboseFlag()) {
                      $this->WriteLog("Info: *File ".$id_value." not created, $item doesn't exist", FALSE, FALSE, 8888, 'System', "");
                    }
                  } elseif ($this->GetVerboseFlag()) {
                    $this->WriteLog("Info: *File ".$id_value." ignored", FALSE, FALSE, 8888, 'System', "");
                  }
                  break;
              }
            } elseif (0 === mb_strpos($config_command, 'deleted')) {
              $deleted = (1 == $config_parameter);
            } elseif (0 === mb_strpos($config_command, 'element-stop')) {
              switch ($type) {
                case 'data':
                  switch ($item) {
                    case 'Configuration':
                      if ($ignore_config) {
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *System configuration data ignored and not updated", FALSE, FALSE, 8888, 'System', "");
                        }
                      } else {
                        $this->WriteConfigData();
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *Configuration updated", FALSE, FALSE, 8888, 'System', "");
                        }
                      }
                      break;
                    case 'Ddns':
                      if (!$deleted) {
                        
                        $this->WriteDdnsData($this->_ddns_data, $this->_ddns_data['id_value'], array("automatically" => $automatically, "sync_process" => $sync_process));
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *DDns ".$id_value." updated", FALSE, FALSE, 8888, 'System', "");
                        }
                      } else {
                        $this->DeleteDdns($id_value, TRUE);
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *DDns ".$id_value." not deleted (not implemented)", FALSE, FALSE, 8888, 'System', "");
                        }
                      }
                      break;
                    case 'Device':
                      if (!$deleted) {
                        $this->WriteDeviceData(array("automatically" => $automatically, "sync_process" => $sync_process));
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *Device ".$id_value." updated", FALSE, FALSE, 8888, 'System', "");
                        }
                      } else {
                        $this->DeleteDevice($id_value, TRUE);
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *Device ".$id_value." deleted", FALSE, FALSE, 8888, 'System', "");
                        }
                      }
                      break;
                    case 'Group':
                      if (!$deleted) {
                        $this->WriteGroupData(array("automatically" => $automatically, "sync_process" => $sync_process));
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *Group ".$id_value." updated", FALSE, FALSE, 8888, 'System', "");
                        }
                      } else {
                        $this->DeleteGroup($id_value, TRUE);
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *Group ".$id_value." deleted", FALSE, FALSE, 8888, 'System', "");
                        }
                      }
                      break;
                    case 'Token':
                      if (!$deleted) {
                        $this->WriteTokenData(array("automatically" => $automatically, "sync_process" => $sync_process));
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *Token ".$id_value." updated", FALSE, FALSE, 8888, 'System', "");
                        }
                      } else {
                        $this->DeleteToken($id_value, TRUE);
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *Token ".$id_value." deleted", FALSE, FALSE, 8888, 'System', "");
                        }
                      }
                      break;
                    case 'User':
                      if (!$deleted) {
                        $this->UserRestoreBeforeWrite();
                        $this->WriteUserData(array("automatically" => $automatically, "sync_process" => $sync_process));
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *User ".$id_value." updated", FALSE, FALSE, 8888, 'System', "");
                        }
                      } else {
                        $this->DeleteUser($id_value, TRUE);
                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Info: *User ".$id_value." deleted", FALSE, FALSE, 8888, 'System', "");
                        }
                      }
                      break;
                  }
                  break;

                case 'file':
                  if (FALSE !== $file_handler) {
                    fclose($file_handler);
                    if ("" != $this->GetLinuxFileMode()) {
                      @chmod($item.$id_value, octdec($this->GetLinuxFileMode()));
                    }
                    if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Info: *File ".$id_value." closed", FALSE, FALSE, 8888, 'System', "");
                    }                      
                  }
                  if ($deleted) {
                    unlink($item.$id_value);
                    if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Info: *File ".$id_value." deleted", FALSE, FALSE, 8888, 'System', "");
                    }
                  } elseif ($this->GetVerboseFlag()) {
                    $this->WriteLog("Info: *File ".$id_value." updated", FALSE, FALSE, 8888, 'System', "");
                  }
                  break;
              }
              $type = "";
              $item = "";
              $id_value = "";
              $deleted = FALSE;
              $file_handler = FALSE;
              $data_array = [];
            }
          } else {
            // Content to restore
            $line_array = explode("=",$line,2);
            $key = isset($line_array[0]) ? $line_array[0] : "";
            $value = isset($line_array[1]) ? $line_array[1] : "";
            if (('#' != mb_substr($line, 0, 1)) && (';' != mb_substr($line, 0, 1)) && ("" != nullable_trim($line)) && (isset($line_array[1]))) {
              if (":" == mb_substr($key, -1)) {
                $key = mb_substr($key, 0, mb_strlen($key) -1);
                $value = $this->Decrypt($key,$value,$restore_key);
              }
              if ('raw_data' == $key) {
                $value = hex2bin($value);
              } else {
                $value = str_replace("<<CRLF>>",chr(10),$value);
              }

              foreach ($ignore_attributes as $one_ignore_attribute) {
                if ((0 === mb_strpos($key, $one_ignore_attribute)) || (mb_substr($key, -mb_strlen($one_ignore_attribute)) == $one_ignore_attribute)) {
                  $key = "";
                  break;
                }
              }
              
              if ("" != $key) {
                if ('file' == $type) {
                  if ($file_handler && (!$deleted)) {
                    fwrite($file_handler, $value);
                  }
                } elseif ('data' == $type) {
                  switch ($item) {
                    case 'Configuration':
                      if (!$ignore_config) {
                        $this->_config_data[$key] = $value;
                      }
                      break;
                    case 'Ddns':
                      $this->_ddns_data[$key] = $value;
                      break;
                    case 'Device':
                      $this->_device_data[$key] = $value;
                      break;
                    case 'Group':
                      $this->_group_data[$key] = $value;
                      break;
                    case 'Token':
                      $this->_token_data[$key] = $value;
                      break;
                    case 'User':
                      $this->_user_data[$key] = $value;
                      break;
                  }
                }
              }
            }
          }
        } // while
        fclose($backup_handler);
      } // if fopen
    } else { // if file_exists
      $result = false; // File doesn't exist
      if ($this->GetVerboseFlag()) {
        $this->WriteLog("Info: *backup File $backup_file doesn't exist", FALSE, FALSE, 8888, 'System', "");
      }
    }
    return $result;
  }


  function SetConsoleAuthentication(
    $value
  ) {
      $this->_config_data['console_authentication'] = ((intval($value) > 0) ? 1 : 0);
  }


  function EnableConsoleAuthentication()
  {
    $this->_config_data['console_authentication'] = 1;
  }


  function DisableConsoleAuthentication()
  {
      $this->_config_data['console_authentication'] = 0;
  }


  function IsConsoleAuthentication()
  {
      return (1 == ($this->_config_data['console_authentication']));
  }


  function SetLogFileName(
      $filename
  ) {
      $this->_log_file_name = nullable_trim($filename);
  }


  function GetLogFileName()
  {
      return $this->_log_file_name;
  }

  function SetLogHeaderWritten(
      $log_header_written
  ) {
      $this->_log_header_written = $log_header_written;
  }


  function GetLogHeaderWritten()
  {
      return $this->_log_header_written;
  }


  function SetLogFolder(
      $folder,
      $create = true
  ) {
      $new_folder = $this->ConvertToUnixPath($folder);
      if (mb_substr($new_folder,-1) != "/") {
          $new_folder.="/";
      }
      if ("/" == $new_folder) {
        $new_folder = "./";
      }
      $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
      $this->_log_folder = $new_folder;
      if ($create && (!file_exists($new_folder))) {
          if (!@mkdir(
                  $new_folder,
                  ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
                  true //recursive
          )) {
              $this->WriteLog("Error: Unable to create the missing config folder ".$new_folder, true, false, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "", 3);
          }
      }
  }


  function GetLogFolder()
  {
      if ("" == $this->_log_folder) {
          $this->SetLogFolder($this->GetScriptFolder()."log/");
      }
      return $this->ConvertToWindowsPathIfNeeded($this->_log_folder);
  }


  /**
   * @brief   Write information in the log file/database, to the syslog server and on the screen
   *
   * @param   string|array $text           Information to log, or full array information
   * 
   * Legacy parameters
   * @param   boolean $file_only           Define that the information must not be written in the database
   *                                        (in case of database error for example)
   * @param   boolean $hide_on_display     Define that the information must never be displayed
   * @param   int     $error_code          Error code (to define a matching severity, which can be overwritten if needed)
   * @param   string  $category            Define the category, will be "Authentication" if not defined
   * @param   string  $user                Define the user concerned, the default value will take $this->GetUser()
   * @param   int     $overwrite_severity  Define the severity (0-7), -1 will take the default severity, based on error number
   * @param   boolean $no_syslog_param     Don't send log to the syslog server
   * @param   boolean $mail_alert_param    Send log also per mail
   * 
   * Parameters in the array format:
   *   string text
   *   bool   file_only
   *   bool   hide_on_display
   *   int    error_code
   *   string category
   *   string user
   *   int    facility
   *   int    overwrite_severity
   *   bool   no_syslog
   *   bool   mail_alert
   *   int    local_only
   *   string source
   *   array  details: array with all details (key => data)
   * 
   * @retval  void
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.10.0.7
   * @date    2025-12-05
   * @since   2010-12-19
   */
  function WriteLog(
    $text_array = array('text' => ""),
    $file_only_param = false,
    $hide_on_display_param = false,
    $error_code_param = 9999,
    $category_param = '*DEFAULT*',
    $user_param = '*DEFAULT*',
    $overwrite_severity_param = -1,
    $no_syslog_param = false,
    $mail_alert_param = false
  ) {
    if (is_array($text_array)) {
      $log_array = $text_array;
    } else {
      $log_array = [
        "text" => $text_array,
        "file_only" => $file_only_param,
        "hide_on_display" => $hide_on_display_param,
        "error_code" => $error_code_param,
        "category" => $category_param,
        "user" => $user_param,
        "overwrite_severity" => $overwrite_severity_param,
        "no_syslog" => $no_syslog_param,
        "mail_alert" => $mail_alert_param,
      ];
    }
    $text = isset($log_array['text'])?$log_array['text']:"";
    $file_only = isset($log_array['file_only'])?(true === $log_array['file_only']):false;
    $hide_on_display = isset($log_array['hide_on_display'])?(true === $log_array['hide_on_display']):false;
    $error_code = isset($log_array['error_code'])?intval($log_array['error_code']):9999;
    $category = isset($log_array['category'])?$log_array['category']:'*DEFAULT*';
    $user = isset($log_array['user'])?$log_array['user']:'*DEFAULT*';
    $facility = isset($log_array['facility'])?$log_array['facility']:_MULTIOTP_DEFAULT_SYSLOG_FACILITY_;
    $overwrite_severity = isset($log_array['overwrite_severity'])?$log_array['overwrite_severity']:-1;
    $no_syslog = isset($log_array['no_syslog'])?(true === $log_array['no_syslog']):false;
    $mail_alert = isset($log_array['mail_alert'])?(true === $log_array['mail_alert']):false;
    $local_only = isset($log_array['local_only'])?intval($log_array['local_only']):0;
    $source = isset($log_array['source'])?$log_array['source']:'';
    $details = isset($log_array['details'])?$log_array['details']:[];

    $source_log = $source;

    $log_id = substr(bigdec2hex((time()-mktime(1,1,1,1,1,2000)).mt_rand(10000,99999)).'@'.$this->GetCreateHost(), 0, 100);

    $user_log = ('*DEFAULT*' != $user) ? $user : $this->GetUser();

    $category_log = ('*DEFAULT*' != $category) ? $category : "Authentication";

    // Severity level based on error code (if not overwritted)
    // >= 8888 debug     (7)
    //    1-19 info      (6)
    //       0 notice    (5)
    //   20-99 warning   (4)
    // 100-199 error     (3)
    // 200-299 critical  (2)
    // 300-399 alert     (1)
    // 400-499 emergency (0)
    if (0 == intval($error_code)) {
        $severity_level = 5;
    } elseif (intval($error_code) >= 8888) {
        $severity_level = 7;
    } elseif (intval($error_code) < 20) {
        $severity_level = 6;
    } elseif (intval($error_code) < 100) {
        $severity_level = 4;
    } elseif (intval($error_code) < 200) {
        $severity_level = 3;
    } elseif (intval($error_code) < 300) {
        $severity_level = 2;
    } elseif (intval($error_code) < 400) {
        $severity_level = 1;
    } elseif (intval($error_code) < 500) {
        $severity_level = 0;
    } else {
        $severity_level = 3;
    }
    
    if ((intval($overwrite_severity) >= 0) && (intval($overwrite_severity) <= 7)) {
        $severity_level = intval($overwrite_severity);
    }
    
    switch ($severity_level) {
        case 0:
            $severity_level_txt = 'emergency';
            break;
        case 1:
            $severity_level_txt = 'alert';
            break;
        case 2:
            $severity_level_txt = 'critical';
            break;
        case 3:
            $severity_level_txt = 'error';
            break;
        case 4:
            $severity_level_txt = 'warning';
            break;
        case 5:
            $severity_level_txt = 'notice';
            break;
        case 6:
            $severity_level_txt = 'info';
            break;
        case 7:
            $severity_level_txt = 'debug';
            break;
        default:
            $severity_level_txt = 'error';
    }

    /* TODO : enhanced log_text here
    if (is_array($details)) {
    } else {
    }
    */

    $post_text = "";
    $pre_text = "";
    if (!empty(nullable_trim($this->GetSourceIp().$this->GetSourceMac()))) {
      $post_text.= "from ";
      if (!empty($this->GetSourceIp())) {
        $post_text.= $this->GetSourceIp().' ';
        if (empty($source_log)) {
          $source_log = $this->GetSourceIp();
        }
      }
      if (!empty($this->GetSourceMac())) {
        $post_text.= '['.$this->GetSourceMac().'] ';
        if (empty($source_log)) {
          $source_log = $this->GetSourceMac();
        }
      }
    }
    if (!empty(($this->GetCallingIp().$this->GetCallingMac()))) {
      $post_text.= "for ";
      if (!empty($this->GetCallingIp())) {
        $post_text.= $this->GetCallingIp().' ';
        if (empty($source_log)) {
          $source_log = $this->GetCallingIp();
        }
      }
      if (!empty($this->GetCallingMac())) {
        $post_text.= '['.$this->GetCallingMac().'] ';
        if (empty($source_log)) {
          $source_log = $this->GetCallingMac();
        }
      }
    }
    if (empty($source_log) && (!empty($_SERVER['REMOTE_ADDR']))) {
      $source_log = $_SERVER['REMOTE_ADDR'];
    }
    $log_text = nullable_trim(nullable_trim($pre_text).' '.$text.' '.nullable_trim($post_text));

    // Cleaning the log text, just to be sure that we don't have tabs (\t)
    // in them, and also that the CRLF, CR or LF is the good done (\n)
    $log_text = str_replace(chr(13).chr(10), "<<CRLF>>", $log_text);
    $log_text = str_replace(chr(13), "<<CRLF>>", $log_text);
    $log_text = str_replace(chr(10), "<<CRLF>>", $log_text);
    $log_text = str_replace("<<CRLF>>", "\n", $log_text);
    $log_text = str_replace("\t", " ", $log_text);

    $log_time = time();
    $log_datetime = date("Y-m-d H:i:s", $log_time);
    
    // In the logfile, we don't want to have several lines for one entry,
    // therefore we are replacing the "\n" with "; " (or <br /> if we want to debug in HTML mode
    
    $logfile_content = $log_datetime."\t".$severity_level_txt."\t".$user_log."\t".$category_log."\t".str_replace("\n", $this->IsDebugViaHtml()?"<br />":"; ", $log_text)."\t".$local_only."\t".$this->GetCreateHost()."\t".$source_log;

    if (($this->GetDisplayLogFlag()) && (!$hide_on_display) && (!$this->GetNoDisplayLogFlag())) {
      $display_text = "\nLOG ".$log_datetime.' '.$severity_level_txt.' '.(("" == $user_log)?"":'(user '.$user_log.') ').$category_log.' '.$log_text."\n";
      if ($this->IsDebugViaHtml()) {
        $display_text = str_replace("\n","<br />\n", htmlentities($display_text));
      }
      echo $display_text;
    }

    if (("" != nullable_trim($this->GetSysLogServer())) && (!$this->IsSysLogServerBad()) && (!$no_syslog)) {
        if ($severity_level <= $this->GetSyslogMaxLevel()) {
            $syslog_server = $this->GetSysLogServer();
            if (!is_valid_ipv4($syslog_server)) {
                $syslog_server = gethostbyname($syslog_server);
            }
            if (is_valid_ipv4($syslog_server)) {
                $syslog_month     = date("M", $log_time);
                $syslog_day       = mb_substr("  ".date("j", $log_time), -2);
                $syslog_hhmmss    = date("H:i:s", $log_time);
                $syslog_timestamp = $syslog_month." ".$syslog_day." ".$syslog_hhmmss;

                $syslog_port = $this->GetSysLogPort();
                $syslog_timeout = $this->GetSysLogTimeout();
                $syslog_severity_facility = $severity_level + 8 * $this->GetSyslogFacility();
                $syslog_hostname = $this->GetSystemName();
                $syslog_process = 'multiOTP';
                $syslog_ip_from = $this->GetLocalIpAddress();
                $syslog_content = str_replace("\n", "; ", $log_text);
                $syslog_fqdn = $this->GetSystemName().(("" != $this->GetDomainName())?'.'.$this->GetDomainName():"");

                // Do an asynchronous SysLog if possible (Linux)
                $cli_command = "";
                if (file_exists('/bin/nc')) {
                  // https://nelsonslog.wordpress.com/2013/04/19/faking-out-remote-syslog-via-netcat/
                  $cli_command = "echo \"<$syslog_severity_facility>$syslog_timestamp $syslog_hostname $syslog_process: $syslog_fqdn $syslog_ip_from $syslog_content\" | /bin/nc $syslog_server -u $syslog_port -w $syslog_timeout > /dev/null 2>&1";
                  exec("nohup $cli_command &", $output);
                }

                // Otherwise, or in the verbose mode also, PHP SysLog class
                if (("" == $cli_command) || ($this->GetVerboseFlag())) {
                    $duplicated = (("" == $cli_command) ? "" : "(duplicated using native syslog library for debug) ");
                    $syslog = new MultiotpSyslog();
                    $syslog->SetTimeout($syslog_timeout);
                    $syslog->SetFacility($this->GetSyslogFacility());
                    $syslog->SetSeverity($severity_level);
                    $syslog->SetHostname($syslog_hostname);
                    $syslog->SetFqdn($syslog_fqdn);
                    $syslog->SetIpFrom($syslog_ip_from);
                    $syslog->SetProcess($syslog_process);
                    $syslog->SetContent($duplicated.$syslog_content);
                    $syslog->SetServer($syslog_server);
                    $syslog->SetPort($syslog_port);
                    $syslog_result = $syslog->Send();

                    if ('ERROR' == mb_substr($syslog_result, 0, 5)) {
                        $this->EnableBadSysLogServer();
                        if ($this->GetVerboseFlag()) {
                            $this->WriteLog("Warning: *Error with the Syslog server ".$this->GetSysLogServer().": $syslog_result", false, false, _MULTIOTP_ERROR_, 'System', "", -1, true);
                        } else {
                            $this->WriteLog("Warning: Error with the Syslog server ".$this->GetSysLogServer(), false, false, _MULTIOTP_ERROR_, 'System', "", -1, true);
                        }
                    }
                }
            } else {
                $this->EnableBadSysLogServer();
                $this->WriteLog("Warning: resolution name error for the Syslog server ".$this->GetSysLogServer(), false, false, _MULTIOTP_ERROR_, 'System', "", -1, true);
            }
        }
    }

    $log_link = NULL;
    if ($this->IsLogEnabled()) {
        if ((!$file_only) &&
          (('mysql' == $this->GetBackendType()) || ('pgsql' == $this->GetBackendType())) &&
          $this->GetBackendTypeValidated() && 
          ("" != $this->_config_data['sql_log_table']) &&
          (!$this->IsLogForcedInFile())
        ) {
            if ('mysql' == $this->GetBackendType()) {
                if ($this->OpenMysqlDatabase()) {
                    $log_severity_escaped = escape_mysql_string($severity_level_txt);
                    $log_user_escaped = escape_mysql_string($user_log);
                    $log_category_escaped = escape_mysql_string($category_log);
                    if (!$this->IsDeveloperMode()) {
                      $log_text_escaped = escape_mysql_string(mb_substr($log_text,0,255));
                    } else {
                      $log_text_escaped = escape_mysql_string($log_text);
                    }
                    $log_create_host = escape_mysql_string($this->GetCreateHost());
                    $log_source_escaped = escape_mysql_string($source_log);

                    $sQuery  = "INSERT INTO `".$this->_config_data['sql_log_table']."` ".
                    "(`datetime`,`severity`,`user`,`category`,`logentry`,`local_only`, `create_host`, `source`, `create_time`) VALUES ('".
                    $log_datetime."','".
                    $log_severity_escaped."','".
                    $log_user_escaped."','".
                    $log_category_escaped."','".
                    $log_text_escaped."',".
                    intval($local_only).",'".
                    $log_create_host."', '".
                    $log_source_escaped."', ".
                    $log_time.")";

                    if (is_object($this->_mysqli)) {
                        if (!($rResult = $this->_mysqli->query($sQuery))) {
                            $this->WriteLog("Error: SQL query error ($sQuery) : ".nullable_trim($this->_mysqli->error), true, false, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                        }
                    } elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                        $this->WriteLog("Error: SQL query error ($sQuery) : ".mysql_error(), true, false, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                    }
                }
                //mysql_close($log_link);
            } elseif ('pgsql' == $this->GetBackendType()) {
                if ($this->OpenPGSQLDatabase()) {
                    $log_severity_escaped = pg_escape_string($this->_pgsql_database_link, $severity_level_txt);
                    $log_user_escaped = pg_escape_string($this->_pgsql_database_link, $user_log);
                    $log_category_escaped = pg_escape_string($this->_pgsql_database_link, $category_log);
                    
                    if (!$this->IsDeveloperMode()) {
                      $log_text_escaped = pg_escape_string($this->_pgsql_database_link, mb_substr($log_text,0,255));
                    } else {
                      $log_text_escaped = pg_escape_string($this->_pgsql_database_link, $log_text);
                    }
                    $log_create_host = pg_escape_string($this->_pgsql_database_link, $this->GetCreateHost());
                    $log_source_escaped = pg_escape_string($this->_pgsql_database_link, $source_log);

                    $sQuery  = "INSERT INTO \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_log_table'].
                    "\" (\"datetime\",\"severity\",\"user\",\"category\",\"logentry\",\"local_only\",\"create_host\", \"source\", \"create_time\") VALUES ('".
                    $log_datetime."','".
                    $log_severity_escaped."','".
                    $log_user_escaped."','".
                    $log_category_escaped."','".
                    $log_text_escaped."',".
                    intval($local_only).",'".
                    $log_create_host."', '".
                    $log_source_escaped."', ".
                    $log_time.")";

                    if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                        $this->WriteLog("Error: SQL query error ($sQuery) : ".pg_last_error($this->_pgsql_database_link), true, false, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                    }
                }
                //pg_close($log_link);
            }
        } else {
          if (!file_exists($this->GetLogFolder())) {
            @mkdir(
              $this->GetLogFolder(),
              ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
              true //recursive
            );
          }
          $file_created = (!file_exists($this->GetLogFolder().$this->GetLogFileName()));
          if ($log_file_handle = @fopen($this->GetLogFolder().$this->GetLogFileName(),"ab+")) {
            flock($log_file_handle, LOCK_EX);
            if ($this->GetVerboseFlag()) {
              if (!$this->GetLogHeaderWritten()) {
                fwrite($log_file_handle,str_repeat("=",40)."\n");
                fwrite($log_file_handle,'multiotp '.$this->GetVersion()."\n");
                if ($this->GetVerboseFlag()) {
                  fwrite($log_file_handle,'Your script is running from '.$this->GetScriptFolder()."\n");
                }
              }
              $this->SetLogHeaderWritten(TRUE);
            }
            
            fwrite($log_file_handle,$logfile_content."\n");
            fclose($log_file_handle);
            if ($file_created && ("" != $this->GetLinuxFileMode())) {
                @chmod($this->GetLogFolder().$this->GetLogFileName(), octdec($this->GetLinuxFileMode()));
            }
          } elseif ($this->GetVerboseFlag()) {
            echo "ERROR: Log file ".$this->GetLogFolder().$this->GetLogFileName()." cannot be written.\n";
          }
        }
    }
  }


  // ShowLog($as_result = false) : $as_result : don't echo the log content, only return it
  function ShowLog(
    $as_result = false,
    $max_lines = 0
  ) {
      $maxlines = (abs(intval($max_lines)) > 1000) ? 1000 : abs(intval($max_lines));
      $result = "";
      if ((!$this->IsLogForcedInFile()) && ('mysql' == $this->GetBackendType())) {
          if ($this->OpenMysqlDatabase()) {
              $sQuery  = "SELECT * FROM `".$this->_config_data['sql_log_table']."` ".
                "ORDER BY datetime DESC, create_time DESC, unique_id DESC ";
              if ($maxlines > 0) {
                // $sQuery  = "SELECT * FROM (SELECT * FROM `".$this->_config_data['sql_log_table']."` ORDER BY datetime DESC, create_time DESC LIMIT $maxlines) AS sub ORDER BY datetime ASC, create_time ASC;";
                $sQuery.= "LIMIT $maxlines";
              }
              $sQuery.= ";";
              if (is_object($this->_mysqli)) {
                  if (!($rResult = $this->_mysqli->query($sQuery))) {
                      $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                      $result = FALSE;
                  } else {
                      while ($aRow = $rResult->fetch_assoc()) {
                        $result.= nullable_trim($aRow['datetime'].' '.$aRow['user']).' '.$aRow['logentry']."\n";
                      }                         
                  }
              } elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                  $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                  $result = FALSE;
              } else {
                  while ($aRow = mysql_fetch_assoc($rResult)) {
                    $result.= nullable_trim($aRow['datetime'].' '.$aRow['user']).' '.$aRow['logentry']."\n";
                  }                         
              }
          }
          //mysql_close($log_link);
      } elseif ((!$this->IsLogForcedInFile()) && ('pgsql' == $this->GetBackendType())) {
          if ($this->OpenPGSQLDatabase()) {
              $sQuery  = "SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_log_table']."\" ".
              "ORDER BY datetime DESC, create_time DESC, unique_id DESC ";
              if ($maxlines > 0) {
                // $sQuery  = "SELECT * FROM (SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_log_table']."\"  ORDER BY datetime DESC, create_time DESC LIMIT $maxlines) AS sub ORDER BY datetime ASC, create_time ASC;";
                $sQuery.= "LIMIT $maxlines";
              }
              if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                  $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                  $result = FALSE;
              } else {
                  while ($aRow = pg_fetch_assoc($rResult)) {
                    $result.= nullable_trim($aRow['datetime'].' '.$aRow['user']).' '.$aRow['logentry']."\n";
                  }                         
              }
          }
          //pg_close($log_link);
      } elseif($maxlines > 0) {
        $index = 0;
        $lines = [];
        foreach (array('.9','.8','.7','.6','.5','.4','.3','.2','.1','') as $postfix_file) {
            if (file_exists($this->GetLogFolder().$this->GetLogFileName().$postfix_file)) {
              if ($log_file_handle = @fopen($this->GetLogFolder().$this->GetLogFileName().$postfix_file,"r")) {
                flock($log_file_handle, LOCK_SH);
                while (!feof($log_file_handle)) {
                  $one_line = nullable_trim(fgets($log_file_handle));
                  if (!empty($one_line)) {
                    $lines[$index] = $one_line;
                    $index = ($index + 1) % $maxlines;
                  }
                }
                fclose($log_file_handle);
              }
            } // if file exists
        } // foreach
        $part1 = array_slice($lines,    $index);
        $part2 = array_slice($lines, 0, $index);
        $ordered_lines = array_merge($part1, $part2);
        $temp_array = array_reverse($ordered_lines);

        foreach($temp_array as $one_line) {
          $result.= $one_line."\n";
        }
      } elseif (file_exists($this->GetLogFolder().$this->GetLogFileName())) {
        if ($log_file_handle = @fopen($this->GetLogFolder().$this->GetLogFileName(),"r")) {
          flock($log_file_handle, LOCK_SH);
          while (!feof($log_file_handle)) {
            $result.= nullable_trim(fgets($log_file_handle))."\n";
          }
          fclose($log_file_handle);
        }
      }
      if (false !== $result) {
        if (!$as_result) {
          echo $result;
          return true;
        } else {
          return $result;
        }
      } else {
        return $result;
      }
  }


  function ClearLog($days = 0)
  {
      $result = TRUE;
      if ('mysql' == $this->GetBackendType()) {
          if ($this->OpenMysqlDatabase()) {
              if (intval($days) > 0) {
                $sQuery  = "DELETE FROM `".$this->_config_data['sql_log_table']."` WHERE datetime < (NOW() - INTERVAL ".intval($days)." DAY)";
              } else {
                $sQuery  = "TRUNCATE `".$this->_config_data['sql_log_table']."`";
              }
              
              if (is_object($this->_mysqli)) {
                  if (!($rResult = $this->_mysqli->query($sQuery))) {
                      $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                      $result = FALSE;
                  }
              } elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                  $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                  $result = FALSE;
              }
          } else {
              $result = FALSE;
          }
      } elseif ('pgsql' == $this->GetBackendType()) {
          if ($this->OpenPGSQLDatabase()) {
              if (intval($days) > 0) {
                $sQuery  = "DELETE FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_log_table']."\" WHERE datetime < (NOW() - INTERVAL '".intval($days)." DAY')";
              } else {
                $sQuery  = "TRUNCATE \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_log_table']."\"";
              }
              
              if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                  $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                  $result = FALSE;
              }
          } else {
              $result = FALSE;
          }
      }

      if (intval($days) <= 0) {
        if (file_exists($this->GetLogFolder().$this->GetLogFileName())) {
          unlink($this->GetLogFolder().$this->GetLogFileName());
        }
      }
      foreach (glob($this->GetLogFolder().$this->GetLogFileName().".*") as $filename) {
        if ($this->GetVerboseFlag()) {
          $this->WriteLog("Debug: *ClearLog: check modification date for log file $filename : ".filemtime($filename), FALSE, FALSE, 8888, 'System', "");
        }
        if ((filemtime($filename) + ($days * 86400)) < time()) {
          unlink($filename);
          if ($this->GetVerboseFlag()) {
            $this->WriteLog("Debug: *ClearLog: log file $filename deleted".filemtime($filename), FALSE, FALSE, 8888, 'System', "");
          }
        }
      }
      return $result;
  }


  function EnableLog()
  {
      $this->_log_flag = TRUE;
      if ("" == $this->_log_folder) {
          $this->SetLogFolder($this->GetScriptFolder()."log/");
      }
  }


  function IsLogEnabled()
  {
      return (TRUE === $this->_log_flag);
  }


  function DisableLog()
  {
      $this->_log_flag = FALSE;
  }


  function EnableVerboseLog()
  {
      $this->EnableLog();
      $this->_log_verbose_flag = TRUE;
  }


  function DisableVerboseLog()
  {
      $this->_log_verbose_flag = FALSE;
  }


  function GetVerboseFlag()
  {
      return $this->_log_verbose_flag;
  }


  function ForceNoDisplayLog() {
      $this->_no_display_log = true;
  }

  
  function DisableNoDisplayLog() {
      $this->_no_display_log = false;
  }

  
  function GetNoDisplayLogFlag() {
      return $this->_no_display_log;
  }


  function EnableDisplayLog()
  {
      $this->_log_display_flag = TRUE;
  }


  function DisableDisplayLog()
  {
      $this->_log_display_flag = FALSE;
  }


  function GetDisplayLogFlag()
  {
      return $this->_log_display_flag;
  }


  function SetCliMode($value) {
      $this->_cli_mode = (true == $value);
  }


  function GetCliMode() {
      return (true == $this->_cli_mode);
  }


  function SetCliProxyMode($value) {
      $this->_cli_proxy_mode = (true == $value);
  }


  function GetCliProxyMode() {
      return (true == $this->_cli_proxy_mode);
  }


  function SetCredentialProviderMode($value) {
      $this->_cp_mode = (true == $value);
  }


  function GetCredentialProviderMode() {
      return (true == $this->_cp_mode);
  }


  function SetCredentialProviderRequest($value) {
      $this->_cp_request = ((true === $value) || (1 == $value));
  }


  function IsCredentialProviderRequest() {
      return (true == $this->_cp_request);
  }


  function SetIgnoreNoPrefixCredentialProvider($value) {
    $this->_config_data['ignore_no_prefix_cp'] = ((intval($value) > 0)?1:0);
  }


  function EnableIgnoreNoPrefixCredentialProvider($value) {
    $this->_config_data['ignore_no_prefix_cp'] = 1;
  }


  function DisableIgnoreNoPrefixCredentialProvider($value) {
    $this->_config_data['ignore_no_prefix_cp'] = 0;
  }


  function IsIgnoreNoPrefixCredentialProvider() {
    return (1 == intval($this->_config_data['ignore_no_prefix_cp']));
  }


  function SetDemoMode($value) {
      $this->_config_data['demo_mode'] = ((intval($value) > 0)?1:0);
  }


  function EnableDemoMode() {
      $this->_config_data['demo_mode'] = 1;
  }


  function DisableDemoMode()
  {
      $this->_config_data['demo_mode'] = 0;
  }


  function IsDemoMode()
  {
      return (1 == ($this->_config_data['demo_mode']));
  }


  function SetCacheLdapHash(
      $value
  ) {
      $this->_config_data['cache_ldap_hash'] = ((intval($value) > 0)?1:0);
  }


  function EnableCacheLdapHash()
  {
      $this->_config_data['cache_ldap_hash'] = 1;
  }


  function DisableCacheLdapHash()
  {
      $this->_config_data['cache_ldap_hash'] = 0;
  }


  function IsCacheLdapHash()
  {
      return (1 == ($this->_config_data['cache_ldap_hash']));
  }


  function IsLdapServerReachable()
  {
      return (TRUE === $this->_ldap_server_reachable);
  }


  function SetLdapServerReachable(
      $value
  ) {
      $this->_ldap_server_reachable = (TRUE === $value);
  }


  function SetEncryptionKeyFullPath(
      $full_path
  ) {
      $this->_config_data['encryption_key_full_path'] = $full_path;
  }


  function GetEncryptionKeyFullPath()
  {
      return nullable_trim(isset($this->_config_data['encryption_key_full_path'])?$this->_config_data['encryption_key_full_path']:"");
  }


  function SetHashSaltFullPath(
      $full_path
  ) {
      $this->_config_data['hash_salt_full_path'] = $full_path;
  }


  function GetHashSaltFullPath()
  {
      return nullable_trim($this->_config_data['hash_salt_full_path']);
  }


  /**
   * @brief   Set the configuration folder (for the config file).
   *
   * @param   string  $folder       Full path to the config folder.
   * @param   boolean $create       Create the folder if it doesn't exists.
   * @param   boolean $read_config  Read directly the configuration file.
   * @retval  void
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.0.0
   * @date    2013-05-13
   * @since   2013-05-13
   */
  function SetConfigFolder(
      $folder,
      $create = true,
      $read_config = true
  ) {
      $new_folder = $this->ConvertToUnixPath($folder);
      if (mb_substr($new_folder,-1) != "/") {
          $new_folder.="/";
      }
      if ("/" == $new_folder) {
        $new_folder = "./";
      }
      $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
      $this->_config_folder = $new_folder;
      if ($create && (!file_exists($new_folder))) {
          if (!@mkdir(
                  $new_folder,
                  ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
                  true //recursive
          )) {
              $this->WriteLog("Error: Unable to create the missing config folder ".$new_folder, true, false, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "", 3);
          }
      }
      if ($read_config) {
          $this->ReadConfigData();
      }
  }


  /**
   * @brief   Get the configuration folder (for the config file).
   *
   * @param   boolean $create_if_not_exist Create the folder if it doesn't exists.
   * @retval  string                       Full path to the config folder.
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.0.0
   * @date    2013-05-13
   * @since   2013-05-13
   */
  function GetConfigFolder(
      $create_if_not_exist = false,
      $read_config = true
  ) {
      $config_folder = $this->ConvertToWindowsPathIfNeeded($this->_config_folder);
      if ("" == $config_folder) {
          $this->SetConfigFolder($this->GetScriptFolder()."config/", $create_if_not_exist, $read_config);
      } elseif (!file_exists($config_folder)) {
          if ($create_if_not_exist) {
              if (!@mkdir(
                      $config_folder,
                      ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
                      true //recursive
              )) {
                  $this->WriteLog("Error: Unable to create the missing config folder ".$config_folder, FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "", 3);
              }
          }
      }
      return $this->ConvertToWindowsPathIfNeeded($this->_config_folder);
  }


  /**
   * @brief   Set the cache folder (for the cache file).
   *
   * @param   string  $folder       Full path to the cache folder.
   * @param   boolean $create       Create the folder if it doesn't exists.
   * @param   boolean $read_cache   Read directly the cache file.
   * @retval  void
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.2.5.0
   * @date    2014-07-25
   * @since   2014-07-25
   */
  function SetCacheFolder(
      $folder,
      $create = true,
      $read_cache = true
  ) {
      $new_folder = $this->ConvertToUnixPath($folder);
      if (mb_substr($new_folder,-1) != "/") {
          $new_folder.="/";
      }
      if ("/" == $new_folder) {
        $new_folder = "./";
      }
      $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
      $this->_cache_folder = $new_folder;
      if ($create && (!file_exists($new_folder))) {
          if (!@mkdir(
                  $new_folder,
                  ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
                  true //recursive
          )) {
              $this->WriteLog("Error: Unable to create the missing cache folder ".$new_folder, TRUE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "", 3);
          }
      }
      if ($read_cache) {
          $this->ReadCacheData();
      }
  }


  /**
   * @brief   Get the cache folder (for the cache file).
   *
   * @param   boolean $create_if_not_exist Create the folder if it doesn't exists.
   * @retval  string                       Full path to the cache folder.
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.2.5.0
   * @date    2014-07-25
   * @since   2014-07-25
   */
  function GetCacheFolder(
    $create_if_not_exist = false
  ) {
    $cache_folder = $this->ConvertToWindowsPathIfNeeded($this->_cache_folder);
    if ("" == $cache_folder) {
      $this->SetCacheFolder($this->GetScriptFolder()."cache/", $create_if_not_exist);
    } elseif (!file_exists($cache_folder)) {
      if ($create_if_not_exist) {
        if (!@mkdir(
            $cache_folder,
            ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
            true //recursive
        )) {
          $this->WriteLog("Error: Unable to create the missing cache folder ".$cache_folder, FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "", 3);
        }
      }
    }
    return $this->ConvertToWindowsPathIfNeeded($this->_cache_folder);
  }


  function GetLocalIpAddress()
  {
    $ip = "";
    if (mb_strtolower(mb_substr(PHP_OS, 0, 3),'UTF-8') === 'win') { // Windows
      $output = [];
      exec("ipconfig /all", $output);
      foreach ($output as $line) {
        $line.= "  ";
        if (preg_match("/.*IPv4.*[^\.]+([[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3})[^\.]+/", $line)) {
          preg_match_all("/[^\.[:xdigit:]]+([[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3})/", $line, $result_array, PREG_SET_ORDER);
          if (isset($result_array[0][1])) {
            $temp = nullable_trim($result_array[0][1]);
            if ('0.0.0.0' != $temp) {
              $ip = $temp;
              break;
            }
          }
        }
      }
    } else { // Linux
      $output = [];
      // inet -> works for old (inet addr) and new (inet)
      exec("hostname -I | grep -o -E '([[:xdigit:]]{1,3}\.){3}[[:xdigit:]]{1,3}'", $output);
      $ip = (isset($output[0])?$output[0]:"");
    }
    return $ip;
  }


  function GetNetworkInfo(
    $custom_script = ""
  ) {
    
    $cmd = "/usr/local/bin/multiotp/scripts/" . (empty(trim($custom_script)) ? "multiotp-service.sh" : trim($custom_script));

    // These are demo values for the development
    $mode    = "";
    $ip      = "";
    $mask    = "";
    $gateway = "";
    $dns     = [];
    $dns[0]  = "";
    $dns[1]  = "";
    $interface_name = "";
    $fixed_gateway = false;
    
    if (mb_strtolower(mb_substr(PHP_OS, 0, 3),'UTF-8') === 'win') {
      // Windows
      // The last route (without an interface address) is the default one
      $output = [];
      exec("route print | find \"0.0.0.0\"", $output);
      foreach ($output as $line) {
        $line.= "  ";
        if (preg_match("/.*0.0.0.0.*[^\.]+0.0.0.0.*[^\.]+([[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3})[^\.]+/", $line)) {
          $result_array = [];
          preg_match_all("/[^\.[:xdigit:]]+([[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3})/", $line, $result_array, PREG_SET_ORDER);
          if ((!$fixed_gateway) && (isset($result_array[2][1]))) {
            $temp = nullable_trim($result_array[2][1]);
            if ('0.0.0.0' != $temp) {
              $gateway = $temp;
              if (!isset($result_array[3][1])) {
                $fixed_gateway = true;
              }
            }
          }
        }
      }
      $output = [];
      exec("ipconfig /all", $output);
      $next_is_mask = false;
      foreach ($output as $line) {
        $line.= "  ";
        if ($next_is_mask || preg_match("/.*IPv4.*[^\.]+([[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3})[^\.]+/", $line)) {
          $result_array = [];
          preg_match_all("/[^\.[:xdigit:]]+([[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3})/", $line, $result_array, PREG_SET_ORDER);
          if (isset($result_array[0][1])) {
            $temp = nullable_trim($result_array[0][1]);
            if ($next_is_mask) {
              $mask = $temp;
              $cidr_mask = mask2cidr($mask);
              $gw_long_subnet = (ip2long($gateway) >> (32-$cidr_mask));
              $ip_long_subnet = (ip2long($ip) >> (32-$cidr_mask));
              if ($ip_long_subnet != $gw_long_subnet) {
                $next_is_mask = false;
              } else {
                break;
              }
            } elseif ('0.0.0.0' != $temp) {
              $ip = $temp;
              $next_is_mask = true;
            }
          }
        }
      }

      $output = [];
      exec("netsh interface dump | find \"".$ip."\"", $output);
      foreach ($output as $line) {
        $line.= "  ";
        $result_array = [];
        preg_match_all("/[^\"]+\"([^\"]*)\".*/", $line, $result_array, PREG_SET_ORDER);
        if (isset($result_array[0][1])) {
          // We receive something back which is probably coded in CP850
          $interface_name = mb_convert_encoding (nullable_trim($result_array[0][1]),"UTF-8","CP850");
          $mode = "static";
          break;
        }
      }
      if ("" == $interface_name) {
        $mode = "dhcp";
        $output = [];
        exec("netsh interface dump", $output);
        $ip4config = false;
        foreach ($output as $line) {
          $line.= "  ";
          if (0 === mb_strpos(nullable_trim($line),"pushd interface ipv4")) {
            $ip4config = true;
          } elseif ($ip4config && (0 === mb_strpos(nullable_trim($line),"popd"))) {
            $ip4config = false;
          }
          if ($ip4config) {
            $result_array = [];
            preg_match_all("/^set interface[^\"]+\"([^\"]*)\".*metric=1.*/", $line, $result_array, PREG_SET_ORDER);
            if (isset($result_array[0][1])) {
              // We receive something back which is probably coded in CP850
              $interface_name = mb_convert_encoding (nullable_trim($result_array[0][1]),"UTF-8","CP850");
              break;
            }
          }
        }
      }

      $dns_count = 0;
      $output = [];
      exec("netsh interface ip show dnsservers \"".mb_convert_encoding ($interface_name,"ISO-8859-15","UTF-8")."\"", $output);
      foreach ($output as $line) {
        $line.= "  ";
        if (preg_match("/[^\.[:xdigit:]]+([[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3})[^\.]+/", $line)) {
          $result_array = [];
          preg_match_all("/[^\.[:xdigit:]]+([[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3}[\.][[:xdigit:]]{1,3})/", $line, $result_array, PREG_SET_ORDER);
          if (($dns_count < 2) && isset($result_array[0][1])) {
            $dns[$dns_count] = nullable_trim($result_array[0][1]);
            $dns_count++;
          }
        }
      }
    } else {
      // Linux information extraction
      if (file_exists($cmd)) {
        exec("sudo $cmd get-iface-info", $outputArray);
        $first_line = $outputArray[0];
        preg_match('/^(\S+) - (\S+) : (\S+) \/ (\S+) > (\S+) \| "([^"]*)" @ (\S+)$/', $first_line, $resultArray);
        $interface  = $resultArray[1]; // eth0
        $mode       = $resultArray[2]; // dhcp|static
        $ip         = $resultArray[3]; // 192.168.1.2
        $mask       = $resultArray[4]; // 255.255.255.0
        $gateway    = $resultArray[5]; // 192.168.1.1
        $dns_list   = $resultArray[6]; // 1.1.1.1,8.8.8.8
        $mac        = $resultArray[7]; // 12:34:56:78:90:ab
        
        $dns_parts = explode(',', $dns_list, 3);
        $dns[0] = $dns_parts[0] ?? '';
        $dns[1] = $dns_parts[1] ?? '';
      }
    }
    $network_info = $mode."\t".$ip."\t".$mask."\t".$gateway."\t".$dns[0]."\t".$dns[1]."\t";
    $network_array = explode("\t",$network_info);
    
    for ($i=count($network_array); $i <= 5; $i++) {
      $network_array[$i] = "";
    }
    return $network_array;
  }


  function SetNetworkInfo(
    $ip = "",
    $mask = "",
    $gateway = "",
    $dns1 = "",
    $dns2 = "",
    $write_config = true,
    $if_down_up = true,
    $custom_script = ""
  ) {
    
    $cmd = "/usr/local/bin/multiotp/scripts/" . (empty(trim($custom_script)) ? "multiotp-service.sh" : trim($custom_script));

    if ($write_config) {
      $this->SetNetworkIp     ($ip);
      $this->SetNetworkMask   ($mask);
      $this->SetNetworkGateway($gateway);
      $this->SetNetworkDns1   ($dns1);
      $this->SetNetworkDns2   ($dns2);

      $this->WriteConfigData();
    }
    
    $domain_name = $this->GetDomainName();
    if (empty(trim($domain_name))) {
      $domain_name = "multiotp.local";
    }

    $dns = "$dns1,$dns2";
    if ("," == substr($dns, 0, 1)) {
      $dns = substr($dns, 1);
    }
    if ("," == substr($dns, -1, 1)) {
      $dns = substr($dns, 0, -1);
    }

    $result = false;
    if (!$this->IsDemoMode()) {
      if (mb_strtolower(mb_substr(PHP_OS, 0, 3),'UTF-8') !== 'win') { // Only for non-windows machine
        if (file_exists($cmd)) {
          if ("" != $ip) {
            exec("sudo nohup $cmd set-ip static $ip $mask $gateway \"$dns\" $domain_name > /dev/null 2>&1", $output);
          } else {
            exec("sudo nohup $cmd set-ip dhcp > /dev/null 2>&1", $output);
          }
          $result = true;
        }
      }
    }

    return $result;
  }


  function GetCreateHost()
  {
      return $this->GetSystemName();
  }


  function GetSystemName()
  {
      $system_name = nullable_trim(php_uname('n'));
      return $system_name;
  }


  function SetDomainName(
      $value
  ) {
      if (!$this->IsDemoMode()) {
          $this->_config_data['domain_name'] = ($value);
      }
  }


  function GetDomainName()
  {
    return (isset($this->_config_data['domain_name']) ? $this->_config_data['domain_name'] : "");
  }


  function SetEmailAdminAddress(
      $value
  ) {
      $this->_config_data['email_admin_address'] = ($value);
  }


  function GetEmailAdminAddress()
  {
    return (isset($this->_config_data['email_admin_address']) ? $this->_config_data['email_admin_address'] : "");
  }


  function SetHashSalt(
      $salt
  ) {
      $this->_hash_salt  = nullable_trim($salt);
  }


  function GetHashSalt()
  {
    $salt = $this->_hash_salt;
    if ((("" == $salt) ||
        ("MySalt" == $salt) ||
        (_MULTIOTP_DEFAULT_HASH_SALT_ == $salt)
      ) &&
      (!empty($this->GetHashSaltFullPath())) &&
      file_exists($this->GetHashSaltFullPath())) {
      if ($hash_salt_file_handler = @fopen($this->GetHashSaltFullPath(), "rt")) {
        flock($hash_salt_file_handler, LOCK_SH);
        $temp = nullable_trim(fgets($hash_salt_file_handler));
        if ("" != $temp) {
          $salt = $temp;
        }
        fclose($hash_salt_file_handler);
      }
    }
    return nullable_trim($salt);
  }


  function SetRandomSalt(
      $salt
  ) {
      $this->_random_salt  = nullable_trim($salt);
  }


  function GetRandomSalt()
  {
      return nullable_trim($this->_random_salt);
  }


  function SetAdminPassword(
      $password
  ) {
      if (!$this->IsDemoMode()) {
          return $this->SetConfigAttribute('admin_password_hash',md5($this->GetHashSalt().$password.$this->GetHashSalt()));
      } else {
          return false;
      }
  }


  function SetAdminPasswordHash(
      $password_hash
  ) {
      if (!$this->IsDemoMode()) {
          return $this->SetConfigAttribute('admin_password_hash',$password_hash);
      } else {
          return false;
      }
  }

  // Weak security check: the client side must return password (for internal call only)
  function CheckAdminPassword(
    $password
  ) {
    return ($this->GetConfigAttribute('admin_password_hash') == md5($this->GetHashSalt().$password.$this->GetHashSalt()));
  }


  // Regular security check: the client side must return md5(hash_salt + password + hash_salt)
  function CheckAdminPasswordHash(
      $password_hash_with_salt
  ) {
    if (32 == mb_strlen($password_hash_with_salt)) {
      return ($this->GetConfigAttribute('admin_password_hash') == $password_hash_with_salt);
    } else {
      return false;
    }
  }


  // Better security check: the client side must return md5(salt + md5(hash_salt + password + hash_salt) + salt)
  function CheckAdminPasswordHashWithRandomSalt(
      $password_hash_with_salt
  ) {
    if (32 == mb_strlen($password_hash_with_salt)) {
      return (md5($this->GetRandomSalt().$this->GetConfigAttribute('admin_password_hash').$this->GetRandomSalt()) == $password_hash_with_salt);
    } else {
      return false;
    }
  }


  function EnableDebugViaHtml()
  {
      $this->_debug_via_html = TRUE;
  }


  function IsDebugViaHtml()
  {
      return ($this->_debug_via_html);
  }


  function EnableKeepLocal()
  {
      $this->_keep_local = TRUE;
  }


  function IsKeepLocal()
  {
      return ($this->_keep_local);
  }


  function SetLinuxFileMode(
      $mode
  ) {
      $this->_linux_file_mode = $mode;
  }


  function GetLinuxFileMode()
  {
      return ($this->_linux_file_mode);
  }


  function SetLinuxFolderMode(
      $mode
  ) {
      $this->_linux_folder_mode = $mode;
  }


  function GetLinuxFolderMode()
  {
      return ($this->_linux_folder_mode);
  }


  function SetConfigData(
      $key,
      $value
  ) {
      if (isset($this->_config_data[$key])) {
          $this->_config_data[$key] = $value;
      }
  }


  function SetLogOption(
      $value
  ) {
      $this->_config_data['log'] = $value;
      if (1 == $this->_config_data['log']) {
          $this->EnableLog();
      }
  }


  function GetLogOption() {
      return intval($this->_config_data['log']);
  }


  function SetLogForcedInFile(
      $value
  ) {
      $this->_config_data['log_forced_in_file'] = $value;
  }


  function GetLogForcedInFile() {
    return intval(isset($this->_config_data['log_forced_in_file']) ? $this->_config_data['log_forced_in_file'] : 0);
  }


  function IsLogForcedInFile() {
      return (1 == intval($this->_config_data['log_forced_in_file']));
  }


  function SetDebugOption(
      $value
  ) {
      $this->_config_data['debug'] = intval($value);
      if (1 == $this->_config_data['debug']) {
          $this->EnableVerboseLog();
      }
  }


  function GetDebugOption() {
    return intval(isset($this->_config_data['debug']) ? $this->_config_data['debug'] : 0);
  }


  function IsDebugOption() {
      return (1 == intval($this->_config_data['debug']));
  }


  function SetDeveloperMode(
      $value
  ) {
      $this->_config_data['developer_mode'] = intval($value);
  }


  function EnableDeveloperMode() {
    $this->_config_data['developer_mode'] = 0;
  }


  function DisableDeveloperMode() {
    $this->_config_data['developer_mode'] = 1;
  }


  function GetDeveloperMode() {
    return intval(isset($this->_config_data['developer_mode']) ? $this->_config_data['developer_mode'] : 0);
  }


  function IsDeveloperMode() {
    return (1 == intval($this->_config_data['developer_mode']));
  }


  function SetDisplayLogOption(
      $value
  ) {
      $this->_config_data['display_log'] = $value;
      if (1 == intval($this->_config_data['display_log'])) {
          $this->EnableDisplayLog();
      }
  }


  function SetMigrationFromFile(
      $value
  ) {
      $this->_migration_from_file = ($value?TRUE:FALSE);
  }


  function GetMigrationFromFile()
  {
      return $this->_migration_from_file;
  }


  function SetBackendType(
      $type
  ) {
      $this->_config_data['backend_type'] = $type;
      $this->_config_data['backend_type_validated'] = 0;
  }


  function GetBackendType()
  {
      return (isset($this->_config_data['backend_type']) ? $this->_config_data['backend_type'] : "");
  }


  function SetBackendTypeValidated(
      $backend_type_validated,
      $value
  ) {
      if ("" != $backend_type_validated) {
          $this->_config_data['backend_type'] = $backend_type_validated;
      }
      $this->_config_data['backend_type_validated'] = ($value ? 1 : 0);
  }

  function GetBackendTypeValidated()
  {
      return (1 == (isset($this->_config_data['backend_type_validated'])?$this->_config_data['backend_type_validated']:0));
  }

  function SetScratchPasswordsDigits(
      $value
  ) {
      $this->_config_data['scratch_passwords_digits'] = $value;
  }


  function GetScratchPasswordsDigits()
  {
    return intval(isset($this->_config_data['scratch_passwords_digits']) ? $this->_config_data['scratch_passwords_digits'] : 6);
  }


  function SetDefaultUserGroup(
      $value
  ) {
      $this->_config_data['default_user_group'] = $value;
  }


  function GetDefaultUserGroup()
  {
    return (isset($this->_config_data['default_user_group']) ? $this->_config_data['default_user_group'] : "");
  }


  function SetGroupAttribute(
      $value
  ) {
      $this->_config_data['group_attribute'] = $value;
  }


  function GetGroupAttribute()
  {
    return (isset($this->_config_data['group_attribute']) ? $this->_config_data['group_attribute'] : "");
  }


  function SetIssuer(
      $value
  ) {
      $this->_config_data['issuer'] = $value;
  }


  function GetIssuer()
  {
    return (isset($this->_config_data['issuer']) ? $this->_config_data['issuer'] : "");
  }


  function SetClearOtpAttribute(
      $value
  ) {
      $this->_config_data['clear_otp_attribute'] = $value;
  }


  function GetClearOtpAttribute()
  {
    return (isset($this->_config_data['clear_otp_attribute']) ? $this->_config_data['clear_otp_attribute'] : "");
  }


  function SetSqlServer(
      $server
  ) {
      $this->_config_data['sql_server'] = $server;
  }


  function GetSqlServer()
  {
      return nullable_trim($this->_config_data['sql_server']);
  }


  function SetSqlUsername(
      $username
  ) {
      $this->_config_data['sql_username'] = $username;
  }


  function SetSqlPassword(
      $password
  ) {
      $this->_config_data['sql_password'] = $password;
  }


  function SetSqlSchema(
      $schema
  ) {
      $this->_config_data['sql_schema'] = $schema;
  }


  function SetSqlDatabase(
      $database
  ) {
      $this->_config_data['sql_database'] = $database;
  }


  function SetSqlTableName(
      $table_to_define,
      $table_name
  ) {
      if (isset($this->_config_data['sql_'.$table_to_define.'_table'])) {
          $this->_config_data['sql_'.$table_to_define.'_table'] = $table_name;
      }
  }


  function MySqlAddRowIfNeeded(
      $table,
      $column,
      $column_type,
      $column_default,
      $is_an_index = FALSE
  ) {
      $result = FALSE;
      $index_special_suffix = ((mb_strtoupper($column_type, 'UTF-8') == "TEXT") ? "(77)" : "");
      if (is_object($this->_mysqli)) {
        $sql_query = "SELECT `".$column."` FROM ".$table;
        try {
          $select_row = $this->_mysqli->query($sql_query);
          // Backward compatibility
          if (!$select_row) {
            $sql_query = "ALTER TABLE ".$table." ADD `".$column."` ".$column_type;
            if ($column_default != NULL) {
              $sql_query.= " DEFAULT ".$column_default;
            }
            if ($is_an_index) {
              $sql_query.= " , ADD INDEX `$column` ( `".$column."`$index_special_suffix )";
            }
            if (!$this->_mysqli->query($sql_query)) {
              $this->WriteLog("Error: ".nullable_trim($this->_mysqli->error)." ".$sql_query, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
              $result = FALSE;
            }
          } else {
            $result = TRUE;
            $sql_query = "ALTER TABLE ".$table." CHANGE `".$column."` `".$column."` ".$column_type;
            if ($column_default != NULL) {
              $sql_query.= " DEFAULT ".$column_default;
            }
            $sql_query_index = "ALTER TABLE ".$table." DROP INDEX `".$column."`;";
            try { $this->_mysqli->query($sql_query_index); } catch(Exception $e) { }
            if ($is_an_index) {
              $sql_query.= " , ADD INDEX `$column` ( `".$column."`$index_special_suffix )";
            }
            if (!$this->_mysqli->query($sql_query)) {
              $this->WriteLog("Error: ".nullable_trim($this->_mysqli->error)." ".$sql_query, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
              $result = FALSE;
            }
          }
        }
        catch(Exception $e) {
          $sql_query = "ALTER TABLE ".$table." ADD `".$column."` ".$column_type;
          if ($column_default != NULL) {
            $sql_query.= " DEFAULT ".$column_default;
          }
          if ($is_an_index) {
            $sql_query.= " , ADD INDEX `$column` ( `".$column."`$index_special_suffix )";
          }
          if (!$this->_mysqli->query($sql_query)) {
            $this->WriteLog("Error: ".nullable_trim($this->_mysqli->error)." ".$sql_query, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
            $result = FALSE;
          }
        }
      } elseif (NULL != $this->_mysql_database_link) {
        $sql_query = "SELECT `".$column."` FROM ".$table;
        try {
          $select_row = mysql_query($sql_query, $this->_mysql_database_link);
          // Backward compatibility
          if (!$select_row) {
            $sql_query = "ALTER TABLE ".$table." ADD `".$column."` ".$column_type;
            if ($column_default != NULL) {
              $sql_query.= " DEFAULT ".$column_default;
            }
            if ($is_an_index) {
              $sql_query.= " , ADD INDEX `$column` ( `".$column."`$index_special_suffix )";
            }
            if (!mysql_query($sql_query, $this->_mysql_database_link)) {
              $this->WriteLog("Error: ".mysql_error()." ".$sql_query, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
              $result = FALSE;
            }
          } else {
            $select_row = mysql_query($sql_query, $this->_mysql_database_link);
            $result = TRUE;
            $sql_query = "ALTER TABLE ".$table." CHANGE `".$column."` `".$column."` ".$column_type;
            if ($column_default != NULL) {
              $sql_query.= " DEFAULT ".$column_default;
            }
            $sql_query_index = "ALTER TABLE ".$table." DROP INDEX `".$column."`;";
            try {mysql_query($sql_query_index, $this->_mysql_database_link); } catch(Exception $e) { }
            if ($is_an_index) {
              $sql_query.= " , ADD INDEX `$column` ( `".$column."`$index_special_suffix )";
            }
            if (!mysql_query($sql_query, $this->_mysql_database_link)) {
              $this->WriteLog("Error: ".mysql_error()." ".$sql_query, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
              $result = FALSE;
            }
          }
        }
        catch(Exception $e) {
          $sql_query = "ALTER TABLE ".$table." ADD `".$column."` ".$column_type;
          if ($column_default != NULL) {
            $sql_query.= " DEFAULT ".$column_default;
          }
          if ($is_an_index) {
            $sql_query.= " , ADD INDEX `$column` ( `".$column."`$index_special_suffix )";
          }
          if (!mysql_query($sql_query, $this->_mysql_database_link)) {
            $this->WriteLog("Error: ".mysql_error()." ".$sql_query, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
            $result = FALSE;
          }
        }
      } elseif ($this->GetVerboseFlag()) {
          $this->WriteLog("Error: *The database link is down!", TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
      }
      return $result;
  }


  function OpenMysqlDatabase()
  {
      if ((is_object($this->_mysqli)) || (NULL != $this->_mysql_database_link)) {
          $result = TRUE;
      } else {
          $result = FALSE;
          if (("" != $this->_config_data['sql_server']) &&
              ("" != $this->_config_data['sql_username']) &&
              ("" != $this->_config_data['sql_password']) &&
              ("" != $this->_config_data['sql_database'])) {
              if (class_exists('mysqli')) {
                  $sql_server = $this->_config_data['sql_server'];
                  if (FALSE !== ($pos = mb_strpos($this->_config_data['sql_server'], ":"))) {
                      $sql_server_array = explode(":", $sql_server, 2);
                      $sql_server = $sql_server_array[0];
                      $sql_port   = $sql_server_array[1];
                  } else {
                      $sql_port = "3306";
                  }

                  try {
                    $this->_mysqli = @new mysqli($sql_server,
                                                 $this->_config_data['sql_username'],
                                                 $this->_config_data['sql_password'],
                                                 $this->_config_data['sql_database'],
                                                 $sql_port);
                    if (0 != $this->_mysqli->connect_errno) {
                        $this->WriteLog("Error: Bad SQL authentication parameters, ".$this->_mysqli->connect_errno.', '.nullable_trim($this->_mysqli->connect_error), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                        unset($this->_mysqli);
                        $this->_mysqli = NULL;
                    } else {
                        $result = TRUE;
                    }
                  }
                  catch(Exception $e) {
                    $this->WriteLog("Error: SQL connection error", TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                    unset($this->_mysqli);
                    $this->_mysqli = NULL;
                  }
              } elseif (!($this->_mysql_database_link = mysql_connect($this->_config_data['sql_server'],
                                                                      $this->_config_data['sql_username'],
                                                                      $this->_config_data['sql_password']))) {
                  $this->WriteLog("Error: Bad SQL authentication parameters, ".mysql_error(), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
              } else {
                  if (!mysql_select_db($this->_config_data['sql_database'])) {
                      $this->WriteLog("Error: Bad SQL database", TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                      mysql_close($this->_mysql_database_link);
                      $this->_mysql_database_link = NULL;
                  } else {
                      $result = TRUE;
                  }
              }
          }
      }
      return $result;
  }


  function PGSQLAddRowIfNeeded(
      $table,
      $column,
      $column_type,
      $column_default,
      $is_an_index = FALSE
  ) {
      $result = FALSE;
      if (NULL != $this->_pgsql_database_link) {
          $sql_query = "SELECT \"".$column."\" FROM \"".$this->_config_data['sql_schema']."\".\"".$table."\"";
          if (($select_row = @pg_query($this->_pgsql_database_link, $sql_query))) {
              $result = TRUE;
              $sql_query = "ALTER TABLE \"".$this->_config_data['sql_schema']."\".\"".$table."\" ALTER COLUMN \"".$column."\" TYPE ".$column_type;
              if (!@pg_query($this->_pgsql_database_link, $sql_query)) {
                  $this->WriteLog("Error: ".pg_last_error($this->_pgsql_database_link)." ".$sql_query, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                  $result = FALSE;
              } else {
                  if ($column_default != NULL) {
                      $sql_query = "ALTER TABLE \"".$this->_config_data['sql_schema']."\".\"".$table."\" ALTER COLUMN \"".$column."\" SET DEFAULT ".$column_default;
                  } else {
                      $sql_query = "ALTER TABLE \"".$this->_config_data['sql_schema']."\".\"".$table."\" ALTER COLUMN \"".$column."\" DROP DEFAULT";
                  }
                  if (!@pg_query($this->_pgsql_database_link, $sql_query)) {
                      $this->WriteLog("Error: ".pg_last_error($this->_pgsql_database_link)." ".$sql_query, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                      $result = FALSE;
                  }
              }
          } elseif (!$select_row) {
              $sql_query = "ALTER TABLE \"".$this->_config_data['sql_schema']."\".\"".$table."\" ADD COLUMN \"".$column."\" ".$column_type.($column_default != NULL ? " DEFAULT ".$column_default : "");
              if (!@pg_query($this->_pgsql_database_link, $sql_query)) {
                  $this->WriteLog("Error: ".pg_last_error($this->_pgsql_database_link)." ".$sql_query, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                  $result = FALSE;
              }
              if ($is_an_index) {
                  $sql_query = "CREATE INDEX \"".$table."_".$column."_idx\" ON \"".$this->_config_data['sql_schema']."\".\"".$table."\" ( \"".$column."\" )";
                  if(!@pg_query($this->_pgsql_database_link, $sql_query)) {
                      $this->WriteLog("Error: ".pg_last_error($this->_pgsql_database_link)." ".$sql_query, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                      $result = FALSE;
                  }
              }
          }
      } elseif ($this->GetVerboseFlag()) {
          $this->WriteLog("Error: *The database link is down!", TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
      }

      return $result;
  }


  function OpenPGSQLDatabase()
  {
      if (NULL != $this->_pgsql_database_link) {
          $result = TRUE;
      } else {
          $result = FALSE;
          if (("" != $this->_config_data['sql_server']) &&
              ("" != $this->_config_data['sql_username']) &&
              ("" != $this->_config_data['sql_password']) &&
              ("" != $this->_config_data['sql_database']) &&
              ("" != $this->_config_data['sql_schema'])) {
              $sql_server = $this->_config_data['sql_server'];
              if (FALSE !== ($pos = mb_strpos($this->_config_data['sql_server'], ":"))) {
                  $sql_server_array = explode(":", $sql_server, 2);
                  $sql_server = $sql_server_array[0];
                  $sql_port   = $sql_server_array[1];
              } else {
                  $sql_port = "5432";
              }
              $pgsql_connect_string = "host=$sql_server ";
              $pgsql_connect_string.= "port=$sql_port ";
              $pgsql_connect_string.= "dbname=".$this->_config_data['sql_database']." ";
              $pgsql_connect_string.= "user=".$this->_config_data['sql_username']." ";
              $pgsql_connect_string.= "password=".$this->_config_data['sql_password'];
              try {
                if (!($this->_pgsql_database_link = @pg_connect($pgsql_connect_string))) {
                    $this->_pgsql_database_link = NULL;
                    $this->WriteLog("Error: Bad SQL authentication", TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                } else {
                    $result = TRUE;
                }
              }
              catch(Exception $e)
              {
                $this->_pgsql_database_link = NULL;
                $this->WriteLog("Error: Bad SQL authentication", TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
              }
          }
      }
      return $result;
  }


  function InitializeBackend()
  {
      $write_config_data = false;
      $backend_type = $this->GetBackendType();
      if ('mysql' == $backend_type) {
          if ($this->OpenMysqlDatabase()) {
              foreach ($this->_sql_tables as $sql_table) {
                  if ("" != $this->_config_data['sql_'.$sql_table.'_table']) {
                      $sql_query = "CREATE TABLE IF NOT EXISTS `".$this->_config_data['sql_'.$sql_table.'_table']."` (unique_id bigint(20) NOT NULL AUTO_INCREMENT, PRIMARY KEY (unique_id));";
                      if (is_object($this->_mysqli)) {
                          if (!($result = $this->_mysqli->query($sql_query))) {
                              $this->WriteLog("Error: Bad SQL request ($sql_query), ".nullable_trim($this->_mysqli->error), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                              return _MULTIOTP_SYSTEM_SQL_ERROR_;
                          }
                      } elseif ((NULL != $this->_mysql_database_link) && (!mysql_query($sql_query, $this->_mysql_database_link))) {
                          $this->WriteLog("Error: Bad SQL request (CREATE TABLE ".$this->_config_data['sql_'.$sql_table.'_table']."), ".mysql_error(), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                          return _MULTIOTP_SYSTEM_SQL_ERROR_;
                      }
                      foreach ($this->_sql_tables_schema[$sql_table] as $valid_key => $valid_format) {
                          $row_format = $valid_format;
                          $row_default = NULL;
                          $pos = mb_strpos(mb_strtoupper($row_format,'UTF-8'), 'DEFAULT');
                          if ($pos !== FALSE) {
                            $row_default = nullable_trim(mb_substr($valid_format, $pos + mb_strlen("DEFAULT")));
                            $row_format = nullable_trim(mb_substr($row_format, 0, $pos));
                            if (mb_strtolower(mb_substr($row_format, 0, 4),'UTF-8') == "text") {
                                // No default in database for TEXT format
                                $row_default = NULL;
                            }
                          }
                          $this->MySqlAddRowIfNeeded($this->_config_data['sql_'.$sql_table.'_table'], $valid_key, $row_format, $row_default, (FALSE !== mb_strpos($this->_sql_tables_index[$sql_table], "*".$valid_key."*")));
                      }
                  }
              }
              $this->SetBackendTypeValidated($backend_type, TRUE);
              $write_config_data = true;
          }
      } elseif ('pgsql' == $backend_type) {
          if ($this->OpenPGSQLDatabase()) {
              foreach ($this->_sql_tables as $sql_table) {
                  if ("" != $this->_config_data['sql_'.$sql_table.'_table']) {
                      $sql_query = "CREATE TABLE IF NOT EXISTS \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_'.$sql_table.'_table']."\" (\"unique_id\" BIGSERIAL PRIMARY KEY);";
                      if (!pg_query($this->_pgsql_database_link, $sql_query)) {
                          $this->WriteLog("Error: Bad SQL request (CREATE TABLE ".$_config_data['sql_schema'].".".$this->_config_data['sql_'.$sql_table.'_table']."), ".pg_last_error($this->_pgsql_database_link), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                          return _MULTIOTP_SYSTEM_SQL_ERROR_;
                      }
                      foreach ($this->_sql_tables_schema[$sql_table] as $valid_key => $valid_format) {
                          $row_format = $valid_format;
                          $row_default = NULL;
                          if (mb_strtolower(mb_substr($row_format, 0, 4),'UTF-8') == "int(") {
                            $row_format = str_ireplace("int(", "numeric(", $row_format);
                          } elseif (mb_strtolower(mb_substr($row_format, 0, 8),'UTF-8') == "datetime") {
                            $row_format = str_ireplace("datetime", "timestamp", $row_format);
                          }
                          $pos = mb_strpos(mb_strtoupper($row_format,'UTF-8'), 'DEFAULT');
                          if ($pos !== FALSE) {
                            $row_default = nullable_trim(mb_substr($valid_format, $pos + mb_strlen("DEFAULT")));
                            $row_format = nullable_trim(mb_substr($row_format, 0, $pos));
                          }
                          $this->PGSQLAddRowIfNeeded($this->_config_data['sql_'.$sql_table.'_table'], $valid_key, $row_format, $row_default, (FALSE !== mb_strpos($this->_sql_tables_index[$sql_table], "*".$valid_key."*")));
                      }
                  }
              }
              $this->SetBackendTypeValidated($backend_type, TRUE);
              $write_config_data = true;
          }
      }
      if ($write_config_data) {
          $this->WriteConfigData([], true);
      }
      return _MULTIOTP_OPERATION_DONE_;
  }


  function IsOptionInSchema(
      $schema,
      $option
  ) {
      $in_the_schema = FALSE;
      if (isset($this->_sql_tables_schema[$schema])) {
          foreach ($this->_sql_tables_schema[$schema] as $valid_key => $valid_format) {
              if ($valid_key == $option) {
                  $in_the_schema = TRUE;
                  break;
              }
          }
      }
      return $in_the_schema;
  }


  function ReadConfigData(
      $encryption_only = false,
      $encryption_key_param = ""
  ) {
      $result = FALSE;
      
      $encryption_key = $encryption_key_param ;

      if ("" == $encryption_key) {
          $encryption_key = $this->GetEncryptionKey();
      }

      // We initialize the encryption hash to empty
      $this->_config_data['encryption_hash'] = "";

      // We initialize the local encryption check variable
      $local_encryption_check = false;

      // First, we read the config file in any case
      $config_filename = 'multiotp.ini'; // File exists in v3 format only, we don't need any conversion
      if (file_exists($this->GetConfigFolder(false, !$encryption_only).$config_filename))
      {
        if ($file_handler = @fopen($this->GetConfigFolder(false, !$encryption_only).$config_filename, "rt")) {
          flock($file_handler, LOCK_SH);
          $first_line = nullable_trim(fgets($file_handler));
          
          while (!feof($file_handler))
          {
            $line = str_replace(chr(10), "", str_replace(chr(13), "", fgets($file_handler)));
            $line_array = explode("=",$line,2);
            if (('#' != mb_substr($line, 0, 1)) && (';' != mb_substr($line, 0, 1)) && ("" != nullable_trim($line)) && (isset($line_array[1])))
            {
              if (":" == mb_substr($line_array[0], -1))
              {
                $line_array[0] = mb_substr($line_array[0], 0, mb_strlen($line_array[0]) -1);
                $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$encryption_key);
                $local_encryption_check = $this->_encryption_check;
              }
              $line_array[1] = str_replace("<<CRLF>>",chr(10),isset($line_array[1]) ? $line_array[1] : "");
              if ("" != $line_array[0])
              {
                $this->_config_data[mb_strtolower($line_array[0],'UTF-8')] = $line_array[1];
              }
            }
          }
          fclose($file_handler);
          $result = TRUE;
          if (("" != $this->_config_data['encryption_hash']) && (!$encryption_only) && ($local_encryption_check)) {
            if ($this->_config_data['encryption_hash'] != $this->CalculateControlHash($encryption_key))
            {
              $this->_config_data['encryption_hash'] = "ERROR";
              $this->WriteLog("Error: the configuration encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "", 3);
              $result = FALSE;
            }
          }
        }
      }

      if (!$encryption_only) {
          if ($this->_initialize_backend) {
              $this->SetBackendTypeValidated("", FALSE);
              $this->WriteConfigData();
          }
          // And now, we override the values if another backend type is defined
          if ($this->GetBackendTypeValidated()) {
              switch ($this->GetBackendType()) {
                  case 'mysql':
                      if ($this->OpenMysqlDatabase())
                      {
                          if ("" != $this->_config_data['sql_config_table'])
                          {
                              $sQuery  = "SELECT * FROM `".$this->_config_data['sql_config_table']."` ";
                              
                              $aRow = NULL;

                              if (is_object($this->_mysqli)) {
                                  if (!($result = @$this->_mysqli->query($sQuery))) {
                                      $this->WriteLog("Error: ".nullable_trim($this->_mysqli->error)." ".$sQuery, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                                      $result = FALSE;
                                  } else {
                                      $aRow = $result->fetch_assoc();
                                  }
                              } else {
                                  if (!($rResult = @mysql_query($sQuery, $this->_mysql_database_link))) {
                                      $this->WriteLog("Error: ".mysql_error()." ".$sQuery, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                                      $result = FALSE;
                                  } else {
                                      $aRow = mysql_fetch_assoc($rResult);
                                  }
                              }

                              if (NULL != $aRow) {
                                  $result = TRUE;
                                  foreach ($aRow as $key => $value) {
                                      $in_the_schema = FALSE;
                                      foreach ($this->_sql_tables_schema['config'] as $valid_key => $valid_format) {
                                          if ($valid_key == $key) {
                                              $in_the_schema = TRUE;
                                              if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                                $value = "";
                                              }
                                              break;
                                          }
                                      }
                                      if ($in_the_schema) {
                                          if (FALSE === mb_strpos($this->_sql_tables_ignore['config'], "*".$valid_key."*")) {
                                              if (('ENC:' == mb_substr($value,0,4)) && (':ENC' == mb_substr($value,-4))) {
                                                  $value = mb_substr($value,4);
                                                  $value = mb_substr($value,0,mb_strlen($value)-4);
                                                  $this->_config_data[$key] = $this->Decrypt($key,$value,$encryption_key);
                                                  $local_encryption_check = $this->_encryption_check;
                                              } else {
                                                  $this->_config_data[$key] = $value;
                                              }
                                              $this->_config_data[$key] = str_replace("<<CRLF>>",chr(10),$this->_config_data[$key]);
                                          }
                                      } elseif (('unique_id' != $key) && $this->GetVerboseFlag()) {
                                          $this->WriteLog("Warning: *the key ".$key." is not in the config database schema", FALSE, FALSE, 8888, 'System', "", 3);
                                      }
                                  }
                              }
                          }
                          if (("" != $this->_config_data['encryption_hash']) && ($local_encryption_check)) {
                              if ($this->_config_data['encryption_hash'] != $this->CalculateControlHash($encryption_key)) {
                                  $this->_config_data['encryption_hash'] = "ERROR";
                                  $this->WriteLog("Error: the configuration mysql encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              }
                          }
                      }
                      break;
                  case 'pgsql':
                      if ($this->OpenPGSQLDatabase()) {
                          if ("" != $this->_config_data['sql_config_table']) {
                              $sQuery  = "SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_config_table']."\" ";
                              
                              $aRow = NULL;

                              if (!($rResult = @pg_query($this->_pgsql_database_link, $sQuery))) {
                                  $this->WriteLog("Error: ".pg_last_error($this->_pgsql_database_link)." ".$sQuery, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  $aRow = pg_fetch_assoc($rResult);
                              }

                              if (NULL != $aRow) {
                                  $result = TRUE;
                                  foreach ($aRow as $key => $value) {
                                      $in_the_schema = FALSE;
                                      foreach ($this->_sql_tables_schema['config'] as $valid_key => $valid_format) {
                                          if ($valid_key == $key) {
                                              $in_the_schema = TRUE;
                                              if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                                $value = "";
                                              }
                                              break;
                                          }
                                      }
                                      if ($in_the_schema) {
                                          if (FALSE === mb_strpos($this->_sql_tables_ignore['config'], "*".$valid_key."*")) {
                                              if (('ENC:' == mb_substr($value,0,4)) && (':ENC' == mb_substr($value,-4))) {
                                                  $value = mb_substr($value,4);
                                                  $value = mb_substr($value,0,mb_strlen($value)-4);
                                                  $this->_config_data[$key] = $this->Decrypt($key,$value,$encryption_key);
                                                  $local_encryption_check = $this->_encryption_check;
                                              } else {
                                                  $this->_config_data[$key] = $value;
                                              }
                                              $this->_config_data[$key] = str_replace("<<CRLF>>",chr(10),$this->_config_data[$key]);
                                          }
                                      } elseif (('unique_id' != $key) && $this->GetVerboseFlag()) {
                                          $this->WriteLog("Warning: *the key ".$key." is not in the config database schema", FALSE, FALSE, 8888, 'System', "", 3);
                                      }
                                  }
                              }
                          }
                          if (("" != $this->_config_data['encryption_hash']) && ($local_encryption_check)) {
                              if ($this->_config_data['encryption_hash'] != $this->CalculateControlHash($encryption_key)) {
                                  $this->_config_data['encryption_hash'] = "ERROR";
                                  $this->WriteLog("Error: the configuration pgsql encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              }
                          }
                      }
                      break;
                  default:
                  // Nothing to do if the backend type is unknown
                      break;
              }
          }
          
          if (isset($this->_config_data['log']) && (1 == $this->_config_data['log'])) {
              $this->EnableLog();
          }

          if (isset($this->_config_data['debug']) && (1 == $this->_config_data['debug'])) {
              $this->EnableVerboseLog();
          }

          if (isset($this->_config_data['display_log']) && (1 == $this->_config_data['display_log'])) {
              $this->EnableDisplayLog();
          }
          
          $this->SetAttributesToEncrypt(nullable_trim(isset($this->_config_data['attributes_to_encrypt'])?$this->_config_data['attributes_to_encrypt']:""));
          
          $timezone = $this->GetTimezone(); // Read the timezone (and set it in PHP automatically)
          
          //We do the rest also only if we are not in encryption_only mode
          if ((!isset($this->_config_data['server_secret'])) || ("" == $this->_config_data['server_secret'])) {
              $this->_config_data['server_secret'] = 'ClientServerSecret';
          }
          
          $this->_config_data_read = $this->_config_data;
          
          // ReadStatData automatically
          $this->ReadStatData();
          
          $array_to_parse = $this->_sql_tables_schema['stat'];
          foreach ($array_to_parse as $stat_key => $stat_format) {
            $pos = mb_strpos(mb_strtoupper($stat_format,'UTF-8'), 'DEFAULT');
            $default_value = "";
            if ($pos !== FALSE) {
              $default_value = nullable_trim(mb_substr($stat_format, $pos + mb_strlen("DEFAULT")));
              if (("'" == mb_substr($default_value,0,1)) && ("'" == mb_substr($default_value,-1))) {
                $default_value = mb_substr($default_value,1,-1);
              }
            }
            if (isset($this->_stat_data[$stat_key]) && ($this->_stat_data[$stat_key] != $default_value)) {
              $this->_config_data[$stat_key] = $this->_stat_data[$stat_key];
              if ($this->IsDeveloperMode()) {
                $this->WriteLog("Developer: **ReadConfigData, stat value for $stat_key was " . $this->_stat_data[$stat_key], FALSE, FALSE, 8888, 'System', "");
              }
            }
          }
      }
      return $result;
  }


  function ReadStatData()
  {
      /*
      $stat_table_info = "";
      foreach ($this->_sql_tables_schema['stat'] as $key => $value) {
          $stat_table_info.= $key.' ';
      }
      */
      
      $this->ResetStatArray();
      $result = false;
      
      // First, we read the stat file if the backend is files or when migration is enabled
      if (('files' == $this->GetBackendType()) || ($this->GetMigrationFromFile())) {
        $stat_filename = 'stat.ini'; // File exists in v3 format only, we don't need any conversion
        if (file_exists($this->GetConfigFolder().$stat_filename)) {
          if ($file_handler = @fopen($this->GetConfigFolder().$stat_filename, "rt")) {
            flock($file_handler, LOCK_SH);
            $first_line = nullable_trim(fgets($file_handler));
            
            while (!feof($file_handler)) {
              $line = str_replace(chr(10), "", str_replace(chr(13), "", fgets($file_handler)));
              $line_array = explode("=",$line,2);
              if (('#' != mb_substr($line, 0, 1)) && (';' != mb_substr($line, 0, 1)) && ("" != nullable_trim($line)) && (isset($line_array[1]))) {
                if ("" != $line_array[0]) {
                  $this->_stat_data[mb_strtolower($line_array[0],'UTF-8')] = $line_array[1];
                }
              }
            }
            fclose($file_handler);
            $result = TRUE;
          }
        }
      }
      
      // And now, we override the values if another backend type is defined
      if ($this->GetBackendTypeValidated()) {
          switch ($this->GetBackendType()) {
              case 'mysql':
                  if ($this->OpenMysqlDatabase()) {
                      if ("" != $this->_config_data['sql_stat_table']) {
                          $sQuery  = "SELECT * FROM `".$this->_config_data['sql_stat_table']."` ";
                          
                          $aRow = NULL;

                          if (is_object($this->_mysqli)) {
                              if (!($result = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: ".nullable_trim($this->_mysqli->error)." ".$sQuery, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  $aRow = $result->fetch_assoc();
                              }
                          } else {
                              if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                                  $this->WriteLog("Error: ".mysql_error()." ".$sQuery, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  $aRow = mysql_fetch_assoc($rResult);
                              }
                          }

                          if (NULL != $aRow) {
                              $result = TRUE;
                              foreach ($aRow as $key => $value) {
                                  $in_the_schema = FALSE;
                                  foreach ($this->_sql_tables_schema['stat'] as $valid_key => $valid_format) {
                                      if ($valid_key == $key) {
                                          $in_the_schema = TRUE;
                                          if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                            $value = "";
                                          }
                                          break;
                                      }
                                  }
                                  if ($in_the_schema) {
                                      $this->_stat_data[$key] = $value;
                                  } elseif (('unique_id' != $key) && $this->GetVerboseFlag()) {
                                      $this->WriteLog("Warning: *the key ".$key." is not in the stat database schema", FALSE, FALSE, 8888, 'System', "");
                                  }
                              }
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      if ("" != $this->_config_data['sql_stat_table']) {
                          $sQuery  = "SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_stat_table']."\" ";
                          
                          $aRow = NULL;

                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: ".pg_last_error($this->_pgsql_database_link)." ".$sQuery, TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              $result = FALSE;
                          } else {
                              $aRow = pg_fetch_assoc($rResult);
                          }

                          if (NULL != $aRow) {
                              $result = TRUE;
                              foreach ($aRow as $key => $value) {
                                  $in_the_schema = FALSE;
                                  foreach ($this->_sql_tables_schema['stat'] as $valid_key => $valid_format) {
                                      if ($valid_key == $key) {
                                          $in_the_schema = TRUE;
                                          if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                            $value = "";
                                          }
                                          break;
                                      }
                                  }
                                  if ($in_the_schema) {
                                      $this->_stat_data[$key] = $value;
                                  } elseif (('unique_id' != $key) && $this->GetVerboseFlag()) {
                                      $this->WriteLog("Warning: *the key ".$key." is not in the stat database schema", FALSE, FALSE, 8888, 'System', "");
                                  }
                              }
                          }
                      }
                  }
                  break;
              default:
              // Nothing to do if the backend type is unknown
                  break;
          }
      }
      
      $this->_stat_data_read = $this->_stat_data;

      return $result;
  }


  function WriteConfigData(
      $write_config_data_array = [],
      $force_file = false
  ) {
      if ($this->IsDeveloperMode()) {
        $backtrace = version_compare(PHP_VERSION, '5.3.6', '>=') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();
        foreach ($backtrace as $one_backtrace) {
          $file = isset($one_backtrace['file'])?$one_backtrace['file']:"";
          $line = isset($one_backtrace['line'])?$one_backtrace['line']:"";
          $class = isset($one_backtrace['class'])?$one_backtrace['class']."::":"";
          $function = isset($one_backtrace['function'])?$one_backtrace['function']:"";
          $this->WriteLog("Developer: *WriteConfigData $file:$line $class$function()", FALSE, FALSE, 8888, 'Debug', "");
        }
      }
      
      $write_needed = false;
      // foreach ([] as $key => $value) // this is not working well in PHP4
      foreach ($this->_config_data as $key => $value) {
        $in_the_stat_schema = FALSE;
        foreach ($this->_sql_tables_schema['stat'] as $stat_key => $stat_format) {
          if (('last_sync_update' != $stat_key) && ('last_sync_update_host' != $stat_key) && ('last_update' != $stat_key) && ('last_update_host' != $stat_key) && ('create_time' != $stat_key) && ('create_host' != $stat_key) && ($stat_key == $key)) {
            $in_the_stat_schema = TRUE;
            break;
          }
        }
        $old_value = (isset($this->_config_data_read[$key]) ? $this->_config_data_read[$key] : "");
        if ($value != $old_value) {
          if (!$in_the_stat_schema) {
            $write_needed = true;
          } else {
              if ($this->GetVerboseFlag()) {
                $this->WriteLog("Debug: **New configuration value to write in stat for $key: '$value' (was '$old_value' before)", FALSE, FALSE, 8888, 'Debug', "");
              }
          }
          if ($this->GetVerboseFlag()) {
            $this->WriteLog("Debug: **New configuration value for $key: '$value' (was '$old_value' before)", FALSE, FALSE, 8888, 'Debug', "");
          }
        }
      }
      
      // We need to write the content in any case if we do a backup file or if with request the return of the content
      if (("@" == (isset($write_config_data_array['backup_file']) ? $write_config_data_array['backup_file'] : "")) ||
          (true === (isset($write_config_data_array['return_content']) ? $write_config_data_array['return_content'] : false))
         ) {
        $write_needed = true;
        if ($this->GetVerboseFlag()) {
          $this->WriteLog("Debug: **New configuration value to backup for $key: '$value' (was '$old_value' before)", FALSE, FALSE, 8888, 'Debug', "");
        }
      }
      
      if (true === (isset($write_config_data_array['force_write_needed']) ? $write_config_data_array['force_write_needed'] : false)) {
        $write_needed = true;
      }
      
      if ($write_needed) {
          if ($this->GetVerboseFlag()) {
            $this->WriteLog("Debug: **Writing configuration data needed", FALSE, FALSE, 8888, 'Debug', "");
          }
          $result = $this->WriteData(array_merge(array('item'       => 'Configuration',
                                                       'table'      => 'config',
                                                       'folder'     => $this->GetConfigFolder(true),
                                                       'data_array' => $this->_config_data,
                                                       'force_file' => $force_file
                                                      ), $write_config_data_array));
      } else {
          if ($this->GetVerboseFlag()) {
            $this->WriteLog("Debug: **Writing configuration data not needed (no change)", FALSE, FALSE, 8888, 'Debug', "");
          }
          $result = true;
      }

      if (false !== $result) {
        $this->WriteStatData($write_config_data_array);
      }
      
      return $result;
  }


  function WriteStatData(
      $write_stat_data_array = []
  ) {
      
      $write_needed = false;
      foreach ($this->_config_data as $key => $value) {
        foreach ($this->_sql_tables_schema['stat'] as $stat_key => $stat_format) {
          if (('last_sync_update' != $stat_key) && ('last_sync_update_host' != $stat_key) && ('last_update' != $stat_key) && ('last_update_host' != $stat_key) && ('create_time' != $stat_key) && ('create_host' != $stat_key) && ($stat_key == $key)) {
            $this->_stat_data[$stat_key] = $this->_config_data[$key];
            $old_value = (isset($this->_stat_data_read[$key]) ? $this->_stat_data_read[$key] : "");
            
            if ($this->GetVerboseFlag()) {
              if ('available_firmware_version' == $key) {
                $this->WriteLog("Debug: **WriteStatData, stat value for $key: '$value' (was '$old_value' before)", FALSE, FALSE, 8888, 'Debug', "");
              }
            }

            if ($value != $old_value) {
              $write_needed = true;
              if ($this->GetVerboseFlag()) {
                $this->WriteLog("Debug: **New stat value for $key: '$value' (was '$old_value' before)", FALSE, FALSE, 8888, 'Debug', "");
              }
            }
            break;
          }
        }
      }
        
      if ($this->IsDeveloperMode()) {
        $backtrace = version_compare(PHP_VERSION, '5.3.6', '>=') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();
        foreach ($backtrace as $one_backtrace) {
          $file = isset($one_backtrace['file'])?$one_backtrace['file']:"";
          $line = isset($one_backtrace['line'])?$one_backtrace['line']:"";
          $class = isset($one_backtrace['class'])?$one_backtrace['class']."::":"";
          $function = isset($one_backtrace['function'])?$one_backtrace['function']:"";
          $this->WriteLog("Developer: *WriteStatData $file:$line $class$function()", FALSE, FALSE, 8888, 'Debug', "");
        }
      }
      
      if ($write_needed) {
          if ($this->GetVerboseFlag()) {
            $this->WriteLog("Debug: **Writing stat data needed", FALSE, FALSE, 8888, 'Debug', "");
          }
          $result = $this->WriteData(array_merge(array('item'       => 'Stat',
                                                       'table'      => 'stat',
                                                       'folder'     => $this->GetConfigFolder(true),
                                                       'data_array' => $this->_stat_data
                                                      ), $write_stat_data_array));
      } else {
          if ($this->GetVerboseFlag()) {
            $this->WriteLog("Debug: **Writing stat data not needed (no change)", FALSE, FALSE, 8888, 'Debug', "");
          }
          $result = true;
      }
      return $result;
  }


  // Reset the temporary user array
  function ResetTempUserArray()
  {
      $temp_user_array = [];

      // First, we reset all values (we know the key based on the schema)
      foreach ($this->_sql_tables_schema['users'] as $valid_key => $valid_format) {
          $pos = mb_strpos(mb_strtoupper($valid_format,'UTF-8'), 'DEFAULT');
          $value = "";
          if ($pos !== FALSE) {
              $value = nullable_trim(mb_substr($valid_format, $pos + mb_strlen("DEFAULT")));
              if (("'" == mb_substr($value,0,1)) && ("'" == mb_substr($value,-1))) {
                  $value = mb_substr($value,1,-1);
              }
          }
          $temp_user_array[$valid_key] = $value;
      }

      // Request the pin as a prefix of the returned token value
      $temp_user_array['request_prefix_pin'] = $this->GetDefaultRequestPrefixPin();
      
      return $temp_user_array;
  }


  // Reset the user array
  function ResetUserArray()
  {
      $this->_user_data = [];
      $this->_user_data = $this->ResetTempUserArray();

      // The user data array is not read actually
      $this->SetUserDataReadFlag(false);
      return $this->_user_data;
  }


  function ResetTokenArray()
  {
      $this->_token_data = [];
      // First, we reset all values (we know the key based on the schema)
      foreach ($this->_sql_tables_schema['tokens'] as $valid_key => $valid_format) {
          $pos = mb_strpos(mb_strtoupper($valid_format,'UTF-8'), 'DEFAULT');
          $value = "";
          if ($pos !== FALSE) {
              $value = nullable_trim(mb_substr($valid_format, $pos + mb_strlen("DEFAULT")));
              if (("'" == mb_substr($value,0,1)) && ("'" == mb_substr($value,-1))) {
                  $value = mb_substr($value,1,-1);
              }
          }
          $this->_token_data[$valid_key] = $value;
      }
      $this->_token_data['issuer'] = $this->GetIssuer();
      
      // The token data array is not read actually
      $this->SetTokenDataReadFlag(FALSE);
  }


  function ResetDdnsArray()
  {
      $this->_ddns_data = [];
      foreach ($this->_sql_tables_schema['ddns'] as $valid_key => $valid_format) {
          $pos = mb_strpos(mb_strtoupper($valid_format,'UTF-8'), 'DEFAULT');
          $value = "";
          if ($pos !== FALSE) {
              $value = nullable_trim(mb_substr($valid_format, $pos + mb_strlen("DEFAULT")));
              if (("'" == mb_substr($value,0,1)) && ("'" == mb_substr($value,-1))) {
                  $value = mb_substr($value,1,-1);
              }
          }
          $this->_ddns_data[$valid_key] = $value;
      }
  }


  function ResetDeviceArray()
  {
      $this->_device_data = [];
      foreach ($this->_sql_tables_schema['devices'] as $valid_key => $valid_format) {
          $pos = mb_strpos(mb_strtoupper($valid_format,'UTF-8'), 'DEFAULT');
          $value = "";
          if ($pos !== FALSE) {
              $value = nullable_trim(mb_substr($valid_format, $pos + mb_strlen("DEFAULT")));
              if (("'" == mb_substr($value,0,1)) && ("'" == mb_substr($value,-1))) {
                  $value = mb_substr($value,1,-1);
              }
          }
          $this->_device_data[$valid_key] = $value;
      }
  }


  function ResetGroupArray()
  {
      $this->_group_data = [];
      foreach ($this->_sql_tables_schema['groups'] as $valid_key => $valid_format) {
          $pos = mb_strpos(mb_strtoupper($valid_format,'UTF-8'), 'DEFAULT');
          $value = "";
          if ($pos !== FALSE) {
              $value = nullable_trim(mb_substr($valid_format, $pos + mb_strlen("DEFAULT")));
              if (("'" == mb_substr($value,0,1)) && ("'" == mb_substr($value,-1))) {
                  $value = mb_substr($value,1,-1);
              }
          }
          $this->_group_data[$valid_key] = $value;
      }
  }


  function CleanPhoneNumber(
      $phone_number
  ) {
      $pn = nullable_trim(preg_replace('[\D]', "", $phone_number));
      // $pn_len = mb_strlen($pn);
    
      if ('00' == mb_substr($pn,0, 2)) {
          $pn = mb_substr($pn, 2);
      } elseif ('0' == mb_substr($pn,0, 1)) {
          $pn = $this->GetTelDefaultCountryCode() . mb_substr($pn, 1);
      }
      return $pn;
  }


  function GetClassName()
  {
      return $this->_class;
  }


  function GetVersion()
  {
      return $this->_version;
  }


  function GetDate()
  {
      return $this->_date;
  }


  function GetVersionDate()
  {
      return $this->_version." (".$this->_date.")";
  }


  function GetCurrentDateTime(
    $text_output = true
  ) {
    return ($text_output ? date('Y-m-d H:i:s').' '.date('T').' (GMT'.date('O').')' : time());
  }


  function GetUptime(
      $text_output = true
  ) {
    $upsecs = 0;
    $uptime = "";
    if (file_exists('/proc/uptime')) {
      if ($file = @fopen('/proc/uptime', 'r')) {
        $data = @fread($file, 128);
        if ($data !== false) {
          $upsecs = (int)mb_substr($data, 0, mb_strpos($data, ' '));
          $days = floor($upsecs/60/60/24);
          $hours = $upsecs/60/60%24;
          $minutes = $upsecs/60%60;
          $seconds = $upsecs%60;
          // $uptime = Array ( 'days' => $days, 'hours' => $hours, 'minutes' => $minutes, 'seconds' => $seconds );
          $uptime = $days." day".(($days>1)?'s':"").", ".mb_substr('00'.$hours, -2).':'.mb_substr('00'.$minutes, -2).':'.mb_substr('00'.$seconds, -2);
        }
        fclose($file);
      }
    } elseif (mb_strtolower(mb_substr(PHP_OS, 0, 3),'UTF-8') === 'win') {
      $upsecs = $this->ReadBootCacheValue("upsecs", _MULTIOTP_DEFAULT_UPTIME_DELAY_);
      if (is_null($upsecs)) {
        $upsecs = intval(nullable_trim(shell_exec('powershell -Command "(New-TimeSpan -Start (Get-CimInstance Win32_OperatingSystem).LastBootUpTime).TotalSeconds"')));
        if (empty($upsecs)) {
          $pagefile = 'C:\\pagefile.sys';
          if (!file_exists($pagefile)) {
            $pagefile = 'D:\\pagefile.sys';
            if (!file_exists($pagefile)) {
              $pagefile = "";
            }
          }
          if (!empty($pagefile)) {
            $gettime = (time() - filemtime($pagefile));
            $upsecs = $gettime;
          }
        }
        $this->WriteBootCacheValue("upsecs", $upsecs);
      }
      $days = floor($upsecs / 86400);
      $hours = floor(($upsecs % 86400) / 3600);
      $minutes = floor(($upsecs % 3600) / 60);
      $seconds = floor($upsecs % 60);
      $uptime = $days." day".(($days>1)?'s':"").", ".mb_substr('00'.$hours, -2).':'.mb_substr('00'.$minutes, -2).':'.mb_substr('00'.$seconds, -2);
    }
    return ($text_output ? $uptime : $upsecs);
  }


  function GetFullVersionInfo()
  {
      return $this->_class." ".$this->_version." (".$this->_date.")";
  }


  function GetCopyright()
  {
      return $this->_copyright;
  }


  function GetWebsite()
  {
      return $this->_website;
  }


  function SetSourceTag(
      $value = ""
  ) {
      $this->_source_tag = nullable_trim($value);
  }


  function GetSourceTag()
  {
      return nullable_trim($this->_source_tag);
  }


  function SetSourceIp(
      $value = ""
  ) {
      $this->_source_ip = $value;
  }


  function GetSourceIp()
  {
      return $this->_source_ip;
  }


  function SetSourceMac(
      $value
  ) {
      $this->_source_mac = (("00:00:00:00:00:00" != $value) ? $value : "");
  }


  function GetSourceMac()
  {
      return ("00:00:00:00:00:00" != $this->_source_mac) ? $this->_source_mac : "";
  }


  function SetCallingIp(
      $value
  ) {
      $this->_calling_ip = $value;
  }


  function GetCallingIp()
  {
      return $this->_calling_ip;
  }


  function SetCallingMac(
      $value
  ) {
      $this->_calling_mac = (("00:00:00:00:00:00" != $value) ? $value : "");
  }


  function GetCallingMac()
  {
      return ("00:00:00:00:00:00" != $this->_calling_mac) ? $this->_calling_mac : "";
  }


  function SetChapChallenge(
      $hex_value
  ) {
      $pos = mb_strpos(mb_strtolower($hex_value,'UTF-8'), 'x');
      if (FALSE === $pos) {
          $temp = $hex_value;
      } else {
          $temp = mb_substr($hex_value, $pos+1);
      }
      $this->_chap_challenge = mb_strtolower($temp,'UTF-8');
  }


  function GetChapChallenge()
  {
      return mb_strtolower($this->_chap_challenge,'UTF-8');
  }


  function SetChapPassword(
      $hex_value
  ) {
      $pos = mb_strpos(mb_strtolower($hex_value,'UTF-8'), 'x');
      if (FALSE === $pos) {
          $temp = $hex_value;
      } else {
          $temp = mb_substr($hex_value, $pos+1);
      }
      
      if (32 < mb_strlen($temp)) {
          $this->SetChapId(mb_substr($temp, 0, 2));
          $temp = mb_substr($temp, 2);
      }
      $this->_chap_password = mb_strtolower($temp,'UTF-8');
  }


  function GetChapPassword()
  {
      return mb_strtolower($this->_chap_password,'UTF-8');
  }


  function SetMsChapChallenge(
      $hex_value
  ) {
      $pos = mb_strpos(mb_strtolower($hex_value,'UTF-8'), 'x');
      if (FALSE === $pos) {
          $temp = $hex_value;
      } else {
          $temp = mb_substr($hex_value, $pos+1);
      }
      $this->_ms_chap_challenge = mb_strtolower($temp,'UTF-8');
  }


  function GetMsChapChallenge()
  {
      return mb_strtolower($this->_ms_chap_challenge,'UTF-8');
  }


  function SetMsChapResponse(
      $hex_value
  ) {
      $pos = mb_strpos(mb_strtolower($hex_value,'UTF-8'), 'x');
      if (FALSE === $pos) {
          $temp = $hex_value;
      } else {
          $temp = mb_substr($hex_value, $pos+1);
      }
      $this->_ms_chap_response = mb_strtolower($temp,'UTF-8');
  }


  function GetMsChapResponse()
  {
      return mb_strtolower((is_null($this->_ms_chap_response) ? "" : $this->_ms_chap_response),'UTF-8');
  }


  function SetMsChap2Response(
      $hex_value
  ) {
      $pos = mb_strpos(mb_strtolower($hex_value,'UTF-8'), 'x');
      if (FALSE === $pos) {
          $temp = $hex_value;
      } else {
          $temp = mb_substr($hex_value, $pos+1);
      }
      $this->_ms_chap2_response = mb_strtolower($temp,'UTF-8');
  }


  function GetMsChap2Response()
  {
      return mb_strtolower((is_null($this->_ms_chap2_response) ? "" : $this->_ms_chap2_response),'UTF-8');
  }


  function SetChapId(
      $hex_value
  ) {
      $pos = mb_strpos(mb_strtolower($hex_value,'UTF-8'), 'x');
      if (FALSE === $pos) {
          $temp = $hex_value;
      } else {
          $temp = mb_substr($hex_value, $pos+1);
      }
      $this->_chap_id = mb_strtolower($temp,'UTF-8');
  }


  function GetChapId()
  {
      return mb_strtolower($this->_chap_id,'UTF-8');
  }


  function SetNtKey(
      $hex_value
  ) {
      $temp = $hex_value;
      if (16 == strlen($temp)) {
          $temp = nullable_bin2hex($temp);
      }
      $pos = strpos(strtolower($temp), 'x');
      if (FALSE !== $pos) {
          $temp = substr($temp, $pos+1);
      }
      if (32 != strlen($temp)) {
          $temp = "";
      }
      $this->_ms_nt_key = strtoupper($temp); // ! THIS NON-MB strtoupper must stay as is !
  }


  function GetNtKey()
  {
      $temp = $this->_ms_nt_key;
      if (16 == strlen($temp)) {
          $temp = nullable_bin2hex($temp);
      } elseif (32 != strlen($temp)) {
          $temp = "";
      }
      return strtoupper($temp); // ! THIS NON-MB strtoupper must stay as is !
  }


  function GenerateNTMLv2(
    $account,
    $password,
    $domain = "",
    $client_challenge = "",
    $server_challenge = ""
  ) {
    $unicode_password= iconv ( 'UTF-8', 'UTF-16LE', $password );

    $NTLM_Key = mhash ( MHASH_MD4, $unicode_password);
    $NTLM_Hash = mhash ( MHASH_MD5, iconv ( 'UTF-8', 'UTF-16LE', strtoupper ( $account ) . $domain ), $NTLM_Key );
    $NTLM_Chal_Hash = mhash ( MHASH_MD5, pack ( "H*", $server_challenge . $client_challenge ), $NTLM_Hash );

    return strtoupper ( bin2hex ( $NTLM_Chal_Hash ) );
  }

  function SetState(
      $value
  ) {
      if ((0 === mb_strpos($value, '0x')) && (0 == (mb_strlen($value) % 2))) {
          $value = hex2bin(mb_substr($value, 2));
      }
      $this->_state = nullable_trim($value);
  }


  function GetState()
  {
    $value = $this->_state;
      if ((0 === mb_strpos($value, '0x')) && (0 == (mb_strlen($value) % 2))) {
          $value = hex2bin(mb_substr($value, 2));
      }
      return nullable_trim($value);
  }


  function GetSmsProvidersArray(
    $named = false
  ) {
    $sms = new MultiotpSms();
    $result = $sms->getSmsProvidersArray();
    if ($named) {
      return $result;
    } else {
      $unnamed_result = [];
      foreach ($result as $one_result) {
        $unnamed_result[] = [
                             $one_result["provider"],
                             $one_result["name"],
                             $one_result["web"],
                             $one_result["auth"],
                            ];
      }
      return $unnamed_result;
    }
  }


  function GetSmsProvidersList(
    $in_array = false
  ) {
    $result = [];
    $providers_list = "";
    foreach ($this->GetSmsProvidersArray(true) as $one_provider) {
      $result[] = $one_provider["name"].
                  (("" != $one_provider["provider"])?' ('.$one_provider["provider"].')':"").
                  (("" != $one_provider["web"])?', '.$one_provider["web"]:"");
    }
    return ($in_array ? $result : implode("\t", $result));
  }

  
  function SetSmsProvider(
      $value
  ) {
      $this->_config_data['sms_provider'] = $value;
  }


  function GetSmsProvider()
  {
    return (isset($this->_config_data['sms_provider']) ? $this->_config_data['sms_provider'] : "");
  }


  function SetSmsOriginator(
      $value
  ) {
      $this->_config_data['sms_originator'] = $value;
  }


  function GetSmsOriginator()
  {
    return (isset($this->_config_data['sms_originator']) ? $this->_config_data['sms_originator'] : "");
  }


  function SetTelDefaultCountryCode(
      $value
  ) {
      $this->_config_data['tel_default_country_code'] = $value;
  }


  function GetTelDefaultCountryCode()
  {
    return (isset($this->_config_data['tel_default_country_code']) ? $this->_config_data['tel_default_country_code'] : "");
  }


  function SetSmsUserkey(
      $value
  ) {
      $this->_config_data['sms_userkey'] = $value;
  }


  function SetSmsUsername(
      $value
  ) {
    $this->SetSmsUserkey($value);
  }


  function GetSmsUserkey()
  {
    return (isset($this->_config_data['sms_userkey']) ? $this->_config_data['sms_userkey'] : "");
  }


  function GetSmsUsername()
  {
      return $this->GetSmsUserkey();
  }


  function SetSmsPassword(
      $value
  ) {
      $this->_config_data['sms_password'] = $value;
  }


  function GetSmsPassword()
  {
    return (isset($this->_config_data['sms_password']) ? $this->_config_data['sms_password'] : "");
  }


  function SetSmsApiId(
      $value
  ) {
      $this->_config_data['sms_api_id'] = $value;
  }


  function GetSmsApiId()
  {
    return (isset($this->_config_data['sms_api_id']) ? $this->_config_data['sms_api_id'] : "");
  }


  function SetSmsUrl(
      $value
  ) {
      $this->_config_data['sms_url'] = $value;
  }


  function GetSmsUrl()
  {
    return (isset($this->_config_data['sms_url']) ? $this->_config_data['sms_url'] : "");
  }


  function SetSmsIp(
      $value
  ) {
      $this->_config_data['sms_ip'] = $value;
  }


  function GetSmsIp()
  {
    return (isset($this->_config_data['sms_ip']) ? $this->_config_data['sms_ip'] : "");
  }


  function SetSmsPort(
      $value
  ) {
      $this->_config_data['sms_port'] = $value;
  }


  function GetSmsPort()
  {
    return (isset($this->_config_data['sms_port']) ? $this->_config_data['sms_port'] : "");
  }


  function SetSmsSendTemplate(
      $value
  ) {
      $this->_config_data['sms_send_template'] = $value;
  }


  function GetSmsSendTemplate()
  {
    return (isset($this->_config_data['sms_send_template']) ? $this->_config_data['sms_send_template'] : "");
  }


  function SetSmsMethod(
      $value
  ) {
      $this->_config_data['sms_method'] = $value;
  }


  function GetSmsMethod()
  {
    return (isset($this->_config_data['sms_method']) ? $this->_config_data['sms_method'] : "");
  }


  function SetSmsHeader(
      $value
  ) {
      $this->_config_data['sms_header'] = $value;
  }


  function GetSmsHeader()
  {
    return (isset($this->_config_data['sms_header']) ? $this->_config_data['sms_header'] : "");
  }


  function SetSmsEncoding(
      $value
  ) {
      $this->_config_data['sms_encoding'] = $value;
  }


  function GetSmsEncoding()
  {
    return (isset($this->_config_data['sms_encoding']) ? $this->_config_data['sms_encoding'] : "");
  }


  function SetSmsStatusSuccess(
      $value
  ) {
      $this->_config_data['sms_status_success'] = $value;
  }


  function GetSmsStatusSuccess()
  {
    return (isset($this->_config_data['sms_status_success']) ? $this->_config_data['sms_status_success'] : "");
  }


  function SetSmsContentSuccess(
      $value
  ) {
      $this->_config_data['sms_content_success'] = $value;
  }


  function GetSmsContentSuccess()
  {
    return (isset($this->_config_data['sms_content_success']) ? $this->_config_data['sms_content_success'] : "");
  }


  function SetSmsNoDoubleZero(
      $value
  ) {
      $this->_config_data['sms_no_double_zero'] = ((intval($value) > 0)?1:0);
  }


  function GetSmsNoDoubleZero()
  {
      return (($this->_config_data['sms_no_double_zero'] > 0)?1:0);
  }


  function SetSmsInternationalFormat(
      $value
  ) {
      $this->_config_data['sms_international_format'] = ((intval($value) > 0)?1:0);
  }


  function GetSmsInternationalFormat()
  {
      return (($this->_config_data['sms_international_format'] > 0)?1:0);
  }


  function SetSmsBasicAuth(
      $value
  ) {
      $this->_config_data['sms_basic_auth'] = ((intval($value) > 0)?1:0);
  }


  function GetSmsBasicAuth()
  {
      return (($this->_config_data['sms_basic_auth'] > 0)?1:0);
  }


  function SetSmsContentEncoding(
      $value
  ) {
      $this->_config_data['sms_content_encoding'] = $value;
  }


  function GetSmsContentEncoding()
  {
    return (isset($this->_config_data['sms_content_encoding']) ? $this->_config_data['sms_content_encoding'] : "");
  }


  function SetDefaultAlgorithm(
      $value
  ) {
      $this->_config_data['default_algorithm'] = mb_strtolower($value,'UTF-8');
  }


  function GetDefaultAlgorithm()
  {
      return mb_strtolower($this->_config_data['default_algorithm'],'UTF-8');
  }


  function SetSmsCodeAllowed(
      $value
  ) {
      $this->_config_data['sms_code_allowed'] = ((intval($value) > 0)?1:0);
  }


  function GetSmsCodeAllowed()
  {
      return intval($this->_config_data['sms_code_allowed']);
  }


  function IsSmsCodeAllowed()
  {
      return (1 == intval($this->_config_data['sms_code_allowed']));
  }


  function SetEmailCodeAllowed(
      $value
  ) {
      $this->_config_data['email_code_allowed'] = ((intval($value) > 0)?1:0);
  }


  function GetEmailCodeAllowed()
  {
      return intval($this->_config_data['email_code_allowed']);
  }


  function IsEmailCodeAllowed()
  {
      return (1 == intval($this->_config_data['email_code_allowed']));
  }


  function SetDefaultRequestLdapPwd(
      $value
  ) {
      $this->_config_data['default_request_ldap_pwd'] = ((intval($value) > 0)?1:0);
  }


  function GetDefaultRequestLdapPwd()
  {
    return (isset($this->_config_data['default_request_ldap_pwd']) ? $this->_config_data['default_request_ldap_pwd'] : "");
  }


  function IsDefaultRequestLdapPwd()
  {
      return (1 == ($this->_config_data['default_request_ldap_pwd']));
  }


  function SetOverwriteRequestLdapPwd(
      $value
  ) {
      $this->_config_data['overwrite_request_ldap_pwd'] = ((intval($value) > 0)?1:0);
  }


  function GetOverwriteRequestLdapPwd()
  {
    return intval(isset($this->_config_data['overwrite_request_ldap_pwd']) ? $this->_config_data['overwrite_request_ldap_pwd'] : 0);
  }


  function IsOverwriteRequestLdapPwd()
  {
      return (1 == ($this->_config_data['overwrite_request_ldap_pwd']));
  }


  function SetDefaultPushEnabled(
      $value
  ) {
      $this->_config_data['default_push_enabled'] = ((intval($value) > 0)?1:0);
  }


  function GetDefaultPushEnabled()
  {
    return (isset($this->_config_data['default_push_enabled']) ? $this->_config_data['default_push_enabled'] : "");
  }


  function IsDefaultPushEnabled()
  {
      return (1 == ($this->_config_data['default_push_enabled']));
  }


  function SetOverwritePushEnabled(
      $value
  ) {
      $this->_config_data['overwrite_push_enabled'] = ((intval($value) > 0)?1:0);
  }


  function GetOverwritePushEnabled()
  {
    return intval(isset($this->_config_data['overwrite_push_enabled']) ? $this->_config_data['overwrite_push_enabled'] : 0);
  }


  function IsOverwritePushEnabled()
  {
      return (1 == ($this->_config_data['overwrite_push_enabled']));
  }


  function SetDefaultRequestPrefixPin(
      $value
  ) {
      $this->_config_data['default_request_prefix_pin'] = ((intval($value) > 0)?1:0);
  }


  function GetDefaultRequestPrefixPin()
  {
    return intval(isset($this->_config_data['default_request_prefix_pin']) ? $this->_config_data['default_request_prefix_pin'] : 0);
  }


  function IsDefaultRequestPrefixPin()
  {
      return (1 == ($this->_config_data['default_request_prefix_pin']));
  }


  function EnableLdapError()
  {
      $this->_last_ldap_error = TRUE;
  }


  function DisableLdapError()
  {
      $this->_last_ldap_error = FALSE;
  }


  function IsLdapError()
  {
      return $this->_last_ldap_error;
  }


  function SetLdapActivated(
      $value
  ) {
      $this->_config_data['ldap_activated'] = ((intval($value) > 0) ? 1 : 0);
      if (1 > intval($value)) {
          $this->PurgeLdapCacheFolder();
      }
  }


  function EnableLdapActivated()
  {
      $this->SetLdapActivated(1);
  }


  function DisableLdapActivated()
  {
      $this->SetLdapActivated(0);
  }


  function IsLdapActivated()
  {
      return (1 == ($this->_config_data['ldap_activated']));
  }


  function SetLdapDontAutoDisable(
    $value
  ) {
    $this->_config_data['ldap_dont_auto_disable'] = ((intval($value) > 0) ? 1 : 0);
  }


  function GetLdapDontAutoDisable()
  {
    return intval(isset($this->_config_data['ldap_dont_auto_disable']) ? $this->_config_data['ldap_dont_auto_disable'] : 0);
  }


  function IsLdapDontAutoDisable()
  {
    return (1 == ($this->_config_data['ldap_dont_auto_disable']));
  }


  function SetLdapDontAutoEnable(
    $value
  ) {
    $this->_config_data['ldap_dont_auto_enable'] = ((intval($value) > 0) ? 1 : 0);
  }


  function GetLdapDontAutoEnable()
  {
    return intval(isset($this->_config_data['ldap_dont_auto_enable']) ? $this->_config_data['ldap_dont_auto_enable'] : 0);
  }


  function IsLdapDontAutoEnable()
  {
    return (1 == ($this->_config_data['ldap_dont_auto_enable']));
  }


  function SetLdapSsl(
      $value
  ) {
      $this->_config_data['ldap_ssl'] = ((intval($value) > 0)?1:0);
  }


  function EnableLdapSsl()
  {
      $this->_config_data['ldap_ssl'] = 1;
  }


  function DisableLdapSsl()
  {
      $this->_config_data['ldap_ssl'] = 0;
  }


  function IsLdapSsl()
  {
      return (1 == ($this->_config_data['ldap_ssl']));
  }


  function SetLdapLanguageAttribute(
      $value
  ) {
      if ("" != nullable_trim($value)) {
          $this->_config_data['ldap_language_attribute'] = nullable_trim($value);
      }
  }


  function GetLdapLanguageAttribute()
  {
    return (isset($this->_config_data['ldap_language_attribute']) ? $this->_config_data['ldap_language_attribute'] : "");
  }


  function SetLdapAccountSuffix(
      $value
  ) {
      $this->_config_data['ldap_account_suffix'] = $value;
  }


  function GetLdapAccountSuffix()
  {
    return (isset($this->_config_data['ldap_account_suffix']) ? $this->_config_data['ldap_account_suffix'] : "");
  }


  function SetLdapCnIdentifier(
      $value
  ) {
    if ("" != nullable_trim($value)) {
        $this->_config_data['ldap_cn_identifier'] = nullable_trim($value);
    }
  }


  function GetLdapCnIdentifier()
  {
    $value = $this->_config_data['ldap_cn_identifier'];
    if ("" == nullable_trim($value)) {
      $value = "sAMAccountName";
    }
    return ($value);
  }


  function SetLdapSyncedUserAttribute(
      $value
  ) {
    $this->_config_data['ldap_synced_user_attribute'] = nullable_trim($value);
  }


  function GetLdapSyncedUserAttribute()
  {
    $result = ($this->_config_data['ldap_synced_user_attribute']);
    if ("" == nullable_trim($result)) {
      $result = $this->GetLdapCnIdentifier();
    }
    return $result;
  }


  function SetLdapDefaultAlgorithm(
      $value
  ) {
      if ($this->IsValidAlgorithm($value)) {
        $this->_config_data['ldap_default_algorithm'] = mb_strtolower($value,'UTF-8');
      }
  }


  function GetLdapDefaultAlgorithm()
  {
      return mb_strtolower($this->_config_data['ldap_default_algorithm'],'UTF-8');
  }


  function SetLdapGroupCnIdentifier(
      $value
  ) {
      if ("" != nullable_trim($value)) {
          $this->_config_data['ldap_group_cn_identifier'] = nullable_trim($value);
      }
  }


  function GetLdapGroupCnIdentifier()
  {
    $value = $this->_config_data['ldap_group_cn_identifier'];
    if ("" == nullable_trim($value)) {
      $value = "cn";
    }
    return ($value);
  }


  function GetLdapFieldsArray()
  {
      if ((1 == $this->GetLdapServerType()) || (3 == $this->GetLdapServerType())) { // Active Directory (enhanced or legacy)
          // "department" removed, while not used
          $ldap_fields = array($this->GetLdapCnIdentifier(),
                               "mail",
                               $this->GetLdapGroupAttribute(),
                               "displayName",
                               "description",
                               "telephoneNumber",
                               "primaryGroupID",
                               "mobile",
                               "msNPAllowDialin",
                               "userAccountControl", // (userAccountControl & 2) -> Account disabled
                               "ms-DS-User-Account-Control-Computed", // (ms-DS-User-Account-Control-Computed & 16) -> Account locked
                               "accountExpires", // Expiration of the account in 100-nanosecond (10000000 x epoch time !)
                               "distinguishedName",
                               "msRADIUSFramedIPAddress", // Static IP address to be assigned to the account
                               $this->GetLdapLanguageAttribute(),
                               $this->GetLdapSyncedUserAttribute(),
                               "userPrincipalName" // post-2000 Windows account
                              );
      } else { // Generic LDAP (2) or eDirectory (4), no attribute like "msNPAllowDialin" or "msRADIUSFramedIPAddress"
          /*
           * shadowexpire: -1: not used, otherwise, number of days since 01.01.1970 when the account will be disabled
           * sambaAcctFlags (details: http://pig.made-it.com/samba-accounts.html):
           *  U    Regular user account.
           *  W    Workstation Trust Account.
           *  S    Server Trust Account.
           *  I    Domain Trust Account.
           *  M    MNS logon user account (Majority Node Set (MNS) logon account).
           *  H    Home directory required.
           *  N    No password required. This means the account has no password.
           *  X    Password does not expire.
           *  D    Account disabled.
           *  T    Temporary duplicate of other account.
           *  L    The account has been automatically locked.
           */
          $ldap_fields = array($this->GetLdapCnIdentifier(),
                               "mail",
                               $this->GetLdapGroupAttribute(),
                               "displayName",
                               "description",
                               "gecos", // general information about the account
                               "telephoneNumber",
                               "gidNumber",
                               "mobile",
                               "sambaAcctFlags",
                               "shadowExpire",
                               "distinguishedName",
                               "radiusFramedIPAddress",
                               "radiusFramedIPNetmask",
                               $this->GetLdapLanguageAttribute(),
                               $this->GetLdapSyncedUserAttribute()
                              );
      }
      return ($ldap_fields);
  }


  function SetLdapBaseDn(
      $value
  ) {
      $this->_config_data['ldap_base_dn'] = $value;
  }


  function GetLdapBaseDn(
      $return_alternate_if_needed = FALSE
  )  {
      if ($return_alternate_if_needed && ("" == nullable_trim($this->_config_data['ldap_base_dn']))) {
        return $this->_config_data['ldap_users_dn'];
      } else {
        return $this->_config_data['ldap_base_dn'];
      }
  }


  function SetLdapUsersDn(
      $value
  ) {
      $this->_config_data['ldap_users_dn'] = $value;
  }


  function GetLdapUsersDn(
      $return_alternate_if_needed = FALSE
  )  {
      if ($return_alternate_if_needed && ("" == nullable_trim($this->_config_data['ldap_users_dn']))) {
        return $this->_config_data['ldap_base_dn'];
      } else {
        return $this->_config_data['ldap_users_dn'];
      }
  }


  function SetLdapBindDn(
      $value
  ) {
      $this->_config_data['ldap_bind_dn'] = $value;
  }


  function GetLdapBindDn()
  {
      return encode_utf8_if_needed($this->_config_data['ldap_bind_dn']);
  }


  function SetLdapDomainControllers(
      $value
  ) {
      $this->_config_data['ldap_domain_controllers'] = nullable_trim($value);
  }


  function GetLdapDomainControllers()
  {
    return (isset($this->_config_data['ldap_domain_controllers']) ? $this->_config_data['ldap_domain_controllers'] : "");
  }


  function GetLdapPrimaryController()
  {
      $domain_controllers = str_replace(","," ",str_replace(";"," ",$this->GetLdapDomainControllers()));
      $controllers_array = explode(" ",$this->GetLdapDomainControllers());
      return nullable_trim(isset($controllers_array[0])?$controllers_array[0]:"");
  }


  function GetLdapSecondaryController()
  {
      $domain_controllers = str_replace(","," ",str_replace(";"," ",$this->GetLdapDomainControllers()));
      $controllers_array = explode(" ",$this->GetLdapDomainControllers());
      return nullable_trim(isset($controllers_array[1])?$controllers_array[1]:"");
  }


  function SetLdapInGroup(
      $value
  ) {
      $this->_config_data['ldap_in_group'] = nullable_trim($value);

      $ldap_in_group_array = explode("\t",nullable_trim(str_replace(",","\t",str_replace(";","\t",$value))));

      $groups_array = [];
      $list = explode("\t", $this->GetGroupsList());
      $n = count($list);
      for($i = 0; $i < $n; $i++) {
          if($list[$i] != "") {
              $this->SetGroup($list[$i]);
              $groups_array[] = $this->GetGroupName();
          }
      }

      foreach ($ldap_in_group_array as $one_group) {
          if (!in_array(nullable_trim($one_group), $groups_array)) {
              if ("" != nullable_trim($one_group)) {
                  $this->CreateGroup("", nullable_trim($one_group), nullable_trim($one_group));
              }
          }
      }
  }


  function GetLdapInGroup($included_without2fa = false)
  {
    $groups = nullable_trim($this->_config_data['ldap_in_group']);
    if ((true === $included_without2fa) && ("" != $groups) && ("" != $this->GetLdapWithout2faInGroup())) {
      $groups = $this->GetLdapWithout2faInGroup() . "," . $groups;
    }
    return $groups;
  }


  function SetLdapWithout2faInGroup(
      $value
  ) {
      $this->_config_data['ldap_without2fa_in_group'] = nullable_trim($value);

      $ldap_without2fa_in_group_array = explode("\t",nullable_trim(str_replace(",","\t",str_replace(";","\t",$value))));

      $groups_array = [];
      $list = explode("\t", $this->GetGroupsList());
      $n = count($list);
      for($i = 0; $i < $n; $i++) {
          if($list[$i] != "") {
              $this->SetGroup($list[$i]);
              $groups_array[] = $this->GetGroupName();
          }
      }

      foreach ($ldap_without2fa_in_group_array as $one_group) {
          if (!in_array(nullable_trim($one_group), $groups_array)) {
              if ("" != nullable_trim($one_group)) {
                  $this->CreateGroup("", nullable_trim($one_group), nullable_trim($one_group));
              }
          }
      }
  }


  function GetLdapWithout2faInGroup()
  {
    return (isset($this->_config_data['ldap_without2fa_in_group']) ? $this->_config_data['ldap_without2fa_in_group'] : "");
  }


  function SetLdapGroupAttribute(
      $value
  ) {
      if ("" != nullable_trim($value)) {
          $this->_config_data['ldap_group_attribute'] = nullable_trim($value);
      }
  }


  function GetLdapGroupAttribute()
  {
    $value = $this->_config_data['ldap_group_attribute'];
    if ("" == nullable_trim($value)) {
      $value = "memberOf";
    }
    return ($value);
  }


  function SetLdapServerPassword(
      $value
  ) {
      $this->_config_data['ldap_server_password'] = $value;
  }


  function GetLdapServerPassword()
  {
      return encode_utf8_if_needed($this->_config_data['ldap_server_password']);
  }


  function SetLdapPort(
      $value
  ) {
      $this->_config_data['ldap_port'] = intval($value);
  }


  function GetLdapPort()
  {
    return intval(isset($this->_config_data['ldap_port']) ? $this->_config_data['ldap_port'] : 389);
  }


  function SetLdapServerType(
      $value,
      $default_parameters = false
  ) {
      $this->_config_data['ldap_server_type'] = intval($value);
      
      // These values are not in the options for now
      if (1 == $value) { // enhanced Active Directory
          $this->SetLdapCnIdentifier('sAMAccountName');
          $this->SetLdapGroupAttribute("memberOf");
          $this->SetLdapGroupCnIdentifier('cn');
      } elseif (3 == $value) { // legacy Active Directory
          $this->SetLdapCnIdentifier('sAMAccountName');
          $this->SetLdapGroupAttribute("memberOf");
          $this->SetLdapGroupCnIdentifier('cn');
      } elseif (4 == $value) { // eDirectory
          $this->SetLdapCnIdentifier('uid');
          $this->SetLdapGroupAttribute("groupMembership");
          $this->SetLdapGroupCnIdentifier('cn');
      } elseif (5 == $value) { // Microsoft Entra ID /* TODO : implement Microsoft Entra ID */
          $this->SetLdapCnIdentifier("");
          $this->SetLdapGroupAttribute("");
          $this->SetLdapGroupCnIdentifier("");
      } else { // Generic LDAP or eDirectory
          $this->SetLdapCnIdentifier('uid');
          $this->SetLdapGroupAttribute("memberOf");
          $this->SetLdapGroupCnIdentifier('cn');
      }
      if ($default_parameters) {
          if (1 == $value) { // enhanced Active Directory
              $this->SetLdapCnIdentifier('sAMAccountName');
              $this->SetLdapGroupAttribute("memberOf");
          } elseif (3 == $value) { // legacy Active Directory
              $this->SetLdapCnIdentifier('sAMAccountName');
              $this->SetLdapGroupAttribute("memberOf");
          } elseif (4 == $value) { // eDirectory
              $this->SetLdapCnIdentifier('uid');
              $this->SetLdapGroupAttribute("groupMembership");
          } else { // Generic LDAP
              $this->SetLdapCnIdentifier('uid');
              $this->SetLdapGroupAttribute("memberOf");
          }
      }
  }


  function GetLdapServerType()
  {
    return intval(isset($this->_config_data['ldap_server_type']) ? $this->_config_data['ldap_server_type'] : 1);
  }


  function SetLdapFilter(
      $value = ""
  ) {
      $this->_config_data['ldap_filter'] = nullable_trim($value);
  }


  function GetLdapFilter()
  {
    return (isset($this->_config_data['ldap_filter']) ? $this->_config_data['ldap_filter'] : "");
  }


  function SetLdapTimeLimit(
      $value
  ) {
      $this->_config_data['ldap_time_limit'] = intval($value);
  }


  function GetLdapTimeLimit()
  {
    return intval(isset($this->_config_data['ldap_time_limit']) ? $this->_config_data['ldap_time_limit'] : 30);
  }


  function SetSyncDeleteRetentionDays(
      $value
  ) {
    $this->_config_data['sync_delete_retention_days'] = intval($value);
  }


  function GetSyncDeleteRetentionDays()
  {
    return intval($this->_config_data['sync_delete_retention_days']);
  }


  function SetLdapNetworkTimeout(
      $value
  ) {
      $this->_config_data['ldap_network_timeout'] = intval($value);
  }


  function GetLdapNetworkTimeout()
  {
      return intval($this->_config_data['ldap_network_timeout']);
  }


  function SetLdapHashCacheTime(
      $value
  ) {
      $this->_config_data['ldap_hash_cache_time'] = intval($value);
  }


  function GetLdapHashCacheTime()
  {
    return intval(isset($this->_config_data['ldap_hash_cache_time']) ? $this->_config_data['ldap_hash_cache_time'] : 604800);
  }


  function SetLdapTlsReqcert(
      $value
  ) {
      $this->_config_data['ldaptls_reqcert'] = nullable_trim($value);
  }


  function GetLdapTlsReqcert()
  {
    $value = $this->_config_data['ldaptls_reqcert'];
    if ("" == nullable_trim($value)) {
      $value = "auto";
    }
    return ($value);
  }


  function SetLdapTlsCipherSuite(
      $value
  ) {
      $this->_config_data['ldaptls_cipher_suite'] = nullable_trim($value);
  }


  function GetLdapTlsCipherSuite()
  {
    $value = $this->_config_data['ldaptls_cipher_suite'];
    if ("" == nullable_trim($value)) {
      $value = "auto";
    }
    return ($value);
  }


  function SetLdapTlsOptions() {
      $ldaptls_reqcert = $this->GetLdapTlsReqcert();
      if ('auto' == $ldaptls_reqcert) {
          if (mb_strtolower(mb_substr(PHP_OS, 0, 3),'UTF-8') === 'win') {
              // Ignore the LDAP certificate validity (for Windows only)
              putenv('LDAPTLS_REQCERT=never');
          }
      } elseif ("" != $ldaptls_reqcert) {
          putenv('LDAPTLS_REQCERT='.$ldaptls_reqcert);
      }

      $ldaptls_cipher_suite = $this->GetLdapTlsCipherSuite();
      if ('auto' == $ldaptls_cipher_suite) {
          if (!version_compare(phpversion(), '7', '>=')) {
              // Don't handle the TLS1.2 protocol during LDAP synchronization
              // (not compatible with Windows 2012 implementation)
              // GnuTLS Cipher information: http://gnutls.org/manual/html_node/Priority-Strings.html
              putenv('LDAPTLS_CIPHER_SUITE=NORMAL:!VERS-TLS1.2');
          }
      } elseif ("" != $ldaptls_cipher_suite) {
          putenv('LDAPTLS_CIPHER_SUITE='.$ldaptls_cipher_suite);
      }
  }


  function SetSmsMessage(
      $value
  ) {
      $this->_config_data['sms_message_prefix'] = $value;
  }


  function GetSmsMessage()
  {
        $value = $this->_config_data['sms_message_prefix'];
        if ("" == nullable_trim($value)) {
          $value = "%s is your SMS-Code";
        }
      return ($value);
  }


  function SetSmsDigits(
    $value
  ) {
    $digits = intval($value);
    if ($digits < 6) {
      $digits = 6;
    } elseif ($digits > 32) {
      $digits = 32;
    }
    $this->_config_data['sms_digits'] = intval($digits);
  }


  function GetSmsDigits()
  {
    $value = intval($this->_config_data['sms_digits']);
    if ($value < 4) {
      $value = 4;
    } elseif ($value > 32) {
      $value = 32;
    }
    return intval($value);
  }


  function SetSmsTimeout(
      $value
  ) {
      $this->_config_data['sms_timeout'] = intval($value);
  }


  function GetSmsTimeout()
  {
    return intval(isset($this->_config_data['sms_timeout']) ? $this->_config_data['sms_timeout'] : 180);
  }


  function SetEmailDigits(
      $value
  ) {
      $this->_config_data['email_digits'] = intval($value);
  }


  function GetEmailDigits()
  {
    return intval(isset($this->_config_data['email_digits']) ? $this->_config_data['email_digits'] : 6);
  }


  function SetEmailCodeTimeout(
      $value
  ) {
      $this->_config_data['email_code_timeout'] = intval($value);
  }


  function GetEmailCodeTimeout()
  {
    return intval(isset($this->_config_data['email_code_timeout']) ? $this->_config_data['email_code_timeout'] : 600);
  }


  function SetDefaultPinDigits(
      $value
  ) {
    $digits = intval($value);
    if ($digits < 4) {
      $digits = 4;
    } elseif ($digits > 32) {
      $digits = 32;
    }
    $this->_config_data['default_pin_digits'] = intval($digits);
  }


  function GetDefaultPinDigits()
  {
    $value = intval($this->_config_data['default_pin_digits']);
    if ($value < 4) {
      $value = 4;
    } elseif ($value > 32) {
      $value = 32;
    }
    return intval($value);
  }


  function SetDefault2faDigits(
      $value
  ) {
    $digits = intval($value);
    if ($digits < 6) {
      $digits = 6;
    } elseif ($digits > 16) {
      $digits = 16;
    }
    $this->_config_data['default_2fa_digits'] = intval($digits);
  }


  function GetDefault2faDigits()
  {
    $value = intval($this->_config_data['default_2fa_digits']);
    if ($value < 6) {
      $value = 6;
    } elseif ($value > 16) {
      $value = 16;
    }
    return intval($value);
  }


  function GeneratePin(
    $value = 0
  ) {
    $digits = intval($value);
    if ($digits < 1) {
      $digits = $this->GetDefaultPinDigits();
    } elseif ($digits < 4) {
      $digits = 4;
    } elseif ($digits > 32) {
      $digits = 32;
    }
    do {
      $regenerate = false;
      $result = "";
      $last_digit = "";
      $same_counter = 0;
      $increment_counter = 0;
      $decrement_counter = 0;
      for ($i = 0; $i < $digits; $i++) {
        $digit = strval(mt_rand(0,9));
        if (strval($digit) == strval($last_digit)) {
          $same_counter++;
          $increment_counter = 0;
          $decrement_counter = 0;
        } elseif (intval($digit-1) == intval($last_digit)) {
          $same_counter = 0;
          $increment_counter++;
          $decrement_counter = 0;
        } elseif (intval($digit+1) == intval($last_digit)) {
          $same_counter = 0;
          $increment_counter = 0;
          $decrement_counter++;
        } else {
          $same_counter = 0;
          $increment_counter = 0;
          $decrement_counter = 0;
        }
        if (($same_counter >= 2) || ($increment_counter >= 3) || ($decrement_counter >= 3)) {
          $regenerate = true;
          break;
        }
        $last_digit = $digit;
        $result.= $digit;
      }
    } while ($regenerate);
    return $result;
  }


  function SetConfigAttribute(
      $attribute,
      $value
  ) {
      $result = FALSE;
      if ($this->IsOptionInSchema('config',$attribute)) {
          $this->_config_data[$attribute] = $value;
          $result = TRUE;
      }
      return $result;
  }


  function GetConfigAttribute(
      $attribute
  ) {
      return isset($this->_config_data[$attribute])?$this->_config_data[$attribute]:"";
  }


  function SetMaxTimeWindow(
      $time_window
  ) {
      $this->_config_data['max_time_window'] = intval($time_window);
  }


  function GetMaxTimeWindow()
  {
      return $this->_config_data['max_time_window'];
  }


  function SetMaxTimeResyncWindow(
      $time_resync_window
  ) {
      $this->_config_data['max_time_resync_window'] = intval($time_resync_window);
  }


  function GetMaxTimeResyncWindow()
  {
      return $this->_config_data['max_time_resync_window'];
  }


  function SetMaxEventWindow(
      $event_window
  ) {
      $this->_config_data['max_event_window'] = intval($event_window);
  }


  function GetMaxEventWindow()
  {
      return $this->_config_data['max_event_window'];
  }


  function SetMaxEventResyncWindow(
      $event_resync_window
  ) {
      $this->_config_data['max_event_resync_window'] = intval($event_resync_window);
  }


  function GetMaxEventResyncWindow()
  {
      return $this->_config_data['max_event_resync_window'];
  }


  function SetMaxBlockFailures(
      $max_failures
  ) {
      $this->_config_data['max_block_failures'] = $max_failures;
  }


  function GetMaxBlockFailures()
  {
      return $this->_config_data['max_block_failures'];
  }


  function SetServerCacheLevel(
      $value
  ) {
      $this->_config_data['server_cache_level'] = intval($value);
  }


  function GetServerCacheLevel()
  {
      return intval($this->_config_data['server_cache_level']);
  }


  function SetServerCacheLifetime(
      $value
  ) {
      $this->_config_data['server_cache_lifetime'] = intval($value);
  }


  function GetServerCacheLifetime()
  {
      return intval($this->_config_data['server_cache_lifetime']);
  }


  function SetServerChallenge(
      $value
  ) {
      $this->_server_challenge = $value;
  }


  function GetServerChallenge()
  {
      return $this->_server_challenge;
  }


  function SetServerSecret(
      $value
  ) {
      $this->_config_data['server_secret'] = $value;
  }


  function GetServerSecret($specific_ip = "") {
      $value = $this->_config_data['server_secret'];
      if ("" == nullable_trim($value)) {
        $value = "ClientServerSecret";
      }
      return ($value);
  }


  function SetServerType(
      $value
  ) {
      $this->_config_data['server_type'] = $value;
  }


  function GetServerType()
  {
      $value = $this->_config_data['server_type'];
      if ("" == nullable_trim($value)) {
        $value = "xml";
      }
      return ($value);
  }


  function SetServerTimeout(
      $value
  ) {
      $this->_config_data['server_timeout'] = intval($value);
  }


  function GetServerTimeout()
  {
      return intval($this->_config_data['server_timeout']);
  }


  function SetServerUrl(
      $value
  ) {
      $cleaned_server_url = nullable_trim(str_replace(",",";",str_replace(" ",";",$value)));
      $this->_config_data['server_url'] = $cleaned_server_url;
  }


  function GetServerUrl()
  {
      $cleaned_server_url = nullable_trim(str_replace(",",";",str_replace(" ",";",$this->_config_data['server_url'])));
      $this->_config_data['server_url'] = $cleaned_server_url;
      return $cleaned_server_url;
  }


  function SetSelfRegistration(
      $value
  ) {
      $this->_config_data['self_registration'] = ((intval($value) > 0)?1:0);
  }


  function EnableSelfRegistration()
  {
      $this->_config_data['self_registration'] = 1;
  }


  function DisableSelfRegistration()
  {
      $this->_config_data['self_registration'] = 0;
  }


  function IsSelfRegistrationEnabled()
  {
      return (1 == ($this->_config_data['self_registration']));
  }


  function SetAutoResync(
      $value
  ) {
      $this->_config_data['auto_resync'] = ((intval($value) > 0)?1:0);
  }


  function EnableAutoResync()
  {
      $this->_config_data['auto_resync'] = 1;
  }


  function DisableAutoResync()
  {
      $this->_config_data['auto_resync'] = 0;
  }


  function IsAutoResync()
  {
      return (1 == ($this->_config_data['auto_resync']));
  }


  function SetCacheData(
      $value
  ) {
      $this->_config_data['cache_data'] = ((intval($value) > 0)?1:0);
  }


  function EnableCacheData()
  {
      $this->_config_data['cache_data'] = 1;
  }


  function DisableCacheData()
  {
      $this->_config_data['cache_data'] = 0;
  }


  function IsCacheData()
  {
      return (1 == ($this->_config_data['cache_data']));
  }

  
  function SetGlobalChallengeResponse($value) {
      $this->_config_data['challenge_response_enabled'] = ((intval($value) > 0)?1:0);
  }


  function EnableGlobalChallengeResponse() {
      $this->_config_data['challenge_response_enabled'] = 1;
  }


  function DisableGlobalChallengeResponse() {
      $this->_config_data['challenge_response_enabled'] = 0;
  }


  function IsGlobalChallengeResponse() {
      return (1 == ($this->_config_data['challenge_response_enabled']));
  }

  
  function SetGlobalSmsChallenge($value) {
      $this->_config_data['sms_challenge_enabled'] = ((intval($value) > 0)?1:0);
  }


  function EnableGlobalSmsChallenge() {
      $this->_config_data['sms_challenge_enabled'] = 1;
  }


  function DisableGlobalSmsChallenge() {
      $this->_config_data['sms_challenge_enabled'] = 0;
  }


  function IsGlobalSmsChallenge() {
      return (1 == ($this->_config_data['sms_challenge_enabled']));
  }


  function SetGlobalTextSmsChallenge($value) {
      $this->_config_data['text_sms_challenge'] = nullable_trim($value);
  }


  function GetGlobalTextSmsChallenge() {
    $value = nullable_trim($this->_config_data['text_sms_challenge']);
    if ("" == nullable_trim($value)) {
      $value = "Please enter the code received on your mobile phone";
    }
    return ($value);
  }


  function SetGlobalTextTokenChallenge($value) {
      $this->_config_data['text_token_challenge'] = nullable_trim($value);
  }


  function GetGlobalTextTokenChallenge() {
    $value = nullable_trim($this->_config_data['text_token_challenge']);
    if ("" == nullable_trim($value)) {
      $value = "Please enter the code displayed on the token";
    }
    return ($value);
  }


  function SetCaseSensitiveUsers($value)
  {
      $this->_config_data['case_sensitive_users'] = ((intval($value) > 0)?1:0);
  }


  function EnableCaseSensitiveUsers()
  {
      $this->_config_data['case_sensitive_users'] = 1;
  }


  function DisableCaseSensitiveUsers()
  {
      $this->_config_data['case_sensitive_users'] = 0;
  }


  function IsCaseSensitiveUsers()
  {
      return (1 == ($this->_config_data['case_sensitive_users']));
  }


  function SetEncodeFileId()
  {
      $this->_config_data['encode_file_id'] = ((intval($value) > 0)?1:0);
  }


  function EnableEncodeFileId()
  {
      $this->_config_data['encode_file_id'] = 1;
  }


  function DisableEncodeFileId()
  {
      $this->_config_data['encode_file_id'] = 0;
  }


  function IsEncodeFileId()
  {
      return (1 == ($this->_config_data['encode_file_id']));
  }


  function EncodeFileId($id, $case_sensitive = FALSE, $force_regular = FALSE) {
      if ($this->IsEncodeFileId() && (!$force_regular)) {
          if ($case_sensitive) {
              return "id0x".nullable_bin2hex($id);
          } else {
              return "id0x".nullable_bin2hex(mb_strtolower($id,'UTF-8'));
          }
      } else {
          if ($case_sensitive) {
              return str_replace('/',"",$id);
          } else {
              return mb_strtolower(str_replace('/',"",$id),'UTF-8');
          }
      }
  }


  function DecodeFileId($id) {
      if ("id0x" == mb_substr($id."  ", 0, 4)) {
          return hex2bin(mb_substr($id, 4));
      } else {
         return $id;
      }
  }


  function SetNtpServer(
      $ntp_server
  ) {
      $this->_config_data['ntp_server'] = $ntp_server;
  }


  function GetNtpServer()
  {
      return nullable_trim($this->_config_data['ntp_server']);
  }


  function SetRadiusReplyAttributor(
      $radius_reply_attributor
  ) {
      $this->_config_data['radius_reply_attributor'] = $radius_reply_attributor;
  }


  function GetRadiusReplyAttributor()
  {
      return ($this->_config_data['radius_reply_attributor']);
  }


  function SetRadiusReplySeparator(
      $radius_reply_separator
  ) {
      switch (mb_strtolower($radius_reply_separator,'UTF-8')) {
          case 'colon':
              $radius_reply_separator = ':';
              break;
          case 'comma':
              $radius_reply_separator = ',';
              break;
          case 'cr':
              $radius_reply_separator = chr(13);
              break;
          case 'crlf':
              $radius_reply_separator = chr(13).chr(10);
              break;
          case 'lf':
              $radius_reply_separator = chr(10);
              break;
          case 'semicolon':
              $radius_reply_separator = ';';
              break;
      }
      $this->_config_data['radius_reply_separator_hex'] = nullable_bin2hex($radius_reply_separator);
  }


  function GetRadiusReplySeparator()
  {
      return hex2bin($this->_config_data['radius_reply_separator_hex']);
  }

  
  function SetRadiusErrorReplyMessage(
      $radius_error_reply_message
  ) {
      $this->_config_data['radius_error_reply_message'] = intval($radius_error_reply_message);
  }


  function GetRadiusErrorReplyMessage()
  {
      return intval($this->_config_data['radius_error_reply_message']);
  }


  function IsRadiusErrorReplyMessage(): bool {
    return (1 == $this->_config_data['radius_error_reply_message']);
  }


  function SetTimezone($timezone): void {
    $this->_config_data['timezone'] = $timezone;
    if (function_exists('date_default_timezone_set'))
    {
      date_default_timezone_set($timezone);
    }
  }


  function GetTimezone() {
    $timezone = nullable_trim(isset($this->_config_data['timezone'])?$this->_config_data['timezone']:"");
    if (("" != $timezone) && (function_exists('date_default_timezone_set'))) {
      date_default_timezone_set($timezone);
    }
    return $timezone;
  }


  function SetSmtpAuth(int $value): void {
    $this->_config_data['smtp_auth'] = ((intval($value) > 0)?1:0);
  }


  function GetSmtpAuth(): int {
    return (($this->_config_data['smtp_auth'] > 0)?1:0);
  }


  function IsSmtpAuth(): bool {
    return (1 == ($this->_config_data['smtp_auth']));
  }


  function SetSmtpPassword(string $value): void {
    $this->_config_data['smtp_password'] = $value;
  }


  function GetSmtpPassword(): string {
    return $this->_config_data['smtp_password'];
  }


  function SetSmtpPort(int $value): void {
    $this->_config_data['smtp_port'] = intval($value);
  }


  function GetSmtpPort(): int {
    return intval($this->_config_data['smtp_port']);
  }


  function SetSmtpSender(string $value): void {
    $this->_config_data['smtp_sender'] = $value;
  }


  function GetSmtpSenderName(): string {
    return $this->_config_data['smtp_sender_name'];
  }


  function SetSmtpSenderName(string $value): void {
    $this->_config_data['smtp_sender_name'] = $value;
  }


  function GetSmtpSender(): string {
    return $this->_config_data['smtp_sender'];
  }


  function SetSmtpServer(string $value): void {
    if (!$this->IsDemoMode()) {
      $this->_config_data['smtp_server'] = $value;
    }
  }


  function GetSmtpServer(): string {
    return $this->_config_data['smtp_server'];
  }


  function SetSmtpSsl(int $value): void {
    $this->_config_data['smtp_ssl'] = ((intval($value) > 0)?1:0);
  }


  function GetSmtpSsl(): int {
    return (($this->_config_data['smtp_ssl'] > 0)?1:0);
  }


  function SetSmtpUsername(string $value): void {
    $this->_config_data['smtp_username'] = $value;
  }


  function GetSmtpUsername(): string {
    return (isset($this->_config_data['smtp_username']) ? $this->_config_data['smtp_username'] : "");
  }


  function SetSyslogFacility(int $value): void {
    $syslog_facility = $value;
    if (($syslog_facility < 16) || ($syslog_facility > 23)) {
      $syslog_facility = _MULTIOTP_DEFAULT_SYSLOG_FACILITY_;
    }
    $this->_config_data['syslog_facility'] = $syslog_facility;
  }


  function GetSyslogFacility(): int {
    $syslog_facility = (!empty($this->_config_data['syslog_facility'])) ? intval($this->_config_data['syslog_facility']) : _MULTIOTP_DEFAULT_SYSLOG_FACILITY_;
    if (($syslog_facility < 16) || ($syslog_facility > 23)) {
      $syslog_facility = _MULTIOTP_DEFAULT_SYSLOG_FACILITY_;
    }
    return $syslog_facility;
  }


  function SetSyslogLevel(int $value): void {
    $syslog_level = $value;
    if (($syslog_level < 0) || ($syslog_level > 7)) {
      $syslog_level = _MULTIOTP_DEFAULT_SYSLOG_SEVERITY_LEVEL_;
    }
    $this->_config_data['syslog_level'] = $syslog_level;
  }

  // NOTE - DEPRECATED
  function GetSyslogLevel(): int {
    return GetSyslogMaxLevel();
  }


  function GetSyslogMaxLevel(): int {
    $syslog_level = (!empty($this->_config_data['syslog_level'])) ? intval($this->_config_data['syslog_level']) : _MULTIOTP_DEFAULT_SYSLOG_FACILITY_;
    if (($syslog_level < 0) || ($syslog_level > 7)) {
      $syslog_level = _MULTIOTP_DEFAULT_SYSLOG_SEVERITY_LEVEL_;
    }
    return $syslog_level;
  }


  function SetSysLogPort(int $value): void {
    $this->_config_data['syslog_port'] = intval($value);
  }


  function GetSysLogPort(): int {
    return intval($this->_config_data['syslog_port']);
  }


  function SetSysLogServer($value): void {
    $this->_config_data['syslog_server'] = $value;
  }


  function GetSysLogServer(): string {
    return (isset($this->_config_data['syslog_server']) ? $this->_config_data['syslog_server'] : "");
  }


  function IsSysLogServerBad(): bool {
    return $this->_bad_syslog_server;
  }


  function EnableBadSysLogServer(): void {
    $this->_bad_syslog_server = true;
  }


  function SetSysLogTimeout(int $value): void {
    $this->_config_data['syslog_timeout'] = intval($value);
  }


  function GetSysLogTimeout(): int {
    $value = intval(isset($this->_config_data['syslog_timeout']) ? $this->_config_data['syslog_timeout'] : _MULTIOTP_DEFAULT_SYSLOG_TIMEOUT_);
    if (0 == $value) {
      $value = _MULTIOTP_DEFAULT_SYSLOG_TIMEOUT_;
    }
    return $value;
  }




  /**
   * @brief   DEPRECATED: Define the SQL parameters for the MySQL backend
   *
   * @param   string  $sql_server        MySQL server
   * @param   string  $sql_user          MySQL user
   * @param   string  $sql_passwd        MySQL password
   * @param   string  $sql_db            MySQL database
   * @param   string  $sql_log_table     MySQL log table
   * @param   string  $sql_users_table   MySQL users table
   * @param   string  $sql_tokens_table  MySQL tokens table
   * @retval  void
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.0.1
   * @date    2013-06-09
   * @since   2010-12-18
   */
  function DefineMySqlConnection(
    $sql_server,
    $sql_user,
    $sql_passwd,
    $sql_db,
    $sql_log_table = null,
    $sql_users_table = null,
    $sql_tokens_table = null
  ) {
    // Backend storage type
    $this->SetBackendType('mysql');
    $this->SetSqlServer($sql_server);
    $this->SetSqlUsername($sql_user);
    $this->SetSqlPassword($sql_passwd);
    $this->SetSqlDatabase($sql_db);
    
    // If table names are not defined, we keep the default value defined in the class constructor.
    if (null !== $sql_log_table) {
      $this->SetSqlTableName('log', $sql_log_table);
    }
    if (null !== $sql_users_table) {
      $this->SetSqlTableName('users', $sql_users_table);
    }
    if (null !== $sql_tokens_table) {
      $this->SetSqlTableName('tokens', $sql_tokens_table);
    }
  }


  /*********************************************************************
   *
   * Name: ComputeMotp
   * Short description: Compute the mOTP result
   *
   * Creation 2010-06-07
   * Update 2013-12-23
   * @package multiotp
   * @version 2.0.0
   * @author SysCo/al
   *
   * @param   string  $seed_and_pin  Key used to compute the mOTP result (seed is in hexa)
   * @param   int     $timestep      Timestep used to calculate the token
   * @param   int     $token_size    Token size
   * @return  string                 mOTP result
   *
   *********************************************************************/
  function ComputeMotp(
    $seed_and_pin,
    $timestep,
    $token_size
  ) {
    return strtolower(substr(md5($timestep.$seed_and_pin),0,$token_size));
  }


  /*********************************************************************
   *
   * Name: GenerateOathHotp
   * Short description: Compute the HOTP token
   *
   * Creation 2013-11-26
   * Update 2013-12-23
   * @package multiotp
   * @version 4.1.0
   * @author SysCo/al
   *
   * @param   string  $key      Key used to compute the HOTP token
   * @param   int     $counter  Counter position
   * @param   int     $length   Token size
   * @return  string            HOTP token
   *
   *********************************************************************/
  function GenerateOathHotp(
    $key,
    $counter,
    $length = 6,
    $hash_algo = 'HMAC-SHA1'
  ) {
    return $this->ComputeOathTruncate($this->ComputeOathHotp($key, $counter, $hash_algo),$length);
  }    


  /*********************************************************************
   *
   * Name: ComputeOathHotp
   * Short description: Compute the OATH defined hash
   *
   * Creation 2010-06-07
   * Update 2021-03-14
   * @package multiotp
   * @version 3.0.0
   * @author SysCo/al
   *
   * @param   string  $key      Key used to compute the OATH hash
   * @param   int     $counter  Counter position
   * @return  string            Full OATH hash
   *
   *********************************************************************/
  function ComputeOathHotp(
    $key,
    $counter,
    $hash_algo = 'HMAC-SHA1'
  ) {
    // Counter
    //the counter value can be more than one byte long, so we need to go multiple times
    $cur_counter = array(0,0,0,0,0,0,0,0);
    for($i=7;$i>=0;$i--) {
      $cur_counter[$i] = pack ('C*', $counter);
      $counter = $counter >> 8;
    }
    $bin_counter = implode($cur_counter);
    // Pad to 8 chars
    if (strlen ($bin_counter) < 8) {
      $bin_counter = str_repeat(chr(0), 8 - strlen($bin_counter)) . $bin_counter;
    }

    // HMAC hash
    if ('HMAC-SHA512' == strtoupper($hash_algo)) { // ! THIS NON-MB strtoupper must stay as is !
      $hash = hash_hmac('sha512', $bin_counter, $key);
    } elseif ('HMAC-SHA256' == strtoupper($hash_algo)) { // ! THIS NON-MB strtoupper must stay as is !
      $hash = hash_hmac('sha256', $bin_counter, $key);
    } elseif ('HMAC-MD5' == strtoupper($hash_algo)) { // ! THIS NON-MB strtoupper must stay as is !
      $hash = hash_hmac('md5', $bin_counter, $key);
    } else {
      $hash = hash_hmac('sha1', $bin_counter, $key);
    }
    return $hash;
  }


  /*********************************************************************
   *
   * Name: ComputeOathTruncate
   * Short description: Truncate the result as defined by the OATH
   *
   * Creation 2010-06-07
   * Update   2014-01-15
   * @package multiotp
   * @version 4.1.1
   * @author SysCo/al
   *
   * @param   string  $hash     Full OATH hash to be truncated
   * @param   int     $length   Length of the result token
   * @return  string            Truncated OATH hash
   *
   *********************************************************************/
  function ComputeOathTruncate(
      $hash,
      $length = 6
  ) {
      // Convert hash to decimal
      foreach (str_split($hash,2) as $hex)
      {
          $hmac_result[]=hexdec($hex);
      }

      // Find offset
      $offset = $hmac_result[(strlen($hash)/2)-1] & 0xf;

      // Adapted algorithm ("substr -10" instead of "% pow(10,$length)")
      $result = substr(str_repeat('0',$length).
                       sprintf('%u',
                                    (($hmac_result[$offset+0] & 0x7f) << 24 ) |
                                    (($hmac_result[$offset+1] & 0xff) << 16 ) |
                                    (($hmac_result[$offset+2] & 0xff) << 8 ) |
                                     ($hmac_result[$offset+3] & 0xff)
                              ),
                       -$length);
      return $result;
  }


  function CalculateChapPassword(
      $secret,
      $hex_chap_id = "",
      $hex_chap_challenge = ""
  ) {
      
      if ($hex_chap_id != "")
      {
          $id = hex2bin($hex_chap_id);
      }
      elseif (32 < strlen($this->GetChapPassword()))
      {
          $id = hex2bin(substr($this->GetChapPassword(),0,2));
      }
      else
      {
          $id = hex2bin($this->GetChapId());
      }
      
      if ($hex_chap_challenge != "")
      {
          $challenge = hex2bin($hex_chap_challenge);
      }
      else
      {
          $challenge = hex2bin($this->GetChapChallenge());
      }
      
      return md5($id.$secret.$challenge);
  }


  function Convert2Unicode(
      $value
  ) {
      $unicode = "";
      $string = (string) $value;
      for ($i = 0; $i < mb_strlen($string); $i++)
      {
          $asc = ord($string[$i]) << 8;
          $unicode .= sprintf("%X", $asc);
      }
      return pack('H*', $unicode);
  }


  function Padding7to8(
      $value
  ) {
      static $odd_parity = array(  1,  1,  2,  2,  4,  4,  7,  7,  8,  8, 11, 11, 13, 13, 14, 14,
                                  16, 16, 19, 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, 31, 31,
                                  32, 32, 35, 35, 37, 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47,
                                  49, 49, 50, 50, 52, 52, 55, 55, 56, 56, 59, 59, 61, 61, 62, 62,
                                  64, 64, 67, 67, 69, 69, 70, 70, 73, 73, 74, 74, 76, 76, 79, 79,
                                  81, 81, 82, 82, 84, 84, 87, 87, 88, 88, 91, 91, 93, 93, 94, 94,
                                  97, 97, 98, 98,100,100,103,103,104,104,107,107,109,109,110,110,
                                 112,112,115,115,117,117,118,118,121,121,122,122,124,124,127,127,
                                 128,128,131,131,133,133,134,134,137,137,138,138,140,140,143,143,
                                 145,145,146,146,148,148,151,151,152,152,155,155,157,157,158,158,
                                 161,161,162,162,164,164,167,167,168,168,171,171,173,173,174,174,
                                 176,176,179,179,181,181,182,182,185,185,186,186,188,188,191,191,
                                 193,193,194,194,196,196,199,199,200,200,203,203,205,205,206,206,
                                 208,208,211,211,213,213,214,214,217,217,218,218,220,220,223,223,
                                 224,224,227,227,229,229,230,230,233,233,234,234,236,236,239,239,
                                 241,241,242,242,244,244,247,247,248,248,251,251,253,253,254,254);

      $raw = "";
      for ($i = 0; $i < strlen($value); $i++)
      {
          $raw .= sprintf('%08s', decbin(ord($value[$i])));
      }

      $str1 = explode('-', substr(chunk_split($raw, 7, '-'), 0, -1));
      $x = "";
      foreach ($str1 as $char)
      {
          $x .= sprintf('%02s', dechex($odd_parity[bindec($char. '0')]));
      }

      return pack('H*', $x);
  }


  function DesHashEcb(
      $clear
  ) {
      $cipher = new Crypt_DES(CRYPT_DES_MODE_ECB);
      $cipher->setKey($this->Padding7to8($clear));
      return $cipher->encrypt('KGS!@#$%');
  }


  function LmPasswordHash(
      $clear
  ) {
      $clear = substr(strtoupper($clear.str_repeat("\0",14)), 0, 14); // ! THIS NON-MB strtoupper must stay as is !
      return substr($this->DesHashEcb(substr($clear, 0, 7)),0,8).substr($this->DesHashEcb(substr($clear, 7, 7)),0,8);
  }


  function NtPasswordHash(
      $clear
  ) {
      return pack('H*',hash('md4', $this->Convert2Unicode($clear)));
  }


  function NtPasswordHashHash(
      $hash
  ) {
      return pack('H*',hash('md4', $hash));
  }


  function CalculateMsChapResponse(
      $secret,
      $hex_mschap_challenge = "",
      $hex_mschap_response = ""
  ) {
      $temp_challenge = ("" != $hex_mschap_challenge)?$hex_mschap_challenge:$this->GetMsChapChallenge();
      $pos = strpos(strtolower($temp_challenge), 'x');
      if (FALSE !== $pos)
      {
          $temp_challenge = substr($temp_challenge, $pos+1);
      }

      $temp_response  = ("" != $hex_mschap_response)?$hex_mschap_response:$this->GetMsChapResponse();
      $this->SetMsChapResponse($temp_response);
      $pos = strpos(strtolower($temp_response), 'x');
      if (FALSE !== $pos)
      {
          $temp_response = substr($temp_response, $pos+1);
      }

      $mschap_challenge = hex2bin($temp_challenge);
      $mschap_response  = hex2bin($temp_response);

      if (24 == strlen($mschap_response))
      {
          $mschap_response = str_repeat("\0",2+24).$mschap_response;
      }
      
      $id          = substr($mschap_response,0,1);
      $flag        = ord(substr($mschap_response,1,1));   // 1 = use NT-Response, 0 = use LM-Response
      $lm_response = substr($mschap_response,2,24);  // LM-Response
      $nt_response = substr($mschap_response,26,24); // NT-Response
      
      if (1 == $flag)
      {
          $hash = $this->NtPasswordHash($secret);
          $response = $nt_response;
          $hash_for_nt_key = $hash;
          
      }
      else
      {
          $hash = $this->LmPasswordHash($secret);
          $response = $lm_response;
          $hash_for_nt_key = $this->NtPasswordHash($secret);
      }

      $this->SetNtKey(nullable_bin2hex($this->NtPasswordHashHash($hash)));

      $challenge = $mschap_challenge;

      $hash = substr($hash.str_repeat("\0",21), 0, 21);

      $cipher = new Crypt_DES(CRYPT_DES_MODE_ECB);
      $cipher->setKey($this->Padding7to8(substr($hash, 0, 7)));
      $part1 = substr($cipher->encrypt(substr($challenge,0,8)),0,8);
      
      $cipher->setKey($this->Padding7to8(substr($hash, 7, 7)));
      $part2 = substr($cipher->encrypt(substr($challenge,0,8)),0,8);
      
      $cipher->setKey($this->Padding7to8(substr($hash, 14, 7)));
      $part3 = substr($cipher->encrypt(substr($challenge,0,8)),0,8);
      
      $calculated_response = $part1.$part2.$part3;
      
      if ($calculated_response == $response)
      {
          $result = strtolower(nullable_bin2hex($mschap_response));
      }
      else
      {
          $result = 'Error: '.nullable_bin2hex($calculated_response).' instead of '.nullable_bin2hex($nt_response);
      }
      return $result;
  }


  function CheckMsChapResponse(
      $secret,
      $hex_mschap_challenge = "",
      $hex_mschap_response = ""
  ) {
      $result = $this->CalculateMsChapResponse($secret, $hex_mschap_challenge, $hex_mschap_response);
      
      return ($this->GetMsChapResponse() == strtolower($result));
  }


  function CalculateMsChap2Response(
      $account,
      $secret,
      $domain = "",
      $hex_mschap_challenge = "",
      $hex_mschap2_response = ""
  ) {
      $temp_challenge = ("" != $hex_mschap_challenge)?$hex_mschap_challenge:$this->GetMsChapChallenge();
      $pos = strpos(strtolower($temp_challenge), 'x');
      if (FALSE !== $pos)
      {
          $temp_challenge = substr($temp_challenge, $pos+1);
      }

      $temp_response  = ("" != $hex_mschap2_response)?$hex_mschap2_response:$this->GetMsChap2Response();
      $this->SetMsChap2Response($temp_response);
      $pos = strpos(strtolower($temp_response), 'x');
      if (FALSE !== $pos)
      {
          $temp_response = substr($temp_response, $pos+1);
      }

      $mschap_challenge = hex2bin($temp_challenge);
      $mschap2_response = hex2bin($temp_response);

      if (24 == strlen($mschap2_response))
      {
          $mschap2_response = str_repeat("\0",2+24).$mschap2_response;
      }

      $id             = substr($mschap2_response,0,1);
      $flag           = ord(substr($mschap2_response,1,1)); // 0 (reserved for future use)
      $peer_challenge = substr($mschap2_response,2,16);
      $empty          = substr($mschap2_response,18,8);
      $nt_response    = substr($mschap2_response,26,24);
      
      $hash = $this->NtPasswordHash($secret);

      $this->SetNtKey(nullable_bin2hex($this->NtPasswordHashHash($hash)));

      /*
      $kr = hash_hmac('md5',
                      pack('H*',hash('md4', $hash)),
                      $this->Convert2Unicode(strtoupper($account).$domain)
                     ); // ! THIS NON-MB strtoupper must stay as is !
                     
      $nt_response_sig = hash_hmac('md5',
                                   $kr,
                                   $nt_response
                                  );

      $nt_key = hash_hmac('md5',
                          $kr,
                          $nt_response_sig
                         );

      $this->SetNtKey($nt_key);
      */

      if (8 == strlen($mschap_challenge))
      {
          $challenge = $mschap_challenge;
      }
      else
      {
          $challenge = substr(pack('H*',hash('sha1', $peer_challenge.$mschap_challenge.$account)), 0, 8);
      }

      $hash = substr($hash.str_repeat("\0",21), 0, 21);
      
      $cipher = new Crypt_DES(CRYPT_DES_MODE_ECB);
      $cipher->setKey($this->Padding7to8(substr($hash, 0, 7)));
      $part1 = substr($cipher->encrypt(substr($challenge,0,8)),0,8);
      
      $cipher->setKey($this->Padding7to8(substr($hash, 7, 7)));
      $part2 = substr($cipher->encrypt(substr($challenge,0,8)),0,8);
      
      $cipher->setKey($this->Padding7to8(substr($hash, 14, 7)));
      $part3 = substr($cipher->encrypt(substr($challenge,0,8)),0,8);
      
      $calculated_response = $part1.$part2.$part3;

      if ($calculated_response == $nt_response)
      {
          $result = strtolower(nullable_bin2hex($mschap2_response));
      }
      else
      {
          $result = 'Error: '.nullable_bin2hex($calculated_response).' instead of '.nullable_bin2hex($nt_response);
      }
      return $result;
  }


  function CheckMsChap2Response(
      $account,
      $secret,
      $domain = "",
      $hex_mschap_challenge = "",
      $hex_mschap2_response = ""
  ) {
      $result = $this->CalculateMsChap2Response($account, $secret, $domain, $hex_mschap_challenge, $hex_mschap2_response);
      
      return ($this->GetMsChap2Response() == strtolower($result));
  }


  function SetEncryptionKey(
      $key,
      $read_config = TRUE
  ) {
      $this->_encryption_key = $key;
      if ($read_config)
      {
          $this->ReadConfigData();
      }
  }


  function GetEncryptionKey()
  {
      return $this->_encryption_key;
  }


  function CalculateControlHash(
      $value_to_hash
  ) {
      return strtoupper(md5("CaLcUlAtE".$value_to_hash."cOnTrOlHaSh")); // DEV WARNING ! THIS NON-MB strtoupper must stay as is !
  }


  function Encrypt(
      $key,
      $value,
      $encryption_key
  ) {
      $result = "";
      if (strlen($encryption_key) > 0)
      {
          if (0 < strlen($value))
          {
              for ($i=0;  $i < strlen($value); $i++)
              {
                  $encrypt_char = ord(substr($encryption_key,$i % strlen($encryption_key),1));
                  $key_char = ord(substr($key,$i % strlen($key),1));
                  $result .= chr($encrypt_char^$key_char^ord(substr($value,$i,1)));
              }
              $result = base64_encode($result);
          }
      }
      else
      {
          $result = $value;
      }
      return $result;
  }


  /**
   * @brief   Decrypt the encrypted value of a label using an encryption key.
   *
   * @param   string  $key             Label of the value.
   * @param   string  $value           Encrypted value.
   * @param   string  $encryption_key  Encryption key.
   * @retval  string                   Decrypted value.
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 1.1.0
   * @date    2013-11-02
   * @since   2013-11-02
   */
  function Decrypt(
      $key,
      $value,
      $encryption_key
  ) {
      $result = "";
      if (strlen($encryption_key) > 0)
      {
          if (0 < strlen($value))
          {
              $value_to_decrypt = base64_decode($value);
              for ($i=0;  $i < strlen($value_to_decrypt); $i++)
              {
                  $encrypt_char = ord(substr($encryption_key,$i % strlen($encryption_key),1));
                  $key_char = ord(substr($key,$i % strlen($key),1));
                  $result .= chr($encrypt_char^$key_char^ord(substr($value_to_decrypt,$i,1)));
              }
          }
      }
      else
      {
          $result = $value;
      }
      return $result;
  }


  function SetMaxDelayedFailures(
      $failures
  ) {
      $this->_config_data['max_delayed_failures'] = $failures;
  }


  function GetMaxDelayedFailures()
  {
      return $this->_config_data['max_delayed_failures'];
  }


  function SetMaxDelayedTime(
      $seconds
  ) {
      $this->_config_data['failure_delayed_time'] = $seconds;
  }


  function GetMaxDelayedTime()
  {
      return $this->_config_data['failure_delayed_time'];
  }


  function SetActualVersion(
      $value
  ) {
      $this->_config_data['actual_version'] = $value;
  }


  function GetActualVersion()
  {
      return $this->_config_data['actual_version'];
  }


  /*********************************************************************
   *
   * Name: CreateUser
   * Short description: Create a new user
   *
   * Creation 2013-02-08
   * Update 2014-03-03
   * @package multiotp
   * @version 4.2.2
   * @author SysCo/al
   *
   * @param   string  $user      
   * @param   int     $prefix_pin_needed [-1|0|1]
   * @param   string  $algorithm
   * @param   string  $seed
   * @param   string  $pin
   * @param   string  $number_of_digits
   * @param   string  $time_interval_or_next_event
   * @param   string  $email
   * @param   string  $sms
   * @param   string  $description
   * @param   string  $group
   * @param   string  $token_algo_suite
   * @param   int     $activated [1|0]
   * @param   int     $synchronized [0|1]
   * @param   int     $ldap_pwd_needed [0|1]
   * @param   boolean $automatically
   * @param   boolean $sync_process
   * @param   int     $push_enabled_needed [0|1]
   * @return  boolean
   *
   *********************************************************************/
  function CreateUser($user_raw,
                      $prefix_pin_needed = -1,
                      $algorithm = 'totp',
                      $seed = "",
                      $pin = "",
                      $forced_number_of_digits = -1,
                      $time_interval_or_next_event = -1,
                      $email = "",
                      $sms = "",
                      $description = "",
                      $group = "",
                      $token_algo_suite = "",
                      $activated = 1,
                      $synchronized = 0,
                      $ldap_pwd_needed = -1,
                      $automatically = FALSE,
                      $sync_process = FALSE,
                      $push_enabled_needed = -1
  ) {
      if (intval($forced_number_of_digits) > 0) {
        $number_of_digits = $forced_number_of_digits;
      } else {
        $number_of_digits = $this->GetDefault2faDigits();
      }
    
      // A user cannot be created with one (or more) leading backslash
      $user = str_replace("\\", "", $user_raw);
      
      if (!$this->IsCaseSensitiveUsers()) {
        $user = mb_strtolower($user,'UTF-8');
      }
      
      $result = FALSE;
      if ("" != nullable_trim($user)) {
          if ((intval($ldap_pwd_needed) < 0) && (1 == $synchronized)) {
              $request_ldap_pwd = $this->GetDefaultRequestLdapPwd();
          } else {
              $request_ldap_pwd = intval($ldap_pwd_needed);
          }
          if ((intval($push_enabled_needed) < 0) && (1 == $synchronized)) {
              $push_enabled = $this->GetDefaultPushEnabled();
          } else {
              $push_enabled = intval($push_enabled_needed);
          }
          if (intval($prefix_pin_needed) < 0) {
              $request_prefix_pin = $this->GetDefaultRequestPrefixPin();
          } else {
              $request_prefix_pin = intval($prefix_pin_needed);
          }
          if ($this->ReadUserData($user, TRUE, TRUE) || ("" == $user)) {
              $result = FALSE; // ERROR: User already exists, or user is not set
              if ("" == $user) {
                  $this->WriteLog("Error: User is not set", FALSE, FALSE, _MULTIOTP_USER_MISSING_, 'User', "");
              } else {
                  $this->WriteLog("Error: User ".$user." already exists", FALSE, FALSE, _MULTIOTP_USER_EXISTS_, 'User', $user);
              }
          } else {
              $this->SetUser($user);
              $this->SetUserPrefixPin($request_prefix_pin);
              $this->SetUserRequestLdapPassword($request_ldap_pwd);
              $this->SetUserPushEnabled($push_enabled);
              $this->SetUserAlgorithm($algorithm);
              $this->SetUserTokenAlgoSuite($token_algo_suite);

              $the_pin = $pin;
              if ("" == $the_pin) {
                $the_pin = $this->GeneratePin();
              }
              $this->SetUserTokenNumberOfDigits($number_of_digits);

              $the_seed = (("" == $seed)?mb_substr(md5(date("YmdHis").mt_rand(100000,999999)),0,20).mb_substr(md5(mt_rand(100000,999999).date("YmdHis")),0,20):$seed);
              
              if (('hotp' == mb_strtolower($algorithm,'UTF-8')) || ('yubicootp' == mb_strtolower($algorithm,'UTF-8'))) {
                $next_event = ((-1 == $time_interval_or_next_event)?0:$time_interval_or_next_event);
                $time_interval = 0;
              } elseif (('totp' == mb_strtolower($algorithm,'UTF-8')) || ('motp' == mb_strtolower($algorithm,'UTF-8'))) {
                $next_event = 0;
                $time_interval = ((-1 == $time_interval_or_next_event)?_MULTIOTP_DEFAULT_TOTP_INTERVAL_:$time_interval_or_next_event);
                if ('motp' == mb_strtolower($algorithm,'UTF-8')) {
                  // $the_seed = (("" == $seed)?mb_substr(md5(date("YmdHis").mt_rand(100000,999999)),0,16):$seed);
                  $time_interval = 10;
                  if ((mb_strlen($the_pin) < 4) || (0 == intval($the_pin))) {
                    $the_pin = $this->GeneratePin(4); // PIN of 4 digits is mandatory for motp
                  }
                  $the_pin = mb_substr($the_pin, 0, 4); // PIN of 4 digits is mandatory for motp
                }
              } else { // without2FA or unknown
                  $next_event = 0;
                  $time_interval = ((-1 == $time_interval_or_next_event)?_MULTIOTP_DEFAULT_TOTP_INTERVAL_:$time_interval_or_next_event);
              }

              $this->SetUserPin($the_pin);
              $this->SetUserTokenSeed($the_seed);
              $this->SetUserTokenLastEvent($next_event - 1);
              $this->SetUserTokenTimeInterval($time_interval);
              
              $this_email = nullable_trim($email);
              if (("" == $this_email) && (FALSE !== mb_strpos($user, '@'))) {
                  $this_email = $user;
              }

              $this->SetUserEmail($this_email);
              $this->SetUserGroup(nullable_trim($group));
              $this->SetUserSms($sms);
              $this->SetUserDescription($description);
              $this->SetUserActivated($activated);
              $this->SetUserSynchronized($synchronized);
              // WriteUserData write in the log file
              $result = $this->WriteUserData(array("automatically" => $automatically, "sync_process" => $sync_process));

          }
      }
      return $result;
  }


  /*********************************************************************
   * Name: CreateUserFromToken
   * Short description: Create a new user based on a token
   *
   * Creation 2013-02-17
   * Update 2017-11-04
   * @package multiotp
   * @version 5.0.5.6
   * @author SysCo/al
   *
   * @param   string  $user
   * @param   string  $token
   * @param   int     $email
   * @param   int     $sms
   * @param   string  $pin
   * @param   int     $prefix_pin_needed [0|1]
   * @param   string  $description
   * @param   string  $group
   * @return  int
   *********************************************************************/
  function CreateUserFromToken($user_raw,
                               $token,
                               $email = "",
                               $sms = "",
                               $pin = "",
                               $prefix_pin_needed = -1,
                               $description = "",
                               $group = ""
  ) {
      // A user cannot be created with one (or more) leading backslash
      $user = str_replace("\\", "", $user_raw);

      if (intval($prefix_pin_needed) < 0) {
          $request_prefix_pin = $this->GetDefaultRequestPrefixPin();
      } else {
          $request_prefix_pin = intval($prefix_pin_needed);
      }
      if ($this->ReadUserData($user, TRUE, TRUE) || ("" == $user)) {
          $result = FALSE;
          if ("" == $user) {
              $this->WriteLog("Error: User is not set", FALSE, FALSE, _MULTIOTP_USER_MISSING_, 'User', "");
          } else {
              $this->WriteLog("Error: User ".$user." already exists", FALSE, FALSE, _MULTIOTP_USER_EXISTS_, 'User', $user);
          }
      } elseif (!$this->ReadTokenData($token)) {
          $result = FALSE;
          $this->WriteLog("Error: information about token ".$token." for user $user cannot be accessed", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_, 'Token', $token);
      } else {
          $this->AddTokenAttributedUsers($user);
          if (!$this->WriteTokenData()) {
              $result = _MULTIOTP_DATA_UPDATE_ERROR_; // ERROR: Unable to write the changes in the file
              $this->WriteLog("Error: Unable to write the changes in the file for the token ".$this->GetToken(), FALSE, FALSE, $result, 'Token', $user);
          } else {
              $this->SetUser($user);
              $this->SetUserPrefixPin($request_prefix_pin);
              $this->SetUserKeyId($token);
              $this->SetUserTokenSerialNumber($token);
              $this->SetUserAlgorithm($this->GetTokenAlgorithm());
              $this->SetUserTokenAlgoSuite($this->GetTokenAlgoSuite());
              $this->SetUserTokenSeed($this->GetTokenSeed());
              $this->SetUserTokenPrivateId($this->GetTokenPrivateId());
              $this->SetUserTokenNumberOfDigits($this->GetTokenNumberOfDigits());
              $this->SetUserTokenTimeInterval($this->GetTokenTimeInterval());
              $this->SetUserTokenLastEvent($this->GetTokenLastEvent());

              $the_pin = $pin;
              if ("" == $the_pin) {
                $the_pin = $this->GeneratePin();
              }
              
              $this_email = nullable_trim($email);
              if (("" == $this_email) && (FALSE !== mb_strpos($user, '@')))
              {
                  $this_email = $user;
              }
              
              $this->SetUserPin($the_pin);
              $this->SetUserEmail($this_email);
              $this->SetUserGroup(nullable_trim($group));
              $this->SetUserSms($sms);
              $this->SetUserDescription($description);
              
              $result = $this->WriteUserData(); // WriteUserData write in the log file
          }
      }
      return $result;
  }


  /**
   * @brief   Create the QRcode for the current user.
   *
   * @param   string  $user
   * @param   string  $display_name
   * @param   string  $file_name
   * @return  boolean or binary
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.0.6
   * @date    2013-08-25
   * @since   2013-02-17
   */
  function GetUserTokenQrCode(
      $user = "",
      $display_name = "",
      $file_name = 'binary',
      $qrcode_format = ""
  ) {
      $result = FALSE;
      if (!function_exists('ImageCreate')) {
          $this->WriteLog("Error: PHP GD library is not installed", FALSE, FALSE, _MULTIOTP_OPERATION_CANCELLED_, 'System', "", 3);
          return $result;
      } else {
          $data = $this->GetUserTokenUrlLink($user, $display_name, $qrcode_format);
          if($data) {
              $result = $this->qrcode($data, $file_name);
          }
          return $result;
      }
  }


  /**
   * @brief   Create the QRcode for the current token.
   *
   * @param   string  $token
   * @param   string  $display_name
   * @param   string  $file_name
   * @return  boolean or binary
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.0.5.1
   * @date    2017-09-29
   * @since   2013-02-18
   */
  function GetTokenQrCode(
      $token = "",
      $display_name = "",
      $file_name = 'binary',
      $qrcode_format = ""
  ) {
      $result = FALSE;
      if (!function_exists('ImageCreate'))
      {
          $this->WriteLog("Error: PHP GD library is not installed", FALSE, FALSE, _MULTIOTP_OPERATION_CANCELLED_, 'System', "", 3);
          return $result;
      }
      else
      {
          $data = $this->GetTokenUrlLink($token, $display_name, $qrcode_format);
          if($data)
          {
              $result = $this->qrcode($data, $file_name);
          }
          return $result;
      }
  }


  function GenerateHtmlQrCode(
      $select_user = "",
      $alternate_html_template = "",
      $keep_qrcode_tags = FALSE
  ) {
      $code_width=200;
      $code_height=200;

      if ("" != $select_user) {
          $user = $this->SetUser($select_user);
      } else {
        $user = $this->GetUser();
      }

      $user = encode_utf8_if_needed($user);
      
      $descr = encode_utf8_if_needed($this->GetUserDescription());
      $descr = encode_utf8_if_needed(empty($descr) ? $user : $descr);
      $display_name = encode_utf8_if_needed($this->GetUserDisplayName());

      if ("" != nullable_trim($alternate_html_template)) {
          $html = $alternate_html_template;
      } else {
        //Get template file
        $file_name = "template";
        $file_to_show = $this->GetTemplatesFolder().$file_name.'.html';
        if(file_exists($file_to_show)) {
          $html = (file_get_contents($file_to_show));
        } else {
          $html = "";
        }
      }

      $html = encode_utf8_if_needed($html);
      
      $qrcode_format = "";
      $regex_size='/motp-qrcode-format=(.*?)\s-/';
      if(preg_match($regex_size, $html, $values)) {
        $qrcode_format = ('xml' == $values[1]) ? 'xml' : ($qrcode_format = ('token2' == $values[1]) ? 'token2' : "");
      }

      // Keep or clean LDAP information if not used
      // if ($this->IsUserSynchronized() && ('LDAP' == $this->GetUserSynchronizedChannel()) && $this->IsUserRequestLdapPasswordEnabled())
      if ($this->IsUserRequestLdapPasswordEnabled()) {
          $request_ldap_pwd = TRUE;
          $html = preg_replace('/<!--\s*\{\/IfMultiotpUserLdapPwd\}\s*-->/i', "", $html);
          $html = preg_replace('/<!--\s*\{IfMultiotpUserLdapPwd\}\s*-->/i', "", $html);
      } else {
          $request_ldap_pwd = FALSE;
          $html = preg_replace('/<!--\s*\{\/IfMultiotpUserLdapPwd\}\s*-->/i', ' -- {/IfMultiotpUserLdapPwd} -->', $html);
          $html = preg_replace('/<!--\s*\{IfMultiotpUserLdapPwd\}\s*-->/i', '<!-- {/IfMultiotpUserLdapPwd} -- ', $html);
      }

      // Keep or clean pin information if not used
      if ($this->IsUserPrefixPin() && (!$request_ldap_pwd)) {
          $html = preg_replace('/<!--\s*\{\/IfMultiotpUserPin\}\s*-->/i', "", $html);
          $html = preg_replace('/<!--\s*\{IfMultiotpUserPin\}\s*-->/i', "", $html);
      } else {
          $html = preg_replace('/<!--\s*\{\/IfMultiotpUserPin\}\s*-->/i', ' -- {/IfMultiotpUserPin} -->', $html);
          $html = preg_replace('/<!--\s*\{IfMultiotpUserPin\}\s*-->/i', '<!-- {/IfMultiotpUserPin} -- ', $html);
      }
      
      $token_serial = nullable_trim($this->GetUserTokenSerialNumber());
      if (("" == $token_serial) || (1 > mb_strlen($token_serial))) {
          $html = preg_replace('/<!--\s*\{\/IfMultiotpUserTokenSerial\}\s*-->/i', ' -- {/IfMultiotpUserTokenSerial} -->', $html);
          $html = preg_replace('/<!--\s*\{IfMultiotpUserTokenSerial\}\s*-->/i', '<!-- {/IfMultiotpUserTokenSerial} -- ', $html);
      } else {
          $html = preg_replace('/<!--\s*\{\/IfMultiotpUserTokenSerial\}\s*-->/i', "", $html);
          $html = preg_replace('/<!--\s*\{IfMultiotpUserTokenSerial\}\s*-->/i', "", $html);
      }

      // Simplify current algorithm info
      $html = preg_replace('/IfMultiotpUserAlgorithm="[2ABCFHIMOPTUWY,]*'.mb_strtoupper($this->GetUserAlgorithm(),'UTF-8').'[2ABCFHIMOPTUWY,]*"}/i', 'IfMultiotpUserAlgorithm="'.mb_strtoupper($this->GetUserAlgorithm(),'UTF-8').'"}', $html);

      // Clean other algorithms info
      foreach ($this->GetAlgorithmsList(true) as $algorithm_one) {
          if (mb_strtoupper($algorithm_one,'UTF-8') != mb_strtoupper($this->GetUserAlgorithm(),'UTF-8')) {
              $html = preg_replace('/<!--\s*\{\/IfMultiotpUserAlgorithm="[2ABCFHIMOPTUWY,]*'.mb_strtoupper($algorithm_one,'UTF-8').'[2ABCFHIMOPTUWY,]*"\}\s*-->/i', ' -- {/IfMultiotpUserAlgorithm="DELETE"} -->', $html);
              $html = preg_replace('/<!--\s*\{IfMultiotpUserAlgorithm="[2ABCFHIMOPTUWY,]*'.mb_strtoupper($algorithm_one,'UTF-8').'[2ABCFHIMOPTUWY,]*"\}\s*-->/i', '<!-- {IfMultiotpUserAlgorithm="DELETE"} -- ', $html);
          }
      }

      // Check if a specific language exists in the tags
      $specific_language = $this->GetUserLanguage();
      if (false === mb_stripos($html, '{IfMultiotpLanguage="'.$specific_language.'"')) {
        $specific_language = $this->GetLanguage();
        if (false === mb_stripos($html, '{IfMultiotpLanguage="'.$specific_language.'"')) {
          $specific_language = 'en';
        }
      }
      // Clean other languages info
      $html = preg_replace('/<!--\s*\{\/IfMultiotpLanguage="'.$specific_language.'"\}\s*-->/i', "", $html);
      $html = preg_replace('/<!--\s*\{IfMultiotpLanguage="'.$specific_language.'"\}\s*-->/i', "", $html);
      $html = preg_replace('/<!--\s*\{\/IfMultiotpLanguage="[a-z]*"\}\s*-->/i', ' -- {/IfMultiotpLanguage="other"} {ML} -->', $html);
      $html = preg_replace('/<!--\s*\{IfMultiotpLanguage="[a-z]*"\}\s*-->/i', '<!-- {ML} {IfMultiotpLanguage="other"} -- ', $html);

      // Clean language comments
      $html_cleaned = "";
      $html_slice = explode("{ML} -->",$html);
      foreach ($html_slice as $one_slice) {
          $comment_pos = mb_strpos($one_slice,'<!-- {ML}');
          if(FALSE !== $comment_pos) {
            $html_cleaned.= mb_substr($one_slice,0,$comment_pos);
          }
      }
      $html_cleaned .= end($html_slice);
      $html = $html_cleaned."\n";

      // Clean comments
      $html_cleaned = "";
      $html_slice = explode("-->",$html);
      foreach ($html_slice as $one_slice) {
          $comment_pos = mb_strpos($one_slice,'<!--');
          if(FALSE !== $comment_pos) {
            $html_cleaned.= mb_substr($one_slice,0,$comment_pos);
          }
      }
      $html_cleaned .= end($html_slice);
      $html = $html_cleaned."\n";

      $html = str_replace('{MultiotpVersion}', $this->GetVersion(), $html);
      $html = str_replace('{MultiotpUserDescriptionUC}', mb_strtoupper($descr, 'UTF-8'), $html);
      $html = str_replace('{MultiotpUserDescription}', $descr, $html);
      $html = str_replace('{MultiotpUserAccount}', $user, $html);
      $html = str_replace('{MultiotpUserDisplayName}', $display_name, $html);

      $html = str_replace('{MultiotpUserPin}', $this->GetUserPin(), $html);
      $html = str_replace('{MultiotpUserAlgorithm}', mb_strtoupper($this->GetUserAlgorithm(),'UTF-8'), $html);
      $html = str_replace('{MultiotpUserTokenSeed}', $this->GetUserTokenSeed(), $html);
      $html = str_replace('{MultiotpUserTokenSeedBase32}', base32_encode(hex2bin($this->GetUserTokenSeed())), $html);
      $html = str_replace('{MultiotpUserTokenNumberOfDigits}', $this->GetUserTokenNumberOfDigits(), $html);
      $html = str_replace('{MultiotpUserTokenTimeInterval}', $this->GetUserTokenTimeInterval(), $html);
      $html = str_replace('{MultiotpUserTokenNextEvent}', 1+$this->GetUserTokenLastEvent(), $html);
      $html = str_replace('{MultiotpUserTokenSerial}', $token_serial, $html);

      $regex_url='/\surl=(.*?)[\}\s}]/';
      $regex_format='/\sformat=\"?([^\"\}]*)\"?.*\}/';
      $regex_w='/\swidth=[\D]*([\d]*)[\D]*/';
      $regex_h='/\sheight=[\D]*([\d]*)[\D]*/';
      $regex_name='/\saccount=(.*?)[\}\s]/';

      // Date and time replacement
      $regex_tag='/\{MultiotpDateTime(.*)\}/';
      $format = "Y-m-d H:i:s";
      if(preg_match_all($regex_tag, $html, $matches)) {
          foreach ($matches[0] as $item) {
              if(!empty($item)) {
                  if(preg_match($regex_format, $item, $values)) {
                      $format = $values[1];
                  }
                  $html = str_replace($item, date($format), $html);
              }
          }
      }

      if (!$keep_qrcode_tags) {
          // Smartphone apps qrcode
          $regex_tag='/\{MultiotpQrCodeUrl\s(.*?)\}/';
          if(preg_match_all($regex_tag, $html, $matches)) {
              foreach ($matches[0] as $item) {
                  $url = "";
                  $w = $code_width;
                  $h = $code_height;
                  if(!empty($item)) {
                      if(preg_match($regex_url, $item, $values)) {
                          $url= str_replace('"', "", explode('=', $values[0],2));
                          $url = $url[1];
                      }
                      if(preg_match($regex_w, $item, $values)) {
                          $w = str_replace('"', "", explode('=', $values[0],2));
                          $w = nullable_trim(str_replace('}', "", $w[1]));
                      }
                      if(preg_match($regex_h, $item, $values)) {
                          $h = str_replace('"', "", explode('=', $values[0],2));
                          $h = nullable_trim(str_replace('}', "", $h[1]));
                      }
                      $html = str_replace($item, "<a id=\"QrCodeUrl\" href=\"".$url."\" target=\"blank\"><img border=\"0\" width=\"".$w."\" height=\"".$h."\" src=\"data:image/png;base64,".base64_encode($this->qrcode($url, 'binary'))."\" /></a>", $html);
                  }
              }
          }
          // User token qrcode
          $regex_tag='/\{MultiotpQrCodeUserToken\s(.*?)\}/';

          if(preg_match_all($regex_tag, $html, $matches)) {
              foreach ($matches[0] as $item) {
                  $url_display_name = $descr;
                  $w = $code_width;
                  $h = $code_height;
                  if(!empty($item)) {
                      if(preg_match($regex_w, $item, $values)) {
                          $w = str_replace('"', "", explode('=', $values[0],2));
                          $w = nullable_trim(str_replace('}', "", $w[1]));
                      }
                      if(preg_match($regex_h, $item, $values)) {
                          $h = str_replace('"', "", explode('=', $values[0],2));
                          $h = nullable_trim(str_replace('}', "", $h[1]));
                      }
                      if(preg_match($regex_name, $item, $values)) {
                          $name_tag = str_replace('"', "", explode('=', $values[0],2));
                          $name_tag = strtoupper(nullable_trim(str_replace('}', "", $name_tag[1])));
                          if (strtoupper("MultiotpUserDisplayName") == $name_tag) {
                            $url_display_name = $display_name;
                          } elseif (strtoupper("MultiotpUserAccount") == $name_tag) {
                            $url_display_name = $user;
                          } elseif (strtoupper("MultiotpUserDescriptionUC") == $name_tag) {
                            $url_display_name = mb_strtoupper($descr, 'UTF-8');
                          } elseif (strtoupper("MultiotpUserDescription") == $name_tag) {
                            $url_display_name = $descr;
                          } else {
                            $url_display_name = $descr;
                          }
                      }
                      $qrdata = $this->GetUserTokenUrlLink($user, $url_display_name, $qrcode_format);
                      $url    = $qrdata;
                      $target = "blank";
                      if (FALSE !== strpos($qrdata, "<?xml")) {
                        $url    = "#QrCodeUserToken";
                        $target = "_self";
                      }
                      $html = str_replace($item, "<a id=\"QrCodeUserToken\" href=\"".$url."\" target=\"$target\"><img border=\"0\" width=\"".$w."\" height=\"".$h."\" src=\"data:image/png;base64,".base64_encode($this->qrcode($qrdata, 'binary'))."\"></a>", $html);
                  }
              }
          }
      }
      
      return $html;
  }


  function GenerateHtmlScratchlist(
      $select_user = "",
      $alternate_html_template = ""
  ) {

    if ("" != $select_user) {
        $user = $this->SetUser($select_user);
    } else {
      $user = $this->GetUser();
    }

    $user = encode_utf8_if_needed($user);


    //Get template file
    if ("" != nullable_trim($alternate_html_template)) {
      $html = $alternate_html_template;
    } else {
      //Get template file
      $file_name = "scratchtemplate";
      $file_to_show = $this->GetTemplatesFolder().$file_name.'.html';
      if(file_exists($file_to_show)) {
        $html = (file_get_contents($file_to_show));
      } else {
        $html = "";
      }
    }

    $html = encode_utf8_if_needed($html);

    $user_utf8 = encode_utf8_if_needed($user);
    
    $descr = encode_utf8_if_needed($this->GetUserDescription());
    $descr = encode_utf8_if_needed(empty($descr) ? $user_utf8 : $descr);
    $display_name = encode_utf8_if_needed($this->GetUserDisplayName());
    
    $html = str_replace('{MultiotpVersion}', $this->GetVersion(), $html);
    $html = str_replace('{MultiotpUserDescriptionUC}', mb_strtoupper($descr, 'UTF-8'), $html);
    $html = str_replace('{MultiotpUserDescription}', $descr, $html);
    $html = str_replace('{MultiotpUserAccount}', $user_utf8, $html);
    $html = str_replace('{MultiotpUserDisplayName}', $display_name, $html);

    // Check if a specific language exists in the tags
    $specific_language = $this->GetUserLanguage();
    if (false === mb_stripos($html, '{IfMultiotpLanguage="'.$specific_language.'"')) {
      $specific_language = $this->GetLanguage();
      if (false === mb_stripos($html, '{IfMultiotpLanguage="'.$specific_language.'"')) {
        $specific_language = 'en';
      }
    }
    // Clean other languages info
    $html = preg_replace('/<!--\s*\{\/IfMultiotpLanguage="'.$specific_language.'"\}\s*-->/i', "", $html);
    $html = preg_replace('/<!--\s*\{IfMultiotpLanguage="'.$specific_language.'"\}\s*-->/i', "", $html);
    $html = preg_replace('/<!--\s*\{\/IfMultiotpLanguage="[a-z]*"\}\s*-->/i', ' -- {/IfMultiotpLanguage="other"} {ML} -->', $html);
    $html = preg_replace('/<!--\s*\{IfMultiotpLanguage="[a-z]*"\}\s*-->/i', '<!-- {ML} {IfMultiotpLanguage="other"} -- ', $html);

    // Clean language comments
    $html_cleaned = "";
    $html_slice = explode("{ML} -->",$html);
    foreach ($html_slice as $one_slice) {
      $comment_pos = mb_strpos($one_slice,'<!-- {ML}');
      if(FALSE !== $comment_pos) {
        $html_cleaned.=mb_substr($one_slice,0,$comment_pos);
      }
    }
    $html_cleaned .= end($html_slice);
    $html = $html_cleaned."\n";

    $html = preg_replace('/<body.*>/i', "", $html);
    $html = preg_replace('/<\/body>/i', "", $html);

    // Clean comments
    $html_cleaned = "";
    $html_slice = explode("-->",$html);
    foreach ($html_slice as $one_slice) {
        $comment_pos = mb_strpos($one_slice,'<!--');
        if(FALSE !== $comment_pos) {
          $html_cleaned.=mb_substr($one_slice,0,$comment_pos);
        }
    }
    $html_cleaned .= end($html_slice);
    $html = $html_cleaned."\n";
    
    $regex_format='/\sformat=\"?([^\"\}]*)\"?.*\}/';

    $passwords = explode("\t", $this->GetUserScratchPasswordsList($user));
    $values = "";
    foreach ($passwords as $password)
    {
      if("" == $values)
        $values.= $password;
      else
        $values.= ", ".$password;
    }
    $html = str_replace('{MultiotpScratchPasswordList}', $values, $html);

    //date and time replacement
    $regex_tag='/\{MultiotpDateTime(.*)\}/';
    $format = "Y-m-d H:i:s";
    if(preg_match_all($regex_tag, $html, $matches))
    {
      foreach ($matches[0] as $item)
      {
        if(!empty($item))
        {
          if(preg_match($regex_format, $item, $values))
          {
            $format = $values[1];
          }
          $html = str_replace($item, date($format), $html);
        }
      }
    }
    
    /*$table = '<table border="1">';
    foreach ($passwords as $password)
    {
        $table .= '<tr><td>'.$password.'</td></tr>';
    }
    $table .= '</table>';
    $html = str_replace('{MultiotpScratchPasswordList}', $table, $html);*/

    return $html;
  }


  /*********************************************************************
   *
   * Name: GetUserTokenUrlLink
   * Short description: Create the Urllink for the current user
   *
   * Creation 2013-04-29
   * Update 2017-09-29
   * @package multiotp
   * @version 1.1.0
   * @author SysCo/al
   *
   * @param   string  $user
   * @param   string  $display_name
   * @return  boolean (FALSE) or string
   *
   *********************************************************************/
  function GetUserTokenUrlLink(
      $user = "",
      $display_name = "",
      $qrcode_format = ""
  ) {
      $result = false;
      
      $user_array = $this->ReadUserDataArray(("" != $user)?$user:$this->GetUser());
      if (false !== $user_array) {
        $the_user       = $user;
        $description    = $user_array['description'];
        $q_algorithm    = $user_array['algorithm'];
        $q_algo_suite   = $user_array['token_algo_suite'];
        $q_period       = $user_array['time_interval'];
        $q_digits       = $user_array['number_of_digits'];
        $q_seed         = $user_array['token_seed'];
        $q_counter      = $user_array['last_event'] + 1;
        $q_display_name = (("" != $display_name)?$display_name:(("" != $description)?$description:$the_user));
        $q_issuer       = $this->GetIssuer();
        $q_mogw         = $this->GetPushGateway();
        $q_mouid        = $user_array['mouid'];
        $q_mosid        = $this->GetPushId();
        $encoded_issuer_and_separator = ("" != $q_issuer) ? rawurlencode($q_issuer).":" : "";
        // Starting with 5.8.0.0, '&issuer='.rawurlencode($q_issuer) removed at the end, replaced by issuer: at the beginning
        
        switch (mb_strtolower($q_algorithm,'UTF-8')) {
            case 'totp':
                $result = 'otpauth://'.$q_algorithm.'/'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.base32_encode(hex2bin($q_seed)).'&digits='.$q_digits.'&period='.$q_period;
                
                if ($this->IsPushActivated()) {
                  if ($this->IsUserPushEnabled()) {
                    if (!empty($q_mouid)) {
                      if (!empty($q_mogw)) {
                        if ($q_mogw != "*default*") {
                          $result.= '&mogw='.$q_mogw;
                        }
                        $result.= '&mouid='.$q_mouid . '&mosid='.$q_mosid;
                      }
                    }
                  }
                }

                break;
            case 'hotp':
                $result = 'otpauth://'.$q_algorithm.'/'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.base32_encode(hex2bin($q_seed)).'&digits='.$q_digits.'&counter='.$q_counter;
                break;
            case 'motp':
                if ('xml' == $qrcode_format) {
                  $result = "<?xml version=\"1.0\" encoding=\"utf-8\" ?><SSLOTPAuthenticator><mOTPProfile><ProfileName>!ProfileName!</ProfileName><PINType>!PINType!</PINType><PINSecurity>!PINSecurity!</PINSecurity><Secret>!Secret!</Secret><AlgorithmMode>!AlgorithmMode!</AlgorithmMode></mOTPProfile></SSLOTPAuthenticator>";
                  $result = str_replace(chr(13).chr(10),chr(10),$result);
                  $result = str_replace("!ProfileName!",$q_display_name,$result);
                  $result = str_replace("!PINType!","0",$result); // 0=numeric 4 digits, 1=alphanumeric
                  $result = str_replace("!PINSecurity!","0",$result); // 0=keep generating, 1=generate only one OTP
                  $result = str_replace("!Secret!",$q_seed,$result); // Hexadecimal secret
                  $result = str_replace("!AlgorithmMode!","0",$result); // 0=epochTime + secret + pin
                } else if ('motp' == $qrcode_format) {
                  $result = 'motp://'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.$q_seed;
                } else if ('token2' == $qrcode_format) {
                  $result = 'motp://'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.$q_seed;
                } else {
                  $result = 'otpauth://'.$q_algorithm.'/'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.base32_encode(hex2bin($q_seed)).'&digits='.$q_digits.'&period='.$q_period;

                  if ($this->IsPushActivated()) {
                    if ($this->IsUserPushEnabled()) {
                      if (!empty($q_mouid)) {
                        if (!empty($q_mogw)) {
                          if ($q_mogw != "*default*") {
                            $result.= '&mogw='.$q_mogw;
                          }
                          $result.= '&mouid='.$q_mouid . '&mosid='.$q_mosid;
                        }
                      }
                    }
                  }

                }
                break;
            case 'token2':
                  $result = 'motp://'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.$q_seed;
                break;
            default:
                // $result = FALSE;
                $result = 'http://no.qrcode.available/no_qrcode_compatible_client_for_this_algorithm';
                $this->WriteLog("Error: No known URL compatible client for this algorithm", FALSE, FALSE, _MULTIOTP_BAD_ALGORITHM_, 'System', "");
        }
      }
      return $result;
  }


  /**
   * @brief   Create the Urllink for the current token
   *
   * @param   string  $token
   * @param   string  $display_name
   * @return  boolean (FALSE) or string
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.0.5.1
   * @date    2019-09-29
   * @since   2014-01-19
   */
  function GetTokenUrlLink(
      $token = "",
      $display_name = "",
      $qrcode_format = ""
  ) {
      $the_token = mb_strtolower($token,'UTF-8');
      $result = FALSE;

      if ("" != $the_token) {
          $this->SetToken($the_token);
      }

      if ($this->ReadTokenData()) {
          $the_token      = $this->GetToken();
          $q_algorithm    = $this->GetTokenAlgorithm();
          $q_algo_suite   = $this->GetTokenAlgoSuite();
          $q_period       = $this->GetTokenTimeInterval();
          $q_digits       = $this->GetTokenNumberOfDigits();
          $q_seed         = $this->GetTokenSeed();
          $q_counter      = $this->GetTokenLastEvent() + 1;
          $q_display_name = (("" != $display_name)?$display_name:$the_token);
          $q_issuer       = $this->GetTokenIssuer();
          $encoded_issuer_and_separator = ("" != $q_issuer) ? rawurlencode($q_issuer).":" : "";
          // Starting with 5.8.0.0, '&issuer='.rawurlencode($q_issuer) removed at the end, replaced by issuer: at the beginning

          switch (mb_strtolower($q_algorithm,'UTF-8'))
          {
              case 'totp':
                  $result = 'otpauth://'.$q_algorithm.'/'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.base32_encode(hex2bin($q_seed)).'&digits='.$q_digits.'&period='.$q_period;
                  break;
              case 'hotp':
                  $result = 'otpauth://'.$q_algorithm.'/'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.base32_encode(hex2bin($q_seed)).'&digits='.$q_digits.'&counter='.$q_counter;
                  break;
              case 'motp':
                  if ('xml' == $qrcode_format) {
                    $result = "<?xml version=\"1.0\" encoding=\"utf-8\" ?><SSLOTPAuthenticator><mOTPProfile><ProfileName>!ProfileName!</ProfileName><PINType>!PINType!</PINType><PINSecurity>!PINSecurity!</PINSecurity><Secret>!Secret!</Secret><AlgorithmMode>!AlgorithmMode!</AlgorithmMode></mOTPProfile></SSLOTPAuthenticator>";
                    $result = str_replace(chr(13).chr(10),chr(10),$result);
                    $result = str_replace("!ProfileName!",$q_display_name,$result);
                    $result = str_replace("!PINType!","0",$result); // 0=numeric 4 digits, 1=alphanumeric
                    $result = str_replace("!PINSecurity!","0",$result); // 0=keep generating, 1=generate only one OTP
                    $result = str_replace("!Secret!",$q_seed,$result); // Hexadecimal secret
                    $result = str_replace("!AlgorithmMode!","0",$result); // 0=epochTime + secret + pin
                  } else if ('motp' == $qrcode_format) {
                    $result = 'motp://'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.$q_seed;
                  } else if ('token2' == $qrcode_format) {
                    $result = 'motp://'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.$q_seed;
                  } else {
                    $result = 'otpauth://'.$q_algorithm.'/'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.base32_encode(hex2bin($q_seed)).'&digits='.$q_digits.'&period='.$q_period;
                  }
                  break;
              case 'token2':
                    $result = 'motp://'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.$q_seed;
                  break;
              default:
                  // $result = FALSE;
                  $result = 'http://no.qrcode.available/no_qrcode_compatible_client_for_this_algorithm';
                  $this->WriteLog("Error: No known URL compatible client for this algorithm", FALSE, FALSE, _MULTIOTP_BAD_ALGORITHM_, 'System', "");
          }
      }
      else
      {
          // $result = "";
      }
      return $result;
  }


  /*********************************************************************
   *
   * Name: FastCreateUser
   * Short description: Quickly create a new user with a new token (GA compatible)
   *
   * Creation 2013-02-16
   * Update 2017-11-04
   * @package multiotp
   * @version 5.0.5.6
   * @author SysCo/al
   *
   * @param   string  $user      
   * @param   string  $email
   * @param   string  $sms
   * @param   int     $prefix_pin_needed [-1|0|1]
   * @param   string  $algorithm [totp|hotp|motp]
   * @param   int     $activated [1|0]
   * @param   string  $description
   * @param   string  $group
   * @param   int     $synchronized [0|1]
   * @param   string  $pin
   * @param   boolean $automatically         Process is done automatically  
   * @param   string  $synchronized_channel 
   * @param   string  $synchronized_server
   * @param   string  $synchronized_dn
   * @param   string  $ldap_pwd_needed
   * @param   string  $preferred_language
   * @param   string  $dialin_ip_address
   * @param   boolean $sync_process          Process is initiated by sync
   * @param   string  $push_enabled_needed
   * @return  boolean
   *
   *********************************************************************/
  function FastCreateUser($user_raw,
                          $email = "",
                          $sms = "",
                          $prefix_pin_needed = -1,
                          $algorithm = 'totp',
                          $activated=1,
                          $description = "",
                          $group = "*DEFAULT*",
                          $synchronized = 0,
                          $pin = "",
                          $automatically = false,
                          $synchronized_channel = "",
                          $synchronized_server = "",
                          $synchronized_dn = "",
                          $ldap_pwd_needed = -1,
                          $language = "",
                          $dialin_ip_address = "",
                          $sync_process = false,
                          $push_enabled_needed = -1
  ) {
    // A user cannot be created with one (or more) leading backslash
    $user = str_replace("\\", "", $user_raw);

    if (!$this->IsCaseSensitiveUsers()) {
      $user = mb_strtolower($user,'UTF-8');
    }

    $result = FALSE;
    if ("" != nullable_trim($user)) {
      if ($this->ReadUserData($user, TRUE, TRUE) || ("" == $user)) {
        $this->WriteLog("Error: Unable to create the user ".$user." because it already exists", FALSE, FALSE, _MULTIOTP_USER_EXISTS_, 'User', $user);
      } else {
        if ((intval($ldap_pwd_needed) < 0) && (1 == $synchronized)) {
          $request_ldap_pwd = $this->GetDefaultRequestLdapPwd();
        } else {
          $request_ldap_pwd = intval($ldap_pwd_needed);
        }

        if ((intval($push_enabled_needed) < 0) && (1 == $synchronized)) {
          $push_enabled = $this->GetDefaultPushEnabled();
        } else {
          $push_enabled = intval($push_enabled_needed);
        }
                           
        if (intval($prefix_pin_needed) < 0) {
          $prefix_required = $this->GetDefaultRequestPrefixPin();
        } else {
          $prefix_required = intval($prefix_pin_needed);
        }
    
        $this->SetUser($user, false); // This will do also an automatic reset of the user array

        $this->SetUserEmail($email);
        $this->SetUserDescription($description);
        $this->SetUserLanguage($language);
        $this->SetUserGroup(('*DEFAULT*' == $group) ? $this->GetDefaultUserGroup() : $group);
        $this->SetUserSms($sms);
        $this->SetUserAlgorithm($algorithm);
        
        $this->SetUserTokenAlgoSuite(""); // Default algorithm suite (HMAC-SHA1)
        $the_pin = $pin;
        if ("" == $the_pin) {
          $the_pin = $this->GeneratePin();
        }
        $this->SetUserPrefixPin($prefix_required);
        $this->SetUserTokenNumberOfDigits($this->GetDefault2faDigits());
        $next_event = 0;

        $seed = mb_substr(md5(date("YmdHis").mt_rand(100000,999999)),0,20).mb_substr(md5(mt_rand(100000,999999).date("YmdHis")),0,20);

        if ('totp' == mb_strtolower($algorithm,'UTF-8')) {
          $time_interval = _MULTIOTP_DEFAULT_TOTP_INTERVAL_;
        } elseif ('motp' == mb_strtolower($algorithm,'UTF-8')) {
          $seed = mb_substr($seed,0,16);
          $time_interval = 10;
          if ((mb_strlen($the_pin) < 4) || (0 == intval($the_pin))) {
            $the_pin = $this->GeneratePin(4); // PIN of 4 digits is mandatory for motp
          }
          $the_pin = mb_substr($the_pin, 0, 4); // PIN of 4 digits is mandatory for motp
        } else {
          $time_interval = 0;
        }

        $this->SetUserPin($the_pin);
        $this->SetUserTokenSeed($seed);
        $this->SetUserTokenLastEvent($next_event-1);
        $this->SetUserTokenTimeInterval($time_interval);

        $this->SetUserActivated($activated);

        $this->SetUserSynchronized($synchronized);

        if (($automatically) && (1 == $synchronized)) {
          $this->SetUserSynchronizedTime();
          $this->SetUserSynchronizedChannel($synchronized_channel);
          $this->SetUserSynchronizedServer($synchronized_server);
          $this->SetUserSynchronizedDn($synchronized_dn);
        }

        $this->SetUserRequestLdapPassword($request_ldap_pwd);
        $this->SetUserPushEnabled($push_enabled);

        $this->SetUserDialinIpAddress($dialin_ip_address);

        // WriteUserData write in the log file
        $result = $this->WriteUserData(array("automatically" => $automatically, "sync_process" => $sync_process));

      }
    }
    return $result;
  }


  function SetUser(
      $user,
      $auto_read_data = true
  ) {
    $result = TRUE;
    $user_encoded = str_replace("\\", "", $user);
    $this->ResetUserArray();
    if ("" != $user_encoded) {
      if ($user_encoded != $this->GetUser()) {
        $this->_user = $user_encoded;
        $this->_rawUser = $user_encoded;
        if (!$this->IsCaseSensitiveUsers()) {
          $this->_user = mb_strtolower($this->_user,'UTF-8');
        }
      }
      if ($auto_read_data) {
        $result = $this->ReadUserData("", false); // First parameter empty, otherwise it will loop with SetUser !
      }
    }
    return ($result ? $user_encoded : FALSE);
  }


  function RenameCurrentUser(
      $new_user,
      $no_error_info = FALSE
  ) {
      if ($this->IsCaseSensitiveUsers()) {
          $the_new_user = $new_user;
      } else {
          $the_new_user = mb_strtolower($new_user,'UTF-8');
      }
      $the_new_user = str_replace("\\", "", $the_new_user);
      $result = FALSE;
      if ($this->CheckUserExists($the_new_user)) // Check if the new user already exists
      {
          $this->WriteLog("Error: Unable to rename the current user ".$this->GetUser()." to $the_new_user because $the_new_user already exists", FALSE, FALSE, _MULTIOTP_USER_EXISTS_, 'User');
      }
      else
      {
          if ($this->CheckUserExists()) // Check if the current user already exists
          {
              if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data['sql_users_table'])) || ('files' == $this->GetBackendType()))
              {
                  switch ($this->GetBackendType())
                  {
                      case 'mysql':
                          $esc_actual = escape_mysql_string($this->GetUser());
                          $esc_new    = escape_mysql_string($the_new_user);
                          if ($this->OpenMysqlDatabase())
                          {
                              if ("" != $this->_config_data['sql_users_table'])
                              {
                                  $sQuery = "UPDATE `".$this->_config_data['sql_users_table']."` SET user='".mb_strtolower($esc_new,'UTF-8')."' WHERE `user`='".$esc_actual."'";
                                  
                                  if (is_object($this->_mysqli))
                                  {
                                      if (!($rResult = $this->_mysqli->query($sQuery)))
                                      {
                                          if (!$no_error_info)
                                          {
                                              $this->WriteLog("Error: Could not rename the user ".$this->GetUser().": ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'User');
                                          }
                                      }
                                      else
                                      {
                                          $num_rows = $this->_mysqli->affected_rows;
                                      }
                                  }
                                  elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link)))
                                  {
                                      if (!$no_error_info)
                                      {
                                          $this->WriteLog("Error: Could not rename the user ".$this->GetUser().": ".mysql_error(), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'User');
                                      }
                                  }
                                  else
                                  {
                                      $num_rows = mysql_affected_rows($this->_mysql_database_link);
                                  }
                                  
                                  if (0 == $num_rows)
                                  {
                                      $this->WriteLog("Error: Could not rename the user ".$this->GetUser().". User does not exist", FALSE, FALSE, _MULTIOTP_USER_MISSING_, 'User');
                                  }
                                  else
                                  {
                                      $this->WriteLog("Info: User ".$this->GetUser()." successfully renamed to $the_new_user", FALSE, FALSE, _MULTIOTP_USER_UPDATED_, 'User');
                                      $result = TRUE;
                                  }
                              }
                          }
                          break;
                      case 'pgsql':
                          if ($this->OpenPGSQLDatabase()) {
                            $esc_actual = pg_escape_string($this->_pgsql_database_link, $this->GetUser());
                            $esc_new    = pg_escape_string($this->_pgsql_database_link, $the_new_user);
                              if ("" != $this->_config_data['sql_users_table'])
                              {
                                  $sQuery = "UPDATE \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_users_table']."\" SET \"user\" = '".mb_strtolower($esc_new,'UTF-8')."' WHERE \"user\" = '".$esc_actual."'";
                                  
                                  if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery)))
                                  {
                                      if (!$no_error_info)
                                      {
                                          $this->WriteLog("Error: Could not rename the user ".$this->GetUser().": ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'User');
                                      }
                                  }
                                  else
                                  {
                                      $num_rows = pg_affected_rows($rResult);
                                  }
                                  
                                  if (0 == $num_rows)
                                  {
                                      $this->WriteLog("Error: Could not rename the user ".$this->GetUser().". User does not exist", FALSE, FALSE, _MULTIOTP_USER_MISSING_, 'User');
                                  }
                                  else
                                  {
                                      $this->WriteLog("Info: User ".$this->GetUser()." successfully renamed to $the_new_user", FALSE, FALSE, _MULTIOTP_USER_UPDATED_, 'User');
                                      $result = TRUE;
                                  }
                              }
                          }
                          break;
                      case 'files':
                      default:
                          $old_user_filename = $this->EncodeFileId($this->GetUser(), $this->IsCaseSensitiveUsers()).'.db';
                          if (!file_exists($this->GetUsersFolder().$old_user_filename)) {
                              $old_user_filename = $this->EncodeFileId($this->GetUser(), $this->IsCaseSensitiveUsers(), TRUE).'.db';
                          }
                          if (file_exists($this->GetUsersFolder().$old_user_filename)) {
                              $new_user_filename = $this->EncodeFileId($the_new_user, $this->IsCaseSensitiveUsers()).'.db';
                              rename($this->GetUsersFolder().$old_user_filename, $this->GetUsersFolder().$new_user_filename);
                              $result = TRUE;
                          }
                          break;
                  }
              }
          }
          if ($result)
          {
              $this->_user = mb_strtolower($the_new_user,'UTF-8');
          }
      }
      return $result;
  }


  function GetUser()
  {
    return str_replace("\\", "", $this->_user);
  }


  function GetRawUser()
  {
    return str_replace("\\", "", $this->_rawUser);
  }


  /// Return a real username if the initial one is not existing
  function FindRealUserName(
    $user,
    $skip_first = FALSE
  ) {
    $real_user = $user;
    if ($skip_first || (!$this->CheckUserExists($user, FALSE, TRUE, TRUE))) {
      // Try to find the user with a shorter domain name
      if (false !== mb_strpos($user, '@')) {
        $at_user_split = explode('@', $user, 2);
        $at_user_dots = array_reverse(explode('.', $at_user_split[1]));
        $at_remove_length = 0;
        for ($dot_loop = 0; $dot_loop < count($at_user_dots); $dot_loop++) {
          $at_remove_length+= 1 + mb_strlen($at_user_dots[$dot_loop]);
          $cleaned_user = mb_substr($user, 0, mb_strlen($user) - $at_remove_length);
          if (($this->IsDeveloperMode())) {
            $this->WriteLog('Developer: Trying username '.$cleaned_user, false, false, 8888, 'Debug', "");
          }
          if ($this->CheckUserExists($cleaned_user, FALSE, TRUE, TRUE)) {
            $real_user = $cleaned_user;
            break;
          }
        }
      // Clean Windows legacy domain name before the username
      } elseif (false !== mb_strpos($user, "\\")) {
        $cleaned_user = mb_substr($user, mb_strpos($user, "\\")+1);
        if ($this->CheckUserExists($cleaned_user, FALSE, TRUE, TRUE)) {
          $real_user = $cleaned_user;
        }
      // Try to clean a phone number
      } else {
        $clean_phone = $this->CleanPhoneNumber($user);
        if ($this->CheckUserExists($clean_phone, FALSE, TRUE, TRUE)) {
          $real_user = $clean_phone;
        }
      }
    }
    return $real_user;
  }

  // Check if user exists (locally only)
  function CheckUserExists(
      $user = "",
      $no_server_check = FALSE,
      $no_error = FALSE,
      $no_local_check_if_not_on_server = FALSE // Used by FindRealUserName to continue to search the next available user online
  ) {
      $check_user = str_replace("\\", "", ("" != $user)?$user:$this->GetUser());
      $result = FALSE;

      if ("" != nullable_trim($check_user)) {
          $server_result = -1;
          if ((!$no_server_check) && ("" != $this->GetServerUrl())) {
              $server_result = $this->CheckUserExistsOnServer($check_user);
              if (_MULTIOTP_USER_EXISTS_ == $server_result) {
                  // We return only if the user exists, so we check also the local one
                  $result = TRUE;
                  return $result;
              } else if ($no_local_check_if_not_on_server && (_MULTIOTP_USER_MISSING_ == $server_result)) {
                  // Delete the user if it exists locally and not on the server
                  $this->DeleteUser($check_user, TRUE);
                  $result = FALSE;
                  return $result;
              }
          }

          if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data['sql_users_table'])) || ('files' == $this->GetBackendType())) {
              switch ($this->GetBackendType()) {
                  case 'mysql':
                      $check_user_escaped = escape_mysql_string($check_user);
                      if ($this->OpenMysqlDatabase()) {
                          $sQuery  = "SELECT * FROM `".$this->_config_data['sql_users_table']."` WHERE `user` = '{$check_user_escaped}'";
                          
                          if (is_object($this->_mysqli)) {
                              if (!($rResult = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              } else {
                                  $num_rows = $rResult->num_rows;
                              }
                          }
                          elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              $num_rows = mysql_num_rows($this->_mysql_database_link);
                          }
                          
                          if (0 == $num_rows) {
                              if ((!$no_error) || $this->GetVerboseFlag()) {
                                $this->WriteLog("Error: User ".$check_user." does not exist", FALSE, FALSE, _MULTIOTP_USER_MISSING_, 'System', "");
                              }
                              $result = FALSE;
                          } else {
                              if ($this->GetVerboseFlag()) {
                                $this->WriteLog("Info: User ".$check_user." exists", FALSE, FALSE, _MULTIOTP_USER_EXISTS_, 'System', "");
                              }
                              $result = TRUE;
                          }
                      }
                      break;
                  case 'pgsql':
                      if ($this->OpenPGSQLDatabase()) {
                          $check_user_escaped = pg_escape_string($this->_pgsql_database_link, $check_user);
                          $sQuery  = "SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_users_table']."\" WHERE \"user\" = '{$check_user_escaped}';";
                          
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              $num_rows = pg_num_rows($rResult);
                          }
                          
                          if (0 == $num_rows) {
                              if (!$no_error) {
                                  $this->WriteLog("Error: User ".$check_user." does not exist", FALSE, FALSE, _MULTIOTP_USER_MISSING_, 'System', "");
                              }
                              $result = FALSE;
                          } else {
                              $result = TRUE;
                          }
                      }
                      break;
                  case 'files':
                  default:
                      $user_filename = $this->EncodeFileId($check_user, $this->IsCaseSensitiveUsers()).'.db';
                      if (!file_exists($this->GetUsersFolder().$user_filename)) {
                          $user_filename = $this->EncodeFileId($check_user, $this->IsCaseSensitiveUsers(), TRUE).'.db';
                      }
                      $result = file_exists($this->GetUsersFolder().$user_filename);
                      break;
              }
          }
      }
      return $result;
  }


  function LockUser(
      $user = ""
  ) {
      $result = FALSE;
      if ("" != $user) {
          $this->SetUser($user, false);
      }
      if ($this->ReadUserData("", FALSE, TRUE)) {
          // LOCALLY ONLY, not on the server if any
          $this->SetUserLocked(1);
          if ($this->GetVerboseFlag()) {
              $this->WriteLog("Info: *User ".$this->GetUser()." successfully locked", FALSE, FALSE, _MULTIOTP_USER_UPDATED_, 'User');
          }
          $this->WriteUserData();
          $result = TRUE;
      }
      return $result;
  }


  function UnlockUser(
      $user = ""
  ) {
      $result = FALSE;
      if ("" != $user) {
          $this->SetUser($user, false);
      }
      if ($this->ReadUserData("", FALSE, TRUE)) {
          // LOCALLY ONLY, not on the server if any
          $this->SetUserErrorCounter(0);
          $this->SetUserLocked(0);
          if ($this->GetVerboseFlag()) {
              $this->WriteLog("Info: *User ".$this->GetUser()." successfully unlocked", FALSE, FALSE, _MULTIOTP_USER_UPDATED_, 'User');
          }
          $this->WriteUserData();
          $result = TRUE;
      }
      return $result;
  }


  function DeleteUser(
      $user = "",
      $no_error_info = FALSE
  ) {
      if ("" != $user) {
          $this->SetUser($user, false);
      }
      
      $result = FALSE;
      
      // First, we delete the user file if the backend is files or when migration is enabled
      if (('files' == $this->GetBackendType()) || ($this->GetMigrationFromFile())) {

          $user_filename = $this->EncodeFileId($this->GetUser(), $this->IsCaseSensitiveUsers()).'.db';
          if (!file_exists($this->GetUsersFolder().$user_filename)) {
              $user_filename = $this->EncodeFileId($this->GetUser(), $this->IsCaseSensitiveUsers(), TRUE).'.db';
          }
          if (!file_exists($this->GetUsersFolder().$user_filename)) {
              if (!$no_error_info) {
                  if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Error: *Unable to delete user ".$this->GetUser().", the users database file ".$this->GetUsersFolder().$user_filename." does not exist", FALSE, FALSE, _MULTIOTP_USER_MISSING_, 'User');
                  } else {
                      $this->WriteLog("Error: Unable to delete user ".$this->GetUser(), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'User');
                  }
              }
          } else {
              $result = unlink($this->GetUsersFolder().$user_filename);
              if ($result) {
                  $this->WriteLog("Info: User ".$this->GetUser()." successfully deleted", FALSE, FALSE, _MULTIOTP_USER_DELETED_, 'User');
              } elseif (!$this->GetMigrationFromFile()) {
                  if (!$no_error_info) {
                      $this->WriteLog("Error: Unable to delete user ".$this->GetUser(), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'User');
                  }
              }
          }
      }

      if ($this->GetBackendTypeValidated()) {
          switch ($this->_config_data['backend_type']) {
              case 'mysql':
                  $user_escaped = escape_mysql_string($this->GetUser());
                  if ($this->OpenMysqlDatabase()) {
                      if ("" != $this->_config_data['sql_users_table']) {
                          $sQuery  = "DELETE FROM `".$this->_config_data['sql_users_table']."` WHERE `user` = '".$user_escaped."'";
                          
                          if (is_object($this->_mysqli)) {
                              if (!($rResult = $this->_mysqli->query($sQuery))) {
                                  if (!$no_error_info) {
                                      $this->WriteLog("Error: Could not delete user ".$this->GetUser().": ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'User');
                                  }
                              } else {
                                  $num_rows = $this->_mysqli->affected_rows;
                              }
                          } elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete user ".$this->GetUser().": ".mysql_error(), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'User');
                              }
                          } else {
                              $num_rows = mysql_affected_rows($this->_mysql_database_link);
                          }
                          
                          if (0 == $num_rows) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete user ".$this->GetUser().". User does not exist", FALSE, FALSE, _MULTIOTP_USER_MISSING_, 'User');
                              }
                          } else {
                              $this->WriteLog("Info: User ".$this->GetUser()." successfully deleted", FALSE, FALSE, _MULTIOTP_USER_DELETED_, 'User');
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $user_escaped = pg_escape_string($this->_pgsql_database_link, $this->GetUser());
                      if ("" != $this->_config_data['sql_users_table']) {
                          $sQuery  = "DELETE FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_users_table']."\" WHERE \"user\" = '".$user_escaped."'";
                          
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete user ".$this->GetUser().": ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'User');
                              }
                          } else {
                              $num_rows = pg_affected_rows($rResult);
                          }
                          
                          if (0 == $num_rows) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete user ".$this->GetUser().". User does not exist", FALSE, FALSE, _MULTIOTP_USER_MISSING_, 'User');
                              }
                          } else {
                              $this->WriteLog("Info: User ".$this->GetUser()." successfully deleted", FALSE, FALSE, _MULTIOTP_USER_DELETED_, 'User');
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              default:
              // Nothing to do if the backend type is unknown
                  break;
          }                        
      }
      
      if ($result) {
          foreach (explode("\t", $this->GetTokensList()) as $one_token) {
              if ($this->RemoveTokenAttributedUsers($one_token, $this->GetUser())) {
                  $this->WriteTokenData();
              }
          }
          $this->TouchFolder('data',
                             'User',
                             $this->GetUser(),
                             TRUE,
                             "DeleteUser");
      }
      
      return $result;
  }


  function GetUsersCount($no_cache = FALSE)
  {
      if (($this->IsCacheData()) && (intval($this->ReadCacheValue('users_count')) >= 0) && (!$no_cache)) {
          $users_count = intval($this->ReadCacheValue('users_count'));
      } else {
          $users_count = 0;
          switch ($this->GetBackendType()) {
              case 'mysql':
                  if ($this->OpenMysqlDatabase())
                  {
                      $sQuery  = "SELECT COUNT(user) AS counter FROM `".$this->_config_data['sql_users_table']."` ";
                      if (is_object($this->_mysqli)) {
                          if (!($result = $this->_mysqli->query($sQuery))) {
                              $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              $result = FALSE;
                          } else {
                              if ($aRow = $result->fetch_assoc()) {
                                  $users_count = $aRow['counter'];
                              }
                          }
                      } else {
                          if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              if ($aRow = mysql_fetch_assoc($rResult)) {
                                  $users_count = $aRow['counter'];
                              }
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase())
                  {
                      $sQuery  = "SELECT COUNT(\"user\") AS \"counter\" FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_users_table']."\" ";
                      if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                          $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                      } else {
                          if ($aRow = pg_fetch_assoc($rResult)) {
                              $users_count = $aRow['counter'];
                          }
                      }
                  }
                  break;
              case 'files':
              default:
                  if ($users_handle = @opendir($this->GetUsersFolder())) {
                      while ($file = readdir($users_handle)) {
                          if ((mb_substr($file, -3) == ".db") && ($file != '.db')) {
                              $users_count++;
                          }
                      }
                      closedir($users_handle);
                  }
          }
          if (($this->IsCacheData()) && ($users_count >= 0)) {
              $this->WriteCacheValue('users_count', $users_count);
              $this->WriteCacheData();
          }
      }
      return $users_count;
  }


  function ReadUserDataArray(
      $user = "",
      $create = false,
      $no_server_check = false
  ) {
      $array_user = ("" != $user)?$user:$this->GetUser();
      $result = false;

      // We initialize the local encryption check variable
      $local_encryption_check = false;

      // We reset all values (we know the key based on the schema)
      $temp_user_array = $this->ResetTempUserArray();

      // First, we read the user file if the backend is files or when migration is enabled
      if (('files' == $this->GetBackendType()) || ($this->GetMigrationFromFile())) {

          $user_filename = $this->EncodeFileId($array_user, $this->IsCaseSensitiveUsers()).'.db';
          $file_folder = $this->GetUsersFolder();
          $user_filemtime = 0;
          if (!is_readable($file_folder.$user_filename)) {
              $user_filename = $this->EncodeFileId($array_user, $this->IsCaseSensitiveUsers(), TRUE).'.db';
          }

          if (is_readable($file_folder.$user_filename)) {
            $user_filemtime = filemtime($file_folder.$user_filename);
          }
          
          if ("" != $this->GetCurrentUserSid()) {
            $creduser_filename = $this->GetCurrentUserSid().'.db';
            $creduser_folder = $this->GetCredUsersFolder();
            if (is_readable($creduser_folder.$creduser_filename)) {
              $creduser_filetime = filemtime($creduser_folder.$creduser_filename);
              if ($creduser_filetime > $user_filemtime) {
                $file_folder = $creduser_folder;
                $user_filename = $creduser_filename;
              }
            }
          }

          if (!is_readable($file_folder.$user_filename)) {
              if (!$create) {
                  $this->WriteLog("Error: database file ".$file_folder.$user_filename." for user ".$array_user." does not exist or is not readable", FALSE, FALSE, _MULTIOTP_USER_MISSING_, 'System', "");
              }
          } else {
              $temp_user_array['multi_account'] = 0;
              $temp_user_array['time_interval'] = 0;

              $trial_count   = 0;
              $trial_limit   = 5;
              $trial_step_ms = 100; 
              while ((!$result) && ($trial_count < $trial_limit)) {
                if ($trial_count > 0) {
                  $this->WriteLog("Error: database file ".$file_folder.$user_filename." for user ".$array_user." cannot be read (concurrent access)", FALSE, FALSE, _MULTIOTP_OPERATION_CANCELLED_, 'System', "");
                }
                usleep ($trial_step_ms * $trial_count * 1000);
                $trial_count++;
                if ($file_handler = @fopen($file_folder.$user_filename, "rt")) {
                    flock($file_handler, LOCK_SH);
                    $first_line = nullable_trim(fgets($file_handler));
                    $v3 = (false !== mb_strpos(mb_strtolower($first_line,'UTF-8'),"multiotp-database-format-v3"));
                    
                    // First version format support
                    if (false === mb_strpos(mb_strtolower($first_line,'UTF-8'),"multiotp-database-format")) {
                        $temp_user_array['algorithm']          = $first_line;
                        $temp_user_array['token_seed']         = nullable_trim(fgets($file_handler));
                        $temp_user_array['user_pin']           = nullable_trim(fgets($file_handler));
                        $temp_user_array['number_of_digits']   = nullable_trim(fgets($file_handler));
                        $temp_user_array['last_event']         = intval(nullable_trim(fgets($file_handler))) - 1;
                        $temp_user_array['request_prefix_pin'] = intval(nullable_trim(fgets($file_handler)));
                        $temp_user_array['last_login']         = intval(nullable_trim(fgets($file_handler)));
                        $temp_user_array['error_counter']      = intval(nullable_trim(fgets($file_handler)));
                        $temp_user_array['locked']             = intval(nullable_trim(fgets($file_handler)));
                    } else {
                        while (!feof($file_handler)) {
                            $line = nullable_trim(fgets($file_handler));
                            $line_array = explode("=",$line,2);
                            if ($v3) { // v3 format, only tags followed by := instead of = are encrypted
                                if (":" == mb_substr($line_array[0], -1)) {
                                    $line_array[0] = mb_substr($line_array[0], 0, mb_strlen($line_array[0]) -1);
                                    $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                                    $local_encryption_check = $this->_encryption_check;
                                }
                            } else { // v2 format, only defined tags are encrypted
                                if ((FALSE !== mb_strpos(mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'), mb_strtolower('*'.$line_array[0].'*','UTF-8'))) || ("*all*" == mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'))) {
                                    $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                                    $local_encryption_check = $this->_encryption_check;
                                }
                            }
                            $line_array[1] = str_replace("<<CRLF>>",chr(10),isset($line_array[1]) ? $line_array[1] : "");
                            if ("" != nullable_trim($line_array[0])) {
                                $temp_user_array[mb_strtolower($line_array[0],'UTF-8')] = $line_array[1];
                            }
                        }
                    }
                    fclose($file_handler);
                    $result = true;
                } else {
                  $this->WriteLog("Error: database file ".$file_folder.$user_filename." for user ".$array_user." cannot be read", FALSE, FALSE, _MULTIOTP_OPERATION_CANCELLED_, 'System', "");
                }
              } // while
              if (("" != $temp_user_array['encryption_hash']) && ($local_encryption_check)) {
                  if ($temp_user_array['encryption_hash'] != $this->CalculateControlHash($this->GetEncryptionKey())) {
                      $temp_user_array['encryption_hash'] = "ERROR";
                      $this->WriteLog("Error: the user information encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "");
                      $result = false;
                  }
              }
          }
      }

      // And now, we override the values if another backend type is defined
      if ($this->GetBackendTypeValidated()) {
          switch ($this->_config_data['backend_type']) {
              case 'mysql':
                  $array_user_escaped = escape_mysql_string($array_user);
                  if ($this->OpenMysqlDatabase()) {
                      if ("" != $this->_config_data['sql_users_table']) {
                          $sQuery  = "SELECT * FROM `".$this->_config_data['sql_users_table']."` WHERE `user` = '".$array_user_escaped."'";
                          $aRow = NULL;
                          
                          if (is_object($this->_mysqli)) {
                              if (!($rResult = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: SQL query error ($sQuery) : ".nullable_trim($this->_mysqli->error).' ', TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                                  $result = false;
                              } else {
                                  $aRow = $rResult->fetch_assoc();
                              }
                          } else {
                              if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                                  $this->WriteLog("Error: SQL query error ($sQuery) : ".mysql_error(), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                                  $result = false;
                              } else {
                                  $aRow = mysql_fetch_assoc($rResult);
                              }
                          }

                          if (NULL != $aRow) {
                              $result = false;
                              foreach ($aRow as $key => $value) {
                                  $in_the_schema = FALSE;
                                  foreach ($this->_sql_tables_schema['users'] as $valid_key => $valid_format) {
                                      if ($valid_key == $key) {
                                          $in_the_schema = TRUE;
                                          if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                            $value = "";
                                          }
                                          break;
                                      }
                                  }
                                  if (($in_the_schema) && ($key != 'user')) {
                                      if (('ENC:' == mb_substr($value,0,4)) && (':ENC' == mb_substr($value,-4))) {
                                          $value = mb_substr($value,4);
                                          $value = mb_substr($value,0,mb_strlen($value)-4);
                                          $temp_user_array[$key] = $this->Decrypt($key,$value,$this->GetEncryptionKey());
                                          $local_encryption_check = $this->_encryption_check;
                                      } else {
                                          $temp_user_array[$key] = $value;
                                      }
                                      $temp_user_array[$key] = str_replace("<<CRLF>>",chr(10),$temp_user_array[$key]);
                                  } elseif ((!$in_the_schema) && ('unique_id' != $key)  && $this->GetVerboseFlag()) {
                                      $this->WriteLog("Warning: *The key ".$key." is not in the users table schema", FALSE, FALSE, _MULTIOTP_SYSTEM_KEY_NOT_IN_SCHEMA_ERROR_, 'System', "", 3);
                                  }
                                  $result = true;
                              }
                              if(0 == count($aRow) && !$create) {
                                  $this->WriteLog("Error: SQL database entry for user ".$array_user." does not exist", FALSE, FALSE, _MULTIOTP_USER_MISSING_, 'System', "");
                              }
                          }
                      }
                      if (("" != $temp_user_array['encryption_hash']) && ($local_encryption_check)) {
                          if ($temp_user_array['encryption_hash'] != $this->CalculateControlHash($this->GetEncryptionKey())) {
                              $temp_user_array['encryption_hash'] = "ERROR";
                              $this->WriteLog("Error: the users mysql encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "");
                              $result = false;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $array_user_escaped = pg_escape_string($this->_pgsql_database_link, $array_user);
                      if ("" != $this->_config_data['sql_users_table']) {
                          $sQuery  = "SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_users_table']."\" WHERE \"user\" = '".$array_user_escaped ."'";
                          $aRow = NULL;
                          
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: SQL query error ($sQuery) : ".pg_last_error($this->_pgsql_database_link), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                              $result = false;
                          }
                          else {
                              $aRow = pg_fetch_assoc($rResult);
                          }

                          if (NULL != $aRow) {
                              $result = false;
                              foreach ($aRow as $key => $value) {
                                  $in_the_schema = FALSE;
                                  foreach ($this->_sql_tables_schema['users'] as $valid_key => $valid_format) {
                                      if ($valid_key == $key) {
                                          $in_the_schema = TRUE;
                                          if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                            $value = "";
                                          }
                                          break;
                                      }
                                  }
                                  if (is_null($value)) {
                                    $value = "";
                                  }
                                  if (($in_the_schema) && ($key != 'user')) {
                                      if (('ENC:' == mb_substr($value,0,4)) && (':ENC' == mb_substr($value,-4))) {
                                          $value = mb_substr($value,4);
                                          $value = mb_substr($value,0,mb_strlen($value)-4);
                                          $temp_user_array[$key] = $this->Decrypt($key,$value,$this->GetEncryptionKey());
                                          $local_encryption_check = $this->_encryption_check;
                                      } else {
                                          $temp_user_array[$key] = $value;
                                      }
                                      if (is_null($temp_user_array[$key])) {
                                        $temp_user_array[$key] = "";
                                      }
                                      $temp_user_array[$key] = str_replace("<<CRLF>>",chr(10),$temp_user_array[$key]);
                                  } elseif ((!$in_the_schema) && ('unique_id' != $key)  && $this->GetVerboseFlag()) {
                                      $this->WriteLog("Warning: *The key ".$key." is not in the users table schema", FALSE, FALSE, _MULTIOTP_SYSTEM_KEY_NOT_IN_SCHEMA_ERROR_, 'System', "", 3);
                                  }
                                  $result = true;
                              }
                              if(0 == count($aRow) && !$create) {
                                  $this->WriteLog("Error: SQL database entry for user ".$array_user." does not exist", FALSE, FALSE, _MULTIOTP_USER_MISSING_, 'System', "");
                              }
                          }
                      }
                      if (("" != $temp_user_array['encryption_hash']) && ($local_encryption_check)) {
                          if ($temp_user_array['encryption_hash'] != $this->CalculateControlHash($this->GetEncryptionKey())) {
                              $temp_user_array['encryption_hash'] = "ERROR";
                              $this->WriteLog("Error: the users pgsql encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "");
                              $result = false;
                          }
                      }
                  }
                  break;
              default:
              // Nothing to do if the backend type is "file" or unknown
                  break;
          }
      }

      // And now, we do the ReadUserData online on the server
      $server_result = -1;
      if ((!$no_server_check) && ("" != $this->GetServerUrl()))
      {
          $server_result = $this->ReadUserDataOnServer($array_user);
          if (20 < mb_strlen($server_result))
          {
              $temp_user_array['encryption_hash'] = $this->CalculateControlHash($this->GetEncryptionKey());
              $server_array = explode("\n",$server_result);
              $server_result = _MULTIOTP_OPERATION_DONE_;

              foreach ($server_array as $one_line)
              {
                  $line = nullable_trim($one_line);
                  $line_array = explode("=",$line,2);
                  if (":" == mb_substr($line_array[0], -1))
                  {
                      $line_array[0] = mb_substr($line_array[0], 0, mb_strlen($line_array[0]) -1);
                      $line_array[1] = $this->Decrypt($line_array[0], $line_array[1], $this->GetServerSecret());
                      $local_encryption_check = $this->_encryption_check;
                  }
                  $line_array[1] = str_replace("<<CRLF>>",chr(10),isset($line_array[1]) ? $line_array[1] : "");
                  if ("" != nullable_trim($line_array[0]))
                  {
                      if ('encryption_hash' != mb_strtolower($line_array[0],'UTF-8'))
                      {
                          $temp_user_array[mb_strtolower($line_array[0],'UTF-8')] = $line_array[1];
                      }
                  }
              }
              $result = true;
          } elseif (_MULTIOTP_USER_MISSING_ == $server_result) {
            // If user doesn't exist anymore on the server,
            //   reply false and delete local cached file if any
            $this->DeleteUser("", true);
            $result = false;
          }
      }


      // We force the algorithm to be ALWAYS in lowercase
      $temp_user_array['algorithm'] = mb_strtolower($temp_user_array['algorithm'],'UTF-8');

      $now_epoch = time();
      $temp_user_array['delayed_account'] = (($temp_user_array['error_counter'] >= $this->GetMaxDelayedFailures()) && ($now_epoch < ((isset($temp_user_array['last_error']) ? $temp_user_array['last_error'] : 0) + $this->GetMaxDelayedTime())));
      $temp_user_array['delayed_time'] = 0;
      $temp_user_array['delayed_finished'] = 0;
      if ($temp_user_array['delayed_account']) {
          $temp_user_array['delayed_time'] = ((isset($temp_user_array['last_error']) ? $temp_user_array['last_error'] : 0) + $this->GetMaxDelayedTime()) - $now_epoch;
          $temp_user_array['delayed_finished'] = ((isset($temp_user_array['last_error']) ? $temp_user_array['last_error'] : 0) + $this->GetMaxDelayedTime());
      }
      if (false !== $result) {
          return $temp_user_array;
      } else {
          return false;
      }
  }


  function ReadUserData(
      $user = "",
      $create = FALSE,
      $no_server_check = FALSE,
      $return_array_only = FALSE
  ) {
      if (("" != $user) && (!$return_array_only)) {
          $this->SetUser($user, false);
      }
      $result = false;
      $temp_user_array = $this->ReadUserDataArray($user, $create, $no_server_check);
      if (false !== $temp_user_array) {
          if ($return_array_only) {
            $result = $return_array_only;
          } else {
            $this->_user_data = $temp_user_array;
            $result = true;
          }
      } else {
          if (!$return_array_only) {
            $this->_user_data = $this->ResetUserArray();
          }
      }

      if (!$return_array_only) {
        $this->SetUserDataReadFlag($result);
      }
      return $result;
  }


  function WriteUserData(
      $write_user_data_array    = false,
      $update_last_change_param = true
  ) {
      if (is_array($write_user_data_array)) {
        if (!isset($write_user_data_array['automatically'])) {
          $write_user_data_array['automatically'] = false;
        }
        if (!isset($write_user_data_array['update_last_change'])) {
          $write_user_data_array['update_last_change'] = true;
        }
        if (!isset($write_user_data_array['sync_process'])) {
          $write_user_data_array['sync_process'] = false;
        }
      } else {
        $temp_array = [];
        $temp_array['automatically']      = $write_user_data_array;
        $temp_array['update_last_change'] = $update_last_change_param;
        $temp_array['sync_process']       = false;
        $write_user_data_array = $temp_array;
      }

      if ("" == nullable_trim($this->GetUser())) {
        $result = false;
      } else {
        if ((1 == $this->_user_data['push_enabled']) && ($this->_user_data['push_enable_date'] <= 0) && ($this->IsPushActivated())) {
          $temp_mouid = nullable_trim(isset($this->_user_data['mouid']) ? $this->_user_data['mouid'] : "");
          if (empty($temp_mouid)) {
           $temp_mouid = $this->GetMouidPrefix().base32_encode(hex2bin(substr(md5($this->GetPushId().$this->GetUser().sha1(mt_rand(100000,999999).date("YmdHis"))), 0, 30)));
          }

          $subscription_result = $this->UserPushSubscription(array(
            "user"         => $this->GetUser(),
            "mouid"        => $temp_mouid,
            "mosid"        => $this->GetPushId(),
            "push_gateway" => $this->GetPushGateway(true)
          ));
          
          if (200 == $subscription_result) {
            $this->_user_data['mouid'] = $temp_mouid;
            $this->_user_data['mosid'] = $this->GetPushId();
            $this->_user_data['push_gateway'] = $this->GetPushGateway(true);
            $this->_user_data['push_enable_date'] = time();
          }
        }

        $result = $this->WriteData(array_merge(array('item'               => 'User',
                                                     'table'              => 'users',
                                                     'folder'             => $this->GetUsersFolder(),
                                                     'data_array'         => $this->_user_data,
                                                     'force_file'         => false,
                                                     'id_field'           => 'user',
                                                     'id_value'           => $this->GetUser(),
                                                     'encode_file_id'     => $this->IsEncodeFileId(),
                                                     'id_case_sensitive'  => $this->IsCaseSensitiveUsers()
                                                    ), $write_user_data_array));
      }
      return $result;
  }


  function GetUsersList() {
    return $this->GetList('user', 'sql_users_table', $this->GetUsersFolder());
  }


  function GetEnhancedUsersList() {
    $array_result = [];
    $user_list = nullable_trim($this->GetUsersList());
    $users_array = explode("\t", $user_list);
    if (("" != $user_list) && (count($users_array) > 0)) {
      foreach ($users_array as $user) {
        $this->SetUser($user);
        array_push($array_result, $user . "|" . "s" . ((1 == intval($this->GetUserSynchronized())) ? "1" : "0"));
      }
      return implode("\t", $array_result);
    } else {
      return "";
    }
  }


  /**
   * @brief   Get the list of delayed users
   *
   * Based on the request of Frank van der Aa, Vanboxtel BV (NL)
   *   The proposed implementation of Frank was using the cache feature, but
   *   this is not possible as the delayed information is based on the
   *   error_counter and on the last_error date, and the delayed information
   *   is not available in the database. We are not returning locked users.
   *
   * @param   int      $limit   Maximum number of users in the returned list
   *
   * @return  string   List of delayed users
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.0.3.6
   * @date    2017-02-13
   * @since   2017-02-09
   */
  function GetDelayedUsersList(
      $limit = 0
  ) {

      // We initialize the local encryption check variable
      $local_encryption_check = false;

      $delayed_users_list = "";
      $delayed_users_count = 0;
      if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data['sql_users_table'])) || ('files' == $this->GetBackendType())) {
          switch ($this->GetBackendType()) {
              case 'mysql':
                  if ($this->OpenMysqlDatabase()) {
                      $sQuery  = "SELECT user, error_counter, last_error FROM `".$this->_config_data['sql_users_table']."` WHERE (`locked` = 0) ORDER BY user ASC";
                      if ($limit > 0) {
                          $sQuery.= " LIMIT 0,".$limit;
                      }
                      if (is_object($this->_mysqli)) {
                          if (!($result = $this->_mysqli->query($sQuery))) {
                              $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              $result = FALSE;
                          } else {
                              while ($aRow = $result->fetch_assoc()) {
                                  if ("" != $aRow['user']) {
                                      $now_epoch = time();
                                      if (($aRow['error_counter'] >= $this->GetMaxDelayedFailures()) && ($now_epoch < ((isset($aRow['last_error']) ? $aRow['last_error'] : 0) + $this->GetMaxDelayedTime()))) {
                                          $delayed_time = ((isset($aRow['last_error']) ? $aRow['last_error'] : 0) + $this->GetMaxDelayedTime()) - $now_epoch;
                                          $delayed_finished = (isset($aRow['last_error']) ? $aRow['last_error'] : 0) + $this->GetMaxDelayedTime();
                                          $delayed_users_list.= (("" != $delayed_users_list)?"\t":"").$aRow['user'].'|'.$delayed_finished;
                                          $delayed_users_count++;
                                      }
                                  }
                              }
                          }
                      } else {
                          if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              while ($aRow = mysql_fetch_assoc($rResult)) {
                                  if ("" != $aRow['user']) {
                                      $now_epoch = time();
                                      if (($aRow['error_counter'] >= $this->GetMaxDelayedFailures()) && ($now_epoch < ((isset($aRow['last_error']) ? $aRow['last_error'] : 0) + $this->GetMaxDelayedTime()))) {
                                          $delayed_time = ((isset($aRow['last_error']) ? $aRow['last_error'] : 0) + $this->GetMaxDelayedTime()) - $now_epoch;
                                          $delayed_finished = (isset($aRow['last_error']) ? $aRow['last_error'] : 0) + $this->GetMaxDelayedTime();
                                          $delayed_users_list.= (("" != $delayed_users_list)?"\t":"").$aRow['user'].'|'.$delayed_finished;
                                          $delayed_users_count++;
                                      }
                                  }
                              }                         
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $sQuery  = "SELECT \"user\", \"error_counter\", \"last_error\" FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_users_table']."\" WHERE (\"locked\" = 0) ORDER BY \"user\" ASC";
                      if ($limit > 0) {
                          $sQuery.= " LIMIT 0,".$limit;
                      }
                      if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                          $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                      } else {
                          while ($aRow = pg_fetch_assoc($rResult)) {
                              if ("" != $aRow['user']) {
                                  $now_epoch = time();
                                  if (($aRow['error_counter'] >= $this->GetMaxDelayedFailures()) && ($now_epoch < ((isset($aRow['last_error']) ? $aRow['last_error'] : 0) + $this->GetMaxDelayedTime()))) {
                                      $delayed_time = ((isset($aRow['last_error']) ? $aRow['last_error'] : 0) + $this->GetMaxDelayedTime()) - $now_epoch;
                                      $delayed_finished = (isset($aRow['last_error']) ? $aRow['last_error'] : 0) + $this->GetMaxDelayedTime();
                                      $delayed_users_list.= (("" != $delayed_users_list)?"\t":"").$aRow['user'].'|'.$delayed_finished;
                                      $delayed_users_count++;
                                  }
                              }
                          }                         
                      }
                  }
                  break;
              case 'files':
              default:
                  $users_count = 0;
                  if ($users_handle = @opendir($this->GetUsersFolder()))
                  {
                    while ($file = readdir($users_handle))
                    {
                      $error_counter = 0;
                      $last_error = 0;
                      $desactivated = FALSE;
                      $locked = FALSE;
                      if ((mb_substr($file, -3) == ".db") && ($file != '.db'))
                      {
                        $current_user = $this->DecodeFileId(mb_substr($file,0,-3));
                        if ($file_handler = @fopen($this->GetUsersFolder().$file, "rt")) {
                          flock($file_handler, LOCK_SH);
                          $first_line = nullable_trim(fgets($file_handler));
                          $v3 = (FALSE !== mb_strpos(mb_strtolower($first_line,'UTF-8'),"multiotp-database-format-v3"));
                          if (FALSE !== mb_strpos(mb_strtolower($first_line,'UTF-8'),"multiotp-database-format")) // Format V3
                          {
                            while (!feof($file_handler))
                            {
                              $line = nullable_trim(fgets($file_handler));
                              $line_array = explode("=",$line,2);
                              if ($v3) {
                                // v3 format, only tags followed by := instead of = are encrypted
                                if (":" == mb_substr($line_array[0], -1))
                                {
                                  $line_array[0] = mb_substr($line_array[0], 0, mb_strlen($line_array[0]) -1);
                                  $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                                  $local_encryption_check = $this->_encryption_check;
                                }
                              } else {
                                // v2 format, only defined tags are encrypted
                                if ((FALSE !== mb_strpos(mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'), mb_strtolower('*'.$line_array[0].'*','UTF-8'))) || ("*all*" == mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'))) {
                                  $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                                  $local_encryption_check = $this->_encryption_check;
                                }
                              }
                              $line_array[1] = str_replace("<<CRLF>>",chr(10),isset($line_array[1]) ? $line_array[1] : "");
                              if ('error_counter' == nullable_trim($line_array[0]))
                              {
                                $error_counter = intval($line_array[1]);
                              }
                              if ('last_error' == nullable_trim($line_array[0]))
                              {
                                $last_error = $line_array[1];
                              }
                              if ('desactivated' == nullable_trim($line_array[0]))
                              {
                                if (1 == (isset($line_array[1])?$line_array[1]:0))
                                {
                                  $desactivated = TRUE;
                                }
                              }
                              if ('locked' == nullable_trim($line_array[0]))
                              {
                                if (1 == (isset($line_array[1])?$line_array[1]:0))
                                {
                                  $locked = TRUE;
                                }
                              }
                            }
                          }
                          fclose($file_handler);
                          $users_count++;
                          
                          $now_epoch = time();
                          if (($error_counter >= $this->GetMaxDelayedFailures()) && ($now_epoch < ($last_error + $this->GetMaxDelayedTime()))) {
                            $delayed_time = ($last_error + $this->GetMaxDelayedTime()) - $now_epoch;
                            $delayed_finished = $last_error + $this->GetMaxDelayedTime();
                            if (!$locked)
                            {
                                $delayed_users_list.= (("" != $delayed_users_list)?"\t":"").$current_user.'|'.$delayed_finished;
                                $delayed_users_count++;
                            }
                          }
                        }
                      }
                      if (($limit > 0) && ($delayed_users_count >= $limit)) {
                        break;
                      }
                    }
                    closedir($users_handle);
                  }
          }
      }
      return $delayed_users_list;
  }


  function GetLockedUsersList(
      $limit = 0,
      $in_array_param = false
  ) {
      $in_array = $in_array_param;
      if (is_bool($limit)) {
        $in_array = $limit;
        $limit = 0;
      }

      $now_epoch = time();

      // We initialize the local encryption check variable
      $local_encryption_check = false;

      if (($this->IsCacheData()) && (($this->ReadCacheValue('locked_users_list')) != '-1')) {
          $locked_users_list = ($this->ReadCacheValue('locked_users_list'));
      } else {
          $locked_users_list = "";
          if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data['sql_users_table'])) || ('files' == $this->GetBackendType())) {
              switch ($this->GetBackendType()) {
                  case 'mysql':
                      if ($this->OpenMysqlDatabase()) {
                          $sQuery  = "SELECT user FROM `".$this->_config_data['sql_users_table']."` WHERE ((`locked` = 1) OR ((`error_counter` >= ".$this->GetMaxDelayedFailures().") AND (".$now_epoch." < (`last_error` + ".$this->GetMaxDelayedTime().")))) ORDER BY user ASC";
                          if ($limit > 0) {
                              $sQuery.= " LIMIT 0,".$limit;
                          }
                          if (is_object($this->_mysqli)) {
                              if (!($result = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  while ($aRow = $result->fetch_assoc()) {
                                      if ("" != $aRow['user']) {
                                          $locked_users_list.= (("" != $locked_users_list)?"\t":"").$aRow['user'];
                                      }
                                  }
                              }
                          } else {
                              if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                                  $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              } else {
                                  while ($aRow = mysql_fetch_assoc($rResult)) {
                                      if ("" != $aRow['user']) {
                                          $locked_users_list.= (("" != $locked_users_list)?"\t":"").$aRow['user'];
                                      }
                                  }                         
                              }
                          }
                      }
                      break;
                  case 'pgsql':
                      if ($this->OpenPGSQLDatabase()) {
                          $sQuery  = "SELECT \"user\" FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_users_table']."\" WHERE ((\"locked\" = 1) OR ((\"error_counter\" >= ".$this->GetMaxDelayedFailures().") AND (".$now_epoch." < (\"last_error\" + ".$this->GetMaxDelayedTime().")))) ORDER BY \"user\" ASC";
                          if ($limit > 0) {
                              $sQuery.= " LIMIT 0,".$limit;
                          }
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              while ($aRow = pg_fetch_assoc($rResult)) {
                                  if ("" != $aRow['user']) {
                                      $locked_users_list.= (("" != $locked_users_list)?"\t":"").$aRow['user'];
                                  }
                              }                         
                          }
                      }
                      break;
                  case 'files':
                  default:
                      $locked_users_count = 0;
                      $active_users_count = 0;
                      $users_count = 0;
                      if ($users_handle = @opendir($this->GetUsersFolder()))
                      {
                          while ($file = readdir($users_handle))
                          {
                              $locked = FALSE;
                              $error_counter = 0;
                              $last_error = 0;
                              $desactivated = FALSE;
                              if ((mb_substr($file, -3) == ".db") && ($file != '.db'))
                              {
                                  $current_user = $this->DecodeFileId(mb_substr($file,0,-3));
                                  if ($file_handler = @fopen($this->GetUsersFolder().$file, "rt")) {
                                      flock($file_handler, LOCK_SH);
                                      $first_line = nullable_trim(fgets($file_handler));
                                      $v3 = (FALSE !== mb_strpos(mb_strtolower($first_line,'UTF-8'),"multiotp-database-format-v3"));
                                      if (FALSE !== mb_strpos(mb_strtolower($first_line,'UTF-8'),"multiotp-database-format")) // Format V3
                                      {
                                          while (!feof($file_handler))
                                          {
                                              $line = nullable_trim(fgets($file_handler));
                                              $line_array = explode("=",$line,2);
                                              if ($v3) // v3 format, only tags followed by := instead of = are encrypted
                                              {
                                                  if (":" == mb_substr($line_array[0], -1))
                                                  {
                                                      $line_array[0] = mb_substr($line_array[0], 0, mb_strlen($line_array[0]) -1);
                                                      $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                                                      $local_encryption_check = $this->_encryption_check;
                                                  }
                                              }
                                              else // v2 format, only defined tags are encrypted
                                              {
                                                  if ((FALSE !== mb_strpos(mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'), mb_strtolower('*'.$line_array[0].'*','UTF-8'))) || ("*all*" == mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8')))
                                                  {
                                                      $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                                                      $local_encryption_check = $this->_encryption_check;
                                                  }
                                              }
                                              $line_array[1] = str_replace("<<CRLF>>",chr(10),isset($line_array[1]) ? $line_array[1] : "");
                                              if ('locked' == nullable_trim($line_array[0])) {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0))
                                                  {
                                                      $locked = TRUE;
                                                  }
                                              } elseif ('desactivated' == nullable_trim($line_array[0])) {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0))
                                                  {
                                                      $desactivated = TRUE;
                                                  }
                                              } elseif ('error_counter' == nullable_trim($line_array[0])) {
                                                  $error_counter = (isset($line_array[1])?$line_array[1]:0);
                                              } elseif ('last_error' == nullable_trim($line_array[0])) {
                                                  $last_error = (isset($line_array[1])?$line_array[1]:0);
                                              }
                                          }
                                      }
                                      fclose($file_handler);
                                      $users_count++;
                                      
                                      if (($error_counter >= $this->GetMaxDelayedFailures()) && ($now_epoch < $last_error + $this->GetMaxDelayedTime())) {
                                        $locked = true;
                                      }
                                      
                                      if ($locked) {
                                          $locked_users_list.= (("" != $locked_users_list)?"\t":"").$current_user;
                                          $locked_users_count++;
                                      }
                                      if (!$desactivated)
                                      {
                                          $active_users_count++;
                                      }
                                  }
                              }
                              if (($limit > 0) && ($locked_users_count >= $limit)) {
                                break;
                              }
                          }
                          closedir($users_handle);
                          
                          if (($limit <= 0) && ($this->IsCacheData())) {
                              $this->WriteCacheValue('locked_users_list', $locked_users_list);
                              if ($locked_users_count >= 0) {
                                  $this->WriteCacheValue('locked_users_count', $locked_users_count);
                              }
                              if ($active_users_count >= 0) {
                                  $this->WriteCacheValue('active_users_count', $active_users_count);
                              }
                              if ($users_count >= 0) {
                                  $this->WriteCacheValue('users_count', $users_count);
                              }
                              $this->WriteCacheData();
                          }
                          
                      }
              }
          }
      }
      return ($in_array ? (empty($locked_users_list) ? [] : explode("\t", $locked_users_list)) : $locked_users_list);
  }


  function GetLockedUsersCount() {

      $now_epoch = time();

      // We initialize the local encryption check variable
      $local_encryption_check = false;

      if (($this->IsCacheData()) && (intval($this->ReadCacheValue('locked_users_count')) >= 0))
      {
          $locked_users_count = intval($this->ReadCacheValue('locked_users_count'));
      }
      else
      {
          $locked_users_count = 0;
          $active_users_count = -1;
          if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data['sql_users_table'])) || ('files' == $this->GetBackendType()))
          {
              switch ($this->GetBackendType())
              {
                  case 'mysql':
                      if ($this->OpenMysqlDatabase())
                      {
                          $sQuery  = "SELECT COUNT(user) AS counter FROM `".$this->_config_data['sql_users_table']."` WHERE ((`locked` = 1) OR ((`error_counter` >= ".$this->GetMaxDelayedFailures().") AND (".$now_epoch." < (`last_error` + ".$this->GetMaxDelayedTime()."))))";
                          if (is_object($this->_mysqli)) {
                              if (!($result = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  if ($aRow = $result->fetch_assoc()) {
                                      $locked_users_count = $aRow['counter'];
                                  }
                              }
                          } else {
                              if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                                  $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              } else {
                                  if ($aRow = mysql_fetch_assoc($rResult)) {
                                      $locked_users_count = $aRow['counter'];
                                  }
                              }
                          }
                      }
                      break;
                  case 'pgsql':
                      if ($this->OpenPGSQLDatabase())
                      {
                          $sQuery  = "SELECT COUNT(\"user\") AS \"counter\" FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_users_table']."\" WHERE ((\"locked\" = 1) OR ((\"error_counter\" >= ".$this->GetMaxDelayedFailures().") AND (".$now_epoch." < (\"last_error\" + ".$this->GetMaxDelayedTime()."))))";
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              if ($aRow = pg_fetch_assoc($rResult)) {
                                  $locked_users_count = $aRow['counter'];
                              }
                          }
                      }
                      break;
                  case 'files':
                  default:
                      $active_users_count = 0;
                      $users_count = 0;
                      if ($users_handle = @opendir($this->GetUsersFolder())) {
                          while ($file = readdir($users_handle)) {
                              $error_counter = 0;
                              $locked = FALSE;
                              $desactivated = FALSE;
                              if ((mb_substr($file, -3) == ".db") && ($file != '.db')) {
                                  $current_user = $this->DecodeFileId(mb_substr($file,0,-3));
                                  if ($file_handler = @fopen($this->GetUsersFolder().$file, "rt")) {
                                      flock($file_handler, LOCK_SH);
                                      $first_line = nullable_trim(fgets($file_handler));
                                      $v3 = (FALSE !== mb_strpos(mb_strtolower($first_line,'UTF-8'),"multiotp-database-format-v3"));
                                      if (FALSE !== mb_strpos(mb_strtolower($first_line,'UTF-8'),"multiotp-database-format")) { // Format V3
                                          while (!feof($file_handler)) {
                                              $line = nullable_trim(fgets($file_handler));
                                              $line_array = explode("=",$line,2);
                                              if ($v3) { // v3 format, only tags followed by := instead of = are encrypted
                                                  if (":" == mb_substr($line_array[0], -1)) {
                                                      $line_array[0] = mb_substr($line_array[0], 0, mb_strlen($line_array[0]) -1);
                                                      $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                                                      $local_encryption_check = $this->_encryption_check;
                                                  }
                                              } else { // v2 format, only defined tags are encrypted
                                                  if ((FALSE !== mb_strpos(mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'), mb_strtolower('*'.$line_array[0].'*','UTF-8'))) || ("*all*" == mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'))) {
                                                      $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                                                      $local_encryption_check = $this->_encryption_check;
                                                  }
                                              }
                                              $line_array[1] = str_replace("<<CRLF>>",chr(10),isset($line_array[1]) ? $line_array[1] : "");
                                              if ('locked' == nullable_trim($line_array[0])) {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                      $locked = TRUE;
                                                  }
                                              } elseif ('desactivated' == nullable_trim($line_array[0])) {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                      $desactivated = TRUE;
                                                  }
                                              } elseif ('error_counter' == nullable_trim($line_array[0])) {
                                                  $error_counter = (isset($line_array[1])?$line_array[1]:0);
                                              } elseif ('last_error' == nullable_trim($line_array[0])) {
                                                  $last_error = (isset($line_array[1])?$line_array[1]:0);
                                              }
                                          }
                                      }
                                      fclose($file_handler);
                                      $users_count++;
                                      
                                      if (($error_counter >= $this->GetMaxDelayedFailures()) && ($now_epoch < $last_error + $this->GetMaxDelayedTime())) {
                                        $locked = true;
                                      }
                                      
                                      if ($locked) {
                                          $locked_users_count++;
                                      }
                                      if (!$desactivated) {
                                          $active_users_count++;
                                      }
                                  }
                              }
                          }
                          closedir($users_handle);
                      }
              }
          }
          if ($this->IsCacheData()) {
              if ($locked_users_count >= 0) {
                  $this->WriteCacheValue('locked_users_count', $locked_users_count);
              }
              if ($active_users_count >= 0) {
                  $this->WriteCacheValue('active_users_count', $active_users_count);
              }
              if ($users_count >= 0) {
                  $this->WriteCacheValue('users_count', $users_count);
              }
              $this->WriteCacheData();                
          }
      }
      return $locked_users_count;
  }


  function GetActiveUsersList(
      $limit = 0
  ) {
      // We initialize the local encryption check variable
      $local_encryption_check = false;

      $list = "";
      if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data['sql_users_table'])) || ('files' == $this->GetBackendType()))
      {
          switch ($this->GetBackendType())
          {
              case 'mysql':
                  if ($this->OpenMysqlDatabase())
                  {
                      $sQuery  = "SELECT user FROM `".$this->_config_data['sql_users_table']."` WHERE (`desactivated` = 0) ORDER BY user ASC";
                      if ($limit > 0)
                      {
                          $sQuery.= " LIMIT 0,".$limit;
                      }
                      if (is_object($this->_mysqli))
                      {
                          if (!($result = $this->_mysqli->query($sQuery)))
                          {
                              $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              $result = FALSE;
                          }
                          else
                          {
                              while ($aRow = $result->fetch_assoc())
                              {
                                  if ("" != $aRow['user'])
                                  {
                                      $list.= (("" != $list)?"\t":"").$aRow['user'];
                                  }
                              }
                          }
                      } else {
                          if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              while ($aRow = mysql_fetch_assoc($rResult)) {
                                  if ("" != $aRow['user']) {
                                      $list.= (("" != $list)?"\t":"").$aRow['user'];
                                  }
                              }                         
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $sQuery  = "SELECT \"user\" FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_users_table']."\" WHERE (\"desactivated\" = 0) ORDER BY \"user\" ASC";
                      if ($limit > 0) {
                          $sQuery.= " LIMIT 0,".$limit;
                      }
                      if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                          $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                      } else {
                          while ($aRow = pg_fetch_assoc($rResult)) {
                              if ("" != $aRow['user']) {
                                  $list.= (("" != $list)?"\t":"").$aRow['user'];
                              }
                          }                         
                      }
                  }
                  break;
              case 'files':
              default:
                  $active_users_count = 0;
                  $locked_users_count = 0;
                  $users_count = 0;
                  if ($users_handle = @opendir($this->GetUsersFolder())) {
                      while ($file = readdir($users_handle)) {
                          $desactivated = FALSE;
                          $locked = FALSE;
                          if ((mb_substr($file, -3) == ".db") && ($file != '.db')) {
                              $current_user = $this->DecodeFileId(mb_substr($file,0,-3));
                              if ($file_handler = @fopen($this->GetUsersFolder().$file, "rt")) {
                                  flock($file_handler, LOCK_SH);
                                  $first_line = nullable_trim(fgets($file_handler));
                                  $v3 = (FALSE !== mb_strpos(mb_strtolower($first_line,'UTF-8'),"multiotp-database-format-v3"));
                                  if (FALSE !== mb_strpos(mb_strtolower($first_line,'UTF-8'),"multiotp-database-format")) { // Format V3
                                      while (!feof($file_handler)) {
                                          $line = nullable_trim(fgets($file_handler));
                                          $line_array = explode("=",$line,2);
                                          if ($v3) { // v3 format, only tags followed by := instead of = are encrypted
                                              if (":" == mb_substr($line_array[0], -1)) {
                                                  $line_array[0] = mb_substr($line_array[0], 0, mb_strlen($line_array[0]) -1);
                                                  $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                                                  $local_encryption_check = $this->_encryption_check;
                                              }
                                          } else { // v2 format, only defined tags are encrypted
                                              if ((FALSE !== mb_strpos(mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'), mb_strtolower('*'.$line_array[0].'*','UTF-8'))) || ("*all*" == mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'))) {
                                                  $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                                                  $local_encryption_check = $this->_encryption_check;
                                              }
                                          }
                                          $line_array[1] = str_replace("<<CRLF>>",chr(10),isset($line_array[1]) ? $line_array[1] : "");
                                          if ('desactivated' == nullable_trim($line_array[0])) {
                                              if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                  $desactivated = TRUE;
                                              }
                                          }
                                          if ('locked' == nullable_trim($line_array[0])) {
                                              if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                  $locked = TRUE;
                                              }
                                          }
                                      }
                                  }
                                  fclose($file_handler);
                                  $users_count++;
                                  
                                  if (!$desactivated) {
                                      $list.= (("" != $list)?"\t":"").$current_user;
                                      $active_users_count++;
                                  }
                                  if ($locked) {
                                      $locked_users_count++;
                                  }
                              }
                          }
                          if (($limit > 0) && (active_users_count >= $limit)) {
                              break;
                          }
                      }
                      closedir($users_handle);
                      
                      if (($limit <= 0) && ($this->IsCacheData())) {
                          if ($locked_users_count >= 0) {
                              $this->WriteCacheValue('locked_users_count', $locked_users_count);
                          }
                          if ($active_users_count >= 0) {
                              $this->WriteCacheValue('active_users_count', $active_users_count);
                          }
                          if ($users_count >= 0) {
                              $this->WriteCacheValue('users_count', $users_count);
                          }
                          $this->WriteCacheData();
                      }
                  }
          }
      }
      return $list;
  }


  function GetActiveUsersCount($skip_ldap_without2fa = FALSE)
  {
      // We initialize the local encryption check variable
      $local_encryption_check = false;

      if (($this->IsCacheData()) && (intval($this->ReadCacheValue('active_users_count')) >= 0)) {
          $active_users_count = intval($this->ReadCacheValue('active_users_count'));
      } else {
          $active_users_count = 0;
          $locked_users_count = -1;
          if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data['sql_users_table'])) || ('files' == $this->GetBackendType())) {
              switch ($this->GetBackendType()) {
                  case 'mysql':
                      if ($this->OpenMysqlDatabase()) {
                          $sQuery = "SELECT COUNT(user) AS counter FROM `".$this->_config_data['sql_users_table']."` WHERE (`desactivated` = 0)";
                          if ($skip_ldap_without2fa) {
                            $sQuery.= " AND ((`request_ldap_pwd` = 0) OR (`algorithm` NOT LIKE 'without2fa'))";
                          }
                          if (is_object($this->_mysqli)) {
                              if (!($result = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  if ($aRow = $result->fetch_assoc()) {
                                      $active_users_count = $aRow['counter'];
                                  }
                              }
                          } else {
                              if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                                  $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              } else {
                                  if ($aRow = mysql_fetch_assoc($rResult)) {
                                      $active_users_count = $aRow['counter'];
                                  }
                              }
                          }
                      }
                      break;
                  case 'pgsql':
                      if ($this->OpenPGSQLDatabase()) {
                          $sQuery  = "SELECT COUNT(\"user\") AS \"counter\" FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_users_table']."\" WHERE (\"desactivated\" = 0)";
                          if ($skip_ldap_without2fa) {
                            $sQuery.= " AND ((\"request_ldap_pwd\" = 0) OR (\"algorithm\" NOT LIKE 'without2fa'))";
                          }
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              if ($aRow = pg_fetch_assoc($rResult)) {
                                  $active_users_count = $aRow['counter'];
                              }
                          }
                      }
                      break;
                  case 'files':
                  default:
                      $locked_users_count = 0;
                      $users_count = 0;
                      if ($users_handle = @opendir($this->GetUsersFolder())) {
                          while ($file = readdir($users_handle)) {
                              $desactivated = FALSE;
                              $locked = FALSE;
                              $algorithm = "";
                              $request_ldap_pwd = FALSE;
                              $skip_ldap_users_without2fa = FALSE;
                              if ((mb_substr($file, -3) == ".db") && ($file != '.db')) {
                                  $current_user = $this->DecodeFileId(mb_substr($file,0,-3));
                                  if ($file_handler = @fopen($this->GetUsersFolder().$file, "rt")) {
                                      flock($file_handler, LOCK_SH);
                                      $first_line = nullable_trim(fgets($file_handler));
                                      $v3 = (FALSE !== mb_strpos(mb_strtolower($first_line,'UTF-8'),"multiotp-database-format-v3"));
                                      if (FALSE !== mb_strpos(mb_strtolower($first_line,'UTF-8'),"multiotp-database-format")) {
                                          // Format V3
                                          while (!feof($file_handler)) {
                                              $line = nullable_trim(fgets($file_handler));
                                              $line_array = explode("=",$line,2);
                                              if ($v3) {
                                                  // v3 format, only tags followed by := instead of = are encrypted
                                                  if (":" == mb_substr($line_array[0], -1)) {
                                                      $line_array[0] = mb_substr($line_array[0], 0, mb_strlen($line_array[0]) -1);
                                                      $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                                                      $local_encryption_check = $this->_encryption_check;
                                                  }
                                              } else {
                                                  // v2 format, only defined tags are encrypted
                                                  if ((FALSE !== mb_strpos(mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'), mb_strtolower('*'.$line_array[0].'*','UTF-8'))) || ("*all*" == mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'))) {
                                                      $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                                                      $local_encryption_check = $this->_encryption_check;
                                                  }
                                              }
                                              $line_array[1] = str_replace("<<CRLF>>",chr(10),isset($line_array[1]) ? $line_array[1] : "");
                                              if ('desactivated' == nullable_trim($line_array[0])) {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                    $desactivated = TRUE;
                                                  }
                                              }
                                              if ('locked' == nullable_trim($line_array[0])) {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                    $locked = TRUE;
                                                  }
                                              }
                                              if ('request_ldap_pwd' == nullable_trim($line_array[0])) {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                    $request_ldap_pwd = TRUE;
                                                  }
                                              }
                                              if ('algorithm' == nullable_trim($line_array[0])) {
                                                $algorithm = (isset($line_array[1])?$line_array[1]:"");
                                              }
                                          }
                                      }
                                      fclose($file_handler);
                                      $users_count++;

                                      if ($skip_ldap_without2fa) {
                                        if ($request_ldap_pwd && ("without2fa" == strtolower($algorithm))) {
                                          $skip_ldap_users_without2fa = TRUE;
                                        }
                                      }
                                      
                                      if (!$desactivated) {
                                        if (!$skip_ldap_users_without2fa) {
                                          $active_users_count++;
                                        }
                                      }
                                      if ($locked) {
                                        $locked_users_count++;
                                      }
                                  }
                              }
                          }
                          closedir($users_handle);
                      }
              }
          }
          if ($this->IsCacheData())
          {
              if ($active_users_count >= 0)
              {
                  $this->WriteCacheValue('active_users_count', $active_users_count);
              }
              if ($locked_users_count >= 0)
              {
                  $this->WriteCacheValue('locked_users_count', $locked_users_count);
              }
              if ($users_count >= 0)
              {
                  $this->WriteCacheValue('users_count', $users_count);
              }
              $this->WriteCacheData();
          }
      }
      return $active_users_count;
  }


  // Completely new edition 2014-07-21
  function GetDetailedUsersArray()
  {
      $users_array = [];
      $result = $this->GetNextUserArray(TRUE);
      if (isset($result['user'])) {
          $users_array[$result['user']] = $result;
      }
      do {
          if ($result = $this->GetNextUserArray()) {
              if (isset($result['user'])) {
                  $users_array[$result['user']] = $result;
              }
          }
      } while (FALSE !== $result);
      return $users_array;
  }


  function GetNextUserArray(
      $first = FALSE,
      $fields = NULL
  ) {
      if (NULL != $fields) {
          $fields_array = $fields;
      } else {
          $fields_array = array('user',
                                'description',
                                'display_name',
                                'email',
                                'group',
                                'desactivated',
                                'locked',
                                'sms',
                                'synchronized',
                                'synchronized_channel',
                                'synchronized_server',
                                'synchronized_time',
                                'token_serial',
                                'synchronized_dn',
                                'error_counter',
                                'last_error'
                               );
      }

      $now_epoch = time();
          
      $raw_id = $fields_array[0];

      $fields_text = "";
      $fields_separator = "";
      
      $table_name = 'sql_users_table';
      $folder = $this->GetUsersFolder();
      $parser_id = 'GET_NEXT_USER_ARRAY';
      $user_array = false;

      if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data[$table_name])) || ('files' == $this->GetBackendType())) {
          if ($first) {
              switch ($this->GetBackendType()) {
                  case 'mysql':
                      foreach ($fields_array as $one_field) {
                          $fields_text.= $fields_separator.'`'.$one_field.'`';
                          $fields_separator = ',';
                      }
                      if ($this->OpenMysqlDatabase()) {
                          $sQuery = "SELECT ".$fields_text." FROM `".$this->_config_data[$table_name]."` ORDER BY user ASC";
                          if (is_object($this->_mysqli)) {
                              if (!($this->_parser_pointers[$parser_id] = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                                  $this->_parser_pointers[$parser_id] = FALSE;
                                  $result = FALSE;
                                  return $result;
                              }
                          } else {
                              if (!($this->_parser_pointers[$parser_id] = mysql_query($sQuery, $this->_mysql_database_link))) {
                                  $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                                  $this->_parser_pointers[$parser_id] = FALSE;
                                  $result = FALSE;
                                  return $result;
                              }
                          }
                      }
                      break;
                  case 'pgsql':
                      foreach ($fields_array as $one_field) {
                          $fields_text.= $fields_separator.'"'.$one_field.'"';
                          $fields_separator = ',';
                      }
                      if ($this->OpenPGSQLDatabase()) {
                          $sQuery = "SELECT ".$fields_text." FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data[$table_name]."\" ORDER BY \"user\" ASC";
                          if (!($this->_parser_pointers[$parser_id] = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              $this->_parser_pointers[$parser_id] = FALSE;
                              $result = FALSE;
                              return $result;
                          }
                      }
                      break;
                  case 'files':
                  default:
                      if (!($this->_parser_pointers[$parser_id] = @opendir($folder))) {
                          $result = FALSE;
                          return $result;
                      }
              }
          } // if ($first)
          
          if (isset($this->_parser_pointers[$parser_id]) && (FALSE !== $this->_parser_pointers[$parser_id])) {
              switch ($this->GetBackendType()) {
                  case 'mysql':
                      if ($this->OpenMysqlDatabase()) {
                          if (is_object($this->_mysqli)) {
                              do {
                                  $aRow = $this->_parser_pointers[$parser_id]->fetch_assoc();
                              } while ((FALSE !== $aRow) && (NULL !== $aRow) && ("" == $aRow['user']));
                          } else {
                              do {
                                  $aRow = mysql_fetch_assoc($this->_parser_pointers[$parser_id]);
                              } while ((FALSE !== $aRow) && (NULL !== $aRow) && ("" == $aRow['user']));
                          }
                          if (isset($aRow['user'])) {
                              $user_array = array('user'                 => $aRow['user'],
                                                  'description'          => $aRow['description'],
                                                  'display_name'         => $aRow['display_name'],
                                                  'email'                => $aRow['email'],
                                                  'enabled'              => (0 == $aRow['desactivated']),
                                                  'group'                => $aRow['group'],
                                                  'locked'               => (1 == $aRow['locked']),
                                                  'sms'                  => $aRow['sms'],
                                                  'synchronized'         => (1 == $aRow['synchronized']),
                                                  'synchronized_channel' => $aRow['synchronized_channel'],
                                                  'synchronized_server'  => $aRow['synchronized_server'],
                                                  'synchronized_time'    => $aRow['synchronized_time'],
                                                  'token'                => (isset($aRow['token_serial']) ? $aRow['token_serial'] : ""),
                                                  'synchronized_dn'      => $aRow['synchronized_dn'],
                                                  'error_counter'        => $aRow['error_counter'],
                                                  'last_error'           => (isset($aRow['last_error']) ? $aRow['last_error'] : 0)
                                                  );
                          }
                      }
                      break;
                  case 'pgsql':
                      if ($this->OpenPGSQLDatabase()) {
                          do {
                              $aRow = pg_fetch_assoc($this->_parser_pointers[$parser_id]);
                          } while ((FALSE !== $aRow) && (NULL !== $aRow) && ("" == $aRow['user']));
                          if (isset($aRow['user'])) {
                              $user_array = array('user'                 => $aRow['user'],
                                                  'description'          => $aRow['description'],
                                                  'display_name'         => $aRow['display_name'],
                                                  'email'                => $aRow['email'],
                                                  'enabled'              => (0 == $aRow['desactivated']),
                                                  'group'                => $aRow['group'],
                                                  'locked'               => (1 == $aRow['locked']),
                                                  'sms'                  => $aRow['sms'],
                                                  'synchronized'         => (1 == $aRow['synchronized']),
                                                  'synchronized_channel' => $aRow['synchronized_channel'],
                                                  'synchronized_server'  => $aRow['synchronized_server'],
                                                  'synchronized_time'    => $aRow['synchronized_time'],
                                                  'token'                => (isset($aRow['token_serial']) ? $aRow['token_serial'] : ""),
                                                  'synchronized_dn'      => $aRow['synchronized_dn'],
                                                  'error_counter'        => $aRow['error_counter'],
                                                  'last_error'           => (isset($aRow['last_error']) ? $aRow['last_error'] : 0)
                                                  );
                          }
                      }
                      break;
                  case 'files':
                  default:
                      do {
                          $file = readdir($this->_parser_pointers[$parser_id]);
                      } while ((FALSE !== $file) && ((mb_substr($file, -3) != ".db") || ($file == '.db')));
                      if (FALSE !== $file) {
                          $user = $this->DecodeFileId(mb_substr($file,0,-3));
                          $this->SetUser($user);
                          $user_array = array('user'                 => $user,
                                              'description'          => $this->GetUserDescription(),
                                              'display_name'         => $this->GetUserDisplayName(),
                                              'email'                => $this->GetUserEmail(),
                                              'enabled'              => (1 == $this->GetUserActivated()),
                                              'group'                => $this->GetUserGroup(),
                                              'locked'               => (1 == $this->GetUserLocked()),
                                              'sms'                  => $this->GetUserSms(),
                                              'synchronized'         => (1 == $this->GetUserSynchronized()),
                                              'synchronized_channel' => $this->GetUserSynchronizedChannel(),
                                              'synchronized_server'  => $this->GetUserSynchronizedServer(),
                                              'synchronized_time'    => $this->GetUserSynchronizedTime(),
                                              'token'                => $this->GetUserTokenSerialNumber(),
                                              'synchronized_dn'      => $this->GetUserSynchronizedDn(),
                                              'error_counter'        => $this->GetUserErrorCounter(),
                                              'last_error'           => $this->GetUserTokenLastError()
                                             );
                      } else {
                          $user_array = FALSE;
                          closedir($this->_parser_pointers[$parser_id]);
                      }
              }
          }
          
      }
      if (FALSE === $user_array) {
          unset($this->_parser_pointers[$parser_id]);
      } else {
          $user_array['delayed_account'] = (($user_array['error_counter'] >= $this->GetMaxDelayedFailures()) && ($now_epoch < ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime())));
          $user_array['delayed_time'] = 0;
          $user_array['delayed_finished'] = 0;
          if ($user_array['delayed_account']) {
              $user_array['delayed_time'] = ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime()) - $now_epoch;
              $user_array['delayed_finished'] = ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime());
              $user_array['locked'] = TRUE;
          }
      }
      return $user_array;
  }


  function GetAlgorithmsList(
    $in_array = false
  ) {
    $result = [];
    $algorithms_array = explode("*",$this->_valid_algorithms);
    foreach ($algorithms_array as $algorithm_one) {
      if (!empty(nullable_trim($algorithm_one))) {
        $result[] = nullable_trim($algorithm_one);
      }
    }
    return ($in_array ? $result : implode("\t", $result));
  }


  function IsValidAlgorithm(
      $algo_to_check
  ) {
      return (FALSE !== mb_strpos(mb_strtolower($this->_valid_algorithms,'UTF-8'), mb_strtolower('*'.$algo_to_check.'*','UTF-8')));
  }


  function GetUserScratchPasswordsArray(
      $user = ""
  ) {
      if ("" != $user) {
          $this->SetUser($user);
      }
      if ($this->_user_data['scratch_passwords'] != "") {
          return (explode(",",$this->_user_data['scratch_passwords']));
      } else {
          return [];
      }
      return (explode(",",$this->_user_data['scratch_passwords']));
  }


  function RemoveUserUsedScratchPassword(
      $to_remove
  ) {
      $scratch_passwords = nullable_trim($this->_user_data['scratch_passwords']);
      if (FALSE !== ($pos = mb_strpos($scratch_passwords, $to_remove))) {
          $scratch_passwords = nullable_trim(mb_substr($scratch_passwords.' ', $pos+mb_strlen($to_remove)+1));
          $this->_user_data['scratch_passwords'] = $scratch_passwords;
          $result = $this->WriteUserData();
      }
      return TRUE;
  }


  function GetScratchPasswordsAmount()
  {
      return $this->_config_data['scratch_passwords_amount'];
  }


  function SetScratchPasswordsAmount(
      $value
  ) {
      // Must be between 3 and 400
      $amount = intval($value);
      $amount = ($amount < 3)?3:$amount;
      $amount = ($amount > 400)?400:$amount;
      $this->_config_data['scratch_passwords_amount'] = $amount;
      return TRUE;
  }


  function GetUserScratchPasswordsList(
      $user = ""
  ) {
      if ("" != $user) {
          $this->SetUser($user);
      }
      $digits = $this->GetScratchPasswordsDigits();

      $seed = hex2bin(md5('sCratchP@sswordS'.$this->GetUser().bigdec2hex((time()-mktime(1,1,1,1,1,2000)).mt_rand(10000,99999))));

      $scratch_loop = $this->GetScratchPasswordsAmount();
      if (($scratch_loop * (1+$digits) * 2.5) > 65535) {
          $scratch_loop = inval(65535 / ((1+$digits) * 2.5));
          $this->SetScratchPasswordsAmount($scratch_loop);
      }
      $scratch_passwords = nullable_trim(isset($this->_user_data['scratch_passwords']) ? $this->_user_data['scratch_passwords'] : "");
      if (mb_strlen($scratch_passwords) > ((1.5 * $scratch_loop) * (1 + $digits))) {
          $scratch_passwords = "";
      }
      $passwords_list = "";

      for ($i=0; $i<$scratch_loop; $i++) {
          $one_password = $this->GenerateOathHotp($seed,$i,$digits);
          $scratch_passwords.= (("" != $scratch_passwords)?",":"").$one_password;
          $passwords_list.= (("" != $passwords_list)?"\t":"").$one_password;
      }
      $this->_user_data['scratch_passwords'] = $scratch_passwords;
      $result = $this->WriteUserData();
      if (!$result) {
          $passwords_list = "";
      }
      return ($passwords_list);
  }


  function SetUserDataReadFlag(
      $flag
  ) {
      $this->_user_data_read_flag = $flag;
      return TRUE;
  }


  function GetUserDataReadFlag()
  {
      return $this->_user_data_read_flag;
  }


  function SetUserMultiAccount(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = TRUE;
      $data = 0;
      if ($second_param == "*-*") {
          $data = $first_param;
      } else {
          $result = $this->SetUser($first_param);
          $data = $second_param;
      }
      $this->_user_data['multi_account'] = $data;

      return $result;
  }


  function GetUserMultiAccount(
      $user = ""
  ) {
      if ($user != "") {
          $this->SetUser($user);
      }
      return intval(isset($this->_user_data['multi_account']) ? $this->_user_data['multi_account'] : 0);
  }
  

  function IsUserMultiAccount(
      $user = ""
  ) {
      return (1 == ($this->GetUserMultiAccount($user)));
  }


  function SetUserPushGateway(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = TRUE;
      $data = 0;
      if ($second_param == "*-*") {
          $data = $first_param;
      } else {
          $result = $this->SetUser($first_param);
          $data = $second_param;
      }
      $this->_user_data['push_gateway'] = nullable_trim($data);
      return $result;
  }


  function GetUserPushGateway(
    $user = ""
  ) {
    if ($user != "") {
        $this->SetUser($user);
    }
    $user_push_gateway = nullable_trim(isset($this->_user_data['push_gateway']) ? $this->_user_data['push_gateway'] : '');
    if (empty($user_push_gateway)) {
      $user_push_gateway = $this->GetPushGateway(true);
    }
    return $user_push_gateway;
  }
  

  function SetUserPushEnabled(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = TRUE;
      $data = 0;
      if ($second_param == "*-*") {
          $data = $first_param;
      } else {
          $result = $this->SetUser($first_param);
          $data = $second_param;
      }
      $this->_user_data['push_enabled'] = intval($data);
      if (0 == intval($data)) {
        $this->_user_data['push_enable_date'] = 0;
      }
      return $result;
  }


  function GetUserPushEnabled(
    $user = ""
  ) {
    if ($user != "") {
        $this->SetUser($user);
    }
    return intval(isset($this->_user_data['push_enabled']) ? $this->_user_data['push_enabled'] : 0);
  }
  

  function IsUserPushEnabled(
    $user = ""
  ) {
    return (1 == ($this->GetUserPushEnabled($user)));
  }


  function GetUserPushEnableDate(
    $user = ""
  ) {
    if (!empty($user)) {
      if ($this->CheckUserExists($user)) {
        $this->SetUser($user);
      }
    }
    if (!empty($this->GetUser())) {
      $result = intval($this->_user_data['push_enable_date']);
    } else {
      $result = 0;
    }
    return $result;
  }


  function SetUserAttribute(
      $first_param,
      $second_param,
      $third_param = "*-*"
  ) {
      $result = FALSE;
      if ($third_param == "*-*") {
          if ($this->IsOptionInSchema('users', $first_param)) {
              $this->_user_data[$first_param] = $second_param;
              $result = TRUE;
          }
      } else {
          if ($this->IsOptionInSchema('users', $second_param)) {
              $this->SetUser($first_param);
              $this->_user_data[$second_param] = $third_param;
              $result = TRUE;
          }
      }
      return $result;
  }


  function SetUserEmail(
      $first_param,
      $second_param = "*-*"
  ) {
      $valid = FALSE;
      $result = "";
      if ($second_param == "*-*") {
          if (("" == $first_param) || (FALSE !== mb_strpos($first_param, '@'))) {
              $result = $first_param;
              $valid = TRUE;
          }
      } else {
          $this->SetUser($first_param);
          if (("" == $second_param) || (FALSE !== mb_strpos($second_param, '@'))) {
              $result = $second_param;
              $valid = TRUE;
          }
      }
      $this->_user_data['email'] = $result;

      return $valid;
  }


  function GetUserEmail(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return (isset($this->_user_data['email']) ? $this->_user_data['email'] : "");
  }


  function SetUserLastCachedCredential(
      $first_param,
      $second_param = "*-*"
  ) {
      $input = "";
      if ($second_param == "*-*") {
          $input = $first_param;
      } else {
          $this->SetUser($first_param);
          $input = $second_param;
      }
      if ($this->GetVerboseFlag()) {
          $this->WriteLog("Debug: *SetUserLastCachedCredential cached credential: ".str_repeat('x', (mb_strlen($input) >= 6)?mb_strlen($input)-6:0).mb_substr($input, -6), FALSE, FALSE, 8888, 'Debug', "");
      }
      $this->_user_data['last_cached_credential'] = sha1('$+Cred'.$input.'!@#S');

      return true;
  }


  function CompareUserLastCachedCredential(
      $first_param,
      $second_param = "*-*"
  ) {
      $input = "";
      if ($second_param == "*-*") {
          $input = $first_param;
      } else {
          $this->SetUser($first_param);
          $input = $second_param;
      }
      if ($this->GetVerboseFlag()) {
          $this->WriteLog("Debug: *CompareUserLastCachedCredential cached credential: ".str_repeat('x', (mb_strlen($input) >= 6)?mb_strlen($input)-6:0).mb_substr($input, -6), FALSE, FALSE, 8888, 'Debug', "");
      }
      return (sha1('$+Cred'.$input.'!@#S') == $this->_user_data['last_cached_credential']);
  }


  function SetUserLastFailedCredential(
      $first_param,
      $second_param = "*-*"
  ) {
      $input = "";
      if ($second_param == "*-*") {
          $input = $first_param;
      } else {
          $this->SetUser($first_param);
          $input = $second_param;
      }
      if (!$this->CompareUserLastFailedCredential($input)) {
        $this->_user_data['last_failed_time'] = time();
      }
      $this->_user_data['last_failed_credential'] = sha1('$+Cred'.$input.'!@#S');

      return true;
  }


  function CompareUserLastFailedCredential(
      $first_param,
      $second_param = "*-*"
  ) {
      $input = "";
      if ($second_param == "*-*") {
          $input = $first_param;
      } else {
          $this->SetUser($first_param);
          $input = $second_param;
      }
      if ((intval($this->_user_data['last_failed_time']) + intval($this->GetLastFailedWhiteDelay())) > time()) {
          return (sha1('$+Cred'.$input.'!@#S') == $this->_user_data['last_failed_credential']);
      } else {
          return false;
      }
  }


  function SetUserLastSuccessCredential(
      $first_param,
      $second_param = "*-*"
  ) {
      $input = "";
      if ($second_param == "*-*") {
          $input = $first_param;
      } else {
          $this->SetUser($first_param);
          $input = $second_param;
      }
      $this->_user_data['last_success_credential'] = sha1('$+Cred'.$input.'!@#S');

      return true;
  }


  function CompareUserLastSuccessCredential(
      $first_param,
      $second_param = "*-*"
  ) {
      $input = "";
      if ($second_param == "*-*") {
          $input = $first_param;
      } else {
          $this->SetUser($first_param);
          $input = $second_param;
      }
      return (sha1('$+Cred'.$input.'!@#S') == $this->_user_data['last_success_credential']);
  }


  function SetUserLastLoginForCache(
      $first_param,
      $second_param = "*-*"
  ) {
      $input = "";
      if ($second_param == "*-*") {
          $input = $first_param;
      } else {
          $this->SetUser($first_param);
          $input = $second_param;
      }
      $this->_user_data['last_login_for_cache'] = $input;

      return true;
  }


  function GetUserLastLoginForCache(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return $this->_user_data['last_login_for_cache'];
  }


  function SetUserGroup(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetUser($first_param);
          $result = $second_param;
      }
      $this->_user_data['group'] = $result;

      return $result;
  }


  function GetUserGroup(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return (isset($this->_user_data['group']) ? $this->_user_data['group'] : "");
  }


  function SetUserDescription(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetUser($first_param);
          $result = $second_param;
      }
      $result = $this->EncodeForBackend($result);
      $this->_user_data['description'] = $result;
      
      $this->SetUserMultiAccount((FALSE !== mb_strpos($result,'multi_account')) ? 1 : 0);

      return $result;
  }


  function GetUserDescription(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return (isset($this->_user_data['description']) ? $this->_user_data['description'] : "");
  }


  function SetUserDisplayName(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetUser($first_param);
          $result = $second_param;
      }
      $result = $this->EncodeForBackend($result);
      $this->_user_data['display_name'] = $result;
      
      return $result;
  }


  function GetUserDisplayName(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return (isset($this->_user_data['display_name']) ? $this->_user_data['display_name'] : "");
  }


  function SetUserSeedPassword(
      $value
  ) {
      $this->_user_data['seed_password'] = $value;
  }


  function GetUserSeedPassword()
  {
      return $this->_user_data['seed_password'];
  }


  function SetUserSms(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetUser($first_param);
          $result = $second_param;
      }
      $this->_user_data['sms'] = $result;
      return TRUE;
  }


  function GetUserSms(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return (isset($this->_user_data['sms']) ? $this->_user_data['sms'] : "");
  }


  function SetUserPrefixPin(
      $value
  ) {
      $this->_user_data['request_prefix_pin'] = ((intval($value) > 0)?1:0);
      return TRUE;
  }


  function GetUserPrefixPin()
  {
      return $this->_user_data['request_prefix_pin'];
  }


  function EnableUserPrefixPin()
  {
      $this->_user_data['request_prefix_pin'] = 1;
      return TRUE;
  }


  function DisableUserPrefixPin()
  {
      $this->_user_data['request_prefix_pin'] = 0;
      return TRUE;
  }


  function IsUserPrefixPin()
  {
      return (1 == (isset($this->_user_data['request_prefix_pin']) ? $this->_user_data['request_prefix_pin'] : 0));
  }


  function SetUserRequestLdapPassword(
      $value
  ) {
      $this->_user_data['request_ldap_pwd'] = ((intval($value) > 0)?1:0);
      return TRUE;
  }


  function GetUserRequestLdapPassword()
  {
      return ((intval($this->_user_data['request_ldap_pwd']) > 0) ? 1 : 0);
  }


  function EnableUserRequestLdapPassword()
  {
      $this->_user_data['request_ldap_pwd'] = 1;
      return TRUE;
  }


  function DisableUserRequestLdapPassword()
  {
      $this->_user_data['request_ldap_pwd'] = 0;
      return TRUE;
  }


  function IsUserRequestLdapPasswordEnabled()
  {
      return (1 == (isset($this->_user_data['request_ldap_pwd']) ? $this->_user_data['request_ldap_pwd'] : 0));
  }


  function SetUserLdapHashCache(
      $first_param,
      $second_param = "*-*"
  ) {
      $value = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $this->SetUser($first_param);
          $value = $second_param;
      }
      $this->_user_data['ldap_hash_cache'] = $value;
      $this->_user_data['ldap_hash_validity'] = time() + $this->GetLdapHashCacheTime();
      return TRUE;
  }
  
 
  function ResetUserLdapHashCache(
      $user = ""
  ) {
      if ("" != $user) {
          $this->SetUser($user);
      }
      $this->_user_data['ldap_hash_cache'] = "";
      $this->_user_data['ldap_hash_validity'] = 0;
      return TRUE;
  }


  function GetUserLdapHashCache(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      if ($this->_user_data['ldap_hash_validity'] >= time()) {
          $value = $this->_user_data['ldap_hash_cache'];
      } else {
          $this->_user_data['ldap_hash_cache'] = "";
          $value = "";
      }
      return $value;
  }


  function SetUserAlgorithm(
      $algorithm
  ) {
      $result = FALSE;
      if ($this->IsValidAlgorithm($algorithm)) {
          $this->_user_data['algorithm'] = mb_strtolower($algorithm,'UTF-8');
          if (("totp" == mb_strtolower($algorithm,'UTF-8')) && ((!isset($this->_user_data['time_interval'])) || ($this->_user_data['time_interval'] <= 0))) {
            $this->_user_data['time_interval'] = _MULTIOTP_DEFAULT_TOTP_INTERVAL_;
          }
          $result = TRUE;
      } else {
          $this->WriteLog("Error: ".$algorithm." algorithm is unknown", FALSE, FALSE, _MULTIOTP_BAD_ALGORITHM_, 'User');
      }
      return $result;
  }


  function GetUserAlgorithm(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      $result = mb_strtolower((isset($this->_user_data['algorithm']) ? $this->_user_data['algorithm'] : ""),'UTF-8');
      if (FALSE === mb_strpos(mb_strtolower($this->_valid_algorithms,'UTF-8'), mb_strtolower('*'.$result.'*','UTF-8'))) {
          $result = "";
      }

      return $result;
  }


  function SetUserTokenAlgoSuite(
      $token_algo_suite
  ) {
      $this->_user_data['token_algo_suite'] = mb_strtoupper(("" == $token_algo_suite)?'HMAC-SHA1':$token_algo_suite,'UTF-8');
      return TRUE;
  }


  function GetUserTokenAlgoSuite(
      $user = ""
  ) {
      return mb_strtoupper(("" == $this->_user_data['token_algo_suite'])?'HMAC-SHA1':$this->_user_data['token_algo_suite'],'UTF-8');
  }


  function SetUserTokenPrivateId(
      $private_id
  ) {
      $this->_user_data['private_id'] = $private_id;
  }


  function GetUserTokenPrivateId()
  {
      return $this->_user_data['private_id'];
  }


  /**
   * @brief   Set the user token seed in hexadecimal, base32 or raw binary
   *
   * @param   string  $seed  Seed in hexadecimal, base32 or raw binary
   * @return  none
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.0.3.6
   * @date    2017-02-21
   * @since   2010-08-12
   */
  function SetUserTokenSeed(
      $seed
  ) {
      $the_seed = $seed;
      if (!ctype_xdigit($the_seed)) {
          if (FALSE !== base32_decode($seed)) {
              $the_seed = nullable_bin2hex(base32_decode($seed));
          } else {
              $the_seed = nullable_bin2hex($seed);
          }
      }
      $this->_user_data['token_seed'] = $the_seed;
  }


  function GetUserTokenSeed()
  {
      return (isset($this->_user_data['token_seed']) ? $this->_user_data['token_seed'] : "");
  }


  function SetUserSmsOtp(
      $value
  ) {
      $this->_user_data['sms_otp'] = $value;
  }


  function GetUserSmsOtp()
  {
      // Be sure that we never have an SMS OTP smaller than 4 digits
      if (mb_strlen($this->_user_data['sms_otp']) < 4)
      {
          $this->_user_data['sms_otp'] = md5($this->GetEncryptionKey().$this->GetUserTokenSeed().mt_rand(100000,999999).date("YmdHis"));
      }
      return $this->_user_data['sms_otp'];
  }


  function SetUserSmsValidity(
      $value
  ) {
      $this->_user_data['sms_validity'] = $value;
  }


  function GetUserSmsValidity()
  {
      return $this->_user_data['sms_validity'];
  }


  function SetUserEmailOtp(
      $value
  ) {
      $this->_user_data['email_otp'] = $value;
  }


  function GetUserEmailOtp()
  {
      // Be sure that we never have an Email OTP smaller than 4 digits
      if (mb_strlen($this->_user_data['email_otp']) < 4)
      {
          $this->_user_data['email_otp'] = md5($this->GetEncryptionKey().$this->GetUserTokenSeed().mt_rand(100000,999999).date("YmdHis"));
      }
      return $this->_user_data['email_otp'];
  }


  function SetUserEmailValidity(
      $value
  ) {
      $this->_user_data['email_validity'] = $value;
  }


  function GetUserEmailValidity()
  {
      return $this->_user_data['email_validity'];
  }


  function SetUserPin(
      $pin
  ) {
      $this->_user_data['user_pin'] = $pin;
  }


  function GetUserPin(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return (isset($this->_user_data['user_pin']) ? $this->_user_data['user_pin'] : "");

  }


  function SetUserAutolockTime(
      $value
  ) {
      $this->_user_data['autolock_time'] = intval($value);
  }


  function GetUserAutolockTime()
  {
      return intval($this->_user_data['autolock_time']);
  }


  function SetUserTokenDeltaTime(
      $delta_time
  ) {
      $this->_user_data['delta_time'] = $delta_time;
  }


  function GetUserTokenDeltaTime()
  {
      return $this->_user_data['delta_time'];
  }


  function SetUserKeyId(
      $key_id
  ) {
      $this->_user_data['key_id'] = $key_id;
  }


  function GetUserKeyId()
  {
      return (isset($this->_user_data['key_id']) ? $this->_user_data['key_id'] : "");
  }


  function SetUserTokenNumberOfDigits(
      $number_of_digits
  ) {
      $this->_user_data['number_of_digits'] = $number_of_digits;
  }


  function GetUserTokenNumberOfDigits()
  {
      return intval(isset($this->_user_data['number_of_digits']) ? $this->_user_data['number_of_digits'] : 0);
  }


  function SetUserTokenTimeInterval(
      $interval
  ) {
      if (intval($interval) > 0) {
          $this->_user_data['time_interval'] = intval($interval);
      }
  }


  function GetUserTokenTimeInterval()
  {
      return (isset($this->_user_data['time_interval']) ? $this->_user_data['time_interval'] : 0);
  }


  function GetUserEncryptionHash()
  {
      if (isset($this->_user_data['encryption_hash'])) {
        return $this->_user_data['encryption_hash'];
      } else {
        return "";
      }
  }


  function SetUserTokenSerialNumber(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $this->SetUser($first_param);
          $value = $second_param;
      }
      $this->_user_data['token_serial'] = mb_strtolower($value,'UTF-8');

      return $value;
  }


  // TODO Add new method RemoveUserTokenSerialNumber/AddUserTokenSerialNumber like AddTokenAttributedUsers
  
  function GetUserTokenSerialNumber(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return mb_strtolower(isset($this->_user_data['token_serial']) ? $this->_user_data['token_serial'] : "", 'UTF-8');
  }


  function SetUserTokenLastEvent(
      $last_event
  ) {
      $this->_user_data['last_event'] = intval($last_event);
  }


  function GetUserTokenLastEvent()
  {
      return (isset($this->_user_data['last_event']) ? $this->_user_data['last_event'] : 0);
  }


  function SetUserTokenLastLogin(
      $timestamp
  ) {
      $this->_user_data['last_login'] = intval($timestamp);
  }


  function GetUserTokenLastLogin()
  {
      return intval($this->_user_data['last_login']);
  }


  function SetUserLastLogin(
      $timestamp
  ) {
      $this->_user_data['user_last_login'] = intval($timestamp);
  }


  function GetUserLastLogin()
  {
      return intval($this->_user_data['user_last_login']);
  }


  function SetUserTokenLastError(
      $timestamp
  ) {
      // CleanCacheData;
      if ($this->IsCacheData()) {
          $this->ResetCacheArray();
          $this->WriteCacheData();
      }
      
      $this->_user_data['last_error'] = intval($timestamp);
  }


  function GetUserTokenLastError()
  {
      return intval(isset($this->_user_data['last_error']) ? $this->_user_data['last_error'] : 0);
  }


  function SetUserLocked(
    $first_param,
    $second_param = "*-*"
  ) {
    $data = 0;
    if ($second_param == "*-*") {
      $data = $first_param;
    } else {
      $this->SetUser($first_param);
      $data = $second_param;
    }
    $this->_user_data['locked'] = $data;

    // CleanCacheData;
    if ($this->IsCacheData()) {
      $this->ResetCacheArray();
      $this->WriteCacheData();
    }
    
    return $data;
  }


  function GetUserLocked(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return intval(isset($this->_user_data['locked']) ? $this->_user_data['locked'] : 0);
  }


  function GetUserDelayed(
      $user = ""
  ) {
      // delayed_account, delayed_time, delayed_finished
      if ($user != "") {
          $this->SetUser($user);
      }
      return (isset($this->_user_data['delayed_account']) ? $this->_user_data['delayed_account'] : FALSE);
  }


  function GetUserDelayedTime(
      $user = ""
  ) {
      if ($user != "") {
          $this->SetUser($user);
      }
      return intval(isset($this->_user_data['delayed_time']) ? $this->_user_data['delayed_time'] : 0);
  }


  function SetUserActivated(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = TRUE;
      $data = 0;
      if ($second_param == "*-*") {
          $data = $first_param;
      } else {
          $result = $this->SetUser($first_param);
          $data = $second_param;
      }
      $desactive = ($data > 0)?0:1;
      $this->_user_data['desactivated'] = $desactive;
      
      if (0 == $desactive) {
          $this->SetUserErrorCounter(0);
          $this->SetUserLocked(0);
      }

      // CleanCacheData;
      if ($this->IsCacheData()) {
          $this->ResetCacheArray();
          $this->WriteCacheData();
      }
      
      return $result;
  }


  function GetUserActivated(
      $user = ""
  ) {
      $result = TRUE;
      if($user != "") {
          $result = $this->SetUser($user);
      }
      if ($result) {
          $result = intval((isset($this->_user_data['desactivated']) ? $this->_user_data['desactivated'] : 0) > 0)?0:1;
      }
      return $result;
  }


  function SetUserSynchronized(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = TRUE;
      $data = 0;
      if ($second_param == "*-*") {
          $data = $first_param;
      } else {
          $result = $this->SetUser($first_param);
          $data = $second_param;
      }
      $this->_user_data['synchronized'] = $data;

      return $result;
  }


  function GetUserSynchronized(
      $user = ""
  ) {
      if ($user != "") {
          $this->SetUser($user);
      }
      return intval(isset($this->_user_data['synchronized']) ? $this->_user_data['synchronized'] : 0);
  }
  

  function IsUserSynchronized(
      $user = ""
  ) {
      return (1 == ($this->GetUserSynchronized($user)));
  }
  

  function SetUserSynchronizedChannel(
      $first_param,
      $second_param = "*-*"
  ) {
      $data = 0;
      if ($second_param == "*-*") {
          $data = $first_param;
      } else {
          $this->SetUser($first_param);
          $data = $second_param;
      }
      $this->_user_data['synchronized_channel'] = $data;

      return $data;
  }


  function GetUserSynchronizedChannel(
      $user = ""
  ) {
      if ($user != "") {
          $this->SetUser($user);
      }
      return (isset($this->_user_data['synchronized_channel']) ? $this->_user_data['synchronized_channel'] : "");
  }
  

  function SetUserSynchronizedDn(
      $first_param,
      $second_param = "*-*"
  ) {
      $data = 0;
      if ($second_param == "*-*") {
          $data = $first_param;
      } else {
          $this->SetUser($first_param);
          $data = $second_param;
      }
      $this->_user_data['synchronized_dn'] = $data;

      return $data;
  }


  function GetUserSynchronizedDn(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return (isset($this->_user_data['synchronized_dn']) ? $this->_user_data['synchronized_dn'] : "");
  }
  

  function SetUserSynchronizedServer(
      $first_param,
      $second_param = "*-*"
  ) {
      $data = 0;
      if ($second_param == "*-*") {
          $data = $first_param;
      } else {
          $this->SetUser($first_param);
          $data = $second_param;
      }
      $this->_user_data['synchronized_server'] = $data;

      return $data;
  }


  function GetUserSynchronizedServer(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return (isset($this->_user_data['synchronized_server']) ? $this->_user_data['synchronized_server'] : "");
  }
  

  function SetUserSynchronizedTime(
      $first_param = "*-*",
      $second_param = "*-*"
  ) {
      $data = 0;
      if ($second_param == "*-*") {
          if ($first_param == "*-*") {
              $data = time();
          } else {
              $data = $first_param;
          }
      } else {
          $this->SetUser($first_param);
          $data = $second_param;
      }
      $this->_user_data['synchronized_time'] = $data;

      return $data;
  }


  function GetUserSynchronizedTime(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return intval(isset($this->_user_data['synchronized_time']) ? $this->_user_data['synchronized_time'] : 0);
  }


  function IsUserAdminWithOtp(
    $user = ''
  ) {
    if($user != '') {
        $this->SetUser($user);
    }
    return (1 == (isset($this->_user_data['admin_with_otp'])?$this->_user_data['admin_with_otp']:false));
  }


  function SetUserErrorCounter(
      $counter
  ) {
      $this->_user_data['error_counter'] = intval($counter);
  }


  function GetUserErrorCounter()
  {
      return intval(isset($this->_user_data['error_counter']) ? $this->_user_data['error_counter'] : 0);
  }


  /*********************************************************************
   *
   * Name: CreateToken
   * Short description: Create a new token, without parameter, create
   *                      a Google Authenticator compatible token
   *
   * Creation 2013-02-08
   * Update 2013-12-23
   * @package multiotp
   * @version 4.1.0
   * @author SysCo/al
   *
   * @param   string  $serial      
   * @param   string  $algorithm
   * @param   string  $seed
   * @param   int     $number_of_digits
   * @param   int     $time_interval_or_next_event
   * @param   string  $manufacturer
   * @param   string  $issuer
   * @param   string  $description
   * @return  boolean
   *
   *********************************************************************/
  function CreateToken(
      $serial = "",
      $algorithm = 'totp',
      $seed = "",
      $forced_number_of_digits = -1,
      $time_interval_or_next_event = -1,
      $manufacturer = 'multiOTP',
      $issuer = "",
      $description = "",
      $token_algo_suite = ""
  ) {
      if (intval($forced_number_of_digits) > 0) {
        $number_of_digits = $forced_number_of_digits;
      } else {
        $number_of_digits = $this->GetDefault2faDigits();
      }
    
      $the_serial = mb_strtolower($serial,'UTF-8');
      if ("" == $the_serial) {
          $the_serial = mb_strtolower('mu'.bigdec2hex((time()-mktime(1,1,1,1,1,2000)).mt_rand(10000,99999)),'UTF-8');
      }
      $the_description = $description;
      if ("" == $the_description) {
          $the_description = nullable_trim($manufacturer.' '.$the_serial);
      }
      $the_token = mb_strtolower($the_serial,'UTF-8');
      if ($this->ReadTokenData($the_token, TRUE)) {
          return FALSE; // ERROR: token already exists.
      } else {
          $this->SetToken($the_token);
          $this->SetTokenDescription($the_description);
          $this->SetTokenManufacturer(("" != $manufacturer)?$manufacturer:'multiOTP');
          $this->SetTokenIssuer(("" != $issuer)?$issuer:$this->GetIssuer());
          $this->SetTokenSerialNumber($the_serial);
          $this->SetTokenAlgorithm(mb_strtolower($algorithm,'UTF-8'));
          $this->SetTokenAlgoSuite(mb_strtolower($token_algo_suite,'UTF-8'));
          $this->SetTokenKeyAlgorithm(mb_strtolower($algorithm,'UTF-8'));
          $this->SetTokenOtp('TRUE');

          $this->SetTokenFormat('DECIMAL');
          $this->SetTokenNumberOfDigits($number_of_digits);
          $this->SetTokenDeltaTime(0);
          
          $the_seed = (("" == $seed)?mb_substr(md5(date("YmdHis").mt_rand(100000,999999)),0,20).mb_substr(md5(mt_rand(100000,999999).date("YmdHis")),0,20):$seed);

          if ('hotp' == mb_strtolower($algorithm,'UTF-8')) {
              $next_event = ((-1 == $time_interval_or_next_event)?0:$time_interval_or_next_event);
              $time_interval = 0;
          } else {
              $next_event = 0;
              $time_interval = ((-1 == $time_interval_or_next_event)?_MULTIOTP_DEFAULT_TOTP_INTERVAL_:$time_interval_or_next_event);
              if ('motp' == mb_strtolower($algorithm,'UTF-8')) {
                  $the_seed = (("" == $seed)?mb_substr(md5(date("YmdHis").mt_rand(100000,999999)),0,16):$seed);
                  $time_interval = 10;
              }
          }

          $this->SetTokenSeed($the_seed);
          $this->SetTokenLastEvent($next_event - 1);
          $this->SetTokenTimeInterval($time_interval);
          
          return $this->WriteTokenData();
      }
  }


  function AssignTokenToUser(
    $user,
    $token
  ) {
    $the_token = mb_strtolower($token,'UTF-8');
    $this->SetUser($user);
    
    // First, remove the old one (if any)
    $old_token = $this->GetUserTokenSerialNumber();
    if ($this->RemoveTokenAttributedUsers($old_token, $user)) {
        $this->WriteTokenData();
    }
    
    // Attribute the new one
    $this->SetUserTokenSerialNumber($the_token);
    $this->AddUserAttributedTokens($the_token);

    // Read the token attributes
    $this->SetToken($the_token);
    $this->AddTokenAttributedUsers($user);

    // Set the token attributes in the users attributes
    $this->SetUserKeyId($the_token);
    $this->SetUserAlgorithm($this->GetTokenAlgorithm());
    $this->SetUserTokenAlgoSuite($this->GetTokenAlgoSuite());
    $this->SetUserTokenSeed($this->GetTokenSeed());
    $this->SetUserTokenNumberOfDigits($this->GetTokenNumberOfDigits());
    $this->SetUserTokenTimeInterval($this->GetTokenTimeInterval());
    $this->SetUserTokenLastEvent($this->GetTokenLastEvent());

    return ($this->WriteUserData() && $this->WriteTokenData());
  }


  function RemoveTokenFromUser(
    $user
  ) {
    $this->SetUser($user);
    $old_token = $this->GetUserTokenSerialNumber();
    $this->SetUserTokenSerialNumber("");
    $this->RemoveUserAttributedTokens($old_token);
    $this->RemoveTokenAttributedUsers($old_token, $user);
    
    // Generate a new user token
    $this->SetUserAlgorithm('totp');
    $this->SetUserTokenAlgoSuite(""); // Default algorithm suite (HMAC-SHA1)
    $this->SetUserTokenNumberOfDigits($this->GetDefault2faDigits());
    $this->SetUserTokenSeed(mb_substr(md5(date("YmdHis").mt_rand(100000,999999)),0,20).mb_substr(md5(mt_rand(100000,999999).date("YmdHis")),0,20));
    $this->SetUserTokenTimeInterval(_MULTIOTP_DEFAULT_TOTP_INTERVAL_);
    $this->SetUserTokenLastEvent(0 - 1);
    return ($this->WriteUserData() && $this->WriteTokenData());
  }


  function SetTokenSerialNumberLength(
      $value
  ) {
      $this->_config_data['token_serial_number_length'] = nullable_trim($value);
  }
  
  
  function AddTokenSerialNumberLength(
      $length
  ) {
      if (intval($length) > 0) {
          $actual = nullable_trim($this->GetTokenSerialNumberLength());
          $length_exists = FALSE;

          // We add the serial number length only if it is not already attributed
          $token_serial_number_length_array = explode(" ",nullable_trim(str_replace(","," ",str_replace(";"," ",$actual))));
          foreach ($token_serial_number_length_array as $one_length) {
              if (intval($one_length) == intval($length)) {
                  $length_exists = TRUE;
                  break;
              }
          }
          if (!$length_exists) {
              $actual.=' '.intval($length);
              $this->SetTokenSerialNumberLength($actual);

              // And we save this information directly in the configuration
              $this->WriteConfigData();
          }
      }
      return TRUE;
  }


  function GetTokenSerialNumberLength()
  {
      $token_serial_number_length = $this->_config_data['token_serial_number_length'];
      if (FALSE === mb_strpos($token_serial_number_length, '12')) {
          // 12 is the RFC size of the serial number, we must have it and we add it if needed
          $token_serial_number_length.=' 12';
      }
      return $token_serial_number_length;
  }


  function SetTokenOtpListOfLength(
      $value
  ) {
      $this->_config_data['token_otp_list_of_length'] = nullable_trim($value);
  }
  
  
  function AddTokenOtpListOfLength(
      $length
  ) {
      if (intval($length) > 0) {
          $actual = nullable_trim($this->GetTokenOtpListOfLength());
          $length_exists = FALSE;

          // We add the OTP length only if it is not already attributed
          $token_otp_list_of_length_array = explode(" ",nullable_trim(str_replace(","," ",str_replace(";"," ",$actual))));
          foreach ($token_otp_list_of_length_array as $one_length) {
              if (intval($one_length) == intval($length)) {
                  $length_exists = TRUE;
                  break;
              }
          }
          if (!$length_exists) {
              $actual.=' '.intval($length);
              $this->SetTokenOtpListOfLength($actual);

              // And we save this information directly in the configuration
              $this->WriteConfigData();
          }
      }
      return TRUE;
  }


  function GetTokenOtpListOfLength()
  {
      $token_otp_list_of_length = $this->_config_data['token_otp_list_of_length'];
      if (FALSE === mb_strpos($token_otp_list_of_length, '6')) {
          // 6 is an RFC size of the OTP, we should have it and we add it if needed
          $token_otp_list_of_length.=' 6';
      }
      return $token_otp_list_of_length;
  }


  function SetTokenDescription(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $this->SetToken($first_param);
          $value = $second_param;
      }
      $value = $this->EncodeForBackend($value);
      $this->_token_data['description'] = $value;
      return $value;
  }


  function GetTokenDescription(
      $token = ""
  ) {
      $the_token = mb_strtolower($token,'UTF-8');
      if($the_token != "") {
          $this->SetToken($the_token);
      }
      return $this->_token_data['description'];
  }


  function SetToken(
      $token,
      $create = TRUE
  ) {
      $the_token = mb_strtolower($token,'UTF-8');
      $this->ResetTokenArray();
      $this->_token = $the_token;
      $result = $this->ReadTokenData("", $create); // First parameter empty, otherwise it will loop with SetToken !

      return ($create || $result);
  }


  function RenameCurrentToken(
      $new_token,
      $no_error_info = FALSE
  ) {
      $the_new_token = mb_strtolower($new_token,'UTF-8');
      $result = FALSE;
      if ($this->CheckTokenExists($the_new_token, false)) { // Check if the new token already exists
          $this->WriteLog("Error: Unable to rename the current token ".$this->GetToken()." to ".$the_new_token." because it already exists", FALSE, FALSE, _MULTIOTP_TOKEN_EXISTS_, 'Token', "");
      } else {
          if ($this->CheckTokenExists("", false)) { // Check if the current token already exists
              if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data['sql_tokens_table'])) || ('files' == $this->GetBackendType())) {
                  switch ($this->GetBackendType()) {
                      case 'mysql':
                          $esc_actual = escape_mysql_string($this->GetToken());
                          $esc_new    = escape_mysql_string($the_new_token);
                          if ($this->OpenMysqlDatabase()) {
                              if ("" != $this->_config_data['sql_tokens_table']) {
                                  $sQuery = "UPDATE `".$this->_config_data['sql_tokens_table']."` SET token_id='".$esc_new."' WHERE `token_id`='".$esc_actual."'";
                                  
                                  if (is_object($this->_mysqli)) {
                                      if (!($rResult = $this->_mysqli->query($sQuery))) {
                                          if (!$no_error_info) {
                                              $this->WriteLog("Error: Could not rename the token ".$this->GetToken().": ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'Token');
                                          }
                                      } else {
                                          $num_rows = $this->_mysqli->affected_rows;
                                      }
                                  } elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                                      if (!$no_error_info) {
                                          $this->WriteLog("Error: Could not rename the token ".$this->GetToken().": ".mysql_error(), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'Token');
                                      }
                                  } else {
                                      $num_rows = mysql_affected_rows($this->_mysql_database_link);
                                  }
                                  
                                  if (0 == $num_rows) {
                                      $this->WriteLog("Error: Could not rename the token ".$this->GetToken().". Token does not exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_, 'Token');
                                  } else {
                                      $this->WriteLog("Info: Token ".$this->GetToken()." successfully renamed to $the_new_token", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Token');
                                      $result = TRUE;
                                  }
                              }
                          }
                          break;
                      case 'pgsql':
                          if ($this->OpenPGSQLDatabase()) {
                              $esc_actual = pg_escape_string($this->_pgsql_database_link, $this->GetToken());
                              $esc_new    = pg_escape_string($this->_pgsql_database_link, $the_new_token);
                              if ("" != $this->_config_data['sql_tokens_table']) {
                                  $sQuery = "UPDATE \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_tokens_table']."\" SET \"token_id\" = '".$esc_new."' WHERE \"token_id\" = '".$esc_actual."'";
                                  
                                  if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                                      if (!$no_error_info) {
                                          $this->WriteLog("Error: Could not rename the token ".$this->GetToken().": ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'Token');
                                      }
                                  } else {
                                      $num_rows = pg_affected_rows($rResult);
                                  }

                                  if (0 == $num_rows) {
                                      $this->WriteLog("Error: Could not rename the token ".$this->GetToken().". Token does not exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_, 'Token');
                                  } else {
                                      $this->WriteLog("Info: Token ".$this->GetToken()." successfully renamed to $the_new_token", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Token');
                                      $result = TRUE;
                                  }
                              }
                          }
                          break;
                      case 'files':
                      default:
                          $old_token_filename = mb_strtolower($this->GetToken(),'UTF-8').'.db';
                          $new_token_filename = mb_strtolower($the_new_token,'UTF-8').'.db';
                          rename($this->GetTokensFolder().$old_token_filename, $this->GetTokensFolder().$new_token_filename);
                          $result = TRUE;
                          break;
                  }
              }
          }
          if ($result) {
              $this->_token = $the_new_token;
          }
      }
      return $result;
  }


  function GetToken() {
    return mb_strtolower((is_null($this->_token) ? "" : $this->_token),'UTF-8');
  }


  function CheckTokenExists(
      $token = "",
      $log_error = true
  ) {
      $the_token = mb_strtolower($token,'UTF-8');
      $check_token = mb_strtolower(("" != $the_token ? $the_token : $this->GetToken()),'UTF-8');
      $result = FALSE;
      
      if ("" != nullable_trim($check_token)) {
          if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data['sql_tokens_table'])) || ('files' == $this->GetBackendType())) {
              switch ($this->GetBackendType()) {
                  case 'mysql':
                      $check_token_escaped = escape_mysql_string($check_token);
                      if ($this->OpenMysqlDatabase()) {
                          $sQuery  = "SELECT * FROM `".$this->_config_data['sql_tokens_table']."` WHERE `token_id` = '{$check_token_escaped}'";
                          if (is_object($this->_mysqli)) {
                              if (!($rResult = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              } else {
                                  $num_rows = $rResult->num_rows;
                              }
                          } elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              $num_rows = mysql_num_rows($this->_mysql_database_link);
                          }
                          
                          if (0 == $num_rows) {
                              if ($log_error) {
                                  $this->WriteLog("Error: Token ".$check_token.". does not exist", FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              }
                              $result = FALSE;
                          } else {
                              $result = TRUE;
                          }
                      }
                      break;
                  case 'pgsql':
                      if ($this->OpenPGSQLDatabase()) {
                          $check_token_escaped = pg_escape_string($this->_pgsql_database_link, $check_token);
                          $sQuery  = "SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_tokens_table']."\" WHERE \"token_id\" = '{$check_token_escaped}'";
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              $num_rows = pg_num_rows($rResult);
                          }

                          if (0 == $num_rows) {
                              if ($log_error) {
                                  $this->WriteLog("Error: Token ".$check_token.". does not exist", FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              }
                              $result = FALSE;
                          } else {
                              $result = TRUE;
                          }
                      }
                      break;
                  case 'files':
                  default:
                      $token_filename = mb_strtolower($check_token,'UTF-8').'.db';
                      $result = file_exists($this->GetTokensFolder().$token_filename);
                      if ($log_error && (false === $result)) {
                          $this->WriteLog("Error: Token ".$check_token.". does not exist", FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                      }
                      break;
              }
          }
      }
      return $result;
  }


  function ResetLastImportedTokensArray()
  {
      $this->_last_imported_tokens = [];
  }


  function AddLastImportedToken(
      $token
  ) {
      $the_token = mb_strtolower($token,'UTF-8');
      $this->_last_imported_tokens[] = $the_token;
  }


  function GetLastImportedTokens()
  {
      return $this->_last_imported_tokens;
  }


  function SetTokenManufacturer(
      $manufacturer
  ) {
      $this->_token_data['manufacturer'] = $manufacturer;
  }


  function GetTokenManufacturer()
  {
      return $this->_token_data['manufacturer'];
  }


  function SetTokenModel(
      $model
  ) {
      $this->_token_data['model'] = $model;
  }


  function GetTokenModel()
  {
      return $this->_token_data['model'];
  }


  function SetTokenIssueNo(
      $issue_no
  ) {
      $this->_token_data['issue_no'] = nullable_trim($issue_no);
  }


  function GetTokenIssueNo()
  {
      return $this->_token_data['issue_no'];
  }


  function SetTokenKeyId(
      $key_id
  ) {
      $this->_token_data['key_id'] = nullable_trim($key_id);
  }


  function GetTokenKeyId()
  {
      return $this->_token_data['key_id'];
  }


  function SetTokenKeyUsage(
      $key_usage
  ) {
      $this->_token_data['key_usage'] = nullable_trim($key_usage);
  }


  function GetTokenKeyUsage()
  {
      return $this->_token_data['key_usage'];
  }


  function GetTokenEncryptionHash()
  {
      return $this->_token_data['encryption_hash'];
  }


  // This will (re)set only one user to the token
  function SetTokenAttributedUsers(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetToken($first_param);
          $result = $second_param;
      }
      $this->_token_data['attributed_users'] = $result;

      return $result;
  }


  function AddTokenAttributedUsers(
      $first_param,
      $second_param = "*-*"
  ) {
      $data = "";
      $result = FALSE;
      if ($second_param == "*-*") {
          $data = $first_param;
          $token = mb_strtolower($this->GetToken(),'UTF-8');
      } else {
          $token = mb_strtolower($first_param,'UTF-8');
          if ($this->CheckTokenExists($token, false)) {
              $this->SetToken($token);
          }
          $data = $second_param;
      }
      if ($this->CheckTokenExists($token, false)) {
          $actual = nullable_trim($this->GetTokenAttributedUsers());
          // We attribute the user only if it is not already attributed
          if (FALSE === mb_strpos(','.$actual.',', ','.$data.',')) {
              $this->SetTokenAttributedUsers($actual.(("" != $actual)?',':"").$data);
          }
          $result = TRUE;
      }
      return $result;
  }


  function RemoveTokenAttributedUsers(
      $first_param,
      $second_param = "*-*"
  ) {
      $data = "";
      $result = FALSE;
      if ($second_param == "*-*") {
          $data = $first_param;
          $token = mb_strtolower($this->GetToken(),'UTF-8');
      } else {
          $token = mb_strtolower($first_param,'UTF-8');
          if ($this->CheckTokenExists($token, false)) {
              $this->SetToken($token);
          }
          $data = $second_param;
      }
      if ($this->CheckTokenExists($token, false)) {
          if (FALSE !== mb_strpos(','.nullable_trim($this->GetTokenAttributedUsers()).',', ','.$data.',')) {
              $actual = str_replace(','.$data.',',',',','.nullable_trim($this->GetTokenAttributedUsers()).',');
              $this->SetTokenAttributedUsers(mb_substr($actual,1, mb_strlen($actual)-2));
              $result = TRUE;
          }
      }
      return $result;
  }


  function GetTokenAttributedUsers(
      $token = ""
  ) {
      if($token != "") {
          $the_token = mb_strtolower($token,'UTF-8');
          $this->SetToken($the_token);
      }
      return $this->_token_data['attributed_users'];
  }


  // This will (re)set only one token to the user
  function SetUserAttributedTokens(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetUser($first_param);
          $result = $second_param;
      }
      $this->_user_data['attributed_tokens'] = $result;

      return $result;
  }


  function AddUserAttributedTokens(
      $first_param,
      $second_param = "*-*"
  ) {
      $data = "";
      $result = FALSE;
      if ($second_param == "*-*") {
          $data = $first_param;
          $user = $this->GetUser();
      } else {
          $user = $first_param;
          if ($this->CheckUserExists($user)) {
              $this->SetUser($user);
          }
          $data = $second_param;
      }
      if ($this->CheckUserExists($user)) {
          $actual = nullable_trim($this->GetUserAttributedTokens());
          // We attribute the token only if it is not already attributed
          if (FALSE === mb_strpos(','.$actual.',', ','.$data.',')) {
              $this->SetUserAttributedTokens($actual.(("" != $actual)?',':"").$data);
          }
          $result = TRUE;
      }
      return $result;
  }


  function RemoveUserAttributedTokens(
      $first_param,
      $second_param = "*-*"
  ) {
      $data = "";
      $result = FALSE;
      if ($second_param == "*-*") {
          $data = $first_param;
          $user = $this->GetUser();
      } else {
          $user = $first_param;
          if ($this->CheckUserExists($user)) {
              $this->SetUser($user);
          }
          $data = $second_param;
      }
      if ($this->CheckUserExists($user)) {
          if (FALSE !== mb_strpos(','.nullable_trim($this->GetUserAttributedTokens()).',', ','.$data.',')) {
              $actual = str_replace(','.$data.',',',',','.nullable_trim($this->GetUserAttributedTokens()).',');
              $this->SetUserAttributedTokens(mb_substr($actual,1, mb_strlen($actual)-2));
              $result = TRUE;
          }
      }
      return $result;
  }


  function GetUserAttributedTokens(
      $user = ""
  ) {
      if($user != "") {
          $this->SetUser($user);
      }
      return $this->_user_data['attributed_tokens'];
  }


  // SerialNo is the original SerialNo of the key.
  function SetTokenSerialNo(
      $serial_no
  ) {
      $this->_token_data['serial_no'] = mb_strtolower($serial_no,'UTF-8');
  }


  // SerialNo is the original SerialNo of the key.
  function GetTokenSerialNo()
  {
      return mb_strtolower($this->_token_data['serial_no'],'UTF-8');
  }


  function SetTokenSerialNumber(
      $token_serial
  ) {
      $this->_token_data['token_serial'] = mb_strtolower($token_serial,'UTF-8');
      $len_token_serial = mb_strlen($token_serial);
      if ($len_token_serial > 0) {
          // We add this length automatically in the list of the existing serial number length
          $this->AddTokenSerialNumberLength($len_token_serial);
      }
  }


  function GetTokenSerialNumber()
  {
      return mb_strtolower(isset($this->_token_data['token_serial']) ? $this->_token_data['token_serial'] : "",'UTF-8');
  }


  function SetTokenIssuer(
      $issuer
  ) {
      if ("" == $issuer) {
          $this->_token_data['issuer'] = $this->GetIssuer();
      } else {
          $this->_token_data['issuer'] = $issuer;
      }
  }


  function GetTokenIssuer()
  {
      return $this->_token_data['issuer'];
  }


  function SetTokenKeyAlgorithm(
      $key_algorithm
  ) {
      $this->_token_data['key_algorithm'] = $key_algorithm;
  }


  function GetTokenKeyAlgorithm()
  {
      return $this->_token_data['key_algorithm'];
  }


  function SetTokenAlgorithm(
      $algorithm
  ) {
      $result = FALSE;
      if (FALSE === mb_strpos(mb_strtolower($this->_valid_algorithms,'UTF-8'), mb_strtolower('*'.$algorithm.'*','UTF-8'))) {
          $this->WriteLog("Error: ".$algorithm." algorithm unknown for token ".$this->GetToken(), FALSE, FALSE, _MULTIOTP_BAD_ALGORITHM_, 'Token');
      } else {
          $this->_token_data['algorithm'] = mb_strtolower($algorithm,'UTF-8');
          $result = TRUE;
      }
      return $result;
  }


  function GetTokenAlgorithm()
  {
      $result = $this->_token_data['algorithm'];
      if (FALSE === mb_strpos(mb_strtolower($this->_valid_algorithms,'UTF-8'), mb_strtolower('*'.$result.'*','UTF-8'))) {
          $result = "";
      }

      return $result;
  }


  function SetTokenAlgoSuite(
      $token_algo_suite
  ) {
      $this->_token_data['token_algo_suite'] = mb_strtoupper(("" == $token_algo_suite)?'HMAC-SHA1':$token_algo_suite,'UTF-8');
      return TRUE;
  }


  function GetTokenAlgoSuite()
  {
      return mb_strtoupper(("" == $this->_token_data['token_algo_suite'])?'HMAC-SHA1':$this->_token_data['token_algo_suite'],'UTF-8');
  }


  function SetTokenOtp(
      $otp
  ) {
      $this->_token_data['otp'] = $otp;
  }


  function GetTokenOtp()
  {
      return $this->_token_data['otp'];
  }


  function SetTokenFormat(
      $format
  ) {
      $this->_token_data['format'] = $format;
  }


  function GetTokenFormat()
  {
      return $this->_token_data['format'];
  }


  function SetTokenNumberOfDigits(
      $number_of_digits
  ) {
      $this->_token_data['number_of_digits'] = $number_of_digits;
      // We add this number of digits automatically in the list of the existing list of length
      $this->AddTokenOtpListOfLength($number_of_digits);
  }


  function GetTokenNumberOfDigits()
  {
      return $this->_token_data['number_of_digits'];
  }


  function SetTokenLastEvent(
      $last_event
  ) {
      $this->_token_data['last_event'] = $last_event;
  }


  function GetTokenLastEvent()
  {
      return $this->_token_data['last_event'];
  }


  function SetTokenLastLogin(
      $timestamp
  ) {
      $this->_token_data['last_login'] = intval($timestamp);
  }


  function GetTokenLastLogin()
  {
      return intval($this->_token_data['last_login']);
  }


  function SetTokenErrorCounter(
      $counter
  ) {
      $this->_token_data['error_counter'] = $counter;
  }


  function GetTokenErrorCounter()
  {
      return $this->_token_data['error_counter'];
  }


  function SetTokenDeltaTime(
      $delta_time
  ) {
      $this->_token_data['delta_time'] = $delta_time;
  }


  function GetTokenDeltaTime()
  {
      return $this->_token_data['delta_time'];
  }


  function SetTokenTimeInterval(
      $time_interval
  ) {
      $this->_token_data['time_interval'] = $time_interval;
  }


  function GetTokenTimeInterval()
  {
      return $this->_token_data['time_interval'];
  }


  function SetTokenPrivateId(
      $private_id
  ) {
      $this->_token_data['private_id'] = $private_id;
  }


  function GetTokenPrivateId()
  {
      return $this->_token_data['private_id'];
  }


  /**
   * @brief   Set the token seed in hexadecimal, base32 or raw binary
   *
   * @param   string  $seed  Seed in hexadecimal, base32 or raw binary
   * @return  none
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.0.3.6
   * @date    2017-02-21
   * @since   2010-08-12
   */
  function SetTokenSeed(
      $seed
  ) {
      $the_seed = $seed;
      if (!ctype_xdigit($the_seed)) {
          if (FALSE !== base32_decode($seed)) {
              $the_seed = nullable_bin2hex(base32_decode($seed));
          } else {
              $the_seed = nullable_bin2hex($seed);
          }
      }
      $this->_token_data['token_seed'] = $the_seed;
  }


  function GetTokenSeed()
  {
      return $this->_token_data['token_seed'];
  }


  function SetTokensFolder(
      $folder,
      $create = true
  ) {
      $new_folder = $this->ConvertToUnixPath($folder);
      if (mb_substr($new_folder,-1) != "/") {
          $new_folder.="/";
      }
      if ("/" == $new_folder) {
        $new_folder = "./";
      }
      $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
      $this->_tokens_folder = $new_folder;
      if ($create && (!file_exists($new_folder))) {
          if (!@mkdir(
                  $new_folder,
                  ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
                  true //recursive
          )) {
              $this->WriteLog("Error: Unable to create the missing tokens folder ".$new_folder, FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System',  "");
          }
      }
  }


  function GetTokensFolder()
  {
      if ("" == $this->_tokens_folder) {
          $this->SetTokensFolder($this->GetScriptFolder()."tokens/");
      }
      return $this->ConvertToWindowsPathIfNeeded($this->_tokens_folder);
  }


  function GetTokensList()
  {
      return $this->GetList('token_id', 'sql_tokens_table', $this->GetTokensFolder());
  }


  function GetTokensCount($no_cache = FALSE)
  {
      if (($this->IsCacheData()) && (intval($this->ReadCacheValue('tokens_count')) >= 0) && (!$no_cache)) {
          $tokens_count = intval($this->ReadCacheValue('tokens_count'));
      } else {
          $tokens_count = 0;
          switch ($this->GetBackendType()) {

              case 'mysql':
                  if ($this->OpenMysqlDatabase())
                  {
                      $sQuery  = "SELECT COUNT(token_id) AS counter FROM `".$this->_config_data['sql_tokens_table']."` ";
                      if (is_object($this->_mysqli)) {
                          if (!($result = $this->_mysqli->query($sQuery))) {
                              $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              $result = FALSE;
                          } else {
                              if ($aRow = $result->fetch_assoc()) {
                                  $tokens_count = $aRow['counter'];
                              }
                          }
                      } else {
                          if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              if ($aRow = mysql_fetch_assoc($rResult)) {
                                  $tokens_count = $aRow['counter'];
                              }
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase())
                  {
                      $sQuery  = "SELECT COUNT(\"token_id\") AS \"counter\" FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_tokens_table']."\" ";
                      if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                          $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                      } else {
                          if ($aRow = pg_fetch_assoc($rResult)) {
                              $tokens_count = $aRow['counter'];
                          }
                      }
                  }
                  break;
              case 'files':
              default:
                  if ($tokens_handle = @opendir($this->GetTokensFolder())) {
                      while ($file = readdir($tokens_handle)) {
                          if ((mb_substr($file, -3) == ".db") && ($file != '.db')) {
                              $tokens_count++;
                          }
                      }
                      closedir($tokens_handle);
                  }
          }
          if (($this->IsCacheData()) && ($tokens_count >= 0)) {
              $this->WriteCacheValue('tokens_count', $tokens_count);
              $this->WriteCacheData();
          }
      }
      return $tokens_count;
  }


  function DeleteToken(
      $token = "",
      $no_error_info = FALSE
  ) {
      $the_token = mb_strtolower($token,'UTF-8');
      if ("" != $the_token) {
          $this->SetToken($the_token);
      }
      
      $result = FALSE;
      
      // First, we delete the token file if the backend is files or when migration is enabled
      if (('files' == $this->GetBackendType()) || ($this->GetMigrationFromFile())) {
          $token_filename = $this->GetToken().'.db';
          if (!file_exists($this->GetTokensFolder().$token_filename)) {
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Error: *Unable to delete token ".$this->GetToken().", the tokens database file ".$this->GetTokensFolder().$token_filename." does not exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_, 'Token',  "");
              } else {
                  $this->WriteLog("Error: Unable to delete token ".$this->GetToken(), FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_, 'Token',  "");
              }
          } else {
              $result = unlink($this->GetTokensFolder().$token_filename);
              if ($result) {
                  if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Info: *Token ".$this->GetToken()." successfully deleted", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Token', "");
                  }
              } else {
                  $this->WriteLog("Error: Unable to delete token ".$this->GetToken(), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'Token',  "");
              }
          }
      }

      if ($this->GetBackendTypeValidated()) {
          switch ($this->_config_data['backend_type']) {
              case 'mysql':
                  $token_escaped = escape_mysql_string($this->GetToken());
                  if ($this->OpenMysqlDatabase()) {
                      if ("" != $this->_config_data['sql_tokens_table']) {
                          $sQuery  = "DELETE FROM `".$this->_config_data['sql_tokens_table']."` WHERE `token_id` = '".$token_escaped."'";

                          if (is_object($this->_mysqli)) {
                              if (!($rResult = $this->_mysqli->query($sQuery))) {
                                  if (!$no_error_info) {
                                      $this->WriteLog("Error: Could not delete token ".$this->GetToken().": ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'Token', "");
                                  }
                              } else {
                                  $num_rows = $this->_mysqli->affected_rows;
                              }
                          } elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete token ".$this->GetToken().": ".mysql_error(), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'Token', "");
                              }
                          } else {
                              $num_rows = mysql_affected_rows($this->_mysql_database_link);
                          }
                          
                          if (0 == $num_rows) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete token ".$this->GetToken().". Token does not exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_, 'Token', "");
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *token ".$this->GetToken()." successfully deleted", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Token', "");
                              }
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $token_escaped = pg_escape_string($this->_pgsql_database_link, $this->GetToken());
                      if ("" != $this->_config_data['sql_tokens_table']) {
                          $sQuery  = "DELETE FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_tokens_table']."\" WHERE \"token_id\" = '".$token_escaped."'";

                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete token ".$this->GetToken().": ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'Token', "");
                              }
                          } else {
                              $num_rows = pg_affected_rows($rResult);
                          }
                          
                          if (0 == $num_rows) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete token ".$this->GetToken().". Token does not exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_, 'Token', "");
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *token ".$this->GetToken()." successfully deleted", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Token', "");
                              }
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              default:
                  // Nothing to do if the backend type is unknown
                  break;
          }                        
      }
      if ($result) {
          $this->TouchFolder('data',
                             'Token',
                             $this->GetToken(),
                             TRUE,
                             "DeleteToken");
      }
      return $result;
  }


  function ReadTokenData(
      $token = "",
      $create = FALSE
  ) {
      
      // We initialize the local encryption check variable
      $local_encryption_check = false;

      $this->ResetTokenArray();
      $the_token = mb_strtolower($token,'UTF-8');
      if ("" != $the_token) {
          $this->SetToken($the_token);
      }
      $result = FALSE;
      
      // We initialize the encryption hash to empty
      $this->_token_data['encryption_hash'] = "";
      
      // First, we read the user file if the backend is files or when migration is enabled
      if (('files' == $this->GetBackendType()) || ($this->GetMigrationFromFile())) {
          $token_filename = mb_strtolower($this->GetToken(),'UTF-8').'.db';
          if (!file_exists($this->GetTokensFolder().$token_filename)) {
              if (!$create) {
                  $this->WriteLog("Error: database file ".$this->GetTokensFolder().$token_filename." for token ".$this->_token." does not exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_, 'System', "");
              }
          } else {
              if ($file_handler = @fopen($this->GetTokensFolder().$token_filename, "rt")) {
                  flock($file_handler, LOCK_SH);
                  $first_line = nullable_trim(fgets($file_handler));
                  
                  while (!feof($file_handler)) {
                      $line = nullable_trim(fgets($file_handler));
                      $line_array = explode("=",$line,2);
                      if (":" == mb_substr($line_array[0], -1)) {
                          $line_array[0] = mb_substr($line_array[0], 0, mb_strlen($line_array[0]) -1);
                          $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                          $local_encryption_check = $this->_encryption_check;
                      }
                      $line_array[1] = str_replace("<<CRLF>>",chr(10),isset($line_array[1]) ? $line_array[1] : "");
                      if ("" != nullable_trim($line_array[0])) {
                          $this->_token_data[mb_strtolower($line_array[0],'UTF-8')] = $line_array[1];
                      }
                  }
                  
                  fclose($file_handler);
                  $result = TRUE;

                  if (("" != $this->_token_data['encryption_hash']) && ($local_encryption_check)) {
                      if ($this->_token_data['encryption_hash'] != $this->CalculateControlHash($this->GetEncryptionKey())) {
                          $this->_token_data['encryption_hash'] = "ERROR";
                          $this->WriteLog("Error: the token information encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "");
                          $result = FALSE;
                      }
                  }
              }
          }
      }

      // And now, we override the values if another backend type is defined
      if ($this->GetBackendTypeValidated()) {
          switch ($this->_config_data['backend_type']) {
              case 'mysql':
                  $token_escaped = escape_mysql_string($this->_token);
                  if ($this->OpenMysqlDatabase()) {
                      if ("" != $this->_config_data['sql_tokens_table']) {
                          $sQuery  = "SELECT * FROM `".$this->_config_data['sql_tokens_table']."` WHERE `token_id` = '".$token_escaped."'";
                          $aRow = NULL;
                          
                          if (is_object($this->_mysqli)) {
                              if (!($rResult = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: SQL query error ($sQuery) : ".nullable_trim($this->_mysqli->error).' ', TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  $aRow = $rResult->fetch_assoc();
                              }
                          } else {
                              if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                                  $this->WriteLog("Error: SQL query error ($sQuery) : ".mysql_error(), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  $aRow = mysql_fetch_assoc($rResult);
                              }
                          }

                          if (NULL != $aRow) {
                              $result = FALSE;
                              foreach ($aRow as $key => $value) {
                                  $in_the_schema = FALSE;
                                  foreach ($this->_sql_tables_schema['tokens'] as $valid_key => $valid_format) {
                                      if ($valid_key == $key) {
                                          $in_the_schema = TRUE;
                                          if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                            $value = "";
                                          }
                                          break;
                                      }
                                  }
                                  if (($in_the_schema) && ($key != 'token_id')) {
                                      if (('ENC:' == mb_substr($value,0,4)) && (':ENC' == mb_substr($value,-4))) {
                                          $value = mb_substr($value,4);
                                          $value = mb_substr($value,0,mb_strlen($value)-4);
                                          $this->_token_data[$key] = $this->Decrypt($key,$value,$this->GetEncryptionKey());
                                          $local_encryption_check = $this->_encryption_check;
                                      } else {
                                          $this->_token_data[$key] = $value;
                                      }
                                      $this->_token_data[$key] = str_replace("<<CRLF>>",chr(10),$this->_token_data[$key]);
                                  } elseif ((!$in_the_schema) && ('unique_id' != $key)  && $this->GetVerboseFlag()) {
                                      $this->WriteLog("Warning: *The key ".$key." is not in the tokens database schema", FALSE, FALSE, _MULTIOTP_SYSTEM_KEY_NOT_IN_SCHEMA_ERROR_, 'System', "");
                                  }
                                  $result = TRUE;
                              }
                              if(0 == count($aRow) && !$create) {
                                  $this->WriteLog("Error: SQL database entry for token ".$this->_token." does not exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_, 'System', "");
                              }
                          }
                      }
                      if (("" != $this->_token_data['encryption_hash']) && ($local_encryption_check)) {
                          if ($this->_token_data['encryption_hash'] != $this->CalculateControlHash($this->GetEncryptionKey())) {
                              $this->_token_data['encryption_hash'] = "ERROR";
                              $this->WriteLog("Error: the tokens mysql encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "");
                              $result = FALSE;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $token_escaped = pg_escape_string($this->_pgsql_database_link, $this->_token);
                      if ("" != $this->_config_data['sql_tokens_table']) {
                          $sQuery  = "SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_tokens_table']."\" WHERE \"token_id\" = '".$token_escaped."'";
                          $aRow = NULL;
                          
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: SQL query error ($sQuery) : ".pg_last_error($this->_pgsql_database_link), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                              $result = FALSE;
                          } else {
                              $aRow = pg_fetch_assoc($rResult);
                          }

                          if (NULL != $aRow) {
                              $result = FALSE;
                              foreach ($aRow as $key => $value) {
                                  $in_the_schema = FALSE;
                                  foreach ($this->_sql_tables_schema['tokens'] as $valid_key => $valid_format) {
                                      if ($valid_key == $key) {
                                          $in_the_schema = TRUE;
                                          if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                            $value = "";
                                          }
                                          break;
                                      }
                                  }
                                  if (($in_the_schema) && ($key != 'token_id')) {
                                      if (is_null($value)) {
                                        $value = "";
                                      }
                                      if (('ENC:' == mb_substr($value,0,4)) && (':ENC' == mb_substr($value,-4))) {
                                          $value = mb_substr($value,4);
                                          $value = mb_substr($value,0,mb_strlen($value)-4);
                                          $this->_token_data[$key] = $this->Decrypt($key,$value,$this->GetEncryptionKey());
                                          $local_encryption_check = $this->_encryption_check;
                                      } else {
                                          $this->_token_data[$key] = $value;
                                      }
                                      if (is_null($this->_token_data[$key])) {
                                        $this->_token_data[$key] = "";
                                      }
                                      $this->_token_data[$key] = str_replace("<<CRLF>>",chr(10),$this->_token_data[$key]);
                                  } elseif ((!$in_the_schema) && ('unique_id' != $key)  && $this->GetVerboseFlag()) {
                                      $this->WriteLog("Warning: *The key ".$key." is not in the tokens database schema", FALSE, FALSE, _MULTIOTP_SYSTEM_KEY_NOT_IN_SCHEMA_ERROR_, 'System', "");
                                  }
                                  $result = TRUE;
                              }
                              if(0 == count($aRow) && !$create) {
                                  $this->WriteLog("Error: SQL database entry for token ".$this->_token." does not exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_, 'System', "");
                              }
                          }
                      }
                      if (("" != $this->_token_data['encryption_hash']) && ($local_encryption_check)) {
                          if ($this->_token_data['encryption_hash'] != $this->CalculateControlHash($this->GetEncryptionKey())) {
                              $this->_token_data['encryption_hash'] = "ERROR";
                              $this->WriteLog("Error: the tokens pgsql encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "");
                              $result = FALSE;
                          }
                      }
                  }
                  break;
              default:
                  // Nothing to do if the backend type is unknown
                  break;
          }
      }
      $this->SetTokenDataReadFlag($result);
      return $result;
  }


  function WriteTokenData(
      $write_token_data_array = []
  ) {
      if ("" == nullable_trim($this->GetToken())) {
          $result = false;
      } else {
          $result = $this->WriteData(array_merge(array('item'               => 'Token',
                                                       'table'              => 'tokens',
                                                       'folder'             => $this->GetTokensFolder(),
                                                       'data_array'         => $this->_token_data,
                                                       'force_file'         => false,
                                                       'id_field'           => 'token_id',
                                                       'id_value'           => $this->GetToken()
                                                      ), $write_token_data_array));
      }
      return $result;
  }


  function SetLastClearOtpValue(
      $value = ""
  ) {
      $this->_last_clear_otp_value = $value;
  }


  function GetLastClearOtpValue()
  {
      return $this->_last_clear_otp_value;
  }


  function ResetTemporaryBadServer()
  {
      $this->_servers_temp_bad_list = [];
  }


  function AddTemporaryBadServer(
      $server,
      $timestamp
  ) {
      $this->_servers_temp_bad_list[$server] = intval($timestamp);
  }


  function IsTemporaryBadServer(
    $server
  ) {
    $result = FALSE;
    foreach ($this->_servers_temp_bad_list as $badserver => $timestamp) {
      if ($badserver == $server) {
        if (($timestamp + (1 * 60)) >= time()) {
          $result = TRUE;
        }
        break;
      }
    }
    return $result;
  }


  function CheckUserLdapPassword(
      $ldap_username,
      $ldap_password
  ) {
      $this->SetLdapServerReachable(FALSE);
      $result = FALSE;

      // DistinguishedName must be encoded in UTF-8
      $ldap_bind_dn = encode_utf8_if_needed($ldap_username);
      
      if (("" != $ldap_username) && (FALSE === mb_strpos(mb_strtolower($ldap_bind_dn,'UTF-8'), 'cn='))) {
          $ldap_bind_dn = 'CN='.$ldap_bind_dn.','.$this->GetLdapBaseDn();
      }

      if (!function_exists('ldap_connect')) {
          $this->WriteLog("Error: LDAP library not installed", FALSE, FALSE, _MULTIOTP_OPERATION_CANCELLED_, 'System', "", 3);
          $this->EnableLdapError();
      } elseif (("" != $this->GetLdapDomainControllers()) && ("" != $ldap_username) && ("" != $ldap_password)) {
          $domain_controllers = explode(" ",nullable_trim(str_replace(","," ",str_replace(";"," ",$this->GetLdapDomainControllers()))));
          $ldap_options = array('account_suffix'     => $this->GetLdapAccountSuffix(),
                                'ad_password'        => $ldap_password,
                                'ad_username'        => $ldap_bind_dn,
                                'base_dn'            => $this->GetLdapBaseDn(),
                                'cn_identifier'      => $this->GetLdapCnIdentifier(),
                                'domain_controllers' => $domain_controllers,
                                'group_attribute'    => $this->GetLdapGroupAttribute(),
                                'group_cn_identifier'=> $this->GetLdapGroupCnIdentifier(),
                                'ldap_server_type'   => $this->GetLdapServerType(),
                                'ldap_filter'        => $this->GetLdapFilter(),
                                'network_timeout'    => $this->GetLdapNetworkTimeout(),
                                'port'               => $this->GetLdapPort(),
                                'recursive_groups'   => $this->IsLdapRecursiveGroups(),
                                'time_limit'         => $this->GetLdapTimeLimit(),
                                'use_ssl'            => $this->IsLdapSsl(),
                                'cache_support'      => $this->IsLdapCacheOn(),
                                'cache_folder'       => $this->GetLdapCacheFolder(),
                                'expired_password_valid' => $this->IsLdapExpiredPasswordValid()
                               );

          $ldap_connection = new MultiotpAdLdap($ldap_options);

          $this->SetLdapServerReachable($ldap_connection->IsServerReachable());

          $result = !$ldap_connection->IsError();

          if ((!$result) && $this->GetVerboseFlag()) {
              $this->WriteLog($ldap_connection->ErrorMessage(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "", 3);
          }
          unset($ldap_connection);
      }
      return $result;
  }


  function GetLdapUsersList(
      $user_filter = "*"
  ) {
      $include_disabled = TRUE;
      $ignore_in_group = FALSE;

      $users_list = "";

      if (1 == $this->GetLdapServerType()) { // enhanced Active Directory
          $recursive_prefix = "1.2.840.113556.1.4.1941:=";
      } else {
          $recursive_prefix = "";
      }
      
      // Prepare the array "users_in_groups"
      // - if we are using a generic LDAP and an LdapInGroup Filtering
      // - if we are using enhanced Active Directory
      if ("" != nullable_trim($this->GetLdapInGroup(true))) {
        $in_groups_array_raw = explode("\t",nullable_trim(str_replace(",","\t",str_replace(";","\t",$this->GetLdapInGroup(true)))));
      } else {
        $in_groups_array_raw = [];
      }

      if (!function_exists('ldap_connect')) {
          $result = FALSE;
          $this->WriteLog("Error: LDAP library not installed", FALSE, FALSE, _MULTIOTP_OPERATION_CANCELLED_, 'System', "", 3);
          $this->EnableLdapError();
      } elseif (("" != $this->GetLdapDomainControllers()) && ("" != $this->GetLdapBindDn()) && ("" != $this->GetLdapServerPassword())) {
          
          $this->DisableLdapError();
          $in_groups_array = [];
      
          // TODO: later, we could loop in several base-dn (semicolon separated)
          $domain_controllers = explode(" ",nullable_trim(str_replace(","," ",str_replace(";"," ",$this->GetLdapDomainControllers()))));
          
          $users_dn_array_raw = explode("\t",nullable_trim(str_replace(";","\t",$this->GetLdapUsersDn())));
          
          $ldap_options = array('account_suffix'     => $this->GetLdapAccountSuffix(),
                                'ad_password'        => $this->GetLdapServerPassword(),
                                'ad_username'        => $this->GetLdapBindDn(),
                                'base_dn'            => $this->GetLdapBaseDn(),
                                'cn_identifier'      => $this->GetLdapCnIdentifier(),
                                'domain_controllers' => $domain_controllers,
                                'group_attribute'    => $this->GetLdapGroupAttribute(),
                                'group_cn_identifier'=> $this->GetLdapGroupCnIdentifier(),
                                'ldap_server_type'   => $this->GetLdapServerType(),
                                'ldap_filter'        => $this->GetLdapFilter(),
                                'network_timeout'    => $this->GetLdapNetworkTimeout(),
                                'port'               => $this->GetLdapPort(),
                                'recursive_groups'   => $this->IsLdapRecursiveGroups(),
                                'time_limit'         => $this->GetLdapTimeLimit(),
                                'use_ssl'            => $this->IsLdapSsl(),
                                'cache_support'      => $this->IsLdapCacheOn(),
                                'cache_folder'       => $this->GetLdapCacheFolder(),
                                'users_dn'           => $users_dn_array_raw[0]
                               );

          $ldap_connection = new MultiotpAdLdap($ldap_options);

          if ($ldap_connection->IsError()) {
              $this->WriteLog("Error: ".$ldap_connection->ErrorMessage(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
              $this->EnableLdapError();
          } else {
              // We continue only if there is no error

              $in_groups_filtering = [];
              $groups_filtering = "";
              if (1 == count($in_groups_array_raw)) {
                $group_info = $ldap_connection->group_info($in_groups_array_raw[0],array('distinguishedname'));
                if (isset($group_info[0]['distinguishedname'][0])) {
                    $group_info_dn = $group_info[0]['distinguishedname'][0];
                } else {
                    $group_info_dn = $in_groups_array_raw[0];
                }
                $groups_filtering.= "(".$this->GetLdapGroupAttribute().":";
                $groups_filtering.= $recursive_prefix.$group_info_dn.")";
                $in_groups_filtering[] = array('name' => $in_groups_array_raw[0], 'distinguishedname' => "(".$this->GetLdapGroupAttribute().":".$recursive_prefix.$group_info_dn.")");
              } elseif (count($in_groups_array_raw) > 1) {
                $groups_filtering = "(|";
                foreach ($in_groups_array_raw as $one_group) {
                  $group_info = $ldap_connection->group_info($one_group,array('distinguishedname'));
                  if (isset($group_info[0]['distinguishedname'][0])) {
                      $group_info_dn = $group_info[0]['distinguishedname'][0];
                  } else {
                      $group_info_dn = $in_groups_array_raw[0];
                  }
                  $groups_filtering.= "(".$this->GetLdapGroupAttribute().":";
                  $groups_filtering.= $recursive_prefix.$group_info_dn.")";
                  $in_groups_filtering[] = array('name' => $one_group, 'distinguishedname' => "(".$this->GetLdapGroupAttribute().":".$recursive_prefix.$group_info_dn.")");
                }
                $groups_filtering.= ")";
              } else {
                  // No group filtering, we don't need _real_primarygroup
                  $ldap_connection->_real_primarygroup = FALSE;
              }

              // Put all group_cn in cache
              // (don't do that for Enhanced Microsoft Active Directory server)
              if (1 != $this->GetLdapServerType()) {
                  $ldap_connection->group_cn(1, FALSE, TRUE);

                  // Put all recursive_groups in cache
                  if ($ldap_connection->_recursive_groups) {
                      $all_groups = $ldap_connection->all_groups(FALSE, '*', TRUE, TRUE);
                      foreach ($all_groups as $key => $one_group) {
                          $ldap_connection->recursive_groups($one_group);
                      }
                  }
              }

              $ldap_connection->_paging_cookie = "";

              $users_in_groups = [];

              if ("" != nullable_trim($this->GetLdapInGroup(true))) {
                if ((2 == $this->GetLdapServerType()) || (4 == $this->GetLdapServerType())) { // Generic LDAP or eDirectory, eventually no memberOf function like in AD
                  foreach ($in_groups_array_raw as $one_group) {
                    $temp_array = $ldap_connection->group_users($one_group);
                    foreach ($temp_array as $one_temp) {
                      $one_user = $this->EncodeForBackend($one_temp);
                      if (!isset($users_in_groups[$one_user])) {
                        $users_in_groups[$one_user] = $one_group;
                      } else {
                        $users_in_groups[$one_user] = $users_in_groups[$one_user].",".$one_group;
                      }
                    }
                  }
                }
              }

              $users_dn_array_raw = explode("\t",nullable_trim(str_replace(";","\t",$this->GetLdapUsersDn())));
              foreach ($users_dn_array_raw as $one_user_dn) {
                  $ldap_connection->set_users_dn($one_user_dn);
                  do { // LDAP pagination loop
                      if ($ldap_connection->_paging_legacy) {
                        ldap_control_paged_result($ldap_connection->_conn_paged, $ldap_connection->_paging_size, false, $ldap_connection->_paging_cookie);
                      }

                      $one_user = $ldap_connection->one_user_info(true,
                                                                  $user_filter,
                                                                  $this->GetLdapFieldsArray(),
                                                                  $this->IsLdapCacheOn(),
                                                                  $groups_filtering,
                                                                  $in_groups_filtering
                                                                 );

                      if ($ldap_connection->IsError()) {
                          $this->EnableLdapError();
                          $this->WriteLog("Error: LDAP connection failed", false, false, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                          return FALSE;
                      }
                      if ("" != $ldap_connection->get_warning_message()) {
                          $this->WriteLog("Warning: LDAP warning: ".$ldap_connection->get_warning_message(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                      }
                      if ($this->GetVerboseFlag() && ("" != $ldap_connection->get_debug_message())) {
                          $this->WriteLog("Debug: *LDAP debug: ".$ldap_connection->get_debug_message(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                      }

                      do {
                          $accountdisable = FALSE;
                          $groups_lower_array = [];
                          $in_groups_array = [];
                          $in_groups_lower_array = [];
                          $group = "";
                          $user_in_groups = "";

                          $user = $this->EncodeForBackend(isset($one_user[mb_strtolower($this->GetLdapCnIdentifier(),'UTF-8')][0])?($one_user[mb_strtolower($this->GetLdapCnIdentifier(),'UTF-8')][0]):"");
                          $account = $this->EncodeForBackend(isset($one_user[mb_strtolower($this->GetLdapSyncedUserAttribute(),'UTF-8')][0])?($one_user[mb_strtolower($this->GetLdapSyncedUserAttribute(),'UTF-8')][0]):"");
                          if (!$this->IsCaseSensitiveUsers()) {
                              $user = mb_strtolower($user,'UTF-8');
                              $account = mb_strtolower($account,'UTF-8');
                          }

                          if ($account != "") {
                              
                              $one_user['msradiusframedipaddress'][0] = (isset($one_user['msradiusframedipaddress'][0])) ? long2ip32bit($one_user['msradiusframedipaddress'][0]) : "---";
                              $one_user['radiusframedipaddress'][0] = (isset($one_user['radiusframedipaddress'][0])) ? ($one_user['radiusframedipaddress'][0]) : "---";
                              $one_user['radiusframedipnetmask'][0] = (isset($one_user['radiusframedipnetmask'][0])) ? ($one_user['radiusframedipnetmask'][0]) : "---";

                              if (isset($one_user['useraccountcontrol'][0])) {
                                  if (0 != ($one_user['useraccountcontrol'][0] & 2)) {
                                      $accountdisable = TRUE;
                                  }
                              }
                              if (isset($one_user['ms-ds-user-account-control-computed'][0])) {
                                  if (0 != ($one_user['ms-ds-user-account-control-computed'][0] & 16)) {
                                      $accountdisable = TRUE;
                                  }
                              }
                              if (isset($one_user['accountexpires'][0])) {
                                  if (($one_user['accountexpires'][0] > 0) && ((($one_user['accountexpires'][0] / 10000000) - 11644473600) < time())) {
                                      $accountdisable = TRUE;
                                  }
                              }

                              if (isset($one_user['shadowexpire'][0])) {
                                  if (($one_user['shadowexpire'][0] >= 0) && ((86400 * $one_user['shadowexpire'][0]) < time())) {
                                      $accountdisable = TRUE;
                                  }
                              }
                              if (isset($one_user['sambaacctflags'][0])) {
                                  if ((FALSE !== mb_strpos($one_user['sambaacctflags'][0], "D")) || (FALSE !== mb_strpos($one_user['sambaacctflags'][0], "L"))) {
                                      $accountdisable = TRUE;
                                  }
                              }
                              if ($include_disabled || (!$accountdisable)) {
                                  // TODO $in_a_group discovery
                                  if ("" == nullable_trim($this->GetLdapInGroup(true))) {
                                      $in_a_group = TRUE;
                                  } else {
                                      $in_a_group = FALSE;
                                      foreach ($in_groups_array_raw as $one_group) {
                                          $in_groups_array[] = nullable_trim($one_group);
                                          $in_groups_lower_array[] = mb_strtolower(nullable_trim($one_group),'UTF-8');
                                      }


                                      // Enhanced Active Directory
                                      if (1 == $this->GetLdapServerType()) {
                                          $groups_array_raw = $ldap_connection->nice_names($one_user[$ldap_connection->_group_attribute]);
                                          // All groups are already defined
                                          /*
                                          $groups_array_raw=$ldap_connection->user_all_groups($one_user['distinguishedname'][0], $groups_filtering);
                                          foreach ($ldap_connection->nice_names($one_user[$ldap_connection->_group_attribute]) as $level_one_group) {
                                              $add_it = TRUE;
                                              foreach ($groups_array_raw as $one_temp) {
                                                  if (strpos($level_one_group, $one_temp) !== FALSE) {
                                                      $add_it = FALSE;
                                                  }
                                              }
                                              if ($add_it) {
                                                  $groups_array_raw[] = $level_one_group;
                                              }
                                          }
                                          */

                                          foreach ($groups_array_raw as $one_group) {
                                              $this_group = $this->EncodeForBackend($one_group);
                                              $groups_lower_array[] = mb_strtolower($this_group,'UTF-8');
                                          }
                                          
                                          foreach ($in_groups_array as $one_filtered_group) {
                                              if (in_array(mb_strtolower($one_filtered_group,'UTF-8'), $groups_lower_array)) {
                                                  $user_in_groups.= (("" != $user_in_groups) ? ',' : "") . $one_filtered_group;
                                                  $in_a_group = TRUE;
                                                  if ("" == $group) {
                                                      $group = $one_filtered_group;
                                                  }
                                              }
                                          }

                                      // Generic LDAP or eDirectory, eventually no memberOf function like in AD
                                      } elseif (2 == $this->GetLdapServerType()) {

                                          // Prepare the array "users_in_groups" if we are using a generic LDAP and an LdapInGroup Filtering
                                          foreach ($in_groups_array_raw as $one_group) {
                                              $temp_array = $ldap_connection->group_users($one_group);
                                              foreach ($temp_array as $one_temp) {
                                                  $one_user = $this->EncodeForBackend($one_temp);
                                                  if ($user == $one_user) {
                                                      $user_in_groups.= (("" != $user_in_groups) ? ',' : "") . $one_group;
                                                      $in_a_group = TRUE;
                                                  }
                                              }
                                          }

                                          if ($in_a_group) {
                                              $temp_array = explode(",", $user_in_groups);
                                              $group = $temp_array[0];
                                          }

                                      // eDirectory
                                      } elseif (4 == $this->GetLdapServerType()) {
                                          if (isset($one_user[$ldap_connection->_group_attribute])) {
                                            $groups_array_raw = $ldap_connection->nice_names($one_user[$ldap_connection->_group_attribute]);
                                          } else {
                                            $groups_array_raw = [];
                                          }
                                          if ($ldap_connection->_recursive_groups) {
                                              foreach ($groups_array_raw as $id => $group_name){
                                                  $extra_groups=$ldap_connection->recursive_groups($group_name, $this->IsLdapRecursiveCacheOnly());
                                                  if ("" != $ldap_connection->get_warning_message()) {
                                                      $this->WriteLog("Warning: LDAP warning: ".$ldap_connection->get_warning_message(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                                                  }
                                                  $groups_array_raw=array_merge($groups_array_raw,$extra_groups);
                                              }
                                          }

                                          foreach ($groups_array_raw as $one_group) {
                                              $this_group = $this->EncodeForBackend($one_group);
                                              $groups_lower_array[] = mb_strtolower($this_group,'UTF-8');
                                          }
                                          
                                          foreach ($in_groups_array as $one_filtered_group) {
                                              if (in_array(mb_strtolower($one_filtered_group,'UTF-8'), $groups_lower_array)) {
                                                  $user_in_groups.= (("" != $user_in_groups) ? ',' : "") . $one_filtered_group;
                                                  $in_a_group = TRUE;
                                                  if ("" == $group) {
                                                      $group = $one_filtered_group;
                                                  }
                                              }
                                          }

                                      // Legacy Active Directory
                                      } else {
                                          // $groups_array_raw = $ldap_connection->user_groups($user);
                                          $groups_array_raw=$ldap_connection->nice_names($one_user[$ldap_connection->_group_attribute]); //presuming the entry returned is our guy (unique usernames)
                                          if ($ldap_connection->_recursive_groups) {
                                              foreach ($groups_array_raw as $id => $group_name){
                                                  $extra_groups=$ldap_connection->recursive_groups($group_name, $this->IsLdapRecursiveCacheOnly());
                                                  if ("" != $ldap_connection->get_warning_message()) {
                                                      $this->WriteLog("Warning: LDAP warning: ".$ldap_connection->get_warning_message(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                                                  }
                                                  $groups_array_raw=array_merge($groups_array_raw,$extra_groups);
                                              }
                                          }

                                          foreach ($groups_array_raw as $one_group) {
                                              $this_group = $this->EncodeForBackend($one_group);
                                              $groups_lower_array[] = mb_strtolower($this_group,'UTF-8');
                                          }
                                          
                                          foreach ($in_groups_array as $one_filtered_group) {
                                              if (in_array(mb_strtolower($one_filtered_group,'UTF-8'), $groups_lower_array)) {
                                                  $user_in_groups.= (("" != $user_in_groups) ? ',' : "") . $one_filtered_group;
                                                  $in_a_group = TRUE;
                                                  if ("" == $group) {
                                                      $group = $one_filtered_group;
                                                  }
                                              }
                                          }
                                      }
                                  }
                                  
                                  if ($ignore_in_group || $in_a_group) {
                                    $users_list.= (("" != $users_list)?"\t":"").$account;
                                  }
                              }
                          } // if ($account != "")
                      } while (($one_user = $ldap_connection->one_user_info(FALSE, NULL, NULL, FALSE, $groups_filtering, $in_groups_filtering))); // $group_cn_cache_only = TRUE before
                      // Loop of LDAP parsing and synchronization

                      if ($ldap_connection->_paging_legacy) {
                        if (FALSE !== $ldap_connection->_oui_paged_sr) {
                          ldap_control_paged_result_response($ldap_connection->_conn_paged, $ldap_connection->_oui_paged_sr, $ldap_connection->_paging_cookie);
                        }
                      }
                  } while (($ldap_connection->_paging_cookie !== null) && ($ldap_connection->_paging_cookie != "") && ($ldap_connection->_oui_paged_sr !== FALSE));
                  // LDAP pagination loop

                  if ($ldap_connection->_paging_legacy) {
                    // Reset LDAP paged result
                    ldap_control_paged_result($ldap_connection->_conn_paged, $ldap_connection->_paging_size, false);
                  }
              }
          }
      } else {
          $this->WriteLog("Error: No LDAP connection information", FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
      }
      return $users_list;
  }


  function GetLdapUsersInfoArray(
      $user_filter = "*",
      $include_disabled = TRUE,
      $ignore_in_group = FALSE
  ) {
      $result_array = [];

      if (1 == $this->GetLdapServerType()) {
          $recursive_prefix = "1.2.840.113556.1.4.1941:=";
      } else {
          $recursive_prefix = "";
      }
      
      // Prepare the array "users_in_groups"
      // - if we are using a generic LDAP and an LdapInGroup Filtering
      // - if we are using enhanced Active Directory
      if ("" != nullable_trim($this->GetLdapInGroup(true))) {
        $in_groups_array_raw = explode("\t",nullable_trim(str_replace(",","\t",str_replace(";","\t",$this->GetLdapInGroup(true)))));
      } else {
        $in_groups_array_raw = [];
      }

      if (!function_exists('ldap_connect')) {
          $this->WriteLog("Error: LDAP library not installed", FALSE, FALSE, _MULTIOTP_OPERATION_CANCELLED_, 'System', "", 3);
          $this->EnableLdapError();
      } elseif (("" != $this->GetLdapDomainControllers()) && ("" != $this->GetLdapBindDn()) && ("" != $this->GetLdapServerPassword())) {

          $this->DisableLdapError();
          $in_groups_array = [];

          // TODO: later, we could loop in several base-dn (semicolon separated)
          $domain_controllers = explode(" ",nullable_trim(str_replace(","," ",str_replace(";"," ",$this->GetLdapDomainControllers()))));
          
          $users_dn_array_raw = explode("\t",nullable_trim(str_replace(";","\t",$this->GetLdapUsersDn())));
          
          $ldap_options = array('account_suffix'     => $this->GetLdapAccountSuffix(),
                                'ad_password'        => $this->GetLdapServerPassword(),
                                'ad_username'        => $this->GetLdapBindDn(),
                                'base_dn'            => $this->GetLdapBaseDn(),
                                'cn_identifier'      => $this->GetLdapCnIdentifier(),
                                'domain_controllers' => $domain_controllers,
                                'group_attribute'    => $this->GetLdapGroupAttribute(),
                                'group_cn_identifier'=> $this->GetLdapGroupCnIdentifier(),
                                'ldap_server_type'   => $this->GetLdapServerType(),
                                'ldap_filter'        => $this->GetLdapFilter(),
                                'network_timeout'    => $this->GetLdapNetworkTimeout(),
                                'port'               => $this->GetLdapPort(),
                                'recursive_groups'   => $this->IsLdapRecursiveGroups(),
                                'time_limit'         => $this->GetLdapTimeLimit(),
                                'use_ssl'            => $this->IsLdapSsl(),
                                'cache_support'      => $this->IsLdapCacheOn(),
                                'cache_folder'       => $this->GetLdapCacheFolder(),
                                'users_dn'           => $users_dn_array_raw[0]
                               );

          $ldap_connection = new MultiotpAdLdap($ldap_options);

          if ($ldap_connection->IsError()) {
              $this->WriteLog("Error: ".$ldap_connection->ErrorMessage(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
              $this->EnableLdapError();
          } else {
              // We continue only if there is no error

              $in_groups_filtering = [];
              $groups_filtering = "";
              if (1 == count($in_groups_array_raw)) {
                $group_info = $ldap_connection->group_info($in_groups_array_raw[0],array('distinguishedname'));
                if (isset($group_info[0]['distinguishedname'][0])) {
                    $group_info_dn = $group_info[0]['distinguishedname'][0];
                } else {
                    $group_info_dn = $in_groups_array_raw[0];
                }
                $groups_filtering.= "(".$this->GetLdapGroupAttribute().":";
                $groups_filtering.= $recursive_prefix.$group_info_dn.")";
                $in_groups_filtering[] = array('name' => $in_groups_array_raw[0], 'distinguishedname' => "(".$this->GetLdapGroupAttribute().":".$recursive_prefix.$group_info_dn.")");
              } elseif (count($in_groups_array_raw) > 1) {
                $groups_filtering = "(|";
                foreach ($in_groups_array_raw as $one_group) {
                  $group_info = $ldap_connection->group_info($one_group,array('distinguishedname'));
                  if (isset($group_info[0]['distinguishedname'][0])) {
                      $group_info_dn = $group_info[0]['distinguishedname'][0];
                  } else {
                      $group_info_dn = $in_groups_array_raw[0];
                  }
                  $groups_filtering.= "(".$this->GetLdapGroupAttribute().":";
                  $groups_filtering.= $recursive_prefix.$group_info_dn.")";
                  $in_groups_filtering[] = array('name' => $one_group, 'distinguishedname' => "(".$this->GetLdapGroupAttribute().":".$recursive_prefix.$group_info_dn.")");
                }
                $groups_filtering.= ")";
              } else {
                  // No group filtering, we don't need _real_primarygroup
                  $ldap_connection->_real_primarygroup = FALSE;
              }

              // Put all group_cn in cache
              // (don't do that for Enhanced Microsoft Active Directory server)
              if (1 != $this->GetLdapServerType()) {
                  $ldap_connection->group_cn(1, FALSE, TRUE);

                  // Put all recursive_groups in cache
                  if ($ldap_connection->_recursive_groups) {
                      $all_groups = $ldap_connection->all_groups(FALSE, '*', TRUE, TRUE);
                      foreach ($all_groups as $key => $one_group) {
                          $ldap_connection->recursive_groups($one_group);
                      }
                  }
              }

              $ldap_connection->_paging_cookie = "";

              $users_in_groups = [];

              if ("" != nullable_trim($this->GetLdapInGroup(true))) {
                  if (2 == $this->GetLdapServerType()) { // Generic LDAP, eventually no memberOf function like in AD
                      foreach ($in_groups_array_raw as $one_group) {
                          $temp_array = $ldap_connection->group_users($one_group);
                          foreach ($temp_array as $one_temp) {
                              $one_user = $this->EncodeForBackend($one_temp);
                              if (!isset($users_in_groups[$one_user])) {
                                  $users_in_groups[$one_user] = $one_group;
                              } else {
                                  $users_in_groups[$one_user] = $users_in_groups[$one_user].",".$one_group;
                              }
                          }
                      }
                  }
              }

              $users_dn_array_raw = explode("\t",nullable_trim(str_replace(";","\t",$this->GetLdapUsersDn())));
              foreach ($users_dn_array_raw as $one_user_dn) {
                  $ldap_connection->set_users_dn($one_user_dn);
                  do { // LDAP pagination loop
                      if ($ldap_connection->_paging_legacy) {
                        ldap_control_paged_result($ldap_connection->_conn_paged, $ldap_connection->_paging_size, false, $ldap_connection->_paging_cookie);
                      }

                      $one_user = $ldap_connection->one_user_info(true,
                                                                  $user_filter,
                                                                  $this->GetLdapFieldsArray(),
                                                                  $this->IsLdapCacheOn(),
                                                                  $groups_filtering,
                                                                  $in_groups_filtering
                                                                 );

                      if ($ldap_connection->IsError()) {
                          $this->EnableLdapError();
                          $this->WriteLog("Error: LDAP connection failed", false, false, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                          return FALSE;
                      }
                      if ("" != $ldap_connection->get_warning_message()) {
                          $this->WriteLog("Warning: LDAP warning: ".$ldap_connection->get_warning_message(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                      }
                      if ($this->GetVerboseFlag() && ("" != $ldap_connection->get_debug_message())) {
                          $this->WriteLog("Debug: *LDAP debug: ".$ldap_connection->get_debug_message(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                      }

                      do {
                          $accountdisable = FALSE;
                          $groups_lower_array = [];
                          $in_groups_array = [];
                          $in_groups_lower_array = [];
                          $group = "";
                          $user_in_groups = "";

                          $user = $this->EncodeForBackend(isset($one_user[mb_strtolower($this->GetLdapCnIdentifier(),'UTF-8')][0])?($one_user[mb_strtolower($this->GetLdapCnIdentifier(),'UTF-8')][0]):"");
                          $account = $this->EncodeForBackend(isset($one_user[mb_strtolower($this->GetLdapSyncedUserAttribute(),'UTF-8')][0])?($one_user[mb_strtolower($this->GetLdapSyncedUserAttribute(),'UTF-8')][0]):"");
                          if (!$this->IsCaseSensitiveUsers()) {
                              $user = mb_strtolower($user,'UTF-8');
                              $account = mb_strtolower($account,'UTF-8');
                          }

                          if ($account != "") {
                              
                              $one_user['msradiusframedipaddress'][0] = (isset($one_user['msradiusframedipaddress'][0])) ? long2ip32bit($one_user['msradiusframedipaddress'][0]) : "---";
                              $one_user['radiusframedipaddress'][0] = (isset($one_user['radiusframedipaddress'][0])) ? ($one_user['radiusframedipaddress'][0]) : "---";
                              $one_user['radiusframedipnetmask'][0] = (isset($one_user['radiusframedipnetmask'][0])) ? ($one_user['radiusframedipnetmask'][0]) : "---";

                              if (isset($one_user['useraccountcontrol'][0])) {
                                  if (0 != ($one_user['useraccountcontrol'][0] & 2)) {
                                      $accountdisable = TRUE;
                                  }
                              }
                              if (isset($one_user['ms-ds-user-account-control-computed'][0])) {
                                  if (0 != ($one_user['ms-ds-user-account-control-computed'][0] & 16)) {
                                      $accountdisable = TRUE;
                                  }
                              }
                              if (isset($one_user['accountexpires'][0])) {
                                  if (($one_user['accountexpires'][0] > 0) && ((($one_user['accountexpires'][0] / 10000000) - 11644473600) < time())) {
                                      $accountdisable = TRUE;
                                  }
                              }

                              if (isset($one_user['shadowexpire'][0])) {
                                  if (($one_user['shadowexpire'][0] >= 0) && ((86400 * $one_user['shadowexpire'][0]) < time())) {
                                      $accountdisable = TRUE;
                                  }
                              }
                              if (isset($one_user['sambaacctflags'][0])) {
                                  if ((FALSE !== mb_strpos($one_user['sambaacctflags'][0], "D")) || (FALSE !== mb_strpos($one_user['sambaacctflags'][0], "L"))) {
                                      $accountdisable = TRUE;
                                  }
                              }
                              if ($include_disabled || (!$accountdisable)) {
                                  // TODO $in_a_group discovery
                                  if ("" == nullable_trim($this->GetLdapInGroup(true))) {
                                      $in_a_group = TRUE;
                                  } else {
                                      $in_a_group = FALSE;
                                      foreach ($in_groups_array_raw as $one_group) {
                                          $in_groups_array[] = nullable_trim($one_group);
                                          $in_groups_lower_array[] = mb_strtolower(nullable_trim($one_group),'UTF-8');
                                      }


                                      // Enhanced Active Directory
                                      if (1 == $this->GetLdapServerType()) {
                                          $groups_array_raw = $ldap_connection->nice_names($one_user[$ldap_connection->_group_attribute]);
                                          // All groups are already defined
                                          /*
                                          $groups_array_raw=$ldap_connection->user_all_groups($one_user['distinguishedname'][0], $groups_filtering);
                                          foreach ($ldap_connection->nice_names($one_user[$ldap_connection->_group_attribute]) as $level_one_group) {
                                              $add_it = TRUE;
                                              foreach ($groups_array_raw as $one_temp) {
                                                  if (strpos($level_one_group, $one_temp) !== FALSE) {
                                                      $add_it = FALSE;
                                                  }
                                              }
                                              if ($add_it) {
                                                  $groups_array_raw[] = $level_one_group;
                                              }
                                          }
                                          */

                                          foreach ($groups_array_raw as $one_group) {
                                              $this_group = $this->EncodeForBackend($one_group);
                                              $groups_lower_array[] = mb_strtolower($this_group,'UTF-8');
                                          }
                                          
                                          foreach ($in_groups_array as $one_filtered_group) {
                                              if (in_array(mb_strtolower($one_filtered_group,'UTF-8'), $groups_lower_array)) {
                                                  $user_in_groups.= (("" != $user_in_groups) ? ',' : "") . $one_filtered_group;
                                                  $in_a_group = TRUE;
                                                  if ("" == $group) {
                                                      $group = $one_filtered_group;
                                                  }
                                              }
                                          }
                                          
                                      // Generic LDAP, eventually no memberOf function like in AD
                                      } elseif (2 == $this->GetLdapServerType()) {

                                          // Prepare the array "users_in_groups" if we are using a generic LDAP and an LdapInGroup Filtering
                                          foreach ($in_groups_array_raw as $one_group) {
                                              $temp_array = $ldap_connection->group_users($one_group);
                                              foreach ($temp_array as $one_temp) {
                                                  $one_user = $this->EncodeForBackend($one_temp);
                                                  if ($user == $one_user) {
                                                      $user_in_groups.= (("" != $user_in_groups) ? ',' : "") . $one_group;
                                                      $in_a_group = TRUE;
                                                  }
                                              }
                                          }

                                          if ($in_a_group) {
                                              $temp_array = explode(",", $user_in_groups);
                                              $group = $temp_array[0];
                                          }

                                      // eDirectory
                                      } elseif (4 == $this->GetLdapServerType()) {
                                          if (isset($one_user[$ldap_connection->_group_attribute])) {
                                            $groups_array_raw = $ldap_connection->nice_names($one_user[$ldap_connection->_group_attribute]);
                                          } else {
                                            $groups_array_raw = [];
                                          }
                                          if ($ldap_connection->_recursive_groups) {
                                              foreach ($groups_array_raw as $id => $group_name){
                                                  $extra_groups=$ldap_connection->recursive_groups($group_name, $this->IsLdapRecursiveCacheOnly());
                                                  if ("" != $ldap_connection->get_warning_message()) {
                                                      $this->WriteLog("Warning: LDAP warning: ".$ldap_connection->get_warning_message(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                                                  }
                                                  $groups_array_raw=array_merge($groups_array_raw,$extra_groups);
                                              }
                                          }

                                          foreach ($groups_array_raw as $one_group) {
                                              $this_group = $this->EncodeForBackend($one_group);
                                              $groups_lower_array[] = mb_strtolower($this_group,'UTF-8');
                                          }
                                          
                                          foreach ($in_groups_array as $one_filtered_group) {
                                              if (in_array(mb_strtolower($one_filtered_group,'UTF-8'), $groups_lower_array)) {
                                                  $user_in_groups.= (("" != $user_in_groups) ? ',' : "") . $one_filtered_group;
                                                  $in_a_group = TRUE;
                                                  if ("" == $group) {
                                                      $group = $one_filtered_group;
                                                  }
                                              }
                                          }

                                      // Legacy Active Directory
                                      } else {
                                          // $groups_array_raw = $ldap_connection->user_groups($user);
                                          $groups_array_raw=$ldap_connection->nice_names($one_user[$ldap_connection->_group_attribute]); //presuming the entry returned is our guy (unique usernames)
                                          if ($ldap_connection->_recursive_groups) {
                                              foreach ($groups_array_raw as $id => $group_name){
                                                  $extra_groups=$ldap_connection->recursive_groups($group_name, $this->IsLdapRecursiveCacheOnly());
                                                  if ("" != $ldap_connection->get_warning_message()) {
                                                      $this->WriteLog("Warning: LDAP warning: ".$ldap_connection->get_warning_message(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                                                  }
                                                  $groups_array_raw=array_merge($groups_array_raw,$extra_groups);
                                              }
                                          }

                                          foreach ($groups_array_raw as $one_group) {
                                              $this_group = $this->EncodeForBackend($one_group);
                                              $groups_lower_array[] = mb_strtolower($this_group,'UTF-8');
                                          }
                                          
                                          foreach ($in_groups_array as $one_filtered_group) {
                                              if (in_array(mb_strtolower($one_filtered_group,'UTF-8'), $groups_lower_array)) {
                                                  $user_in_groups.= (("" != $user_in_groups) ? ',' : "") . $one_filtered_group;
                                                  $in_a_group = TRUE;
                                                  if ("" == $group) {
                                                      $group = $one_filtered_group;
                                                  }
                                              }
                                          }
                                      }
                                  }

                                  if ($ignore_in_group || $in_a_group) {
                                      $description = "";
                                      if (isset($one_user['description'][0])) {
                                          $description = nullable_trim($one_user['description'][0]);
                                      }
                                      if (("" == $description) && (isset($one_user['gecos'][0]))) {
                                          $description = nullable_trim($one_user['gecos'][0]);
                                      }
                                      if (("" == $description) && (isset($one_user['displayname'][0]))) {
                                          $description = nullable_trim($one_user['displayname'][0]);
                                      }
                                      if ("" == $description) {
                                          $description = $account;
                                      }
                                      $display_name = "";
                                      if (isset($one_user['displayname'][0])) {
                                          $display_name = nullable_trim($one_user['displayname'][0]);
                                      }

                                      $result_array[$user]['user'] = $user;
                                      $result_array[$user]['groups'] = $user_in_groups;
                                      $result_array[$user]['accountdisable'] = $accountdisable;
                                      $result_array[$user]['mail'] = (isset($one_user['mail'][0]) ? $this->EncodeForBackend($one_user['mail'][0]) : "");
                                      $result_array[$user]['description'] = (isset($one_user['description'][0]) ? $this->EncodeForBackend($one_user['description'][0]) : "");
                                      $result_array[$user]['displayname'] = (isset($one_user['displayname'][0]) ? $this->EncodeForBackend($one_user['displayname'][0]) : "");
                                      $result_array[$user]['mobile'] = (isset($one_user['mobile'][0]) ? $this->EncodeForBackend($one_user['mobile'][0]) : "");
                                      $result_array[$user]['msnpallowdialin'] = ("TRUE" == (isset($one_user['msnpallowdialin'][0]) ? ($one_user['msnpallowdialin'][0]) : "FALSE"));
                                      if ("---" != $one_user['msradiusframedipaddress'][0]) {
                                          $result_array[$user]['msradiusframedipaddress'] = $one_user['msradiusframedipaddress'][0];
                                      }
                                      if ("---" != $one_user['radiusframedipaddress'][0]) {
                                          $result_array[$user]['radiusframedipaddress'] = $one_user['radiusframedipaddress'][0];
                                      }
                                      if ("---" != $one_user['radiusframedipnetmask'][0]) {
                                          $result_array[$user]['radiusframedipnetmask'] = $one_user['radiusframedipnetmask'][0];
                                      }
                                      $result_array[$user]['synchronized_dn'] = (isset($one_user['distinguishedname'][0]) ? $this->EncodeForBackend($one_user['distinguishedname'][0]) : "");
                                      $result_array[$user]['language'] = (isset($one_user[mb_strtolower($this->GetLdapLanguageAttribute(),'UTF-8')][0]) ? $this->EncodeForBackend($one_user[mb_strtolower($this->GetLdapLanguageAttribute(),'UTF-8')][0]) : "");
                                      $result_array[$user]['account'] = $account;
                                  }
                              }
                          } // if ($account != "")
                      } while (($one_user = $ldap_connection->one_user_info(FALSE, NULL, NULL, FALSE, $groups_filtering, $in_groups_filtering))); // $group_cn_cache_only = TRUE before
                      // Loop of LDAP parsing and synchronization

                      if ($ldap_connection->_paging_legacy) {
                        if (FALSE !== $ldap_connection->_oui_paged_sr) {
                          ldap_control_paged_result_response($ldap_connection->_conn_paged, $ldap_connection->_oui_paged_sr, $ldap_connection->_paging_cookie);
                        }
                      }
                  } while (($ldap_connection->_paging_cookie !== null) && ($ldap_connection->_paging_cookie != "") && ($ldap_connection->_oui_paged_sr !== FALSE));
                  // LDAP pagination loop

                  if ($ldap_connection->_paging_legacy) {
                    // Reset LDAP paged result
                    ldap_control_paged_result($ldap_connection->_conn_paged, $ldap_connection->_paging_size, false);
                  }
              }
          }
      } else {
          $this->WriteLog("Error: No LDAP connection information", FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
      }

      return $result_array;
  }


  function GetUserInfo(
    $user
  ) {
    $crlf = "\n";
    $result = "";
    if ($this->SetUser($user)) {
      $result.= "   Information for user: ".$user.$crlf;
      $result.= "                 Locked: ".((1 == $this->GetUserLocked()) ? 'yes' : 'no').$crlf;
      $result.= "                Delayed: ".((1 == $this->GetUserDelayed()) ? 'yes' : 'no').$crlf;
      if (1 == $this->GetUserDelayed()) {
        $result.= "           Delayed time: ".$this->GetUserDelayedTime()."seconds".$crlf;
      }
      $result.= "              Activated: ".((1 == $this->GetUserActivated()) ? 'yes' : 'no').$crlf;
      $result.= "   AD/LDAP synchronized: ".((1 == $this->GetUserSynchronized()) ? 'yes' : 'no').$crlf;
      $result.= "      Prefix pin needed: ".(($this->IsUserPrefixPin()) ? 'yes' : 'no').$crlf;
      $result.= "            Description: ".$this->GetUserDescription().$crlf;
      $result.= "            DisplayName: ".$this->GetUserDisplayName().$crlf;
      $result.= "                  Email: ".$this->GetUserEmail().$crlf;
      $result.= "           Mobile phone: ".$this->GetUserSms().$crlf;
      $result.= "                  Group: ".$this->GetUserGroup().$crlf;
      if ("" != $this->GetUserLanguage(TRUE)) {
        $result.= "               Language: ".$this->GetUserLanguage(TRUE).$crlf;
      } else {
        $result.= "   Language (inherited): ".$this->GetLanguage().$crlf;
      }
      if ("" != $this->GetUserKeyId()) {
        $result.= "               Token id: ".$this->GetUserKeyId().$crlf;
      }
      if ("" != $this->GetUserTokenSerialNumber()) {
        $result.= "    Token serial number: ".$this->GetUserTokenSerialNumber().$crlf;
      }
      $result.= "              Algorithm: ".$this->GetUserAlgorithm().$crlf;
      $result.= "             OTP digits: ".$this->GetUserTokenNumberOfDigits().$crlf;
      if (('hotp' == $this->GetUserAlgorithm()) || ('yubicootp' == $this->GetUserAlgorithm())) {
        $result.= "    Next token position: ".($this->GetUserTokenLastEvent()+1).$crlf;
      } elseif (('totp' == $this->GetUserAlgorithm()) || ('motp' == $this->GetUserAlgorithm())) {
        $result.= "         Token timestep: ".$this->GetUserTokenTimeInterval().$crlf;
      }
      if ($this->IsUserPushEnabled()) {
        $result.= "           Push User ID: ". $this->GetUserMouid().$crlf;
        if ($this->GetUserPushEnableDate() > 0) {
          $result.= "         Push Server ID: ". $this->GetUserMosid().$crlf;
          $result.= "       Push enable date: ". date("Y-m-d", $this->GetUserPushEnableDate()).$crlf;
        }
      }
      if (is_valid_ipv4($this->GetUserDialinIpAddress())) {
        $result.= "     Dial-In IP address: ".$this->GetUserDialinIpAddress().$crlf;
      }
      if (is_valid_ipv4($this->GetUserDialinIpMask())) {
        $result.= "        Dial-In IP mask: ".$this->GetUserDialinIpMask().$crlf;
      } elseif (is_valid_ipv4($this->GetDefaultDialinIpMask())) {
        $result.= "Default Dial-In IP mask: ".$this->GetDefaultDialinIpMask().$crlf;
      }
    }
    return $result;
  }


  function TestLdapUser(
      $value
  ) {
      $result = FALSE;
      $user_to_check = ($this->IsCaseSensitiveUsers()?$value:mb_strtolower($value,'UTF-8'));
      $ldap_users_array = $this->GetLdapUsersInfoArray();
      if (!$this->IsLdapError()) {
          foreach ($ldap_users_array as $one_ldap_user) {
              // $user = $one_ldap_user['user'];
              // $user = ($this->IsCaseSensitiveUsers()?$user:mb_strtolower($user,'UTF-8'));
              $account = $one_ldap_user['account'];
              $account = ($this->IsCaseSensitiveUsers()?$user:mb_strtolower($account,'UTF-8'));
              if ($user_to_check == $account) {
                  $result = TRUE;
                  break;
              }
          }
      } elseif ($this->GetVerboseFlag()) {
          $this->WriteLog("Debug: *AD/LDAP error before testing the $value account", FALSE, FALSE, 8888, 'Debug', "");
      }
      return $result;
  }


  /**
   * @brief   Synchronize AD/LDAP users
   *
   * @param   string  $user_filter           User name filter (* by default)
   * @param   boolean $include_disabled      Disabled users will also be synced
   * @param   boolean $ignore_in_group       Don't check if the users are in the selected groups or not
   * @param   boolean $state_info_interval   Number of seconds before a new state is given in the lock file, added 2016-11-22
   *
   * @return  boolean                        Function has been successfully called
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.8.1.0
   * @date    2021-02-13
   * @since   2020-09-20 (delete inexistent AD/LDAP users if sync delete retention days option is used)
   * @since   2018-07-10 (new redesign starting with version 5.2.0.0)
   * @since   2014-11-04 (completely redesigned)
   */
  function SyncLdapUsers(
      $user_filter = "*",
      $include_disabled = TRUE,
      $ignore_in_group = FALSE,
      $state_info_interval = 60,
      $delete_old_synced_users = TRUE
  ) {
      $result = FALSE;
      
      if (1 == $this->GetLdapServerType()) {
          $recursive_prefix = "1.2.840.113556.1.4.1941:=";
      } else {
          $recursive_prefix = "";
      }
      
      
      // Prepare the array "users_in_groups"
      // - if we are using a generic LDAP and an LdapInGroup Filtering
      // - if we are using enhanced Active Directory
      if ("" != nullable_trim($this->GetLdapInGroup(true))) {
        $in_groups_array_raw = explode("\t",nullable_trim(str_replace(",","\t",str_replace(";","\t",$this->GetLdapInGroup(true)))));
      } else {
        $in_groups_array_raw = [];
      }

      $ldap_sync_stop = FALSE;
      $ldap_sync_file_lock = $this->GetLockFolder().$this->GetLdapSyncLockFileName();
      $ldap_sync_file_stop = $this->GetLockFolder().$this->GetLdapSyncStopFileName();

      $info_interval = intval($state_info_interval);
      if ($info_interval < 1) {
        $info_interval = 60;
      }

      clearstatcache();
      
      if (!function_exists('ldap_connect')) {
        $this->WriteLog("Error: LDAP library not installed", FALSE, FALSE, _MULTIOTP_OPERATION_CANCELLED_, 'System', "", 3);
        $this->EnableLdapError();
      } elseif (("" != $this->GetLdapDomainControllers()) && ("" != $this->GetLdapBindDn()) && ("" != $this->GetLdapServerPassword())) {

          // Check if an other sync process is already active yet
          if (file_exists($ldap_sync_file_lock)) {
              // If the process is running more than a specific amount of time
              //   without any update, something is probably wrong and we continue!
              if ((filemtime($ldap_sync_file_lock) + $this->GetLockTime()) >= time()) {
                  $additional_info = "";
                  // Take some info from the sync process
                  $additional_info = nullable_trim(@file_get_contents($ldap_sync_file_lock));
                  $this->WriteLog("Info: Previous AD/LDAP synchronization in progress... $additional_info", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'LDAP', "");
                  return TRUE;
              }
          }

          // As the method is just called now, we remove the stop file if any
          // (the stop file is used to cancel the process during the loop)
          if (file_exists($ldap_sync_file_stop)) {
              unlink($ldap_sync_file_stop);
          }

          $start_sync_time = time();

          $last_touch = time();
          if ($lock_handle = @fopen($ldap_sync_file_lock, "ct")) { // was wt, need ftruncate after flock now
              flock($lock_handle, LOCK_EX);
              ftruncate($lock_handle, 0);
              $additional_info = "started at ".date("H:i:s", $start_sync_time);
              if ($this->GetVerboseFlag()) {
                  $additional_info.= " / Memory used: ".(intval(10*memory_get_usage()/(1024*1024))/10)."MB / Peak: ".(intval(10*memory_get_peak_usage()/(1024*1024))/10)."MB";
                  $this->WriteLog("Debug: *AD/LDAP synchronization ".$additional_info, FALSE, FALSE, 8888, 'LDAP', "");
              }
              fwrite($lock_handle,$additional_info);
              fclose($lock_handle);
              if ("" != $this->GetLinuxFileMode()) {
                  @chmod($ldap_sync_file_lock, octdec($this->GetLinuxFileMode()));
              }
          } 
      
          $this->DisableLdapError();
          $in_groups_array = [];

          $this->WriteLog("Info: AD/LDAP synchronization started", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'LDAP', "");

          // TODO: later, we could loop in several base-dn (semicolon separated)
          $domain_controllers = explode(" ",nullable_trim(str_replace(","," ",str_replace(";"," ",$this->GetLdapDomainControllers()))));

          $users_dn_array_raw = explode("\t",nullable_trim(str_replace(";","\t",$this->GetLdapUsersDn())));

          $ldap_options = array('account_suffix'     => $this->GetLdapAccountSuffix(),
                                'ad_password'        => $this->GetLdapServerPassword(),
                                'ad_username'        => $this->GetLdapBindDn(),
                                'base_dn'            => $this->GetLdapBaseDn(),
                                'cn_identifier'      => $this->GetLdapCnIdentifier(),
                                'domain_controllers' => $domain_controllers,
                                'group_attribute'    => $this->GetLdapGroupAttribute(),
                                'group_cn_identifier'=> $this->GetLdapGroupCnIdentifier(),
                                'ldap_server_type'   => $this->GetLdapServerType(),
                                'ldap_filter'        => $this->GetLdapFilter(),
                                'network_timeout'    => $this->GetLdapNetworkTimeout(),
                                'port'               => $this->GetLdapPort(),
                                'recursive_groups'   => $this->IsLdapRecursiveGroups(),
                                'time_limit'         => $this->GetLdapTimeLimit(),
                                'use_ssl'            => $this->IsLdapSsl(),
                                'cache_support'      => $this->IsLdapCacheOn(),
                                'cache_folder'       => $this->GetLdapCacheFolder(),
                                'users_dn'           => $users_dn_array_raw[0]
                               );

          $ldap_connection = new MultiotpAdLdap($ldap_options);

          if ($ldap_connection->IsError()) {
              $this->WriteLog("Error: ".$ldap_connection->ErrorMessage(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
              $this->EnableLdapError();
          } else {
              // We continue only if there is no error

              $in_groups_filtering = [];
              $groups_filtering = "";
              if (1 == count($in_groups_array_raw)) {
                $group_info = $ldap_connection->group_info($in_groups_array_raw[0],array('distinguishedname'));
                if (isset($group_info[0]['distinguishedname'][0])) {
                    $group_info_dn = $group_info[0]['distinguishedname'][0];
                } else {
                    $group_info_dn = $in_groups_array_raw[0];
                }
                $groups_filtering.= "(".$this->GetLdapGroupAttribute().":";
                $groups_filtering.= $recursive_prefix.$group_info_dn.")";
                $in_groups_filtering[] = array('name' => $in_groups_array_raw[0], 'distinguishedname' => "(".$this->GetLdapGroupAttribute().":".$recursive_prefix.$group_info_dn.")");
              } elseif (count($in_groups_array_raw) > 1) {
                $groups_filtering = "(|";
                foreach ($in_groups_array_raw as $one_group) {
                  $group_info = $ldap_connection->group_info($one_group,array('distinguishedname'));
                  if (isset($group_info[0]['distinguishedname'][0])) {
                      $group_info_dn = $group_info[0]['distinguishedname'][0];
                  } else {
                      $group_info_dn = $in_groups_array_raw[0];
                  }
                  $groups_filtering.= "(".$this->GetLdapGroupAttribute().":";
                  $groups_filtering.= $recursive_prefix.$group_info_dn.")";
                  $in_groups_filtering[] = array('name' => $one_group, 'distinguishedname' => "(".$this->GetLdapGroupAttribute().":".$recursive_prefix.$group_info_dn.")");
                }
                $groups_filtering.= ")";
              } else {
                  // No group filtering, we don't need _real_primarygroup
                  $ldap_connection->_real_primarygroup = FALSE;
              }

              // Put all group_cn in cache
              // (don't do that for Enhanced Microsoft Active Directory server)
              if (1 != $this->GetLdapServerType()) {
                  $ldap_connection->group_cn(1, FALSE, TRUE);

                  // Put all recursive_groups in cache
                  if ($ldap_connection->_recursive_groups) {
                      $all_groups = $ldap_connection->all_groups(FALSE, '*', TRUE, TRUE);
                      foreach ($all_groups as $key => $one_group) {
                          $ldap_connection->recursive_groups($one_group);
                      }
                  }
              }

              $ldap_created_counter = 0;
              $modified_counter = 0;
              $ldap_total_counter = 0;
              $existing_ldap_users_counter = 0;

              $result = TRUE;

              $ldap_connection->_paging_cookie = "";

              $users_in_groups = [];

              if ("" != nullable_trim($this->GetLdapInGroup(true))) {
                if ((2 == $this->GetLdapServerType()) || (4 == $this->GetLdapServerType())) { // Generic LDAP or eDirectory, eventually no memberOf function like in AD
              
                  foreach ($in_groups_array_raw as $one_group) {
                    $temp_array = $ldap_connection->group_users($one_group);
                    foreach ($temp_array as $one_temp) {
                      $one_user = $this->EncodeForBackend($one_temp);
                      if (!isset($users_in_groups[$one_user])) {
                        $users_in_groups[$one_user] = $one_group;
                      } else {
                        $users_in_groups[$one_user] = $users_in_groups[$one_user].",".$one_group;
                      }
                    }
                  }
                }
              }

              $users_dn_array_raw = explode("\t",nullable_trim(str_replace(";","\t",$this->GetLdapUsersDn())));
              foreach ($users_dn_array_raw as $one_user_dn) {

                  $ldap_connection->set_users_dn($one_user_dn);
                  do { // LDAP pagination loop
                      if ($ldap_connection->_paging_legacy) {
                        ldap_control_paged_result($ldap_connection->_conn_paged, $ldap_connection->_paging_size, false, $ldap_connection->_paging_cookie);
                      }

                      $one_user = $ldap_connection->one_user_info(true,
                                                                  $user_filter,
                                                                  $this->GetLdapFieldsArray(),
                                                                  $this->IsLdapCacheOn(),
                                                                  $groups_filtering,
                                                                  $in_groups_filtering
                                                                 );

                      if ($ldap_connection->IsError()) {
                          $this->EnableLdapError();
                          $this->WriteLog("Error: LDAP connection failed", false, false, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                          if (file_exists($ldap_sync_file_lock)) {
                              unlink($ldap_sync_file_lock);
                          }
                          return FALSE;
                      }
                      if ("" != $ldap_connection->get_warning_message()) {
                          $this->WriteLog("Warning: LDAP warning: ".$ldap_connection->get_warning_message(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                      }
                      if ($this->GetVerboseFlag() && ("" != $ldap_connection->get_debug_message())) {
                          $this->WriteLog("Debug: *DEBUG warning: ".$ldap_connection->get_debug_message(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                      }

                      do {
                          // We check also if we have to stop
                          if (file_exists($ldap_sync_file_stop)) {
                            unlink($ldap_sync_file_stop);
                            $ldap_sync_stop = TRUE;
                          }
                          // We also touch the sync lock every half of the lock time limit, or at each info_interval
                          if ((($last_touch + ($this->GetLockTime() / 2)) <= time()) || (($last_touch + $info_interval) <= time())) {
                              $last_touch = time();
                              if ($lock_handle = @fopen($ldap_sync_file_lock, "ct")) { // was wt, need ftruncate after flock now
                                  flock($lock_handle, LOCK_EX);
                                  ftruncate($lock_handle, 0);
                                  $additional_info = "started at ".date("H:i:s", $start_sync_time);
                                  $additional_info.= ", LDAP account #".($ldap_total_counter+1)." at ".date("H:i:s");
                                  if ($this->GetVerboseFlag()) {
                                      $additional_info.= " / Memory used: ".(intval(10*memory_get_usage()/(1024*1024))/10)."MB / Peak: ".(intval(10*memory_get_peak_usage()/(1024*1024))/10)."MB";
                                      $this->WriteLog("Debug: *AD/LDAP synchronization ".$additional_info, FALSE, FALSE, 8888, 'LDAP', "");
                                      // print_r($one_user);
                                  }
                                  fwrite($lock_handle,$additional_info);
                                  fclose($lock_handle);
                                  if ("" != $this->GetLinuxFileMode()) {
                                      @chmod($ldap_sync_file_lock, octdec($this->GetLinuxFileMode()));
                                  }
                              } 
                          }
                          $accountdisable = FALSE;
                          $accountdisable_info = "";
                          $groups_lower_array = [];
                          $in_groups_array = [];
                          $in_groups_lower_array = [];
                          $group = "";
                          $user_in_groups = "";

                          $ldap_total_counter++;

                          $user = $this->EncodeForBackend(isset($one_user[mb_strtolower($this->GetLdapCnIdentifier(),'UTF-8')][0])?($one_user[mb_strtolower($this->GetLdapCnIdentifier(),'UTF-8')][0]):"");
                          $account = $this->EncodeForBackend(isset($one_user[mb_strtolower($this->GetLdapSyncedUserAttribute(),'UTF-8')][0])?($one_user[mb_strtolower($this->GetLdapSyncedUserAttribute(),'UTF-8')][0]):"");
                          if (!$this->IsCaseSensitiveUsers()) {
                              $user = mb_strtolower($user,'UTF-8');
                              $account = mb_strtolower($account,'UTF-8');
                          }

                          if ($account != "") {

                              $one_user['msradiusframedipaddress'][0] = (isset($one_user['msradiusframedipaddress'][0])) ? long2ip32bit($one_user['msradiusframedipaddress'][0]) : "---";
                              $one_user['radiusframedipaddress'][0] = (isset($one_user['radiusframedipaddress'][0])) ? ($one_user['radiusframedipaddress'][0]) : "---";
                              $one_user['radiusframedipnetmask'][0] = (isset($one_user['radiusframedipnetmask'][0])) ? ($one_user['radiusframedipnetmask'][0]) : "---";

                              if (isset($one_user['useraccountcontrol'][0])) {
                                  if (0 != ($one_user['useraccountcontrol'][0] & 2)) {
                                      $accountdisable = TRUE;
                                      $accountdisable_info = (("" != $accountdisable_info) ? ', ' : "") . 'account disabled in the AD';
                                  }
                              }
                              if (isset($one_user['ms-ds-user-account-control-computed'][0])) {
                                  if (0 != ($one_user['ms-ds-user-account-control-computed'][0] & 16)) {
                                      $accountdisable = TRUE;
                                      $accountdisable_info = (("" != $accountdisable_info) ? ', ' : "") . 'account locked out in the AD';
                                  }
                              }
                              if (isset($one_user['accountexpires'][0])) {
                                  if (($one_user['accountexpires'][0] > 0) && ((($one_user['accountexpires'][0] / 10000000) - 11644473600) < time())) {
                                      $accountdisable = TRUE;
                                      $accountdisable_info = (("" != $accountdisable_info) ? ', ' : "") . 'account expired in the AD (account expires)';
                                  }
                              }

                              if (isset($one_user['shadowexpire'][0])) {
                                  if (($one_user['shadowexpire'][0] >= 0) && ((86400 * $one_user['shadowexpire'][0]) < time())) {
                                      $accountdisable = TRUE;
                                      $accountdisable_info = (("" != $accountdisable_info) ? ', ' : "") . 'account expired in the AD (shadow expire)';
                                  }
                              }
                              if (isset($one_user['sambaacctflags'][0])) {
                                  if ((FALSE !== mb_strpos($one_user['sambaacctflags'][0], "D")) || (FALSE !== mb_strpos($one_user['sambaacctflags'][0], "L"))) {
                                      $accountdisable = TRUE;
                                      $accountdisable_info = (("" != $accountdisable_info) ? ', ' : "") . 'account disabled in Samba (Disabeld or automatically Locked)';
                                  }
                              }
                              if ($include_disabled || (!$accountdisable)) {
                                  // TODO $in_a_group discovery
                                  if ("" == nullable_trim($this->GetLdapInGroup(true))) {
                                      $in_a_group = TRUE;
                                  } else {
                                      $in_a_group = FALSE;
                                      foreach ($in_groups_array_raw as $one_group) {
                                          $in_groups_array[] = nullable_trim($one_group);
                                          $in_groups_lower_array[] = mb_strtolower(nullable_trim($one_group),'UTF-8');
                                      }


                                      // Enhanced Active Directory
                                      if (1 == $this->GetLdapServerType()) {
                                          $groups_array_raw = $ldap_connection->nice_names($one_user[$ldap_connection->_group_attribute]);
                                          // All groups are already defined
                                          /*
                                          $groups_array_raw=$ldap_connection->user_all_groups($one_user['distinguishedname'][0], $groups_filtering);
                                          foreach ($ldap_connection->nice_names($one_user[$ldap_connection->_group_attribute]) as $level_one_group) {
                                              $add_it = TRUE;
                                              foreach ($groups_array_raw as $one_temp) {
                                                  if (strpos($level_one_group, $one_temp) !== FALSE) {
                                                      $add_it = FALSE;
                                                  }
                                              }
                                              if ($add_it) {
                                                  $groups_array_raw[] = $level_one_group;
                                              }
                                          }
                                          */

                                          foreach ($groups_array_raw as $one_group) {
                                              $this_group = $this->EncodeForBackend($one_group);
                                              $groups_lower_array[] = mb_strtolower($this_group,'UTF-8');
                                          }
                                          
                                          foreach ($in_groups_array as $one_filtered_group) {
                                              if (in_array(mb_strtolower($one_filtered_group,'UTF-8'), $groups_lower_array)) {
                                                  $user_in_groups.= (("" != $user_in_groups) ? ',' : "") . $one_filtered_group;
                                                  $in_a_group = TRUE;
                                                  if ("" == $group) {
                                                      $group = $one_filtered_group;
                                                  }
                                              }
                                          }
                                          
                                      // Generic LDAP, eventually no memberOf function like in AD
                                      } elseif (2 == $this->GetLdapServerType()) {
                                          // Prepare the array "users_in_groups" if we are using a generic LDAP and an LdapInGroup Filtering
                                          foreach ($in_groups_array_raw as $one_group) {
                                              $temp_array = $ldap_connection->group_users($one_group);
                                              foreach ($temp_array as $one_temp) {
                                                  $one_user = $this->EncodeForBackend($one_temp);
                                                  if ($user == $one_user) {
                                                      $user_in_groups.= (("" != $user_in_groups) ? ',' : "") . $one_group;
                                                      $in_a_group = TRUE;
                                                  }
                                              }
                                          }

                                          if ($in_a_group) {
                                              $temp_array = explode(",", $user_in_groups);
                                              $group = $temp_array[0];
                                          }

                                      // eDirectory
                                      } elseif (4 == $this->GetLdapServerType()) {
                                          if (isset($one_user[$ldap_connection->_group_attribute])) {
                                            $groups_array_raw = $ldap_connection->nice_names($one_user[$ldap_connection->_group_attribute]);
                                          } else {
                                            $groups_array_raw = [];
                                          }
                                          if ($ldap_connection->_recursive_groups) {
                                              foreach ($groups_array_raw as $id => $group_name){
                                                  $extra_groups=$ldap_connection->recursive_groups($group_name, $this->IsLdapRecursiveCacheOnly());
                                                  if ("" != $ldap_connection->get_warning_message()) {
                                                      $this->WriteLog("Warning: LDAP warning: ".$ldap_connection->get_warning_message(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                                                  }
                                                  $groups_array_raw=array_merge($groups_array_raw,$extra_groups);
                                              }
                                          }

                                          foreach ($groups_array_raw as $one_group) {
                                              $this_group = $this->EncodeForBackend($one_group);
                                              $groups_lower_array[] = mb_strtolower($this_group,'UTF-8');
                                          }
                                          
                                          foreach ($in_groups_array as $one_filtered_group) {
                                              if (in_array(mb_strtolower($one_filtered_group,'UTF-8'), $groups_lower_array)) {
                                                  $user_in_groups.= (("" != $user_in_groups) ? ',' : "") . $one_filtered_group;
                                                  $in_a_group = TRUE;
                                                  if ("" == $group) {
                                                      $group = $one_filtered_group;
                                                  }
                                              }
                                          }

                                      // Legacy Active Directory
                                      } else {
                                          // $groups_array_raw = $ldap_connection->user_groups($user);
                                          $groups_array_raw=$ldap_connection->nice_names($one_user[$ldap_connection->_group_attribute]); //presuming the entry returned is our guy (unique usernames)
                                          if ($ldap_connection->_recursive_groups) {
                                              foreach ($groups_array_raw as $id => $group_name){
                                                  $extra_groups=$ldap_connection->recursive_groups($group_name, $this->IsLdapRecursiveCacheOnly());
                                                  if ("" != $ldap_connection->get_warning_message()) {
                                                      $this->WriteLog("Warning: LDAP warning: ".$ldap_connection->get_warning_message(), FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
                                                  }
                                                  $groups_array_raw=array_merge($groups_array_raw,$extra_groups);
                                              }
                                          }

                                          foreach ($groups_array_raw as $one_group) {
                                              $this_group = $this->EncodeForBackend($one_group);
                                              $groups_lower_array[] = mb_strtolower($this_group,'UTF-8');
                                          }
                                          
                                          foreach ($in_groups_array as $one_filtered_group) {
                                              if (in_array(mb_strtolower($one_filtered_group,'UTF-8'), $groups_lower_array)) {
                                                  $user_in_groups.= (("" != $user_in_groups) ? ',' : "") . $one_filtered_group;
                                                  $in_a_group = TRUE;
                                                  if ("" == $group) {
                                                      $group = $one_filtered_group;
                                                  }
                                              }
                                          }
                                      }
                                  }

                                  if ($ignore_in_group || $in_a_group) {
                                      $description = "";
                                      if (isset($one_user['description'][0])) {
                                          $description = nullable_trim($one_user['description'][0]);
                                      }
                                      if (("" == $description) && (isset($one_user['gecos'][0]))) {
                                          $description = nullable_trim($one_user['gecos'][0]);
                                      }
                                      if (("" == $description) && (isset($one_user['displayname'][0]))) {
                                          $description = nullable_trim($one_user['displayname'][0]);
                                      }
                                      if ("" == $description) {
                                          $description = $account;
                                      }
                                      $display_name = "";
                                      if (isset($one_user['displayname'][0])) {
                                          $display_name = nullable_trim($one_user['displayname'][0]);
                                      }

                                      // $user;
                                      $ldap_email = nullable_trim(isset($one_user['mail'][0])?$this->EncodeForBackend($one_user['mail'][0]):"");
                                      $ldap_group = nullable_trim(("" != $group) ? $group : $this->GetDefaultUserGroup());
                                      if ($this->IsMultipleGroupsEnabled()) {
                                          $ldap_group = nullable_trim(("" != $user_in_groups) ? $user_in_groups : $this->GetDefaultUserGroup());
                                      }
                                      $ldap_description = $this->EncodeForBackend($description);
                                      $ldap_display_name = $this->EncodeForBackend($display_name);
                                      $ldap_sms = (isset($one_user['mobile'][0])?$this->EncodeForBackend($one_user['mobile'][0]):"");
                                      $ldap_msnpallowdialin = ("TRUE" == (isset($one_user['msnpallowdialin'][0])?($one_user['msnpallowdialin'][0]):"FALSE"));
                                      $ldap_enabled = ((!$accountdisable)?1:0);
                                      // TODO CYR $ldap_synchronized_dn = nullable_trim(isset($one_user['distinguishedname'][0])?encode_utf8_if_needed($one_user['distinguishedname'][0]):"");
                                      $ldap_synchronized_dn = nullable_trim(isset($one_user['distinguishedname'][0])?$this->EncodeForBackend($one_user['distinguishedname'][0]):"");
                                      $ldap_language = mb_strtolower(mb_substr(nullable_trim(isset($one_user[mb_strtolower($this->GetLdapLanguageAttribute(),'UTF-8')][0])?$this->EncodeForBackend($one_user[mb_strtolower($this->GetLdapLanguageAttribute(),'UTF-8')][0]):""), 0, 2),'UTF-8');

                                      $ldap_framedipaddress = (isset($one_user['msradiusframedipaddress'][0]) ? ($one_user['msradiusframedipaddress'][0]) : "---");
                                      if ("---" == $ldap_framedipaddress) {
                                          $ldap_framedipaddress = (isset($one_user['radiusframedipaddress'][0]) ? ($one_user['radiusframedipaddress'][0]) : "---");
                                      }
                                      $ldap_framedipnetmask = (isset($one_user['radiusframedipnetmask'][0]) ? ($one_user['radiusframedipnetmask'][0]) : "---");

                                      $ldap_algorithm = $this->GetLdapDefaultAlgorithm();
                                      if ("" != nullable_trim($this->GetLdapWithout2faInGroup())) {
                                        $without2fa_in_groups_array_raw = explode("\t",nullable_trim(str_replace(",","\t",str_replace(";","\t",$this->GetLdapWithout2faInGroup()))));
                                        $user_in_groups_array_raw = explode("\t",nullable_trim(str_replace(",","\t",str_replace(";","\t",$user_in_groups))));
                                        foreach ($without2fa_in_groups_array_raw as $one_without2fa_group) {
                                          foreach ($user_in_groups_array_raw as $one_temp) {
                                            if (strtolower($one_without2fa_group) == strtolower($one_temp)) {
                                              $ldap_algorithm = "without2fa";
                                              break;
                                            }
                                          }
                                        }
                                      }

                                      if (!$this->CheckUserExists($account, true, true)) { // $no_server_check = TRUE; $no_error = TRUE
                                      // User doesn't exist yet
                                          if ("" == $ldap_description) {
                                              $ldap_description = $account;
                                          }

                                          if ($ldap_enabled && $this->IsLdapDontAutoEnable()) {
                                            $ldap_enabled = false;
                                          }
                                          $result = $this->FastCreateUser($account,
                                                                          $ldap_email,
                                                                          $ldap_sms,
                                                                          -1, // Prefix pin needed
                                                                          $ldap_algorithm,
                                                                          $ldap_enabled,
                                                                          $ldap_description,
                                                                          $ldap_group,
                                                                          1,  // Synchronized
                                                                          "", // Pin
                                                                          true, // Automatically
                                                                          'LDAP', // Synchronized channel
                                                                          $this->GetLdapDomainControllers(), // Synchronized server
                                                                          $ldap_synchronized_dn,
                                                                          -1, // LDAP prefix set to default value if the user is created  automatically
                                                                          $ldap_language,
                                                                          (("---" != $ldap_framedipaddress) ? $ldap_framedipaddress : "")
                                                                         );
                                          if ($result) {
                                              $this->SyncUserModified(TRUE, $account);
                                              $ldap_created_counter++;
                                          }
                                      } else {
                                      // User already exists
                                          $existing_ldap_users_counter++;
                                          $this->SetUser($account);
                                          if (1 == $this->GetUserSynchronized()) {
                                              $algorithm = $this->GetUserAlgorithm();
                                              $description = $this->GetUserDescription();
                                              $display_name = $this->GetUserDisplayName();
                                              $email = $this->GetUserEmail();
                                              $enabled = $this->GetUserActivated();
                                              $group = $this->GetUserGroup();
                                              $sms = $this->GetUserSms();
                                              $synchronized_channel = $this->GetUserSynchronizedChannel();
                                              $synchronized_dn = $this->GetUserSynchronizedDn();
                                              $synchronized_server = $this->GetUserSynchronizedServer();
                                              $language = $this->GetUserLanguage(TRUE);
                                              $dialin_ip_address = $this->GetUserDialinIpAddress();
                                              $dialin_ip_mask = $this->GetUserDialinIpMask();
                                              $modified = FALSE;
                                              $detailed_modif = "";

                                              // !!! Special without2fa behavior !!!
                                              if ((('without2fa' == $ldap_algorithm) && ($algorithm != $ldap_algorithm)) ||
                                                  (('without2fa' != $ldap_algorithm) && ('without2fa' == $algorithm) && ("" != nullable_trim($this->GetLdapWithout2faInGroup())))
                                                 ) {
                                                $this->SetUserAlgorithm($ldap_algorithm);
                                                $modified = TRUE;
                                                $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "Algorithm: $algorithm -> $ldap_algorithm";
                                              }
                                              
                                              if (("" != $ldap_description) && ($description != $ldap_description)) {
                                                  $this->SetUserDescription($ldap_description);
                                                  $modified = TRUE;
                                                  $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "Description: $description -> $ldap_description";
                                              }
                                              
                                              if (("" != $ldap_display_name) && ($display_name != $ldap_display_name)) {
                                                  $this->SetUserDisplayName($ldap_display_name);
                                                  $modified = TRUE;
                                                  $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "DisplayName: $display_name -> $ldap_display_name";
                                              }
                                              
                                              if (("" != $ldap_email) && ($email != $ldap_email)) {
                                                  $this->SetUserEmail($ldap_email);
                                                  $modified = TRUE;
                                                  $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "Email: $email -> $ldap_email";
                                              }

                                              if ($enabled != $ldap_enabled) {
                                                if ($this->GetVerboseFlag()) {
                                                  $modified = TRUE;
                                                }
                                                if ((!$ldap_enabled) && $this->IsLdapDontAutoDisable()) {
                                                  $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "Not modified: $enabled -> $ldap_enabled";
                                                } elseif ($ldap_enabled && $this->IsLdapDontAutoEnable()) {
                                                  $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "Not modified: $enabled -> $ldap_enabled";
                                                } else {
                                                  $this->SetUserActivated($ldap_enabled);
                                                  $modified = TRUE;
                                                  $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "Activated: $enabled -> $ldap_enabled";
                                                }
                                              }

                                              if (("" != $ldap_group) && ($group != $ldap_group)) {
                                                  $this->SetUserGroup($ldap_group);
                                                  $modified = TRUE;
                                                  $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "Group: $group -> $ldap_group";
                                              }

                                              if (("" != $ldap_sms) && ($sms != $ldap_sms)) {
                                                  $this->SetUserSms($ldap_sms);
                                                  $modified = TRUE;
                                                  $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "SMS: $sms -> $ldap_sms";
                                              }

                                              if ($synchronized_channel != 'LDAP') {
                                                  $this->SetUserSynchronizedChannel('LDAP');
                                                  $modified = TRUE;
                                                  $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "Synchronized channel: $synchronized_channel -> LDAP";
                                              }

                                              if ($synchronized_dn != $ldap_synchronized_dn) {
                                                  $this->SetUserSynchronizedDn($ldap_synchronized_dn);
                                                  $modified = TRUE;
                                                  $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "Synchronized DN: $synchronized_dn -> $ldap_synchronized_dn";
                                              }

                                              if (("" != $this->GetLdapDomainControllers()) && ($synchronized_server != $this->GetLdapDomainControllers())) {
                                                  $this->SetUserSynchronizedServer($this->GetLdapDomainControllers());
                                                  $modified = TRUE;
                                                  $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "AD/LDAP controller : $synchronized_server -> ".$this->GetLdapDomainControllers();
                                              }

                                              if ($language != $ldap_language) {
                                                  $this->SetUserLanguage($ldap_language);
                                                  $modified = TRUE;
                                                  $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "Language: $language -> $ldap_language";
                                              }

                                              if ("---" != $ldap_framedipaddress) {
                                                  if ($dialin_ip_address != $ldap_framedipaddress) {
                                                      $this->SetUserDialinIpAddress($ldap_framedipaddress);
                                                      $modified = TRUE;
                                                      $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "Dialin IP address: $dialin_ip_address -> $ldap_framedipaddress";
                                                  }
                                              }

                                              if ("---" != $ldap_framedipnetmask) {
                                                  if ($dialin_ip_mask != $ldap_framedipnetmask) {
                                                      $this->SetUserDialinIpNetmask($ldap_framedipnetmask);
                                                      $modified = TRUE;
                                                      $detailed_modif = (("" != $detailed_modif) ? ', ' : "") . "Dialin IP mask: $dialin_ip_mask -> $ldap_framedipnetmask";
                                                  }
                                              }

                                              if ($this->IsOverwriteRequestLdapPwd()) {
                                                // We set to the default value for LDAP password if the user is updated by synchronization
                                                if ($this->GetUserRequestLdapPassword() != $this->GetDefaultRequestLdapPwd()) {
                                                  $this->SetUserRequestLdapPassword($this->GetDefaultRequestLdapPwd());
                                                  $modified = TRUE;
                                                }
                                              }

                                              if ($this->IsOverwritePushEnabled()) {
                                                if ($this->GetUserPushEnabled() != $this->GetDefaultPushEnabled()) {
                                                  $this->SetUserPushEnabled($this->GetDefaultPushEnabled());
                                                  $modified = TRUE;
                                                }
                                              }

                                              $this->SetUserSynchronizedTime();
                                              
                                              $this->WriteUserData(TRUE, $modified); // $automatically = TRUE, $update_last_change = $modified
                                              if ($modified) {
                                                  if ($this->GetVerboseFlag()) {
                                                      $this->WriteLog("Debug: *AD/LDAP $account modified: $detailed_modif", FALSE, FALSE, 8888, 'Debug', "");
                                                      if ("" != $accountdisable_info) {
                                                          $this->WriteLog("Debug: *AD/LDAP $account disabled: $accountdisable_info", FALSE, FALSE, 8888, 'Debug', "");
                                                      }
                                                  }
                                                  $this->SyncUserModified(FALSE, $account);
                                                  $modified_counter++;
                                              }
                                          }
                                      }
                                  }
                              }
                          } // if ($account != "")
                      } while ((!$ldap_sync_stop) && ($one_user = $ldap_connection->one_user_info(FALSE, NULL, NULL, FALSE, $groups_filtering, $in_groups_filtering))); // $group_cn_cache_only = TRUE before
                      // Loop of LDAP parsing and synchronization

                      if ($ldap_connection->_paging_legacy) {
                        if (FALSE !== $ldap_connection->_oui_paged_sr) {
                          ldap_control_paged_result_response($ldap_connection->_conn_paged, $ldap_connection->_oui_paged_sr, $ldap_connection->_paging_cookie);
                        }
                      }
                      if ($this->GetVerboseFlag()) {
                        if (($ldap_connection->_paging_cookie !== null) && ($ldap_connection->_paging_cookie != "")) {
                          $this->WriteLog("Debug: *LDAP paging used", FALSE, FALSE, 8888, 'Debug', "");
                        }
                      }
                  } while ((!$ldap_sync_stop) && ($ldap_connection->_paging_cookie !== null) && ($ldap_connection->_paging_cookie != "") && ($ldap_connection->_oui_paged_sr !== FALSE));
                  // LDAP pagination loop

                  if ($ldap_connection->_paging_legacy) {
                    // Reset LDAP paged result
                    ldap_control_paged_result($ldap_connection->_conn_paged, $ldap_connection->_paging_size, false);
                  }
              }

              if (!$ldap_sync_stop) {
                  // Loop on all existing users to disable the "not-synchronized-yet" synchronized users
                  // TODO: cached users information could be updated during this time !
                  $internal_users_loop = 0;
                  $one_user = $this->GetNextUserArray(TRUE);
                  do {
                      // We also touch the sync lock every half of the lock time limit
                      //   and we check also if we have to stop now
                      if (($last_touch + ($this->GetLockTime() / 2)) <= time()) {
                          $last_touch = time();
                          if ($lock_handle = @fopen($ldap_sync_file_lock, "ct")) { // was wt, need ftruncate after flock now
                              flock($lock_handle, LOCK_EX);
                              ftruncate($lock_handle, 0);
                              $additional_info = "started at ".date("H:i:s", $start_sync_time);
                              $additional_info.= ", internal account #".($internal_users_loop+1)." at ".date("H:i:s");
                              if ($this->GetVerboseFlag()) {
                                  $additional_info.= " / Memory used: ".(intval(10*memory_get_usage()/(1024*1024))/10)."MB / Peak: ".(intval(10*memory_get_peak_usage()/(1024*1024))/10)."MB";
                                  $this->WriteLog("Debug: *AD/LDAP synchronization ".$additional_info, FALSE, FALSE, 8888, 'LDAP', "");
                              }
                              fwrite($lock_handle,$additional_info);
                              fclose($lock_handle);
                              if ("" != $this->GetLinuxFileMode()) {
                                  @chmod($ldap_sync_file_lock, octdec($this->GetLinuxFileMode()));
                              }
                          } 
                          if (file_exists($ldap_sync_file_stop)) {
                            unlink($ldap_sync_file_stop);
                            $ldap_sync_stop = TRUE;
                          }
                      }

                      if (isset($one_user['user'])) {
                          $modified_user = $one_user['user'];
                          $modified_description = $one_user['description'];
                          $modified_display_name = $one_user['display_name'];
                          $modified_email = $one_user['email'];
                          $modified_group = nullable_trim($one_user['group']);
                          $modified_enabled = $one_user['enabled'];
                          $modified_locked = $one_user['locked'];
                          $modified_sms = $one_user['sms'];
                          $modified_synchronized = $one_user['synchronized'];
                          $modified_synchronized_channel = $one_user['synchronized_channel'];
                          $modified_synchronized_dn = $one_user['synchronized_dn'];
                          $modified_synchronized_server = $one_user['synchronized_server'];
                          $modified_synchronized_time = $one_user['synchronized_time'];
                          $modified_token_serial = $one_user['token'];

                          if (($modified_synchronized) && ($modified_synchronized_time < $start_sync_time)) {
                              $existing_ldap_users_counter++;
                              // If the existing user is enabled and marked as synchronized but is no more in the external database/LDAP
                              if ($delete_old_synced_users && ($this->GetSyncDeleteRetentionDays() > 0)  && (($modified_synchronized_time + ($this->GetSyncDeleteRetentionDays() * 86400)) < $start_sync_time)) {
                                  // Since 5.8.0.2
                                  $this->DeleteUser($modified_user, TRUE);                              
                                  if ($this->GetVerboseFlag()) {
                                    $accountdisable_info = "account not found anymore in the AD/LDAP with the specified filters ";
                                    $accountdisable_info.= "(synchronized last time the " . date('Y-m-d H:i:s', $modified_synchronized_time) . ") ";
                                    $accountdisable_info.= "with server $modified_synchronized_server, ";
                                    if ("" != $modified_group) {
                                        $accountdisable_info.= "in group $modified_group, ";
                                    }
                                    $accountdisable_info.= "DN was $modified_synchronized_dn)";
                                    $this->WriteLog("Debug: *AD/LDAP $modified_user deleted: $accountdisable_info", FALSE, FALSE, 8888, 'Debug', "");
                                  }
                                  $this->WriteLog("Info: account ".$modified_user." automatically deleted (no more in the AD/LDAP)", FALSE, FALSE, _MULTIOTP_USER_DELETED_, 'System', "");
                                  $modified_counter++;
                              } elseif ($modified_enabled) {
                                $accountdisable_info = "account not found anymore in the AD/LDAP with the specified filters ";
                                $accountdisable_info.= "(synchronized last time the " . date('Y-m-d H:i:s', $modified_synchronized_time) . ") ";
                                $accountdisable_info.= "with server $modified_synchronized_server, ";
                                if ("" != $modified_group) {
                                  $accountdisable_info.= "in group $modified_group, ";
                                }
                                $accountdisable_info.= "DN was $modified_synchronized_dn)";
                                if ($this->IsLdapDontAutoDisable()) {
                                  $this->WriteLog("Info: account ".$modified_user." no more in the AD/LDAP, but STILL ENABLED", FALSE, FALSE, _MULTIOTP_USER_UPDATED_, 'System', "");
                                  if ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *AD/LDAP $modified_user STILL ENABLED, but $accountdisable_info", FALSE, FALSE, 8888, 'Debug', "");
                                  }
                                } else {
                                  $this->SetUser($modified_user);
                                  $this->SetUserActivated(0);
                                  $this->WriteUserData(TRUE);
                                  $this->WriteLog("Info: account ".$modified_user." automatically disabled (no more in the AD/LDAP)", FALSE, FALSE, _MULTIOTP_USER_UPDATED_, 'System', "");
                                  if ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *AD/LDAP $modified_user disabled: $accountdisable_info", FALSE, FALSE, 8888, 'Debug', "");
                                  }
                                  $modified_counter++;
                                }
                              }
                          }
                      }
                      $internal_users_loop++;
                  } while (($one_user = $this->GetNextUserArray()) && (!$ldap_sync_stop));
                  
                  if ($ldap_sync_stop) {
                      $this->WriteLog("Info: LDAP sync stopped", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'LDAP', "");
                  } else {
                      $time_info = gmdate("H:i:s", time()-$start_sync_time);
                      $info_txt = "";
                      
                      if ($modified_counter > 0) {
                          $ldap_counter_suffix = ((1 < $modified_counter)?'s':"");
                          $info_txt.= $modified_counter." user$ldap_counter_suffix updated, based on $ldap_total_counter LDAP entries";
                      }
                      if ($ldap_created_counter > 0) {
                          if ("" != $info_txt) {
                              $info_txt.= ', ';
                          }
                          $ldap_counter_suffix = ((1 < $ldap_created_counter)?'s':"");
                          $info_txt.= $ldap_created_counter." user$ldap_counter_suffix created, based on $ldap_total_counter LDAP entries";
                      }
                      if ("" == $info_txt) {
                          $info_txt = "No update for the $existing_ldap_users_counter LDAP synced users, based on $ldap_total_counter LDAP entries";
                      }
                      $this->WriteLog("Info: $info_txt (processed in $time_info)", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'LDAP', "");
                  }
              } else {
                  $this->WriteLog("Info: LDAP sync stopped", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'LDAP', "");
              }
          } // We have done this loop only if there was no error before
      // End of successful LDAP parameters
      } else {
          $this->EnableLdapError();
          $this->WriteLog("Error: no LDAP connection information", FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'LDAP', "");
      }
      if (file_exists($ldap_sync_file_lock)) {
          unlink($ldap_sync_file_lock);
      }
      return $result;
  }


  // It's possible to overload this stub in order to do something when the current user is modified (or created)
  function SyncUserModified(
      $created = FALSE,
      $user = ""
  ) {
      return TRUE;
  }


  /**
   * @brief   CheckLdapAuthentication (used by CLI ldap-check)
   *
   * @retval  boolean   Result of the test
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.2.4.2
   * @date    2014-04-13
   * @since   2014-02-07
   */
  function CheckLdapAuthentication()
  {
      $result = FALSE;
      if (!function_exists('ldap_connect')) {
          $this->WriteLog("Error: LDAP library not installed", FALSE, FALSE, _MULTIOTP_OPERATION_CANCELLED_, 'System', "", 3);
      } elseif (("" != $this->GetLdapDomainControllers()) && ("" != $this->GetLdapBindDn()) && ("" != $this->GetLdapServerPassword())) {
          $domain_controllers = explode(" ",nullable_trim(str_replace(","," ",str_replace(";"," ",$this->GetLdapDomainControllers()))));
          $ldap_options = array('account_suffix'     => $this->GetLdapAccountSuffix(),
                                'ad_password'        => $this->GetLdapServerPassword(),
                                'ad_username'        => $this->GetLdapBindDn(),
                                'base_dn'            => $this->GetLdapBaseDn(),
                                'cn_identifier'      => $this->GetLdapCnIdentifier(),
                                'domain_controllers' => $domain_controllers,
                                'group_attribute'    => $this->GetLdapGroupAttribute(),
                                'group_cn_identifier'=> $this->GetLdapGroupCnIdentifier(),
                                'ldap_server_type'   => $this->GetLdapServerType(),
                                'ldap_filter'        => $this->GetLdapFilter(),
                                'network_timeout'    => $this->GetLdapNetworkTimeout(),
                                'port'               => $this->GetLdapPort(),
                                'recursive_groups'   => $this->IsLdapRecursiveGroups(),
                                'time_limit'         => $this->GetLdapTimeLimit(),
                                'use_ssl'            => $this->IsLdapSsl()
                               );

          if (!constant_defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
            define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032);
          }
          
          $domain_controllers = explode(" ",nullable_trim(str_replace(","," ",str_replace(";"," ",$this->GetLdapDomainControllers()))));
          mt_srand(doubleval(microtime()) * 100000000); // for older php versions
          $domain_controller = ($domain_controllers[array_rand($domain_controllers)]);

          foreach ($domain_controllers as $dc) {
              $port = $this->GetLdapPort();
              $controller = $dc;
              $protocol = "ldap://";
              if ($this->IsLdapSsl()) {
                  $protocol = "ldaps://";
              }
              $pos = mb_strpos($dc, "://");
              if ($pos !== FALSE) {
                  $protocol = mb_substr($dc, 0, $pos+3);
                  $dc = mb_substr($dc, $pos+3);
              }
              $pos = mb_strpos($dc, ":");
              if ($pos !== FALSE) {
                  $port = mb_substr($dc, $pos+1);
                  $dc = mb_substr($dc, 0, $pos);
              }

              // DEBUG
              // echo "DEBUG PROTOCOL: ".$protocol.$dc.":".$port."\n";
              
              /*
              if ($this->GetVerboseFlag()) {
                  ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, 7);
              }
              */
              
              if ($ldapconn = @ldap_connect($protocol.$dc.":".$port)) {
                ldap_set_option($ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3);
                ldap_set_option($ldapconn, LDAP_OPT_REFERRALS, 0);
                if (@ldap_bind($ldapconn, ($this->GetLdapBindDn().$this->GetLdapAccountSuffix()), ($this->GetLdapServerPassword()))) {
                  /*
                  if ($this->GetVerboseFlag()) {
                    echo "DEBUG\n";
                    if (ldap_get_option($ldapconn, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extended_error)) {
                      echo "Error Binding to LDAP: $extended_error";
                    } else {
                      echo "Error Binding to LDAP: No additional information is available.";
                    }
                  }
                  */
                  $result = TRUE;
                } else {
                  if ($this->GetVerboseFlag()) {
                    // echo "DEBUG LDAP: ".ldap_error($ldapconn);
                    $this->WriteLog("DEBUG: LDAP: ".ldap_error($ldapconn));
                  }
                }
                @ldap_unbind($ldapconn);
              }
              if ($result) {
                  break;
              }
          }
      }
      return $result;
  }


  function SetTokenDataReadFlag(
      $flag
  ) {
      $this->_token_data_read_flag = $flag;
  }


  function GetTokenDataReadFlag()
  {
      return $this->_token_data_read_flag;
  }


  function SetBaseDir(
      $base_dir
  ) {
      $this->_base_dir = $this->ConvertToUnixPath($base_dir);
  }


  function GetBaseDir()
  {
      return ($this->_base_dir);
  }


  /**
   * @brief   Get the folder of this script.
   *
   * @retval  string  Full path to the script folder.
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.0.7
   * @date    2013-08-28
   * @since   2010-06-07
   */
  function GetScriptFolder()
  {
      if ("" != $this->GetBaseDir()) {
          $current_script_folder_detected = $this->ConvertToUnixPath($this->GetBaseDir());
      } else {
          $current_script_folder_detected = $this->ConvertToUnixPath(dirname(__FILE__));
      }

      if (mb_substr($current_script_folder_detected,-1) != "/") {
          $current_script_folder_detected.="/";
      }
      return $this->ConvertToWindowsPathIfNeeded($current_script_folder_detected);
  }


  function ConvertToUnixPath(
      $path
  ) {
      return str_replace("\\","/",$path);
  }


  function ConvertToWindowsPathIfNeeded(
      $path
  ) {
    $result = (is_null($path) ? "" : $path);
    if (FALSE !== mb_strpos($result,":")) {
      $result = str_replace("/","\\",$result);
    }
    return $result;
  }


  function GetReplyMessageForRadius() {
    return (isset($this->_reply_array_for_radius[0]) ? $this->_reply_array_for_radius[0] : "");
  }


  function SetReplyMessageForRadius(
      $value
  ) {
      $this->_reply_array_for_radius = [];
      $this->AddReplyArrayForRadius($value);
  }


  function GetReplyArrayForRadius()
  {
      return $this->_reply_array_for_radius;
  }


  function AddReplyArrayForRadius(
      $value
  ) {
      $this->_reply_array_for_radius[] = $value;
  }


  // Adding extra information for the result (if any)
  function AddExtraRadiusInfo($options = [])
  {
    if (isset($options['multiple_groups'])) {
      $multiple_groups = (isset($options['multiple_groups']) ? (TRUE == $options['multiple_groups']) : FALSE);
    } else {
      $multiple_groups = $this->IsMultipleGroupsEnabled();
    }

    $group = nullable_trim($this->GetUserGroup());

    if ($this->GetVerboseFlag()) {
      $this->WriteLog("Debug: *AddExtraRadiusInfo ".__LINE__.", group(s) to add: {$group}", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
    }

    // Begin add group information
    if ((!$this->IsDeviceDontSendGroups()) && ("" != $group) && ("" != $this->GetGroupAttribute())) {
      $group_array = explode("\t",nullable_trim(str_replace(",","\t",str_replace(";","\t",$group))));
      if ($multiple_groups) {
        foreach ($group_array as $one_group) {
          if ("" != nullable_trim($one_group)) {
            $this->AddReplyArrayForRadius($this->GetGroupAttribute().$this->GetRadiusReplyAttributor().'"'.$one_group.'"');
          }
        }
        if ($this->GetVerboseFlag()) {
          $this->AddReplyArrayForRadius($this->GetGroupAttribute().$this->GetRadiusReplyAttributor().'"'."multiotp-debug-group".'"');
        }
      } else {
        $this->AddReplyArrayForRadius($this->GetGroupAttribute().$this->GetRadiusReplyAttributor().'"'.$group_array[0].'"');
      }
    }
    // End add group information

    if (("" != $this->GetLastClearOtpValue()) && ("" != $this->GetClearOtpAttribute())) {
      $this->AddReplyArrayForRadius($this->GetClearOtpAttribute().$this->GetRadiusReplyAttributor().'"'.$this->GetLastClearOtpValue().'"');
    }

    $dialin_ip_address = nullable_trim($this->GetUserDialinIpAddress());
    if (is_valid_ipv4($dialin_ip_address)) {
      $dialin_ip_mask = nullable_trim($this->GetUserDialinIpMask());
      if (!is_valid_ipv4($dialin_ip_mask)) {
        $dialin_ip_mask = $this->GetDefaultDialinIpMask();
      }
      // IP address and netmask
      $this->AddReplyArrayForRadius('Framed-IP-Address'.$this->GetRadiusReplyAttributor()."".$dialin_ip_address."");
      if (is_valid_ipv4($dialin_ip_mask)) {
        $this->AddReplyArrayForRadius('Framed-IP-Netmask'.$this->GetRadiusReplyAttributor()."".$dialin_ip_mask."");
      }
    }
  }


  function SetVerboseLogPrefix(
      $value
  ) {
      $this->_config_data['verbose_log_prefix'] = $value;
  }


  function GetVerboseLogPrefix()
  {
      return $this->_config_data['verbose_log_prefix'];
  }


  function SetAttributesToEncrypt(
      $attributes_to_encrypt
  ) {
      $attributes = nullable_trim($attributes_to_encrypt);
      if (("" != $attributes) && ('*' == mb_substr($attributes,0,1)) && ('*' == mb_substr($attributes,-1))) {
          $this->_attributes_to_encrypt = $attributes;
      }
  }


  function GetAttributesToEncrypt()
  {
      return $this->_attributes_to_encrypt;
  }



  function SetUsersFolder(
      $folder,
      $create = true
  ) {
      $new_folder = $this->ConvertToUnixPath($folder);
      if (mb_substr($new_folder,-1) != "/") {
          $new_folder.="/";
      }
      if ("/" == $new_folder) {
        $new_folder = "./";
      }
      $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
      $this->_users_folder = $new_folder;
      if ($create && (!file_exists($new_folder))) {
          if (!@mkdir(
                  $new_folder,
                  ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
                  true //recursive
          )) {
              $this->WriteLog("Error: Unable to create the missing users folder ".$new_folder, FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
          }
      }
  }


  function GetUsersFolder()
  {
      if ("" == $this->_users_folder) {
          $this->SetUsersFolder($this->GetScriptFolder()."users/");
      }
      return $this->ConvertToWindowsPathIfNeeded($this->_users_folder);
  }


  function SetCredUsersFolder(
      $folder,
      $create = true
  ) {
      $new_folder = $this->ConvertToUnixPath($folder);
      if (mb_substr($new_folder,-1) != "/") {
          $new_folder.="/";
      }
      if ("/" == $new_folder) {
        $new_folder = "./";
      }
      $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
      $this->_credusers_folder = $new_folder;
      if ($create && (!file_exists($new_folder))) {
          if (!@mkdir(
                  $new_folder,
                  ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
                  true //recursive
          )) {
              $this->WriteLog("Error: Unable to create the missing credusers folder ".$new_folder, FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
          }
      }
  }


  function GetCredUsersFolder()
  {
      if ("" == $this->_credusers_folder) {
          $this->SetCredUsersFolder($this->GetScriptFolder()."credusers/");
      }
      return $this->ConvertToWindowsPathIfNeeded($this->_credusers_folder);
  }


  function SetDevicesFolder(
      $folder,
      $create = true
  ) {
      $new_folder = $this->ConvertToUnixPath($folder);
      if (mb_substr($new_folder,-1) != "/") {
          $new_folder.="/";
      }
      if ("/" == $new_folder) {
        $new_folder = "./";
      }
      $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
      $this->_devices_folder = $new_folder;
      if ($create && (!file_exists($new_folder))) {
          if (!@mkdir(
                  $new_folder,
                  ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
                  true //recursive
          )) {
              $this->WriteLog("Error: Unable to create the missing devices folder ".$new_folder, FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
          }
      }
  }


  function GetDevicesFolder()
  {
      if ("" == $this->_devices_folder) {
          $this->SetDevicesFolder($this->GetScriptFolder()."devices/");
      }
      return $this->ConvertToWindowsPathIfNeeded($this->_devices_folder);
  }


  function SetDdnsFolder(
    $folder,
    $create = true
  ) {
    $new_folder = $this->ConvertToUnixPath($folder);
    if (mb_substr($new_folder,-1) != "/") {
        $new_folder.="/";
    }
    if ("/" == $new_folder) {
      $new_folder = "./";
    }
    $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
    $this->_ddns_folder = $new_folder;
    if ($create && (!file_exists($new_folder))) {
        if (!@mkdir(
                $new_folder,
                ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
                true //recursive
        )) {
            $this->WriteLog("Error: Unable to create the missing devices folder ".$new_folder, FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
        }
    }
  }


  function GetDdnsFolder()
  {
    if ("" == $this->_ddns_folder) {
        $this->SetDdnsFolder($this->GetScriptFolder()."ddns/");
    }
    return $this->ConvertToWindowsPathIfNeeded($this->_ddns_folder);
  }


  function SetQrCodeFolder(
      $folder
  ) {
    return false; // Not used anymore
  }


  function GetQrCodeFolder()
  {
    return false; // Not used anymore
  }


  function SetTemplatesFolder(
      $folder
  ) {
      $new_folder = $this->ConvertToUnixPath($folder);
      if (mb_substr($new_folder,-1) != "/") {
          $new_folder.="/";
      }
      if ("/" == $new_folder) {
        $new_folder = "./";
      }
      $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
      $this->_templates_folder = $new_folder;
  }


  function GetTemplatesFolder()
  {
      if ("" == $this->_templates_folder) {
          $this->SetTemplatesFolder($this->GetScriptFolder()."templates/");
      }
      return $this->ConvertToWindowsPathIfNeeded($this->_templates_folder);
  }


  function SetGroupsFolder(
      $folder,
      $create = true
  ) {
      $new_folder = $this->ConvertToUnixPath($folder);
      if (mb_substr($new_folder,-1) != "/") {
          $new_folder.="/";
      }
      if ("/" == $new_folder) {
        $new_folder = "./";
      }
      $new_folder = $this->ConvertToWindowsPathIfNeeded($new_folder);
      $this->_groups_folder = $new_folder;
      if ($create && (!file_exists($new_folder))) {
          if (!@mkdir(
                  $new_folder,
                  ("" != $this->GetLinuxFolderMode()) ? octdec($this->GetLinuxFolderMode()) : 0777,
                  true //recursive
          )) {
              $this->WriteLog("Error: Unable to create the missing groups folder ".$new_folder, FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
          }
      }
  }


  function GetGroupsFolder()
  {
      if ("" == $this->_groups_folder) {
          $this->SetGroupsFolder($this->GetScriptFolder()."groups/");
      }
      return $this->ConvertToWindowsPathIfNeeded($this->_groups_folder);
  }


  function SendSms(
      $sms_recipient,
      $sms_message_to_send,
      $real_user = "",
      $originator = "",
      $provider = "",
      $username = "",
      $password = "",
      $api_id = "",
      $write_log = true,
      $source_tag = ""
  ) {

      $result = _MULTIOTP_SMS_PROVIDER_ERROR_; // ERROR: SMS provider not supported
      
      $sms_provider   = ('' != $provider) ? $provider : $this->GetSmsProvider();
      $sms_api_id     = (("" != $api_id)?$api_id:$this->GetSmsApiId());
      $sms_username   = (("" != $username)?$username:$this->GetSmsUsername());
      $sms_password   = (("" != $password)?$password:$this->GetSmsPassword());
      $sms_originator = (("" != $originator)?$originator:$this->GetSmsOriginator());

      $sms_number     = $this->CleanPhoneNumber($sms_recipient);
     
      if ("exec" == $sms_provider) {
          $exec_cmd = $sms_api_id;
          $exec_cmd = str_replace('%from', $sms_originator, $exec_cmd);
          $exec_cmd = str_replace('%to',   $sms_number,  $exec_cmd);
          $exec_cmd = str_replace('%msg',  encode_utf8_if_needed($sms_message_to_send),  $exec_cmd);
          exec($exec_cmd, $output);
          $result = _MULTIOTP_USER_STATIC_CODE_REQUESTED_; // INFO: Static code request received
          if ($write_log) {
              $this->WriteLog("Info: Static code request received for ".$real_user.(("" != $source_tag)?" for $source_tag":"")." and sent via ".$exec_cmd, FALSE, FALSE, $result, 'Authentication', $real_user);
          }
      } else {
          $sms_message = new MultiotpSms(array('provider'        => $sms_provider,
                                               'api_id'          => $sms_api_id,
                                               'username'        => $sms_username,
                                               'password'        => $sms_password,
                                               'from'            => $sms_originator,
                                               'to'              => $sms_number,
                                               'msg'             => $sms_message_to_send,
                                               'ip'              => $this->GetSmsIp(),
                                               'port'            => $this->GetSmsPort(),
                                               'url'             => $this->GetSmsUrl(), // can be multiple URLs, separated by space
                                               'send_template'   => $this->GetSmsSendTemplate(),
                                               'method'          => $this->GetSmsMethod(), // GET|POST|POST-XML
                                               'encoding'        => $this->GetSmsEncoding(), // UTF(UTF-8)|ISO(ISO-8859-1)
                                               'status_success'  => $this->GetSmsStatusSuccess(), // 20|""|...
                                               'content_success' => $this->GetSmsContentSuccess(), // OK|"command_status": 0|...
                                               'no_double_zero'  => $this->GetSmsNoDoubleZero(),
                                               'basic_auth'      => $this->GetSmsBasicAuth(),
                                               'content_encoding'=> $this->GetSmsContentEncoding(),
                                               'header'          => $this->GetSmsHeader(),
                                              ));

          if ($sms_message->sendSMS()) {
              $result = _MULTIOTP_USER_STATIC_CODE_REQUESTED_; // INFO: Static code request received
              if ($write_log) {
                  $this->WriteLog("Info: Static code request received for ".$real_user.(("" != $source_tag)?" for $source_tag":"")." and sent via ".$sms_provider." to ".$sms_number, FALSE, FALSE, $result, 'Authentication', $real_user);
              }
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Debug: *Detailed Sent Info successful to $sms_provider: ".$sms_message->getLastSendInfo(), FALSE, FALSE, 8888, 'Debug', "");
                  $this->WriteLog("Debug: *Detailed Sent Info successful returned from $sms_provider: ".$sms_message->getLastReplyInfo(), FALSE, FALSE, 8888, 'Debug', "");
              }
          } else {
              $result = _MULTIOTP_SMS_CODE_TRANSMISSION_ERROR_; // ERROR: SMS code request received, but an error occurred during transmission
              if ($write_log) {
                  $this->WriteLog("Error: SMS code request received for ".$real_user.(("" != $source_tag)?" for $source_tag":"").", but the ".$sms_provider." error ".$sms_message->getLastReplyInfo()." occurred during transmission to ".$sms_number, FALSE, FALSE, $result, 'SMS', $real_user);
              }
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Debug: *Detailed Sent Info error to $sms_provider: ".$sms_message->getLastSendInfo(), FALSE, FALSE, 8888, 'Debug', "");
                  $this->WriteLog("Debug: *Detailed Sent Info error returned from $sms_provider: ".$sms_message->getLastReplyInfo(), FALSE, FALSE, 8888, 'Debug', "");
              }
          }
      }
      return $result;
  }


  function GenerateSmsToken(
      $user = ""
  ) {
      $result = _MULTIOTP_ERROR_;
      if ($this->IsSmsCodeAllowed()) {
        $now_epoch = time();
        if ("" != $user) {
          $this->SetUser($user);
        } else {
          $user = $this->GetUser();
        }
        $sms_number = $this->CleanPhoneNumber($this->GetUserSms());
        if ("" != $sms_number) {
          $sms_message_prefix = nullable_trim($this->GetSmsMessage());
          $sms_token = $this->GeneratePin($this->GetSmsDigits());

          $this->SetUserSmsOtp($sms_token);
          $this->SetUserSmsValidity($now_epoch + $this->GetSmsTimeout());
          $this->WriteUserData();

          $sms_nice_token = $this->ConvertToNiceToken($sms_token);
          
          if (FALSE !== mb_strpos($sms_message_prefix, '%s')) {
              $sms_message_to_send = sprintf($sms_message_prefix, $sms_nice_token);
          } else {
              $sms_message_to_send = $sms_message_prefix.' '.$sms_nice_token;
          }

          $result = $this->SendSms($sms_number, $sms_message_to_send, $user);
        } else {
          $result = _MULTIOTP_SMS_NO_DESTINATION_ERROR_; // ERROR: no information on where to send SMS code
          $this->WriteLog("Error: no information on where to send SMS code for ".$user, FALSE, FALSE, $result, 'SMS', $user);
        }
      } else {
        $result = _MULTIOTP_SMS_REQUEST_UNAUTHORIZED_;
        $this->WriteLog(array('text'       => "ERROR: SMS code request not allowed",
                              'error_code' => $result,
                              'category'   => 'System',
                              'user'       => "")
                       );
      }
      return $result;
  }


  // Generate Email token, and store them in the static token
  // (which was initially the sms token)
  function GenerateEmailToken(
      $user = ""
  ) {
      $result = _MULTIOTP_ERROR_;
      if ($this->IsEmailCodeAllowed()) {
        $now_epoch = time();
        if ("" != $user) {
          $this->SetUser($user);
        } else {
          $user = $this->GetUser();
        }
        $email = $this->GetUserEmail();
        if ("" != $email) {
          $token = $this->GeneratePin($this->GetEmailDigits());

          $this->SetUserEmailOtp($token);
          $this->SetUserEmailValidity($now_epoch + $this->GetEmailCodeTimeout());
          $this->WriteUserData();

          $nice_token = $this->ConvertToNiceToken($token);
          $subject    = "OTP: $nice_token";
          $content    = $subject."<br />\n({MultiotpDateTime format=Y-m-d H:i:s})";
          
          // WARNING! SendMail with a user will Re-read the specified user parameters, be sure parameters are saved first !
          $result = ($this->SendEmail(array('user'    => $user,
                                            'subject' => $subject,
                                            'content' => $content
                                           )
                                     ) ? _MULTIOTP_USER_STATIC_CODE_REQUESTED_ : _MULTIOTP_EMAIL_CODE_TRANSMISSION_ERROR_);
        } else {
          $result = _MULTIOTP_EMAIL_NO_DESTINATION_ERROR_; // ERROR: No information on where to send Email code
          $this->WriteLog(array('text'       => "Error: no information on where to send Email code for ".$user,
                                'error_code' => $result,
                                'category'   => 'Authentication',
                                'user'       => $user)
                         );
        }
      } else {
        $result = _MULTIOTP_EMAIL_REQUEST_UNAUTHORIZED_;
        $this->WriteLog(array('text'       => "ERROR: Email code request not allowed",
                              'error_code' => $result,
                              'category'   => 'System',
                              'user'       => "")
                       );
      }
      return $result;
  }


  function SendEmail(
    $send_email_array = []
  ) {
    $user         = isset($send_email_array['user'])?$send_email_array['user']:'';
    $email        = isset($send_email_array['email'])?$send_email_array['email']:'';
    $from_email   = isset($send_email_array['from_email'])?$send_email_array['from_email']:'';
    $from_name    = isset($send_email_array['from_name'])?$send_email_array['from_name']:'';
    $returnpath   = isset($send_email_array['returnpath'])?$send_email_array['returnpath']:'';
    $subject      = isset($send_email_array['subject'])?$send_email_array['subject']:'';
    $content      = isset($send_email_array['content'])?$send_email_array['content']:'';
    $content_text = isset($send_email_array['content_text'])?$send_email_array['content_text']:'';
    $template     = isset($send_email_array['template'])?$send_email_array['template']:'';
    $var          = isset($send_email_array['var'])?$send_email_array['var']:'';
    $x_sender     = isset($send_email_array['x-sender'])?$send_email_array['x-sender']:'';
    $priority     = isset($send_email_array['priority'])?$send_email_array['priority']:'';
    $language     = isset($send_email_array['language'])?$send_email_array['language']:'';
    $attach_array = isset($send_email_array['attach_array'])?$send_email_array['attach_array']:[];
    
    $result = FALSE;
    $actual_user = $user;
    
    $specific_language = $this->GetLanguage();

    if ('' == $email) {
      $actual_user = ('' != $user)?$user:$this->GetUser();
      if ('' != $actual_user) {
        $this->ReadUserData($actual_user);
        $email = nullable_trim($this->GetUserEmail());
        $specific_language = $this->GetUserLanguage();
      }
    }
    
    if ('' != $language) {
      $specific_language = $language();
    } else {
      $language = $this->GetLanguage();
    }

    if ('' == $this->GetSmtpServer()) {
      $result = _MULTIOTP_SMTP_UNDEFINED_;
      $this->WriteLog(array('text'       => $this->GetErrorText($result),
                            'error_code' => $result,
                            'category'   => 'System',
                            'user'       => ''
                            )
                       );
    } elseif ('' != $email) {
      $user_utf8 = encode_utf8_if_needed($actual_user);

      if ('' == $from_email) {
        $from_email = $this->GetSmtpSender();
      }
      if ('' == $from_email) {
        $from_email = $email;
      }

      if ('' == $from_name) {
        $from_name = nullable_trim($this->GetSmtpSenderName());
      }
      if ('' == $from_name) {
        $from_name = $this->GetSystemName().(('' != $this->GetDomainName())?'.'.$this->GetDomainName():'');
      }

      if ('' == $returnpath) {
        $returnpath = $from_email;
      }

      if ('' == $content) {
        $content = $subject;
      }

      if ('' == $x_sender) {
        $x_sender = $this->GetClassName();
      }
      
      if ('' == $priority) {
        $priority = 'High';
      }
      
      //Get template file
      if ('' != $template) {
        $file_to_show = $this->GetConfigFolder().$template.'.html';
        if(!file_exists($file_to_show)) {
          $file_to_show = $this->GetTemplatesFolder().$template.'.html';
        }
        if(file_exists($file_to_show)) {
          $content = (file_get_contents($file_to_show));
        }
      }

      $content = encode_utf8_if_needed($content);

      // Check if a specific language exists in the tags
      if (false === mb_stripos($content, '{IfMultiotpLanguage="'.$specific_language.'"')) {
        $specific_language = $language;
        if (false === mb_stripos($content, '{IfMultiotpLanguage="'.$specific_language.'"')) {
          $specific_language = 'en';
        }
      }
      // Clean other languages info
      $content = preg_replace('/<!--\s*\{\/IfMultiotpLanguage="'.$specific_language.'"\}\s*-->/i', '', $content);
      $content = preg_replace('/<!--\s*\{IfMultiotpLanguage="'.$specific_language.'"\}\s*-->/i', '', $content);
      $content = preg_replace('/<!--\s*\{\/IfMultiotpLanguage="[a-z]*"\}\s*-->/i', ' -- {/IfMultiotpLanguage="other"} {ML} -->', $content);
      $content = preg_replace('/<!--\s*\{IfMultiotpLanguage="[a-z]*"\}\s*-->/i', '<!-- {ML} {IfMultiotpLanguage="other"} -- ', $content);

      // Clean language comments
      $content_cleaned = "";
      $content_slice = explode("{ML} -->",$content);
      foreach ($content_slice as $one_slice) {
        $comment_pos = mb_strpos($one_slice,'<!-- {ML}');
        if(FALSE !== $comment_pos) {
          $content_cleaned.=mb_substr($one_slice,0,$comment_pos);
        }
      }
      $content_cleaned .= end($content_slice);
      $content = $content_cleaned."\n";

      // Clean comments
      $content_cleaned = "";
      $content_slice = explode("-->",$content);
      foreach ($content_slice as $one_slice) {
        $comment_pos = mb_strpos($one_slice,'<!--');
        if(FALSE !== $comment_pos) {
          $content_cleaned.=mb_substr($one_slice,0,$comment_pos);
        }
      }
      $content_cleaned .= end($content_slice);
      $content = $content_cleaned."\n";

      // Content replacement
      $content = str_replace('{var}', $var, $content);
      
      $regex_url='/\surl=(.*?)[\}\s}]/';
      $regex_format='/\sformat=\"?([^\"\}]*)\"?.*\}/';

      // Date and time replacement
      $regex_tag='/\{MultiotpDateTime(.*)\}/';
      $format = "Y-m-d H:i:s";
      if(preg_match_all($regex_tag, $content, $matches)) {
        foreach ($matches[0] as $item) {
          if(!empty($item)) {
            if(preg_match($regex_format, $item, $values)) {
              $format = $values[1];
            }
            $content = str_replace($item, date($format), $content);
          }
        }
      }

      $title_begin = mb_strpos($content, '<title>');
      $title_end   = mb_strpos($content, '</title>');
      if (($title_begin !== FALSE) && ($title_end !== FALSE)) {
        $subject = mb_substr($content, $title_begin+7, $title_end - ($title_begin+7));
        $subject = preg_replace('@[\r\n][\s]+@', '', $subject);
      }

      if ($content_text != "") {
        $text = $content_text;
      } else {
        $text = html2text($content);
      }

      // Mail are not sent if domain is not valid
      // $helo = (('' != nullable_trim($this->GetDomainName()))?nullable_trim($this->GetDomainName()):'multiOTP.com');
      $helo = 'localhost';
      $auth = NULL;
      $auth_user = NULL;
      $auth_password = NULL;
      if ($this->IsSmtpAuth()) {
        $auth = TRUE;
        $auth_user = $this->GetSmtpUsername();
        $auth_password = $this->GetSmtpPassword();
      }
      if (!constant_defined('XPM4_MESSAGE_ID_CUSTOMIZED')) define('XPM4_MESSAGE_ID_CUSTOMIZED', $helo);

      $ssl_mode = (($this->IsSmtpAuth() && (1 == $this->GetSmtpSsl()))?'ssl':'');

      $mail = new MAIL();
      $mail->AddHeader('X-Sender', $x_sender);
      $mail->Priority($priority);
      $mail->From($from_email, encode_utf8_if_needed($from_name));
      $mail->Path($returnpath); // Return Path
      $mail->AddTo($email);

      if ("" != nullable_trim($subject)) { // Otherwise, it could crash !
        $mail->Subject(encode_utf8_if_needed($subject));
      }

      $mail_html = $content;
      if ("" != nullable_trim($mail_html)) { // Otherwise, it could crash !
        $mail->Html($mail_html);
      }

      $mail_text = $text;
      if ("" != nullable_trim($mail_text)) { // Otherwise, it could crash !
        $mail->Text($mail_text);
      }

      if (count($attach_array) > 0) {
        foreach ($attach_array as $one_array) {
          if (count($one_array) > 2) {
            $mail->Attach($one_array[0], $one_array[1], $one_array[2]);
          }
        }
      }

      // TODO Check why Connect with empty server is not working
      if ($connection = $mail->Connect($this->GetSmtpServer(), $this->GetSmtpPort(), $auth_user, $auth_password, $ssl_mode, 10, $helo, NULL, NULL))
      {
        $result = $mail->Send($connection);
        $mail->Disconnect($connection);
      }
      if (TRUE === $result) {
        $this->WriteLog(array('text'       => "Info: mail sent to $actual_user ($email)",
                              'error_code' => _MULTIOTP_OPERATION_DONE_,
                              'category'   => 'User')
                         );
      } else {
        $this->WriteLog(array('text'       => "Error: mail not sent to $actual_user ($email)",
                              'error_code' => _MULTIOTP_EMAIL_SEND_ERROR_,
                              'category'   => 'User')
                         );
        if ($this->GetVerboseFlag()) {
          $this->WriteLog(array('text'       => "Debug: *subject: $subject",
                                'error_code' => 8888,
                                'category'   => 'User')
                         );
        }
      }
    } else {
      $this->WriteLog(array('text'       => "Error: mail server or email address missing to send mail to $actual_user",
                            'error_code' => _MULTIOTP_EMAIL_SEND_ERROR_,
                            'category'   => 'User',
                            'user'       => '',
                            'overwrite_severity' => 3
                            )
                       );
    }
    // Returned result is TRUE, FALSE, or an error number
    return $result;
  }


  function ConvertToNiceToken(
      $regular_token
  ) {
      $token_length = mb_strlen($regular_token);
      if (9 <= $token_length) {
          $sms_nice_token = mb_substr($regular_token,0,3).'-'.mb_substr($regular_token,3,3).'-'.mb_substr($regular_token,6,($token_length-6));
      } elseif (6 < $token_length) {
          $sms_nice_token = mb_substr($regular_token,0,intval($token_length/2)).'-'.mb_substr($regular_token,intval($token_length/2),$token_length);
      } else {
          $sms_nice_token = $regular_token;
      }
      return $sms_nice_token;
  }


  /**
   * @brief   Resync the token of a user and return true or false
   *
   * @param   string  $user                  User to check
   * @param   string  $input                 Token to check
   * @param   string  $input_sync            Second token to check for resync
   * @param   string  $display_status        Display the status bar
   * @param   string  $ignore_lock           Ignore the fact that the user is locked
   * @param   string  $resync_enc_pass       Resynchronization with an encrypted password
   * @param   string  $no_server_check       Ignore any server(s) (if any)  to do the check
   * @param   string  $self_register_serial  Serial number of the self registered hardware token
   *                                          (if any, and not combined as a prefix of the input)
   * @param   string  $hardware_tokens_list  Comma separated list of hardware tokens also attributed
   * @return  boolean                        Resync was successful or not
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.3.0.0
   * @date    2014-11-04
   * @since   2014-10-17
   */
  function ResyncUserToken($user = "",
                           $input = "",
                           $input_sync = "",
                           $display_status = FALSE,
                           $ignore_lock = FALSE,
                           $resync_enc_pass = FALSE,
                           $no_server_check = FALSE,
                           $self_register_serial = "",
                           $hardware_tokens_list = ""
  ) {
      $the_hardware_tokens_list = $hardware_tokens_list;
      if ("" != $user) {
          $this->SetUser($user);
          $the_hardware_tokens_list = $this->GetUserTokenSerialNumber();
      }
      $result = $this->ResyncToken($input,
                                   $input_sync,
                                   $display_status,
                                   $ignore_lock,
                                   $resync_enc_pass,
                                   $no_server_check,
                                   $self_register_serial,
                                   $the_hardware_tokens_list);
      return $result;
  }


  /**
   * @brief   Resync the token of the current user and return true or false
   *
   * @param   string  $input                 Token to check
   * @param   string  $input_sync            Second token to check for resync
   * @param   string  $display_status        Display the status bar
   * @param   string  $ignore_lock           Ignore the fact that the user is locked
   * @param   string  $resync_enc_pass       Resynchronization with an encrypted password
   * @param   string  $no_server_check       Ignore any server(s) (if any)  to do the check
   * @param   string  $self_register_serial  Serial number of the self registered hardware token
   *                                          (if any, and not combined as a prefix of the input)
   * @param   string  $hardware_tokens_list  Comma separated list of hardware tokens also attributed
   * @return  boolean                        Resync was successful or not
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.3.0.0
   * @date    2014-11-04
   * @since   2014-10-17
   */
  function ResyncToken($input = "",
                       $input_sync = "",
                       $display_status = FALSE,
                       $ignore_lock = FALSE,
                       $resync_enc_pass = FALSE,
                       $no_server_check = FALSE,
                       $self_register_serial = "",
                       $hardware_tokens_list = ""
  ) {
      $result = $this->CheckToken($input,
                                  $input_sync,
                                  $display_status,
                                  $ignore_lock,
                                  $resync_enc_pass,
                                  $no_server_check,
                                  $self_register_serial,
                                  $hardware_tokens_list);

      // Both resynchronization and authentication are TRUE
      return (($result == _MULTIOTP_TOKEN_RESYNCED_) || ($result == 0));
  }


  /**
   * @brief   Check the token of a user and give the result, with resync options.
   *
   * @param   string  $user                  User to check
   * @param   string  $input                 Token to check
   * @param   string  $input_sync            Second token to check for resync
   * @param   string  $display_status        Display the status bar
   * @param   string  $ignore_lock           Ignore the fact that the user is locked
   * @param   string  $resync_enc_pass       Resynchronization with an encrypted password
   * @param   string  $no_server_check       Ignore any server(s) (if any)  to do the check
   * @param   string  $self_register_serial  Serial number of the self registered hardware token
   *                                          (if any, and not combined as a prefix of the input)
   * @param   string  $hardware_tokens_list  Comma separated list of hardware tokens also attributed
   * @return  int                            Error code (0: successful authentication, 1n: info, >=20: error)
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.1.1
   * @date    2014-01-15
   * @since   2010-08-12
   */
  function CheckUserToken(
      $function_array = array('user' => ""),
      $input_param = "",
      $input_sync_param = "",
      $display_status_param = FALSE,
      $ignore_lock_param = FALSE,
      $resync_enc_pass_param = FALSE,
      $no_server_check_param = FALSE,
      $self_register_serial_param = "",
      $hardware_tokens_list_param = ""
  ) {
      if (is_array($function_array)) {
          $user = isset($function_array['user'])?$function_array['user']:"";
          $input = isset($function_array['input'])?$function_array['input']:"";
          $input_sync = isset($function_array['input_sync'])?$function_array['input_sync']:"";
          $display_status = isset($function_array['display_status'])?$function_array['display_status']:FALSE;
          $ignore_lock = isset($function_array['ignore_lock'])?$function_array['ignore_lock']:FALSE;
          $resync_enc_pass = isset($function_array['resync_enc_pass'])?$function_array['resync_enc_pass']:FALSE;
          $no_server_check = isset($function_array['no_server_check'])?$function_array['no_server_check']:FALSE;
          $self_register_serial = isset($function_array['self_register_serial'])?$function_array['self_register_serial']:"";
          $hardware_tokens_list = isset($function_array['hardware_tokens_list'])?$function_array['hardware_tokens_list']:"";
      } else { // backward compatibility
          $user = $function_array;
          $input = $input_param;
          $input_sync = $input_sync_param;
          $display_status = $display_status_param;
          $ignore_lock = $ignore_lock_param;
          $resync_enc_pass = $resync_enc_pass_param;
          $no_server_check = $no_server_check_param;
          $self_register_serial = $self_register_serial_param;
          $hardware_tokens_list = $hardware_tokens_list_param;
      }
  
      if ("" != $user) {
          $this->SetUser($user);
          $hardware_tokens_list = $this->GetUserTokenSerialNumber();
      }
      return $this->CheckToken(array('user' => $user,
                                     'input' => $input,
                                     'input_sync' => $input_sync,
                                     'display_status' => $display_status,
                                     'ignore_lock' => $ignore_lock,
                                     'resync_enc_pass' => $resync_enc_pass,
                                     'no_server_check' => $no_server_check,
                                     'self_register_serial' => $self_register_serial,
                                     'hardware_tokens_list' => $hardware_tokens_list));
  }


  /**
   * @brief   Check the token of the actual user and give the result, with resync options.
   *
   * @param   string  $input_array           Array with token to check and other fields, or token to check as a string
   * @param   string  $input_sync_param      Second token to check for resync
   * @param   string  $display_status        Display the status bar
   * @param   string  $ignore_lock           Ignore the fact that the user is locked
   * @param   string  $resync_enc_pass       Resynchronization with an encrypted password
   * @param   string  $no_server_check       Ignore any server(s) (if any)  to do the check
   * @param   string  $self_register_serial  Serial number of the self registered hardware token
   *                                          (if any, and not combined as a prefix of the input)
   * @param   string  $hardware_tokens_list  Comma separated list of hardware tokens also attributed
   *
   * @return  int                            Error code (0: successful authentication, 1n: info, >=20: error)
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 4.3.4.3
   * @date    2016-06-08
   * @since   2010-06-07
   */
  function CheckToken(
      $input_array = array('input' => ""),
      $input_sync_param = "",
      $display_status_param = FALSE,
      $ignore_lock_param = FALSE,
      $resync_enc_pass_param = FALSE,
      $no_server_check_param = FALSE,
      $self_register_serial_param = "",
      $hardware_tokens_list_param = ""
  ) {
      $now_epoch = time();

      if (is_array($input_array)) {
          $input = isset($input_array['input'])?$input_array['input']:"";
          $input_sync = isset($input_array['input_sync'])?$input_array['input_sync']:"";
          $display_status = isset($input_array['display_status'])?$input_array['display_status']:FALSE;
          $ignore_lock = isset($input_array['ignore_lock'])?$input_array['ignore_lock']:FALSE;
          $resync_enc_pass = isset($input_array['resync_enc_pass'])?$input_array['resync_enc_pass']:FALSE;
          $no_server_check = isset($input_array['no_server_check'])?$input_array['no_server_check']:FALSE;
          $self_register_serial = isset($input_array['self_register_serial'])?$input_array['self_register_serial']:"";
          $hardware_tokens_list = isset($input_array['hardware_tokens_list'])?$input_array['hardware_tokens_list']:"";
          $no_increment_error = isset($input_array['no_increment_error'])?$input_array['no_increment_error']:FALSE;
          $challenge_response_enabled = intval(isset($input_array['challenge_response_enabled'])?$input_array['challenge_response_enabled']:0);
          $sms_challenge_enabled = intval(isset($input_array['sms_challenge_enabled'])?$input_array['sms_challenge_enabled']:0);
          $text_sms_challenge = isset($input_array['text_sms_challenge'])?$input_array['text_sms_challenge']:"";
          $text_token_challenge = isset($input_array['text_token_challenge'])?$input_array['text_token_challenge']:"";
      } else {
          $input = $input_array; // backward compatibility
          $input_sync = $input_sync_param;
          $display_status = $display_status_param;
          $ignore_lock = $ignore_lock_param;
          $resync_enc_pass = $resync_enc_pass_param;
          $no_server_check = $no_server_check_param;
          $self_register_serial = $self_register_serial_param;
          $hardware_tokens_list = $hardware_tokens_list_param;
          // New parameters, only available in the array
          $no_increment_error = FALSE;
          $challenge_response_enabled = 0;
          $sms_challenge_enabled = 0;
          $text_sms_challenge = "";
          $text_token_challenge = "";
      }
      
      if ($this->IsGlobalChallengeResponse()) {
        $challenge_response_enabled = 1;
        $sms_challenge_enabled = ($this->IsGlobalSmsChallenge() ? 1:0);
        $text_sms_challenge = $this->GetGlobalTextSmsChallenge();
        $text_token_challenge = $this->GetGlobalTextTokenChallenge();
      }
  
      $state = nullable_trim($this->GetState());
      if ("" == $state) {
        $state = "multiOTP".mb_substr(md5($this->GetEncryptionKey().time().mt_rand(100000,999999)),0,24);
      }

      $push_timeout_error = false;
      $push_denied_error = false;
      $cache_result_enabled = false;
      $disable_error_counter = false;
      $force_no_prefix_pin = false;
      $multi_account_enabled = false;
      $device_description = "";
      
      $supplemental_login_info = "";

      // Specific device detection, based on the source tag, to check if cache is enabled
      $source_tag = nullable_trim($this->GetSourceTag());
      if ("" != $source_tag) {
          if ("" != $this->GetRadiusTagPrefix()) {
              $device_id = $this->GetRadiusTagPrefix().$source_tag;
          } else {
              $device_id = mb_substr($source_tag, strrpos('-'.$source_tag, '-'));
          }
          if ($this->ReadDeviceData($device_id, FALSE, TRUE)) {
              $cache_result_enabled = $this->IsDeviceCacheResultEnabled();
              $force_no_prefix_pin = $this->IsDeviceForceNoPrefixEnabled();
              $device_description = $this->GetDeviceDescription();
          }
      }

      if (empty($device_description)) {
        $device_description = $this->GetDeviceDescription(array("ip" => isset($_SERVER['REMOTE_ADDR'])?$_SERVER['REMOTE_ADDR']:""));
      }

      if ($this->GetCredentialProviderMode()) {
        $device_description = _MULTIOTP_CREDENTIAL_PROVIDER_;
      } elseif ($this->IsCredentialProviderRequest()) {
        if (empty($device_description)) {
          $device_description = _MULTIOTP_CREDENTIAL_PROVIDER_;
        } else {
          $device_description = _MULTIOTP_CREDENTIAL_PROVIDER_ .  ", " . $device_description;
        }
      }

      // Force no prefix pin to true if the request come from a multiOTP Credential Provider
      if ($this->GetCredentialProviderMode() || $this->IsCredentialProviderRequest()) {
        if ($this->IsIgnoreNoPrefixCredentialProvider()) {
          $supplemental_login_info = " (using Credential Provider, without forcing no prefix PIN)";
        } else {
          $force_no_prefix_pin = true;
          $supplemental_login_info = " (using Credential Provider)";
        }
      }

      $ldap_check_passed = FALSE;

      $this->SendWeeklyAnonymousStat();

      $this->SetLastClearOtpValue();
      $calculated_token = "";

      // 4.3.2.2
      // As external passwords are now supported,
      // we cannot trim or remove the minus anymore.
      // We disabled nullable_trim(str_replace('-',"",$input))
      $input_to_check = $input;
      $real_user = $this->GetUser();
      $real_rawUser = $this->GetRawUser();
      // We don't accept any input without at least 3 characters (like 'sms'), except for without2FA
      $input_is_empty = ("" == $input_to_check);
      if (mb_strlen($input_to_check) < 3) {
          $input_to_check.= "! <3 digits !";
      }
      
      $server_result = -1;
      // Check on the external server(s) first
      if ((!$no_server_check) && ("" != $this->GetServerUrl())) {
          // For multi-account definition, we are also looking on the server(s) if any
          if ($this->ReadUserData($real_user)) {
              // multi account works only if authentication is done with PAP
              if ($this->IsUserMultiAccount()) {
                  $multi_account_enabled = true;
                  $pos = strrpos($input_to_check, " ");
                  if ($pos !== FALSE) {
                      $real_user = mb_substr($input_to_check,0,$pos);
                      $real_rawUser = $real_user;
                      $input_to_check = nullable_trim(mb_substr($input_to_check,$pos+1));
                      $input_is_empty = ("" == $input_to_check);
                      if (mb_strlen($input_to_check) < 3) {
                          $input_to_check.= "!! <3 digits !!";
                      }
                  }
              }
          }

          // If the user is push enabled, wait more time for push operation
          $push_timeout = $this->GetPushTimeout();
          if ($push_timeout > $this->GetServerTimeout()) {
            $this->SetServerTimeout($push_timeout);
          }

          if ("" != $this->GetChapPassword()) {
              if (32 < mb_strlen($this->GetChapPassword())) {
                  $hex_id = mb_substr($this->GetChapPassword(),0,2);
              } else {
                  $hex_id = $this->GetChapId();
              }
      
              $server_result = $this->CheckUserTokenOnServer($real_user, $this->GetChapPassword(), 'CHAP', $hex_id, $this->GetChapChallenge());
          } else {
              // We authorize to check an empty OTP (possibly for without2FA)
              $server_result = $this->CheckUserTokenOnServer($real_user, ($input_is_empty ? "" : $input_to_check));
          }

          if ($this->_xml_dump_in_log) {
              $this->WriteLog("Debug: CheckUserTokenOnServer returns ".$server_result, FALSE, FALSE, 8888, 'Debug', "");
          }
      }

      if ($this->GetVerboseFlag() && $this->IsKeepLocal()) {
          $this->WriteLog("Info: *Local users are kept locally", FALSE, FALSE, 8888, 'System', "");
      }
      if (0 == $server_result) {
          $result = _MULTIOTP_AUTH_OK_;
          $this->WriteLog("Info: User ".$real_user." successfully logged in".$supplemental_login_info." using an external server", FALSE, FALSE, $result, 'User');
      } elseif (_MULTIOTP_USER_STATIC_CODE_REQUESTED_ == $server_result) {
          $result = _MULTIOTP_USER_STATIC_CODE_REQUESTED_; // INFO: Static code request received
          $this->WriteLog("Info: Static code request received and sent for ".$real_user." to ".$this->CleanPhoneNumber($this->GetUserSms()), FALSE, FALSE, $result, 'Authentication', $real_user);
      } elseif ((_MULTIOTP_USER_MISSING_ == $server_result) && (!$this->IsKeepLocal())) {
          $this->DeleteUser($real_user, TRUE); // $no_error_info = TRUE
          $result = _MULTIOTP_USER_MISSING_; // ERROR: User doesn't exist. (on the server)
          $this->WriteLog("Error: User ".$real_user." doesn't exist", FALSE, FALSE, $result, 'User');
      } elseif ((($server_result >= _MULTIOTP_AUTH_OK_) && ($server_result >= _MULTIOTP_USER_EXISTS_) && ($server_result < 70)) || ($server_result >= 90)) {
          // We want to stop only if it's an error (but not -1), except if the user doesn't exist (>= 22), if it's a 7x (server) or 8x (cache) error
          $result = $server_result;
          // Already logged using CheckUserTokenOnServer
          // $this->WriteLog("Error: server sent back the error ".$server_result, FALSE, FALSE, $result, 'Server', "");
      } elseif (!$this->ReadUserData($real_user, FALSE, TRUE)) {
          // LOCALLY ONLY
          $result = _MULTIOTP_USER_MISSING_; // ERROR: User doesn't exist.
          $this->WriteLog("Error: User ".$real_user." doesn't exist", FALSE, FALSE, $result, 'User');
      } else {
          // *********************************************
          // Let's go for the whole authentication process
          // *********************************************
          $result = _MULTIOTP_ERROR_; // Unknown error

          // multi account works only if authentication is done with PAP
          if ($this->IsUserMultiAccount()) {
              $multi_account_enabled = true;
              $pos = strrpos($input_to_check, " ");
              if ($pos !== FALSE) {
                  $real_user = mb_substr($input_to_check,0,$pos);
                  $real_rawUser = $real_user;
                  $input_to_check = nullable_trim(mb_substr($input_to_check,$pos+1));
                  $input_is_empty = ("" == $input_to_check);
                  if (mb_strlen($input_to_check) < 3) {
                      $input_to_check.= "!!! <3 digits !!!";
                  }
              }
      
              // LOCALLY ONLY
              if (!$this->ReadUserData($real_user, FALSE, TRUE)) {
                  $result = _MULTIOTP_USER_LINKED_MISSING_; // ERROR: linked user doesn't exist.
                  $this->WriteLog("Error: linked user ".$real_user." doesn't exist", FALSE, FALSE, $result, 'User', $real_user);
                  return $result;
              }
          }

          // From here now, we know already which user we are testing exactly
          
              
          // First we check if the users is activated or not.
          if (1 != $this->GetUserActivated()) {
              $result = _MULTIOTP_USER_DISABLED_; // ERROR: User is desactivated.
              $this->WriteLog("Error: User ".$real_user." is desactivated", FALSE, FALSE, $result, 'User', $real_user);
              return $result;
          }

          $detected_serial_number = "";

          // Check if self-registration of tokens is enabled and try the autoregistration if needed
          if ($this->IsSelfRegistrationEnabled()) {
              // Self-registration serial number is directly given in the dedicated field
              if ("" != $self_register_serial) {
                  if ($this->CheckTokenExists($self_register_serial, false)) {
                      $detected_serial_number = $self_register_serial;
                  }
              }
          }
          
          if ("" == $detected_serial_number) {
              // Looking for an existing token with this serial number
              $token_serial_number_length = $this->GetTokenSerialNumberLength();
              $token_serial_number_length_array = explode(" ",nullable_trim(str_replace(","," ",str_replace(";"," ",$token_serial_number_length))));
              foreach ($token_serial_number_length_array as $one_serial_number_length) {
                  if (intval($one_serial_number_length) > 0) {
                      $token_otp_list_of_length = $this->GetTokenOtpListOfLength();
                      $token_otp_list_of_length_array = explode(" ",nullable_trim(str_replace(","," ",str_replace(";"," ",$token_otp_list_of_length))));
                      foreach ($token_otp_list_of_length_array as $one_token_otp_length) {
                          if (intval($one_token_otp_length) > 0) {
                              if (mb_strlen($input_to_check) >= (intval($one_serial_number_length) + intval($one_token_otp_length))) {
                                  $check_serial = mb_substr($input_to_check,
                                                         -(intval($one_serial_number_length)+intval($one_token_otp_length)),
                                                         -intval($one_token_otp_length)
                                                        );
                                  if ($this->CheckTokenExists($check_serial, false)) {
                                      $detected_serial_number = $check_serial;
                                      /*
                                      $input_to_check = mb_substr($input_to_check,
                                                               0,
                                                               -(intval($one_serial_number_length)+intval($one_token_otp_length))
                                                              ).
                                                        mb_substr($input_to_check,
                                                               -intval($one_token_otp_length)
                                                              );
                                      */
                                      // It can appears twice, so we do a replace
                                      $input_to_check = str_replace($detected_serial_number, "", $input_to_check);
                                      break(2);
                                  }
                              }
                          }
                      }
                  }
              }
          }

          
          // If detected, we remove the serial number in the input_sync
          if ("" != $detected_serial_number) {
              $input_sync = str_replace($detected_serial_number, "", $input_sync);
          }

          if ($this->IsSelfRegistrationEnabled()) {
              // echo "DEBUG SelfRegisterHardwareToken: $detected_serial_number / $self_input_to_check\n";

              // TODO check if the serial number is in the list (instead of a single token)
              if (("" != $detected_serial_number) && ($detected_serial_number != $this->GetUserTokenSerialNumber())) {
                  $result = $this->SelfRegisterHardwareToken($real_user,$detected_serial_number, $input_to_check, $input);
                  // Self registered access cannot be cached for special device
                  if (0 == $result) {
                      // Self registered access is not cached for cache enabled device
                      return $result;
                  }
              }
          }

          // From here now, we know already which user we are testing exactly,
          // and also if a serial number is defined (and the input to check has been recalculated).
          // TODO: Without serial number we have to check with all tokens attributed to this user
          
          if (($this->GetUserAutolockTime() > 0) && ($this->GetUserAutolockTime() < $now_epoch)) {
              $result = _MULTIOTP_CACHE_TOO_OLD_ERROR_; // ERROR: Cache too old for this user, account autolocked
              $this->WriteLog("Error: cache too old for user ".$real_user.", account autolocked.", FALSE, FALSE, $result, 'User', $real_user);
              return $result;
          }

          if ("" != $this->GetChapPassword()) {
              $input_to_check = $this->GetChapPassword();
          } elseif ("" != $this->GetMsChapResponse()) {
              $input_to_check = $this->GetMsChapResponse();
          } elseif ("" != $this->GetMsChap2Response()) {
              $input_to_check = $this->GetMsChap2Response();
          }

          // Challenge/response stage 1 and 2
          // Don't check prefix PIN if we are in the response stage of the challenge
          if ((1 == $challenge_response_enabled) && (($state == $this->GetUserChallenge()) && ($this->GetUserChallengeValidity() >= $now_epoch))) {
              // Challenge/response stage 2
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Debug: *Challenge stage 2, state is ok, now checking OTP without prefix", FALSE, FALSE, 8888, 'Debug', "");
              }
              $force_no_prefix_pin = true;
          } elseif ((1 == $challenge_response_enabled) && (($state != $this->GetUserChallenge()) || ($this->GetUserChallengeValidity() < $now_epoch))) {
              // Challenge/response stage 1
              if ("" != $input_to_check) {
                  if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Debug: *Challenge stage 1, waiting prefix only", FALSE, FALSE, 8888, 'Debug', "");
                  }
                  if ($this->IsUserRequestLdapPasswordEnabled()) {
                      $ldap_check_passed = FALSE;
                      $ldap_to_check = $input_to_check;
                      if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Debug: *Challenge stage 1, check LDAP prefix", FALSE, FALSE, 8888, 'Debug', "");
                      }
                      if ($this->CheckUserLdapPassword($this->GetUserSynchronizedDn(), $ldap_to_check)) {
                          $ldap_check_passed = TRUE;
                          if ($this->IsCacheLdapHash()) {
                              // The LDAP password is stored in a cache
                              $this->SetUserLdapHashCache(nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check))));
                          }
                      } elseif ($this->IsCacheLdapHash()) {
                          if (!$this->IsLdapServerReachable()) {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Debug: *user LDAP hash password checked in the cache", FALSE, FALSE, 8888, 'Debug', "");
                              }
                              if ($this->GetUserLdapHashCache() === nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check)))) {
                                  $ldap_check_passed = TRUE;
                                  if ($this->GetVerboseFlag()) {
                                      $this->WriteLog("Debug: *user LDAP hash password verified, based on cached hash password", FALSE, FALSE, 8888, 'Debug', "");
                                  }
                              } else {
                                  if ($this->GetVerboseFlag()) {
                                      $this->WriteLog("Debug: *user LDAP hash password verification failed", FALSE, FALSE, 8888, 'Debug', "");
                                  }
                              }
                          } else {
                              $ldap_check_passed = FALSE;
                              $ldap_to_check = '!LDAP_FALSE!';
                              $this->ResetUserLdapHashCache();
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Debug: *user LDAP password false, hash cache cleared", FALSE, FALSE, 8888, 'Debug', "");
                              }
                          }
                      } else { // If the LdapHash cache is disabled
                          $ldap_check_passed = FALSE;
                          $ldap_to_check = '!LDAP_FALSE!';
                          $this->ResetUserLdapHashCache();
                          if ($this->IsLdapServerReachable()) {
                              $this->WriteLog("Error: User $real_user LDAP password false", FALSE, FALSE, _MULTIOTP_LDAP_AUTH_ERROR_, 'User');
                          } else {
                              $this->WriteLog("Error: User $real_user verification failed, unreachable LDAP/AD server(s)", FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'User');
                          }
                      }
                  } else {
                      // It is a real prefix pin, not an LDAP/AD prefix
                      $code_to_check = $this->GetUserPin();
                      if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Debug: *Challenge stage 1, check prefix PIN", FALSE, FALSE, 8888, 'Debug', "");
                      }
                      if ("" != $this->GetChapPassword()) {
                          $code_to_check = $this->CalculateChapPassword($code_to_check);
                      } elseif ("" != $this->GetMsChapResponse()) {
                          $code_to_check = $this->CalculateMsChapResponse($code_to_check);
                      } elseif ("" != $this->GetMsChap2Response()) {
                          $code_to_check = $this->CalculateMsChap2Response($real_rawUser, $code_to_check);
                      }
                  }
                  if ($ldap_check_passed || ($input_to_check === $code_to_check)) {
                      $result = _MULTIOTP_USER_CHALLENGE_; // INFO: Access Challenge returned back to the client
                      $state = "multiOTP".mb_substr(md5($this->GetEncryptionKey().time().mt_rand(100000,999999)),0,24);
                      if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Debug: *Challenge sent to the client: $state", FALSE, FALSE, 8888, 'Debug', "");
                      }
                      $this->SetUserChallenge($state);
                      $this->SetUserChallengeValidity($now_epoch + (5*60)); // The challenge state is valid for 5 minutes
                      $this->SetReplyMessageForRadius('State = "'.$state.'"');

                      if (1 == $sms_challenge_enabled) {
                          if (_MULTIOTP_USER_STATIC_CODE_REQUESTED_ == $this->GenerateSmsToken()) {
                              $this->AddReplyArrayForRadius('Reply-Message'.$this->GetRadiusReplyAttributor().'"'.$text_sms_challenge.'"');
                          } else {
                              $this->AddReplyArrayForRadius('Reply-Message'.$this->GetRadiusReplyAttributor().'"'.$text_token_challenge.'"');
                          }
                      } else {
                          $this->AddReplyArrayForRadius('Reply-Message'.$this->GetRadiusReplyAttributor().'"'.$text_token_challenge.'"');
                      }
                      
                      if (!$this->WriteUserData()) {
                          $result = _MULTIOTP_DATA_UPDATE_ERROR_; // ERROR: Unable to write the changes in the file
                          $this->WriteLog("Error: Unable to write the challenge changes in the file for the user ".$real_user, FALSE, FALSE, $result, 'User');
                      } else {
                          $this->WriteLog("Info: waiting challenge response for user ".$real_user.", state $state.", FALSE, FALSE, $result, 'User', $real_user);
                      }
                      return $result;
                  }
              }
          }


          // Check if we have to validate an SMS code
          if ($this->GetUserSmsValidity() > $now_epoch) {
              $ldap_check_passed = FALSE;
              $ldap_to_check = '!LDAP_FALSE!';
              
              // AD/LDAP case
              if ((($this->IsUserPrefixPin()) && (!$force_no_prefix_pin)) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                  $code_confirmed = $this->GetUserSmsOtp();
                  $this->SetLastClearOtpValue($code_confirmed);
                  $code_to_check = mb_substr($input_to_check, -mb_strlen($code_confirmed));
                  $ldap_to_check = mb_substr($input_to_check, 0, mb_strlen($input_to_check) - mb_strlen($code_to_check));
                  if ($code_to_check === $code_confirmed) {
                      if (("" != $ldap_to_check) && ($this->CheckUserLdapPassword($this->GetUserSynchronizedDn(), $ldap_to_check))) {
                          $ldap_check_passed = TRUE;
                          if ($this->IsCacheLdapHash()) {
                              // The LDAP password is stored in a cache
                              $this->SetUserLdapHashCache(nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check))));
                          }
                      } elseif ($this->IsCacheLdapHash()) {
                          if (!$this->IsLdapServerReachable()) {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Debug: *user LDAP hash password checked in the cache", FALSE, FALSE, 8888, 'Debug', "");
                              }
                              if ($this->GetUserLdapHashCache() === nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check)))) {
                                  $ldap_check_passed = TRUE;
                                  if ($this->GetVerboseFlag()) {
                                      $this->WriteLog("Debug: *user LDAP hash password verified, based on cached hash password", FALSE, FALSE, 8888, 'Debug', "");
                                  }
                              } else {
                                  if ($this->GetVerboseFlag()) {
                                      $this->WriteLog("Debug: *user LDAP hash password verification failed", FALSE, FALSE, 8888, 'Debug', "");
                                  }
                              }
                          } else {
                              $ldap_check_passed = FALSE;
                              $ldap_to_check = '!LDAP_FALSE!';
                              $this->ResetUserLdapHashCache();
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Debug: *user LDAP password false, hash cache cleared", FALSE, FALSE, 8888, 'Debug', "");
                              }
                          }
                      } else { // If the LdapHash cache is disabled
                          $ldap_check_passed = FALSE;
                          $ldap_to_check = '!LDAP_FALSE!';
                          $this->ResetUserLdapHashCache();
                          if ($this->IsLdapServerReachable()) {
                              $this->WriteLog("Error: User $real_user LDAP password false", FALSE, FALSE, _MULTIOTP_LDAP_AUTH_ERROR_, 'User');
                          } else {
                              $this->WriteLog("Error: User $real_user verification failed, unreachable LDAP/AD server(s)", FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'User');
                          }
                      }
                  } // ($code_to_check === $code_confirmed)
              } else {
                  // It is a real prefix pin, not an LDAP/AD prefix
                  $code_confirmed = ((($this->IsUserPrefixPin()) && (!$force_no_prefix_pin))?$this->GetUserPin():"").$this->GetUserSmsOtp();
                  $this->SetLastClearOtpValue($code_confirmed);
                  if ("" != $this->GetChapPassword()) {
                      $code_confirmed = $this->CalculateChapPassword($code_confirmed);
                  } elseif ("" != $this->GetMsChapResponse()) {
                      $code_confirmed = $this->CalculateMsChapResponse($code_confirmed);
                  } elseif ("" != $this->GetMsChap2Response()) {
                      $clear_code_confirmed = $code_confirmed;
                      $code_confirmed = $this->CalculateMsChap2Response($real_rawUser, $code_confirmed);
                      if ($this->IsDeveloperMode()) {
                        $this->WriteLog("Developer: *CalculateMsChap2Response($real_rawUser, $clear_code_confirmed) for SMS: $code_confirmed", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                      }
                  }
              }

              if ($ldap_check_passed || ($input_to_check === $code_confirmed)) {
                  $this->SetUserSmsOtp(md5($this->GetEncryptionKey().mt_rand(100000,999999).$this->GetUserTokenSeed().$now_epoch)); // Now SMS code is no more available, and the next one is difficult to guess ;-)
                  $this->SetUserSmsValidity($now_epoch); // And the validity time is set to the successful login time

                  // We are unlocking the user if needed
                  $this->SetUserErrorCounter(0);
                  $this->SetUserLocked(0);
                  // Finally, we update the last login of the user
                  $this->SetUserLastLogin($now_epoch);
                  $this->SetUserTokenLastLogin($now_epoch);
                  $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct SMS token

                  if ($cache_result_enabled) {
                      $this->SetUserLastCachedCredential(nullable_trim($input.' '.$input_sync));
                      $this->SetUserLastLoginForCache($now_epoch);
                  }
                  $this->SetUserLastSuccessCredential(nullable_trim($input.' '.$input_sync));

                  if (!$this->WriteUserData()) {
                      $result = _MULTIOTP_DATA_UPDATE_ERROR_; // ERROR: Unable to write the changes in the file
                      $this->WriteLog("Error: Unable to write the changes in the file for the user ".$real_user, FALSE, FALSE, $result, 'User');
                  } else {
                      $this->WriteLog("Ok: User ".$real_user." successfully logged in".$supplemental_login_info." with SMS token", FALSE, FALSE, $result, 'User');
                  }
                  
                  if (_MULTIOTP_AUTH_OK_ == $result) {
                      $this->AddExtraRadiusInfo();
                  }
                  return $result;
              }
          }

          // Check if we have to validate an Email code
          if ($this->GetUserEmailValidity() > $now_epoch) {
              $ldap_check_passed = FALSE;
              $ldap_to_check = '!LDAP_FALSE!';
              
              // AD/LDAP case
              if ((($this->IsUserPrefixPin()) && (!$force_no_prefix_pin)) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                  $code_confirmed = $this->GetUserEmailOtp();
                  $this->SetLastClearOtpValue($code_confirmed);
                  $code_to_check = mb_substr($input_to_check, -mb_strlen($code_confirmed));
                  $ldap_to_check = mb_substr($input_to_check, 0, mb_strlen($input_to_check) - mb_strlen($code_to_check));
                  if ($code_to_check === $code_confirmed) {
                      if (("" != $ldap_to_check) && ($this->CheckUserLdapPassword($this->GetUserSynchronizedDn(), $ldap_to_check))) {
                          $ldap_check_passed = TRUE;
                          if ($this->IsCacheLdapHash()) {
                              // The LDAP password is stored in a cache
                              $this->SetUserLdapHashCache(nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check))));
                          }
                      } elseif ($this->IsCacheLdapHash()) {
                          if (!$this->IsLdapServerReachable()) {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Debug: *user LDAP hash password checked in the cache", FALSE, FALSE, 8888, 'Debug', "");
                              }
                              if ($this->GetUserLdapHashCache() === nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check)))) {
                                  $ldap_check_passed = TRUE;
                                  if ($this->GetVerboseFlag()) {
                                      $this->WriteLog("Debug: *user LDAP hash password verified, based on cached hash password", FALSE, FALSE, 8888, 'Debug', "");
                                  }
                              } else {
                                  if ($this->GetVerboseFlag()) {
                                      $this->WriteLog("Debug: *user LDAP hash password verification failed", FALSE, FALSE, 8888, 'Debug', "");
                                  }
                              }
                          } else {
                              $ldap_check_passed = FALSE;
                              $ldap_to_check = '!LDAP_FALSE!';
                              $this->ResetUserLdapHashCache();
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Debug: *user LDAP password false, hash cache cleared", FALSE, FALSE, 8888, 'Debug', "");
                              }
                          }
                      } else { // If the LdapHash cache is disabled
                          $ldap_check_passed = FALSE;
                          $ldap_to_check = '!LDAP_FALSE!';
                          $this->ResetUserLdapHashCache();
                          if ($this->IsLdapServerReachable()) {
                              $this->WriteLog("Error: User $real_user LDAP password false", FALSE, FALSE, _MULTIOTP_LDAP_AUTH_ERROR_, 'User');
                          } else {
                              $this->WriteLog("Error: User $real_user verification failed, unreachable LDAP/AD server(s)", FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'User');
                          }
                      }
                  } // ($code_to_check === $code_confirmed)
              } else {
                  // It is a real prefix pin, not an LDAP/AD prefix
                  $code_confirmed = ((($this->IsUserPrefixPin()) && (!$force_no_prefix_pin))?$this->GetUserPin():"").$this->GetUserEmailOtp();
                  $this->SetLastClearOtpValue($code_confirmed);
                  if ("" != $this->GetChapPassword()) {
                      $code_confirmed = $this->CalculateChapPassword($code_confirmed);
                  } elseif ("" != $this->GetMsChapResponse()) {
                      $code_confirmed = $this->CalculateMsChapResponse($code_confirmed);
                  } elseif ("" != $this->GetMsChap2Response()) {
                      $clear_code_confirmed = $code_confirmed;
                      $code_confirmed = $this->CalculateMsChap2Response($real_rawUser, $code_confirmed);
                      if ($this->IsDeveloperMode()) {
                        $this->WriteLog("Developer: *CalculateMsChap2Response($real_rawUser, $clear_code_confirmed) for Email: $code_confirmed", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                      }
                  }
              }

              if ($ldap_check_passed || ($input_to_check === $code_confirmed)) {
                  $this->SetUserEmailOtp(md5($this->GetEncryptionKey().mt_rand(100000,999999).$this->GetUserTokenSeed().$now_epoch)); // Now Email code is no more available, and the next one is difficult to guess ;-)
                  $this->SetUserEmailValidity($now_epoch); // And the validity time is set to the successful login time

                  // We are unlocking the user if needed
                  $this->SetUserErrorCounter(0);
                  $this->SetUserLocked(0);
                  // Finally, we update the last login of the user
                  $this->SetUserLastLogin($now_epoch);
                  $this->SetUserTokenLastLogin($now_epoch);
                  $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct Email token

                  if ($cache_result_enabled) {
                      $this->SetUserLastCachedCredential(nullable_trim($input.' '.$input_sync));
                      $this->SetUserLastLoginForCache($now_epoch);
                  }
                  $this->SetUserLastSuccessCredential(nullable_trim($input.' '.$input_sync));

                  if (!$this->WriteUserData()) {
                      $result = _MULTIOTP_DATA_UPDATE_ERROR_; // ERROR: Unable to write the changes in the file
                      $this->WriteLog("Error: Unable to write the changes in the file for the user ".$real_user, FALSE, FALSE, $result, 'User');
                  } else {
                      $this->WriteLog("Ok: User ".$real_user." successfully logged in".$supplemental_login_info." with Email token", FALSE, FALSE, $result, 'User');
                  }
                  
                  if (_MULTIOTP_AUTH_OK_ == $result) {
                      $this->AddExtraRadiusInfo();
                  }
                  return $result;
              }
          }


          // Check if we have to validate a scratch password
          foreach ($this->GetUserScratchPasswordsArray() as $one_password) {
              // AD/LDAP case
              if ((($this->IsUserPrefixPin()) && (!$force_no_prefix_pin)) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                  $ldap_check_passed = FALSE;
                  $ldap_to_check = '!LDAP_FALSE!';

                  $code_confirmed = $one_password;
                  $this->SetLastClearOtpValue($code_confirmed);
                  $code_to_check = mb_substr($input_to_check, -mb_strlen($code_confirmed));
                  $ldap_to_check = mb_substr($input_to_check, 0, mb_strlen($input_to_check) - mb_strlen($code_to_check));
                  
                  if ($code_to_check === $code_confirmed) {
                      if (("" != $ldap_to_check) && ($this->CheckUserLdapPassword($this->GetUserSynchronizedDn(), $ldap_to_check))) {
                          $ldap_check_passed = TRUE;
                          if ($this->IsCacheLdapHash()) {
                              // The LDAP password is stored in a cache
                              $this->SetUserLdapHashCache(nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check))));
                          }
                      }
                      elseif ($this->IsCacheLdapHash()) {
                          if (!$this->IsLdapServerReachable()) {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Debug: *user LDAP password checked in the cache", FALSE, FALSE, 8888, 'Debug', "");
                              }
                              if ($this->GetUserLdapHashCache() === nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check)))) {
                                  $ldap_check_passed = TRUE;
                                  // TODO Write a specific message in the log
                              }
                          } else {
                              $ldap_check_passed = FALSE;
                              $ldap_to_check = '!LDAP_FALSE!';
                              $this->ResetUserLdapHashCache();
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Debug: *user LDAP password false, hash cache cleared", FALSE, FALSE, 8888, 'Debug', "");
                              }
                          }
                      } else { // If the LdapHash cache is disabled
                          $ldap_check_passed = FALSE;
                          $ldap_to_check = '!LDAP_FALSE!';
                          $this->ResetUserLdapHashCache();
                          if ($this->IsLdapServerReachable()) {
                              $this->WriteLog("Error: User $real_user LDAP password false", FALSE, FALSE, _MULTIOTP_LDAP_AUTH_ERROR_, 'User');
                          } else {
                              $this->WriteLog("Error: User $real_user verification failed, unreachable LDAP/AD server(s)", FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'User');
                          }
                      }
                  }
              } else {
                  // It is a real prefix pin, not an LDAP/AD prefix
                  $code_confirmed = ((($this->IsUserPrefixPin()) && (!$force_no_prefix_pin))?$this->GetUserPin():"").$one_password;
                  $this->SetLastClearOtpValue($code_confirmed);
                  if ("" != $this->GetChapPassword()) {
                      $code_confirmed = $this->CalculateChapPassword($code_confirmed);
                  } elseif ("" != $this->GetMsChapResponse()) {
                      $code_confirmed = $this->CalculateMsChapResponse($code_confirmed);
                  } elseif ("" != $this->GetMsChap2Response()) {
                      $clear_code_confirmed = $code_confirmed;
                      $code_confirmed = $this->CalculateMsChap2Response($real_rawUser, $code_confirmed);
                      if ($this->IsDeveloperMode()) {
                        $this->WriteLog("Developer: *CalculateMsChap2Response($real_rawUser, $clear_code_confirmed) for scratch password: $code_confirmed", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                      }
                  }
              }
              
              if ($ldap_check_passed || ($input_to_check === $code_confirmed)) {
                  // We are unlocking the regular token if needed
                  $this->SetUserErrorCounter(0);
                  $this->SetUserLocked(0);
                  // Finally, we update the last login of the user
                  $this->SetUserLastLogin($now_epoch);
                  $this->SetUserTokenLastLogin($now_epoch);
                  $this->RemoveUserUsedScratchPassword($one_password);
                  $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct scratch token

                  if ($cache_result_enabled) {
                      $this->SetUserLastCachedCredential(nullable_trim($input.' '.$input_sync));
                      $this->SetUserLastLoginForCache($now_epoch);
                  }
                  $this->SetUserLastSuccessCredential(nullable_trim($input.' '.$input_sync));

                  if (!$this->WriteUserData()) {
                      $result = _MULTIOTP_DATA_UPDATE_ERROR_; // ERROR: Unable to write the changes in the file
                      $this->WriteLog("Error: Unable to write the changes in the file for the user ".$real_user, FALSE, FALSE, $result, 'User');
                  } else {
                      $this->WriteLog("Ok: User ".$real_user." successfully logged in".$supplemental_login_info." with a scratch password", FALSE, FALSE, $result, 'User');
                  }
                  
                  if (_MULTIOTP_AUTH_OK_ == $result) {
                      $this->AddExtraRadiusInfo();
                  }
                  return $result;
              }
          }

          $push_trial = (($this->IsPushActivated() && $this->IsUserPushEnabled() && (!$multi_account_enabled)) ? 1 : 0);

          // Check if a code request per Push is done
          if ((!empty($this->GetUserMouid())) && (!empty($this->GetUserMosid()))) {
            $code_confirmed = 'push';
            $code_confirmed_upper = 'PUSH';
            $code_confirmed_camel = 'Push';
            $this->SetLastClearOtpValue($code_confirmed);
            if ("" != $this->GetChapPassword()) {
              $code_confirmed = mb_strtolower($this->CalculateChapPassword($code_confirmed),'UTF-8');
              $code_confirmed_upper = mb_strtoupper($this->CalculateChapPassword($code_confirmed_upper),'UTF-8');
              $code_confirmed_camel = mb_strtoupper($this->CalculateChapPassword($code_confirmed_camel),'UTF-8');
            } elseif ("" != $this->GetMsChapResponse()) {
              $code_confirmed = mb_strtolower($this->CalculateMsChapResponse($code_confirmed),'UTF-8');
              $code_confirmed_upper = mb_strtoupper($this->CalculateMsChapResponse($code_confirmed_upper),'UTF-8');
              $code_confirmed_camel = mb_strtoupper($this->CalculateMsChapResponse($code_confirmed_camel),'UTF-8');
            } elseif ("" != $this->GetMsChap2Response()) {
              $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed),'UTF-8');
              $code_confirmed_upper = mb_strtoupper($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_upper),'UTF-8');
              $code_confirmed_camel = mb_strtoupper($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_camel),'UTF-8');
            }
            // If something like 'push' or 'PUSH' or 'Push' is detected, we generate a push request
            if ((mb_strtolower($input_to_check,'UTF-8') === $code_confirmed) || (mb_strtoupper($input_to_check,'UTF-8') === $code_confirmed_upper) || (mb_strtoupper($input_to_check,'UTF-8') === $code_confirmed_camel)) {
              $push_trial = 0;
              $push_answer = $this->PushRequest(array(
                "title"    => $this->Translate('push_title', $this->GetUserLanguage()),
                "subtitle" => $device_description
              ));
              if (_MULTIOTP_PUSH_TIMEOUT_CODE_ == $push_answer) {
                $push_answer = "";
                $push_timeout_error = true;
                $disable_error_counter = true;
              } elseif (_MULTIOTP_PUSH_DENIED_CODE_ == $push_answer) {
                $push_answer = "";
                $push_denied_error = true;
                $disable_error_counter = true;
              }
              $input_to_check = $push_answer;
            }
          }
          
          // Check if a code request per SMS is done
          $code_confirmed = 'sms';
          $code_confirmed_upper = 'SMS';
          $code_confirmed_camel = 'Sms';
          $this->SetLastClearOtpValue($code_confirmed);
          if ("" != $this->GetChapPassword()) {
              $code_confirmed = mb_strtolower($this->CalculateChapPassword($code_confirmed),'UTF-8');
              $code_confirmed_upper = mb_strtoupper($this->CalculateChapPassword($code_confirmed_upper),'UTF-8');
              $code_confirmed_camel = mb_strtoupper($this->CalculateChapPassword($code_confirmed_camel),'UTF-8');
          } elseif ("" != $this->GetMsChapResponse()) {
              $code_confirmed = mb_strtolower($this->CalculateMsChapResponse($code_confirmed),'UTF-8');
              $code_confirmed_upper = mb_strtoupper($this->CalculateMsChapResponse($code_confirmed_upper),'UTF-8');
              $code_confirmed_camel = mb_strtoupper($this->CalculateMsChapResponse($code_confirmed_camel),'UTF-8');
          } elseif ("" != $this->GetMsChap2Response()) {
              $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed),'UTF-8');
              $code_confirmed_upper = mb_strtoupper($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_upper),'UTF-8');
              $code_confirmed_camel = mb_strtoupper($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_camel),'UTF-8');
          }
          
          // If something like 'sms' or 'SMS' is detected, we generate an SMS token
          if ((mb_strtolower($input_to_check,'UTF-8') === $code_confirmed) || (mb_strtoupper($input_to_check,'UTF-8') === $code_confirmed_upper) || (mb_strtoupper($input_to_check,'UTF-8') === $code_confirmed_camel)) {
              return $this->GenerateSmsToken();
          }

          // Check if a code request per Email is done
          $code_confirmed = 'email';
          $code_confirmed_upper = 'EMAIL';
          $code_confirmed_camel = 'Email';
          $this->SetLastClearOtpValue($code_confirmed);
          if ("" != $this->GetChapPassword()) {
              $code_confirmed = mb_strtolower($this->CalculateChapPassword($code_confirmed),'UTF-8');
              $code_confirmed_upper = mb_strtoupper($this->CalculateChapPassword($code_confirmed_upper),'UTF-8');
              $code_confirmed_camel = mb_strtoupper($this->CalculateChapPassword($code_confirmed_camel),'UTF-8');
          } elseif ("" != $this->GetMsChapResponse()) {
              $code_confirmed = mb_strtolower($this->CalculateMsChapResponse($code_confirmed),'UTF-8');
              $code_confirmed_upper = mb_strtoupper($this->CalculateMsChapResponse($code_confirmed_upper),'UTF-8');
              $code_confirmed_camel = mb_strtoupper($this->CalculateMsChapResponse($code_confirmed_camel),'UTF-8');
          } elseif ("" != $this->GetMsChap2Response()) {
              $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed),'UTF-8');
              $code_confirmed_upper = mb_strtoupper($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_upper),'UTF-8');
              $code_confirmed_camel = mb_strtoupper($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_camel),'UTF-8');
          }
          
          // If something like 'email', 'EMAIL' or 'Email' is detected, we generate an Email token
          if ((mb_strtolower($input_to_check,'UTF-8') === $code_confirmed) || (mb_strtoupper($input_to_check,'UTF-8') === $code_confirmed_upper) || (mb_strtoupper($input_to_check,'UTF-8') === $code_confirmed_camel)) {
              return $this->GenerateEmailToken();
          }

          // Cached result support
          if ($cache_result_enabled) {
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Debug: *code to check: ".str_repeat('x', (mb_strlen($input_to_check) >= 6)?mb_strlen($input_to_check)-6:0).mb_substr($input_to_check, -6), FALSE, FALSE, 8888, 'Debug', "");
              }
              if ($this->CompareUserLastCachedCredential(nullable_trim($input.' '.$input_sync))) {
                  if (($this->GetUserLastLoginForCache() + $this->GetDeviceCacheTimeout()) >= $now_epoch) {
                      $this->SetLastClearOtpValue(mb_substr($input_to_check, 0, mb_strpos($input_to_check.' ', ' ')));
                      $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct token for cached access, no update of the user record
                      if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Debug: *User ".$real_user." successfully confirmed for cached access", FALSE, FALSE, 8888, 'Debug', "");
                          $this->WriteLog("Debug: *checked code for cache access: ".str_repeat('x', (mb_strlen($input_to_check) >= 6)?mb_strlen($input_to_check)-6:0).mb_substr($input_to_check, -6), FALSE, FALSE, 8888, 'Debug', "");
                      }
                      $this->AddExtraRadiusInfo();
                      return $result;
                  } elseif (($this->GetUserLastLoginForCache() + $this->GetDeviceCacheTimeout() + 86400) >= $now_epoch) {
                      // During one day, no increase of the error counter on a cache enabled device if code is reused
                      if ($this->CompareUserLastCachedCredential(nullable_trim($input.' '.$input_sync))) {
                          $disable_error_counter = true;
                      }
                  }
              }
          }


          // TODO check multiple tokens (loop)

          $algorithm         = mb_strtolower($this->GetUserAlgorithm(),'UTF-8');
          $pin               = ($this->IsUserRequestLdapPasswordEnabled() ? "" : $this->GetUserPin());
          $need_prefix       = ($this->IsUserPrefixPin()) && (!$force_no_prefix_pin);
          $last_event        = $this->GetUserTokenLastEvent();
          $last_login        = $this->GetUserTokenLastLogin();
          $digits            = (('without2fa' == mb_strtolower($algorithm,'UTF-8')) ? 0 : $this->GetUserTokenNumberOfDigits());
          $error_counter     = intval($this->GetUserErrorCounter());
          $time_window       = $this->GetMaxTimeWindow();
          $event_window      = $this->GetMaxEventWindow();
          $time_sync_window  = $this->GetMaxTimeResyncWindow();
          $event_sync_window = $this->GetMaxEventResyncWindow();

          $seed              = $this->GetUserTokenSeed();
          $seed_bin          = hex2bin($seed);
          $private_id        = $this->GetUserTokenPrivateId();
          $delta_time        = $this->GetUserTokenDeltaTime();
          $time_interval     = $this->GetUserTokenTimeInterval();
          $token_algo_suite  = $this->GetUserTokenAlgoSuite();

          $interval = (0 >= $time_interval)?1:$time_interval;

          $now_steps         = intval($now_epoch / $interval);
          $step_window       = intval($time_window / $interval);
          $step_sync_window  = intval($time_sync_window / $interval);
          $last_login_step   = intval($last_login / $interval);
          $delta_step        = intval($delta_time) / intval($interval);

          $prefix_pin = ($need_prefix?$pin:"");
          // 4.3.2.2
          // Check if resynchronisation can be done automatically
          $needed_space_pos = (mb_strlen($input_to_check)-$digits-1);
          if (("" == $input_sync) && ($needed_space_pos >= $digits) && (($needed_space_pos === strrpos($input_to_check, ' ')) || (($needed_space_pos-mb_strlen($prefix_pin)) === strrpos($input_to_check, ' '))) && ($this->IsAutoResync())) {
              if (($need_prefix) && ($this->IsUserRequestLdapPasswordEnabled())) {
                  $ldap_to_check = mb_substr($input_to_check, 0, - ($digits + 1 + $digits));
                  if ("" != $ldap_to_check) {
                      if ($this->CheckUserLdapPassword($this->GetUserSynchronizedDn(), $ldap_to_check)) {
                          $input_sync = mb_substr($input_to_check, -$digits);
                          $input_to_check = mb_substr($input_to_check, 0, - ($digits + 1));
                      }
                  }
              } elseif ($prefix_pin === mb_substr($input_to_check, 0, mb_strlen($prefix_pin))) {
                      $separator_pos = strrpos($input_to_check, ' ');
                      $input_sync = str_replace($prefix_pin, "", mb_substr($input_to_check, $separator_pos+1));
                      $input_to_check = mb_substr($input_to_check, 0, $separator_pos);
              }
          }


          if ((1 == $this->GetUserLocked()) && ("" == $input_sync) && (!$resync_enc_pass) && (!$ignore_lock)) {
              $result = _MULTIOTP_USER_LOCKED_; // ERROR: User locked;
              $this->WriteLog("Error: User ".$real_user." locked after ".$this->GetUserErrorCounter()." failed authentications", FALSE, FALSE, $result, 'User');
          } elseif(($this->GetUserErrorCounter() >= $this->GetMaxDelayedFailures()) && ("" == $input_sync) && ($now_epoch < ($this->GetUserTokenLastError() + $this->GetMaxDelayedTime())) && (!$ignore_lock)) {
              $result = _MULTIOTP_USER_DELAYED_; // ERROR: User delayed;
              $delayed_time = ($this->GetUserTokenLastError() + $this->GetMaxDelayedTime()) - $now_epoch;
              $this->WriteLog("Error: User ".$real_user." still delayed for ".$delayed_time." seconds after ".$this->GetUserErrorCounter()." failed authentications", FALSE, FALSE, $result, 'User');
          } else {
              $ldap_check_passed = FALSE;
              $ldap_to_check = '!LDAP_FALSE!';
              if (($need_prefix) && ($this->IsUserRequestLdapPasswordEnabled())) {
                  if ($input_to_check != "") {
                      $ldap_to_check = mb_substr($input_to_check, 0, mb_strlen($input_to_check) - $digits);
                      if ("" != $ldap_to_check) {
                          if ($this->CheckUserLdapPassword($this->GetUserSynchronizedDn(), $ldap_to_check)) {
                              $ldap_check_passed = TRUE;
                              // TODO Write a specific message in the log
                              if ($this->IsCacheLdapHash()) {
                                  $this->SetUserLdapHashCache(nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check))));
                              }
                          } elseif ($this->IsCacheLdapHash()) {
                              if (!$this->IsLdapServerReachable()) {
                                  if ($this->GetVerboseFlag()) {
                                      $this->WriteLog("Debug: *user LDAP password checked in the cache", FALSE, FALSE, 8888, 'Debug', "");
                                  }
                                  if ($this->GetUserLdapHashCache() === nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check)))) {
                                      $ldap_check_passed = TRUE;
                                      // TODO Write a specific message in the log
                                  }
                              } else {
                                  $ldap_check_passed = FALSE;
                                  $ldap_to_check = '!LDAP_FALSE!';
                                  $this->ResetUserLdapHashCache();
                                  if ($this->GetVerboseFlag()) {
                                      $this->WriteLog("Debug: *user LDAP password false, hash cache cleared", FALSE, FALSE, 8888, 'Debug', "");
                                  }
                              }
                          } else { // If the LdapHash cache is disabled
                              $ldap_check_passed = FALSE;
                              $ldap_to_check = '!LDAP_FALSE!';
                              $this->ResetUserLdapHashCache();
                              if ($this->IsLdapServerReachable()) {
                                  $this->WriteLog("Error: User $real_user LDAP password false", FALSE, FALSE, _MULTIOTP_LDAP_AUTH_ERROR_, 'User');
                              } else {
                                  $this->WriteLog("Error: User $real_user verification failed, unreachable LDAP/AD server(s)", FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'User');
                              }
                          }
                      }
                  }
                  if (!$ldap_check_passed) {
                    $ldap_to_check = '!LDAP_FALSE!';
                    // Special LDAP check if push is enabled
                    if (($need_prefix) && ($this->IsPushActivated() && $this->IsUserPushEnabled() && (!$multi_account_enabled))) {
                        if ($input_to_check != "") {
                            $ldap_to_check = $input_to_check;
                            if ("" != $ldap_to_check) {
                                if ($this->CheckUserLdapPassword($this->GetUserSynchronizedDn(), $ldap_to_check)) {
                                    $ldap_check_passed = TRUE;
                                    // TODO Write a specific message in the log
                                    if ($this->IsCacheLdapHash()) {
                                        $this->SetUserLdapHashCache(nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check))));
                                    }
                                } elseif ($this->IsCacheLdapHash()) {
                                    if (!$this->IsLdapServerReachable()) {
                                        if ($this->GetVerboseFlag()) {
                                            $this->WriteLog("Debug: *user LDAP password checked in the cache", FALSE, FALSE, 8888, 'Debug', "");
                                        }
                                        if ($this->GetUserLdapHashCache() === nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check)))) {
                                            $ldap_check_passed = TRUE;
                                            // TODO Write a specific message in the log
                                        }
                                    } else {
                                        $ldap_check_passed = FALSE;
                                        $ldap_to_check = '!LDAP_FALSE!';
                                        $this->ResetUserLdapHashCache();
                                        if ($this->GetVerboseFlag()) {
                                            $this->WriteLog("Debug: *user LDAP password false, hash cache cleared", FALSE, FALSE, 8888, 'Debug', "");
                                        }
                                    }
                                } else { // If the LdapHash cache is disabled
                                    $ldap_check_passed = FALSE;
                                    $ldap_to_check = '!LDAP_FALSE!';
                                    $this->ResetUserLdapHashCache();
                                    if ($this->IsLdapServerReachable()) {
                                        $this->WriteLog("Error: User $real_user LDAP password false", FALSE, FALSE, _MULTIOTP_LDAP_AUTH_ERROR_, 'User');
                                    } else {
                                        $this->WriteLog("Error: User $real_user verification failed, unreachable LDAP/AD server(s)", FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'User');
                                    }
                                }
                            }
                        }
                    }
                    if (!$ldap_check_passed) {
                      $input_to_check = "LDAP_FAILED_".$input_to_check.'_LDAP_FAILED';
                      $result = _MULTIOTP_LDAP_AUTH_ERROR_;
                    }
                  }
              }

              switch (mb_strtolower($algorithm,'UTF-8')) {
                  case 'motp':
                      if (("" == $input_sync) && (!$resync_enc_pass)) {
                          $max_steps = 2 * $step_window;
                      } else {
                          $max_steps = 2 * $step_sync_window;
                      }

                      // BEGIN Check if a code request per Push is done
                      if ((!empty($this->GetUserMouid())) && (!empty($this->GetUserMosid()))) {
                        $code_confirmed = 'push';
                        $code_confirmed_upper = 'PUSH';
                        $code_confirmed_camel = 'Push';
                        $this->SetLastClearOtpValue($code_confirmed);
                        if ("" != $this->GetChapPassword()) {
                          $code_confirmed = mb_strtolower($this->CalculateChapPassword($code_confirmed),'UTF-8');
                          $code_confirmed_upper = mb_strtoupper($this->CalculateChapPassword($code_confirmed_upper),'UTF-8');
                          $code_confirmed_camel = mb_strtoupper($this->CalculateChapPassword($code_confirmed_camel),'UTF-8');
                        } elseif ("" != $this->GetMsChapResponse()) {
                          $code_confirmed = mb_strtolower($this->CalculateMsChapResponse($code_confirmed),'UTF-8');
                          $code_confirmed_upper = mb_strtoupper($this->CalculateMsChapResponse($code_confirmed_upper),'UTF-8');
                          $code_confirmed_camel = mb_strtoupper($this->CalculateMsChapResponse($code_confirmed_camel),'UTF-8');
                        } elseif ("" != $this->GetMsChap2Response()) {
                          $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed),'UTF-8');
                          $code_confirmed_upper = mb_strtoupper($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_upper),'UTF-8');
                          $code_confirmed_camel = mb_strtoupper($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_camel),'UTF-8');
                        }
                        // If something like 'push' or 'PUSH' or 'Push' is detected, we generate a push request
                        if ((mb_strtolower(substr($input_to_check, -4),'UTF-8') === $code_confirmed) || (mb_strtolower($input_to_check,'UTF-8') === $code_confirmed) || (mb_strtoupper($input_to_check,'UTF-8') === $code_confirmed_upper) || (mb_strtoupper($input_to_check,'UTF-8') === $code_confirmed_camel)) {
                          $push_trial = 0;
                          $push_answer = $this->PushRequest(array(
                            "title"    => $this->Translate('push_title', $this->GetUserLanguage()),
                            "subtitle" => $device_description
                          ));
                          if (_MULTIOTP_PUSH_TIMEOUT_CODE_ == $push_answer) {
                            $push_answer = "";
                            $push_timeout_error = true;
                            $disable_error_counter = true;
                          } elseif (_MULTIOTP_PUSH_DENIED_CODE_ == $push_answer) {
                            $push_answer = "";
                            $push_denied_error = true;
                            $disable_error_counter = true;
                          }
                          $input_to_check = $push_answer;
                        }
                      }
                      // END Check if a code request per Push is done
                      
                      // BEGIN Push trial loop)
                      do {
                        $push_trial--;

                        $check_step = 1;
                        do {
                            $additional_step = (1 - (2 * ($check_step % 2))) * intval($check_step/2);
                            $pure_calculated_token = $this->ComputeMotp($seed.$pin, $now_steps+$additional_step+$delta_step, $digits);
                            $calculated_token = $pure_calculated_token;
                            
                            if (($need_prefix) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                                $code_confirmed_without_pin = $calculated_token;
                                $code_confirmed = $calculated_token;
                                $input_to_check = mb_substr($input_to_check, -mb_strlen($code_confirmed));                            
                                $this->SetLastClearOtpValue($code_confirmed);
                            } else {
                                if ($need_prefix) {
                                    $calculated_token = $pin.$calculated_token;
                                }

                                $code_confirmed_without_pin = $pure_calculated_token;
                                $code_confirmed = $calculated_token;
                                $this->SetLastClearOtpValue($code_confirmed);
                                if ("" != $this->GetChapPassword()) {
                                    $code_confirmed_without_pin = mb_strtolower($this->CalculateChapPassword($code_confirmed_without_pin),'UTF-8');
                                    $code_confirmed = mb_strtolower($this->CalculateChapPassword($code_confirmed),'UTF-8');
                                } elseif ("" != $this->GetMsChapResponse()) {
                                    $code_confirmed_without_pin = mb_strtolower($this->CalculateMsChapResponse($code_confirmed_without_pin),'UTF-8');
                                    $code_confirmed = mb_strtolower($this->CalculateMsChapResponse($code_confirmed),'UTF-8');
                                } elseif ("" != $this->GetMsChap2Response()) {
                                    $clear_code_confirmed = $code_confirmed;
                                    $code_confirmed_without_pin = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_without_pin),'UTF-8');
                                    $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed),'UTF-8');
                                    if ($this->IsDeveloperMode()) {
                                      $this->WriteLog("Developer: *CalculateMsChap2Response($real_rawUser, $clear_code_confirmed) for motp: $code_confirmed", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                    }
                                }
                            }
                            
                            if (("" == $input_sync) && (!$resync_enc_pass)) {
                                if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Debug: *CheckToken intermediate result1 (motp) ".__LINE__.", input_to_check: {$input_to_check} code_confirmed_without_pin: {$code_confirmed_without_pin}", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                }
                                // With mOTP, the code should not be prefixed, so we accept of course always input without prefix!
                                if (($input_to_check === $code_confirmed) || ($input_to_check === $code_confirmed_without_pin)) {
                                    if ($input_to_check === $code_confirmed_without_pin) {
                                        $code_confirmed = $code_confirmed_without_pin;
                                    }
                                    if (($now_steps+$additional_step+$delta_step) > $last_login_step) {
                                        $this->SetUserLastLogin($now_epoch);
                                        $this->SetUserTokenLastLogin(($now_steps+$additional_step+$delta_step) * $interval);
                                        $this->SetUserTokenDeltaTime(($additional_step+$delta_step) * $interval);
                                        $this->SetUserErrorCounter(0);
                                        $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct token
                                        $push_trial = -1;
                                        $this->WriteLog("Ok: User ".$real_user." successfully logged in".$supplemental_login_info." with mOTP token", FALSE, FALSE, $result, 'User');
                                    } else {
                                        $result = _MULTIOTP_TOKEN_REUSED_; // ERROR: this token has already been used
                                        $push_trial = -1;
                                        if ($this->CompareUserLastSuccessCredential(nullable_trim($input.' '.$input_sync))) {
                                            $disable_error_counter = true;
                                        }
                                        if (!$disable_error_counter) {
                                            $this->SetUserErrorCounter($error_counter+1);
                                        }
                                        $this->SetUserTokenLastError($now_epoch);
                                        $this->WriteLog("Error: token of user ".$real_user." already used", FALSE, FALSE, $result, 'User');
                                    }
                                } else {
                                    $check_step++;
                                }
                            } elseif (($input_to_check === $code_confirmed) || ($input_to_check === $code_confirmed_without_pin)) {
                                $pure_sync_calculated_token = $this->ComputeMotp($seed.$pin, $now_steps+$additional_step+$delta_step+1, $digits);
                                $sync_calculated_token = $pure_sync_calculated_token;
                                
                                if (($need_prefix) && ($input_sync != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                                    $input_sync = mb_substr($input_sync, -mb_strlen($code_confirmed));                            
                                } elseif ($need_prefix) {
                                    $sync_calculated_token = $pin.$sync_calculated_token;
                                }
                                if ((($input_sync === $sync_calculated_token) || ($input_sync === $pure_sync_calculated_token)) && (($now_steps+$additional_step+$delta_step+1) > $last_login_step)) {
                                    $this->SetUserLastLogin($now_epoch);
                                    $this->SetUserTokenLastLogin(($now_steps+$additional_step+$delta_step+1) * $interval);
                                    $this->SetUserTokenDeltaTime(($additional_step+$delta_step+1) * $interval);
                                    $this->SetUserErrorCounter(0);
                                    $this->SetUserLocked(0);
                                    $result = _MULTIOTP_TOKEN_RESYNCED_; // INFO: token is now synchronized
                                    $push_trial = -1;
                                    $this->WriteLog("Info: token for user ".$real_user." is now resynchronized with a delta of ".(($additional_step+$delta_step+1) * $interval). " seconds", FALSE, FALSE, $result, 'User');
                                    $result = _MULTIOTP_AUTH_OK_; // INFO: authentication is successful, regardless of the PIN code if needed, as the PIN code is already used to generate the token
                                    $push_trial = -1;
                                } else {
                                    $result = _MULTIOTP_TOKEN_RESYNC_FAILED_; // ERROR: resync failed
                                    $push_trial = -1;
                                    $this->WriteLog("Error: resync for user ".$real_user." has failed", FALSE, FALSE, $result, 'User');
                                }
                            } else {
                                $check_step++;
                                if ($display_status) {
                                    MultiotpShowStatus($check_step, $max_steps);
                                }
                            }
                        } while (($check_step < $max_steps) && ($result >= 90));

                        //BEGIN push tests
                        if (($result >= 90) && ($push_trial >= 0)) {
                          $push_precheck = FALSE;
                          if (($need_prefix) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                            if (!$ldap_check_passed) {
                              $input_to_check.= '_BAD_LDAP_CHECK';
                            } else {
                              $push_precheck = TRUE;
                            }
                          } else {
                            if ($need_prefix) {
                              $code_to_check = $pin;
                              if ($code_to_check != $input_to_check) {

                                // Request could by hashed
                                if ("" != $this->GetChapPassword()) {
                                  $code_to_check = mb_strtolower($this->CalculateChapPassword($pin),'UTF-8');
                                  if ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *CalculateChapPassword()", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  }
                                } elseif ("" != $this->GetMsChapResponse()) {
                                  $code_to_check = mb_strtolower($this->CalculateMsChapResponse($pin),'UTF-8');
                                  if ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *CalculateMsChapResponse()", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  }
                                } elseif ("" != $this->GetMsChap2Response()) {
                                  $code_to_check = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $pin),'UTF-8');
                                  if ($this->IsDeveloperMode()) {
                                    $this->WriteLog("Developer: *CalculateMsChap2Response($real_rawUser, $pin) for without2fa: $code_to_check", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  } elseif ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *CalculateMsChap2Response()", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  }
                                }

                                if ($code_to_check != $input_to_check) {
                                  $input_to_check.= '_BAD_PREFIX';
                                  $this->SetLastClearOtpValue($input_to_check);
                                } else {
                                  $push_precheck = TRUE;
                                }
                                  
                              } elseif (!empty($input_to_check)) {
                                $push_precheck = TRUE;
                              }
                            } elseif ($input_is_empty) {
                              $push_precheck = TRUE;
                            }
                          }
                          if ($push_precheck) {
                            $this->WriteLog("Info: Push request for user $real_user", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'User', $real_user);
                            $push_answer = $this->PushRequest(array(
                              "title"    => $this->Translate('push_title', $this->GetUserLanguage()),
                              "subtitle" => $device_description
                            ));
                            if (_MULTIOTP_PUSH_TIMEOUT_CODE_ == $push_answer) {
                              $push_answer = "";
                              $push_timeout_error = true;
                              $disable_error_counter = true;
                            } elseif (_MULTIOTP_PUSH_DENIED_CODE_ == $push_answer) {
                              $push_answer = "";
                              $push_denied_error = true;
                              $disable_error_counter = true;
                            }
                            $input_to_check = $push_answer; // No prefix PIN for mOTP
                            $this->WriteLog("Info: OTP ($algorithm) received by push for user $real_user", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'User', $real_user);
                          }
                          // END push tests
                        }

                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Debug: *CheckToken intermediate result2 (motp) ".__LINE__.", push_trial: {$push_trial} result: {$result}", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                        }

                      } while ($push_trial >= 0);
                      // END Push trial loop)
                                            
                      if ($display_status) {
                        echo "\r\n";
                      }
                      if ($result >= 90) {
                        if ($this->CompareUserLastFailedCredential(nullable_trim($input.' '.$input_sync))) {
                          $disable_error_counter = true;
                        }
                        if (!$disable_error_counter) {
                          $this->SetUserErrorCounter($error_counter+1);
                        }
                        $this->SetUserTokenLastError($now_epoch);
                      }
                      break;
                  case 'hotp';
                      if (("" == $input_sync)&& (!$resync_enc_pass)) {
                          $max_steps = 2 * $event_window;
                      } else {
                          $max_steps = 2 * $event_sync_window;
                      }
                      $check_step = 1;
                      do {
                          $additional_step = (1 - (2 * ($check_step % 2))) * intval($check_step/2);
                          $pure_calculated_token = $this->GenerateOathHotp($seed_bin,$last_event+$additional_step,$digits,$token_algo_suite);
                          $calculated_token = $pure_calculated_token;
                          if (($need_prefix) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                              $code_confirmed_without_pin = $calculated_token;
                              $code_confirmed = $calculated_token;
                              $input_to_check = mb_substr($input_to_check, -mb_strlen($code_confirmed));                            
                              $this->SetLastClearOtpValue($code_confirmed);
                          } else {
                              if ($need_prefix) {
                                  $calculated_token = $pin.$calculated_token;
                              }
                              
                              $code_confirmed_without_pin = $pure_calculated_token;
                              $code_confirmed = $calculated_token;
                              $this->SetLastClearOtpValue($code_confirmed);
                              if ("" != $this->GetChapPassword()) {
                                  $code_confirmed_without_pin = mb_strtolower($this->CalculateChapPassword($code_confirmed_without_pin),'UTF-8');
                                  $code_confirmed = mb_strtolower($this->CalculateChapPassword($code_confirmed),'UTF-8');
                              } elseif ("" != $this->GetMsChapResponse()) {
                                  $code_confirmed_without_pin = mb_strtolower($this->CalculateMsChapResponse($code_confirmed_without_pin),'UTF-8');
                                  $code_confirmed = mb_strtolower($this->CalculateMsChapResponse($code_confirmed),'UTF-8');
                              } elseif ("" != $this->GetMsChap2Response()) {
                                  $clear_code_confirmed = $code_confirmed;
                                  $code_confirmed_without_pin = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_without_pin),'UTF-8');
                                  $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed),'UTF-8');
                                  if ($this->IsDeveloperMode()) {
                                    $this->WriteLog("Developer: *CalculateMsChap2Response($real_rawUser, $clear_code_confirmed) for hotp: $code_confirmed", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  }
                              }
                          }
                          
                          if (("" == $input_sync) && (!$resync_enc_pass)) {
                              if ($input_to_check === $code_confirmed) {
                                  if ($additional_step >= 1) {
                                      $this->SetUserLastLogin($now_epoch);
                                      $this->SetUserTokenLastLogin($now_epoch);
                                      $this->SetUserTokenLastEvent($last_event+$additional_step);
                                      $this->SetUserErrorCounter(0);
                                      $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct token
                                      $this->WriteLog("OK: User ".$real_user." successfully logged in".$supplemental_login_info." with HOTP token", FALSE, FALSE, $result, 'User');
                                  } else {
                                      $result = _MULTIOTP_TOKEN_REUSED_; // ERROR: this token has already been used
                                      if ($this->CompareUserLastSuccessCredential(nullable_trim($input.' '.$input_sync))) {
                                          $disable_error_counter = true;
                                      }
                                      if (!$disable_error_counter) {
                                          $this->SetUserErrorCounter($error_counter+1);
                                      }
                                      $this->SetUserTokenLastError($now_epoch);
                                      $this->WriteLog("Error: token of user ".$real_user." already used", FALSE, FALSE, $result, 'User');
                                  }
                              } else {
                                  $check_step++;
                              }
                          } elseif (($input_to_check === $code_confirmed) || ($input_to_check === $code_confirmed_without_pin)) {
                              $pure_sync_calculated_token = $this->GenerateOathHotp($seed_bin, $last_event+$additional_step+1,$digits,$token_algo_suite);
                              $sync_calculated_token = $pure_sync_calculated_token;
                              
                              if (($need_prefix) && ($input_sync != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                                  $input_sync = mb_substr($input_sync, -mb_strlen($code_confirmed));                            
                              } elseif ($need_prefix) {
                                  $sync_calculated_token = $pin.$sync_calculated_token;
                              }
                              if ((($input_sync === $sync_calculated_token) || ($input_sync === $pure_sync_calculated_token)) && ($additional_step >= 1)) {
                                  $this->SetUserLastLogin($now_epoch);
                                  $this->SetUserTokenLastLogin($now_epoch);
                                  $this->SetUserTokenLastEvent($last_event+$additional_step+1);
                                  $this->SetUserErrorCounter(0);
                                  $this->SetUserLocked(0);
                                  $result = _MULTIOTP_TOKEN_RESYNCED_; // INFO: token is now synchronized
                                  $this->WriteLog("Info: token for user ".$real_user." is now resynchronized with the last event ".($last_event+$additional_step+1), FALSE, FALSE, $result, 'User');
                                  if ($input_to_check === $code_confirmed) {
                                      $result = _MULTIOTP_AUTH_OK_; // INFO: authentication is successful, as the prefix has also been typed (if any)
                                  }
                              } else {
                                  $result = _MULTIOTP_TOKEN_RESYNC_FAILED_; // ERROR: resync failed
                                  $this->WriteLog("Error: resync for user ".$real_user." has failed", FALSE, FALSE, $result, 'User');
                              }
                          } else {
                              $check_step++;
                              if ($display_status) {
                                  MultiotpShowStatus($check_step, $max_steps);
                              }
                          }
                      } while (($check_step < $max_steps) && (($result >= 90)));
                      if ($display_status) {
                        echo "\r\n";
                      }
                      if ($result >= 90) {
                        if ($this->CompareUserLastFailedCredential(nullable_trim($input.' '.$input_sync))) {
                          $disable_error_counter = true;
                        }
                        if (!$disable_error_counter) {
                          $this->SetUserErrorCounter($error_counter+1);
                        }
                        $this->SetUserTokenLastError($now_epoch);
                      }
                      break;
                  case 'yubicootp';
                      $yubikey_class = new MultiotpYubikey();
                      $bad_precheck = FALSE;
                      if (($need_prefix) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                          if (!$ldap_check_passed) {
                              $input_to_check.= '_BAD_LDAP_CHECK';
                              $bad_precheck = TRUE;
                          }
                          $this->SetLastClearOtpValue($input_to_check);
                      } else {
                          if ($need_prefix) {
                              if ($pin != mb_substr($input_to_check, 0, mb_strlen($pin))) {
                                  $this->SetLastClearOtpValue($input_to_check);
                                  $input_to_check.= '_BAD_PREFIX';
                                  $bad_precheck = TRUE;
                              }
                          }
                      }

                      if (!$bad_precheck) {
                          // Check only the last 32 digits, the first 12 are the serial number
                          // $uid = nullable_bin2hex(mb_substr($decrypted_part,  0, 6));
                          $result = $yubikey_class->CheckYubicoOtp(mb_substr($input_to_check, -32),
                                                                   $seed,
                                                                   $last_event,
                                                                   $private_id);
                      }

                      if (_MULTIOTP_AUTH_OK_ == $result) {
                          $calculated_token = $input_to_check;
                          $this->SetUserLastLogin($now_epoch);
                          $this->SetUserTokenLastLogin($now_epoch);
                          $this->SetUserTokenLastEvent($yubikey_class->GetYubicoOtpLastCount());
                          $this->SetUserErrorCounter(0);
                          $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct token
                          $this->WriteLog("OK: User ".$real_user." successfully logged in".$supplemental_login_info." with YubicoOTP token", FALSE, FALSE, $result, 'User');
                      } elseif (_MULTIOTP_TOKEN_REUSED_ == $result) {
                          $result = _MULTIOTP_TOKEN_REUSED_; // ERROR: this token has already been used
                          if ($this->CompareUserLastSuccessCredential(nullable_trim($input.' '.$input_sync))) {
                              $disable_error_counter = true;
                          }
                          if (!$disable_error_counter) {
                              $this->SetUserErrorCounter($error_counter+1);
                              // Was previously simply $this->SetUserErrorCounter(1);
                          }
                          $this->SetUserTokenLastError($now_epoch);
                          $this->WriteLog("Error: token of user ".$real_user." already used", FALSE, FALSE, $result, 'User');
                      } else {
                          if ($this->CompareUserLastFailedCredential(nullable_trim($input.' '.$input_sync))) {
                              $disable_error_counter = true;
                          }
                          if (!$disable_error_counter) {
                              $this->SetUserErrorCounter($error_counter+1);
                          }
                          $this->SetUserTokenLastError($now_epoch);
                      }
                      break;
                  case 'totp';

                      if (("" == $input_sync) && (!$resync_enc_pass)) {
                          $max_steps = 2 * $step_window;
                      } else {
                          $max_steps = 2 * $step_sync_window;
                      }
                      
                      // BEGIN Check if a code request per Push is done
                      if ((!empty($this->GetUserMouid())) && (!empty($this->GetUserMosid()))) {
                        $code_confirmed = 'push';
                        $code_confirmed_upper = 'PUSH';
                        $code_confirmed_camel = 'Push';
                        $this->SetLastClearOtpValue($code_confirmed);
                        if ("" != $this->GetChapPassword()) {
                          $code_confirmed = mb_strtolower($this->CalculateChapPassword($code_confirmed),'UTF-8');
                          $code_confirmed_upper = mb_strtoupper($this->CalculateChapPassword($code_confirmed_upper),'UTF-8');
                          $code_confirmed_camel = mb_strtoupper($this->CalculateChapPassword($code_confirmed_camel),'UTF-8');
                        } elseif ("" != $this->GetMsChapResponse()) {
                          $code_confirmed = mb_strtolower($this->CalculateMsChapResponse($code_confirmed),'UTF-8');
                          $code_confirmed_upper = mb_strtoupper($this->CalculateMsChapResponse($code_confirmed_upper),'UTF-8');
                          $code_confirmed_camel = mb_strtoupper($this->CalculateMsChapResponse($code_confirmed_camel),'UTF-8');
                        } elseif ("" != $this->GetMsChap2Response()) {
                          $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed),'UTF-8');
                          $code_confirmed_upper = mb_strtoupper($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_upper),'UTF-8');
                          $code_confirmed_camel = mb_strtoupper($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_camel),'UTF-8');
                        }
                        // If something like 'push' or 'PUSH' or 'Push' is detected, we generate a push request
                        if ((mb_strtolower(substr($input_to_check, -4),'UTF-8') === $code_confirmed) || (mb_strtolower($input_to_check,'UTF-8') === $code_confirmed) || (mb_strtoupper($input_to_check,'UTF-8') === $code_confirmed_upper) || (mb_strtoupper($input_to_check,'UTF-8') === $code_confirmed_camel)) {
                          $push_trial = 0;
                          $push_answer = $this->PushRequest(array(
                            "title"    => $this->Translate('push_title', $this->GetUserLanguage()),
                            "subtitle" => $device_description
                          ));
                          if (_MULTIOTP_PUSH_TIMEOUT_CODE_ == $push_answer) {
                            $push_answer = "";
                            $push_timeout_error = true;
                            $disable_error_counter = true;
                          } elseif (_MULTIOTP_PUSH_DENIED_CODE_ == $push_answer) {
                            $push_answer = "";
                            $push_denied_error = true;
                            $disable_error_counter = true;
                          }
                          $input_to_check = $push_answer;
                        }
                      }
                      // END Check if a code request per Push is done
                      
                      do {
                        $push_trial--;
                      
                        $check_step = 1;
                        do {
                            $additional_step = (1 - (2 * ($check_step % 2))) * intval($check_step/2);
                            $pure_calculated_token = $this->GenerateOathHotp($seed_bin,$now_steps+$additional_step+$delta_step,$digits,$token_algo_suite);
                            $calculated_token = $pure_calculated_token;
                            
                            if (($need_prefix) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                                $code_confirmed_without_pin =  $calculated_token;
                                $code_confirmed = $calculated_token;
                                $input_to_check = mb_substr($input_to_check, -mb_strlen($code_confirmed));                            
                                $this->SetLastClearOtpValue($code_confirmed);
                            } else {
                                if ($need_prefix) {
                                    $calculated_token = $pin.$calculated_token;
                                }

                                $code_confirmed_without_pin = $pure_calculated_token;
                                $code_confirmed = $calculated_token;
                                $this->SetLastClearOtpValue($code_confirmed);
                                if ("" != $this->GetChapPassword()) {
                                    $code_confirmed_without_pin = mb_strtolower($this->CalculateChapPassword($code_confirmed_without_pin),'UTF-8');
                                    $code_confirmed = mb_strtolower($this->CalculateChapPassword($code_confirmed),'UTF-8');
                                } elseif ("" != $this->GetMsChapResponse()) {
                                    $code_confirmed_without_pin = mb_strtolower($this->CalculateMsChapResponse($code_confirmed_without_pin),'UTF-8');
                                    $code_confirmed = mb_strtolower($this->CalculateMsChapResponse($code_confirmed),'UTF-8');
                                } elseif ("" != $this->GetMsChap2Response()) {
                                    $clear_code_confirmed = $code_confirmed;
                                    $code_confirmed_without_pin = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_without_pin),'UTF-8');
                                    $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed),'UTF-8');
                                    if ($this->IsDeveloperMode()) {
                                      $this->WriteLog("Developer: *CalculateMsChap2Response($real_rawUser, $clear_code_confirmed) for totp: $code_confirmed", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                    }
                                }
                            }
                            
                            if (("" == $input_sync) && (!$resync_enc_pass)) {
                                if ($input_to_check === $code_confirmed) {
                                    if (($now_steps+$additional_step+$delta_step) > $last_login_step) {
                                        $this->SetUserLastLogin($now_epoch);
                                        $this->SetUserTokenLastLogin(($now_steps+$additional_step+$delta_step) * $interval);
                                        $this->SetUserTokenDeltaTime(($additional_step+$delta_step) * $interval);
                                        $this->SetUserErrorCounter(0);
                                        $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct token
                                        $push_trial = -1;
                                        $this->WriteLog("OK: User ".$real_user." successfully logged in".$supplemental_login_info." with TOTP token", FALSE, FALSE, $result, 'User');
                                    } else {
                                        $result = _MULTIOTP_TOKEN_REUSED_; // ERROR: this token has already been used
                                        $push_trial = -1;
                                        if ($this->CompareUserLastSuccessCredential(nullable_trim($input.' '.$input_sync))) {
                                            $disable_error_counter = true;
                                        }
                                        if (!$disable_error_counter) {
                                            $this->SetUserErrorCounter($error_counter+1);
                                        }
                                        $this->SetUserTokenLastError($now_epoch);
                                        $this->WriteLog("Error: token of user ".$real_user." already used", FALSE, FALSE, $result, 'User');
                                    }
                                } else {
                                    $check_step++;
                                }
                            } elseif (($input_to_check === $code_confirmed) || ($input_to_check === $code_confirmed_without_pin)) {
                                $pure_sync_calculated_token = $this->GenerateOathHotp($seed_bin,$now_steps+$additional_step+$delta_step+1,$digits,$token_algo_suite);
                                $sync_calculated_token = $pure_sync_calculated_token;
                                
                                if (($need_prefix) && ($input_sync != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                                    $input_sync = mb_substr($input_sync, -mb_strlen($code_confirmed));                            
                                } elseif ($need_prefix) {
                                    $sync_calculated_token = $pin.$sync_calculated_token;
                                }
                                if ((($input_sync === $sync_calculated_token) || ($input_sync === $pure_sync_calculated_token)) && (($now_steps+$additional_step+$delta_step) > $last_login_step)) {
                                    $this->SetUserLastLogin($now_epoch);
                                    $this->SetUserTokenLastLogin(($now_steps+$additional_step+$delta_step+1) * $interval);
                                    $this->SetUserTokenDeltaTime(($additional_step+$delta_step+1) * $interval);
                                    $this->SetUserErrorCounter(0);
                                    $this->SetUserLocked(0);
                                    $result = _MULTIOTP_TOKEN_RESYNCED_; // INFO: token is now synchronized
                                    $push_trial = -1;
                                    $this->WriteLog("Info: token for user ".$real_user." is now resynchronized with a delta of ".(($additional_step+$delta_step+1) * $interval). " seconds", FALSE, FALSE, $result, 'User');
                                    if ($input_to_check === $code_confirmed) {
                                        $result = _MULTIOTP_AUTH_OK_; // INFO: authentication is successful, as the prefix has also been typed (if any)
                                        $push_trial = -1;
                                    }
                                } else {
                                    $result = _MULTIOTP_TOKEN_RESYNC_FAILED_; // ERROR: resync failed
                                    $push_trial = -1;
                                    $this->WriteLog("Error: resync for user ".$real_user." has failed", FALSE, FALSE, $result, 'User');
                                }
                            } else {
                                $check_step++;
                                if ($display_status) {
                                    MultiotpShowStatus($check_step, $max_steps);
                                }
                            }
                        } while (($check_step < $max_steps) && ($result >= 90));


                        if ($this->GetVerboseFlag()) {
                          $this->WriteLog("Debug: *CheckToken intermediate result (totp) ".__LINE__.", push_trial: {$push_trial} result: {$result}", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                        }

                        //BEGIN push tests
                        if (($result >= 90) && ($push_trial >= 0)) {
                          $push_precheck = FALSE;
                          if (($need_prefix) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                            if (!$ldap_check_passed) {
                              $input_to_check.= '_BAD_LDAP_CHECK';
                            } else {
                              $push_precheck = TRUE;
                            }
                          } else {
                            if ($need_prefix) {
                              $code_to_check = $pin;
                              if ($code_to_check != $input_to_check) {

                                // Request could by hashed
                                if ("" != $this->GetChapPassword()) {
                                  $code_to_check = mb_strtolower($this->CalculateChapPassword($pin),'UTF-8');
                                  if ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *CalculateChapPassword()", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  }
                                } elseif ("" != $this->GetMsChapResponse()) {
                                  $code_to_check = mb_strtolower($this->CalculateMsChapResponse($pin),'UTF-8');
                                  if ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *CalculateMsChapResponse()", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  }
                                } elseif ("" != $this->GetMsChap2Response()) {
                                  $code_to_check = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $pin),'UTF-8');
                                  if ($this->IsDeveloperMode()) {
                                    $this->WriteLog("Developer: *CalculateMsChap2Response($real_rawUser, $pin) for without2fa: $code_to_check", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  } elseif ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *CalculateMsChap2Response()", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  }
                                }

                                if ($code_to_check != $input_to_check) {
                                  $input_to_check.= '_BAD_PREFIX';
                                  $this->SetLastClearOtpValue($input_to_check);
                                } else {
                                  $push_precheck = TRUE;
                                }
                                  
                              } elseif (!empty($input_to_check)) {
                                $push_precheck = TRUE;
                              }
                            } elseif ($input_is_empty) {
                              $push_precheck = TRUE;
                            }
                          }
                          if ($push_precheck) {
                            $this->WriteLog("Info: Push request for user $real_user", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'User', $real_user);
                            $push_answer = $this->PushRequest(array(
                              "title"    => $this->Translate('push_title', $this->GetUserLanguage()),
                              "subtitle" => $device_description
                            ));
                            if (_MULTIOTP_PUSH_TIMEOUT_CODE_ == $push_answer) {
                              $push_answer = "";
                              $push_timeout_error = true;
                              $disable_error_counter = true;
                            } elseif (_MULTIOTP_PUSH_DENIED_CODE_ == $push_answer) {
                              $push_answer = "";
                              $push_denied_error = true;
                              $disable_error_counter = true;
                            }
                            $input_to_check = ($need_prefix ? $pin : '') . $push_answer;
                            $this->WriteLog("Info: OTP ($algorithm) received by push for user $real_user", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'User', $real_user);
                          }
                          // Push tests END
                        }
                      } while ($push_trial >= 0);


                      if ($display_status) {
                        echo "\r\n";
                      }
                      if ($result >= 90) {
                          
                          // BEGIN to give an info if time based token is probably out of sync (in a window 10 time bigger)
                          do {
                              $additional_step = (1 - (2 * ($check_step % 2))) * intval($check_step/2);
                              $pure_calculated_token = $this->GenerateOathHotp($seed_bin,$now_steps+$additional_step+$delta_step,$digits,$token_algo_suite);
                              $calculated_token = $pure_calculated_token;
                              
                              if (($need_prefix) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                                  $code_confirmed_without_pin =  $calculated_token;
                                  $code_confirmed = $calculated_token;
                                  $input_to_check = mb_substr($input_to_check, -mb_strlen($code_confirmed));                            
                                  $this->SetLastClearOtpValue($code_confirmed);
                              } else {
                                  if ($need_prefix) {
                                      $calculated_token = $pin.$calculated_token;
                                  }

                                  $code_confirmed_without_pin = $pure_calculated_token;
                                  $code_confirmed = $calculated_token;
                                  $this->SetLastClearOtpValue($code_confirmed);
                                  if ("" != $this->GetChapPassword()) {
                                      $code_confirmed_without_pin = mb_strtolower($this->CalculateChapPassword($code_confirmed_without_pin),'UTF-8');
                                      $code_confirmed = mb_strtolower($this->CalculateChapPassword($code_confirmed),'UTF-8');
                                  } elseif ("" != $this->GetMsChapResponse()) {
                                      $code_confirmed_without_pin = mb_strtolower($this->CalculateMsChapResponse($code_confirmed_without_pin),'UTF-8');
                                      $code_confirmed = mb_strtolower($this->CalculateMsChapResponse($code_confirmed),'UTF-8');
                                  } elseif ("" != $this->GetMsChap2Response()) {
                                      $clear_code_confirmed = $code_confirmed;
                                      $code_confirmed_without_pin = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed_without_pin),'UTF-8');
                                      $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $code_confirmed),'UTF-8');
                                  if ($this->IsDeveloperMode()) {
                                        $this->WriteLog("Developer: *CalculateMsChap2Response($real_rawUser, $clear_code_confirmed) for totp: $code_confirmed", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                      }
                                  }
                              }
                             
                              if (("" == $input_sync) && (!$resync_enc_pass) && ($input_to_check === $code_confirmed)) {
                                  $result = _MULTIOTP_AUTH_TOTP_OUTOFSYNC_ERROR_; // ERROR: Authentication failed (time based token probably out of sync)
                              } else {
                                  $check_step++;
                              }
                          } while (($check_step < (10 * $max_steps)) && (_MULTIOTP_AUTH_TOTP_OUTOFSYNC_ERROR_ != $result));
                          // END to give an info if time based token is probably out of sync (in a window 10 time bigger)

                          
                          if ($this->CompareUserLastFailedCredential(nullable_trim($input.' '.$input_sync))) {
                              $disable_error_counter = true;
                          }
                          if (!$disable_error_counter) {
                              $this->SetUserErrorCounter(intval($error_counter)+1);
                          }
                          $this->SetUserTokenLastError($now_epoch);
                      }
                      break;
                  case 'without2fa';
                      // NO without2fa token as multi_account authentication
                      if ($multi_account_enabled) {
                        $result = _MULTIOTP_WITHOUT2FA_TOKEN_UNAUTHORIZED_;
                        $this->WriteLog("Error: authentication failed".$supplemental_login_info." for user ".$real_user." (without2fa token not authorized here)", FALSE, FALSE, $result, 'User');
                        return $result;
                      }

                      $result = _MULTIOTP_AUTH_PASSWORD_FAILED_; // ERROR: Authentication failed (bad password)
                      $bad_precheck = FALSE;
                      if (($need_prefix) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                          if (!$ldap_check_passed) {
                              $input_to_check.= '_BAD_LDAP_CHECK';
                              $bad_precheck = TRUE;
                          }
                          $this->SetLastClearOtpValue($input_to_check);
                      } else {
                          if ($need_prefix) {
                              $code_to_check = $pin;
                              if ($code_to_check != $input_to_check) { // if ($code_to_check != mb_substr($input_to_check, 0, mb_strlen($code_to_check))) {

                                // Request could by hashed
                                if ("" != $this->GetChapPassword()) {
                                  $code_to_check = mb_strtolower($this->CalculateChapPassword($pin),'UTF-8');
                                  if ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *CalculateChapPassword()", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  }
                                } elseif ("" != $this->GetMsChapResponse()) {
                                  $code_to_check = mb_strtolower($this->CalculateMsChapResponse($pin),'UTF-8');
                                  if ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *CalculateMsChapResponse()", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  }
                                } elseif ("" != $this->GetMsChap2Response()) {
                                  $code_to_check = mb_strtolower($this->CalculateMsChap2Response($real_rawUser, $pin),'UTF-8');
                                  if ($this->IsDeveloperMode()) {
                                    $this->WriteLog("Developer: *CalculateMsChap2Response($real_rawUser, $pin) for without2fa: $code_to_check", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  } elseif ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *CalculateMsChap2Response()", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
                                  }
                                }

                                if ($code_to_check != $input_to_check) {
                                  $input_to_check.= '_BAD_PREFIX';
                                  $bad_precheck = TRUE;
                                  $this->SetLastClearOtpValue($input_to_check);
                                }
                                  
                              }
                          } elseif (!$input_is_empty) {
                            $bad_precheck = TRUE;
                          }
                      }
                      if (!$bad_precheck) {
                          // $code_confirmed = ($need_prefix ? (mb_substr($input_to_check, mb_strlen($pin))) : $input_to_check);
                          // if ("" == $code_confirmed) {
                              $this->SetUserLastLogin($now_epoch);
                              $this->SetUserTokenLastLogin($now_epoch);
                              $this->SetUserErrorCounter(0);
                              $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct token
                              $this->WriteLog("OK: User ".$real_user." successfully logged in".$supplemental_login_info." without 2FA token", FALSE, FALSE, $result, 'User');
                          // }
                      } else {
                          if ($this->CompareUserLastFailedCredential(nullable_trim($input.' '.$input_sync))) {
                              $disable_error_counter = true;
                          }
                          if (!$disable_error_counter) {
                              $this->SetUserErrorCounter($error_counter+1);
                          }
                          $this->SetUserTokenLastError($now_epoch);
                      }
                      break;
                  default:
                      $result = _MULTIOTP_BAD_ALGORITHM_;
                      $this->WriteLog("Error: ".$algorithm." algorithm is unknown", FALSE, FALSE, $result, 'User');
              }
              if ($result >= 90) {
                  if ($cache_result_enabled && ((mb_strlen($input_to_check) != mb_strlen($calculated_token)))) {
                      $disable_error_counter = true;
                  }
                  if (!$disable_error_counter) {
                      $this->SetUserErrorCounter(intval($error_counter)+1);
                  }
                  $this->SetUserTokenLastError($now_epoch);
              }
          }

          if (_MULTIOTP_AUTH_OK_ == $result) {
              $this->SetUserLocked(0);
          }
          
          if ($result >= 90) {
              $replayed_text = (($disable_error_counter) ? " (same token replayed)" : "");
              if (("" == $replayed_text) && (_MULTIOTP_AUTH_TOTP_OUTOFSYNC_ERROR_ == $result)) {
                $replayed_text = " (time based token probably out of sync)";
              }
              $this->WriteLog("Error: authentication failed".$supplemental_login_info." for user ".$real_user.$replayed_text, FALSE, FALSE, $result, 'User');
              if (_MULTIOTP_AUTH_PASSWORD_FAILED_ == $result) {
                // We do nothing else, the password is bad.
              } elseif ($this->GetVerboseFlag()) {
                  if ("" != $this->GetChapPassword()) {
                      $this->WriteLog("Info: *(authentication typed by the user is CHAP encrypted)", FALSE, FALSE, $result, 'User');
                  } elseif ("" != $this->GetMsChapResponse()) {
                      $this->WriteLog("Info: *(authentication typed by the user is MS-CHAP encrypted)", FALSE, FALSE, $result, 'User');
                  } elseif ("" != $this->GetMsChap2Response()) {
                      $this->WriteLog("Info: *(authentication typed by the user is MS-CHAP V2 encrypted)", FALSE, FALSE, $result, 'User');
                  } elseif ((mb_strlen($input_to_check) === mb_strlen($calculated_token))) {
                      $this->WriteLog("Info: *(authentication typed by the user: ".$input_to_check.")", FALSE, FALSE, $result, 'User');
                  } else {
                      $result = _MULTIOTP_AUTH_TOKEN_LENGTH_ERROR_;
                      $this->WriteLog("*(authentication typed by the user is ".mb_strlen($input_to_check)." chars long instead of ".mb_strlen($calculated_token)." chars)", FALSE, FALSE, $result, 'User');
                  }
              } elseif (("" == $this->GetChapPassword()) &&
                        ("" == $this->GetMsChapResponse()) &&
                        ("" == $this->GetMsChap2Response()) &&
                        ((mb_strlen($input_to_check) != mb_strlen($calculated_token)))
                       ) {
                  $result = _MULTIOTP_AUTH_TOKEN_LENGTH_ERROR_;
                  $this->WriteLog("Error: authentication typed by the user is ".mb_strlen($input_to_check)." chars long instead of ".mb_strlen($calculated_token)." chars", FALSE, FALSE, $result, 'User');
              }
              if ($this->IsDeveloperMode()) {
                $this->WriteLog("Developer: *authentication typed by the user is $input_to_check", FALSE, FALSE, 8888, 'Debug', "");
              }
          }
          
          if ($this->GetUserErrorCounter() >= $this->GetMaxBlockFailures()) {
              if (1 != $this->GetUserLocked()) {
                $this->WriteLog("Error: User ".$real_user." locked after ".$this->GetUserErrorCounter()." failed authentications", FALSE, FALSE, $result, 'User');
              }
              $this->SetUserLocked(1);
          } elseif (($this->GetUserErrorCounter() >= $this->GetMaxDelayedFailures()) && ($now_epoch == ($this->GetUserTokenLastError()))) {
              $this->WriteLog("Error: User ".$real_user." is now delayed for ".$this->GetMaxDelayedTime()." seconds after ".$this->GetUserErrorCounter()." failed authentications", FALSE, FALSE, $result, 'User');
          }

          if (_MULTIOTP_AUTH_OK_ == $result) {
              // Remove the state to block the challenge
              $this->SetUserChallenge("");
              $this->SetUserChallengeValidity(0);

              if ($cache_result_enabled) {
                  if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Debug: *checked code for future cache access: ".str_repeat('x', (mb_strlen($input_to_check) >= 6)?mb_strlen($input_to_check)-6:0).mb_substr($input_to_check, -6), FALSE, FALSE, 8888, 'Debug', "");
                  }
                  $this->SetUserLastCachedCredential(nullable_trim($input.' '.$input_sync));
                  $this->SetUserLastLoginForCache($now_epoch);
              }
              $this->SetUserLastSuccessCredential(nullable_trim($input.' '.$input_sync));
          } else {
              $this->SetUserLastFailedCredential(nullable_trim($input.' '.$input_sync));
          }

          if (!$this->WriteUserData()) {
              $result = _MULTIOTP_DATA_UPDATE_ERROR_; // ERROR: Unable to write the changes in the file
              $this->WriteLog("Error: Unable to write the changes in the file for the user ".$real_user, FALSE, FALSE, $result, 'User');
          }
      } // end of the else block of the test: if (!$this->ReadUserData($real_user))

      if (($result >= 90) && $push_timeout_error) {
        $result = _MULTIOTP_PUSH_TIMEOUT_ERROR_;
      } elseif (($result >= 90) && $push_denied_error) {
        $result = _MULTIOTP_PUSH_DENIED_ERROR_;
      }

      if (_MULTIOTP_AUTH_OK_ == $result) {
          $this->AddExtraRadiusInfo();
      }
      
      if ($this->GetVerboseFlag()) {
        $this->WriteLog("Debug: *CheckToken intermediate result ".__LINE__.", result: {$result}", false, false, _MULTIOTP_OPERATION_DONE_, 'Debug', "");
      }
      
      return $result;
  }


  function SelfRegisterHardwareToken(
      $user,
      $serial,
      $input,
      $original_input = ""
  ) {
      // TODO the whole process has to be changed to support multi tokens
      $result = _MULTIOTP_ERROR_; // Unknown error
      
      $cache_result_enabled = false;
      $disable_error_counter = false;
      $force_no_prefix_pin = false;

      $supplemental_login_info = "";

      // Specific device detection, based on the source tag, to check if cache is enabled
      $source_tag = nullable_trim($this->GetSourceTag());
      if ("" != $source_tag) {
          if ("" != $this->GetRadiusTagPrefix()) {
              $device_id = $this->GetRadiusTagPrefix().$source_tag;
          } else {
              $device_id = mb_substr($source_tag, strrpos('-'.$source_tag, '-'));
          }
          if ($this->ReadDeviceData($device_id)) {
              $cache_result_enabled = $this->IsDeviceCacheResultEnabled();
              $force_no_prefix_pin = $this->IsDeviceForceNoPrefixEnabled();
          }
      }

      // Force no prefix pin to true if the request came from a multiOTP Credential Provider
      if ($this->GetCredentialProviderMode() || $this->IsCredentialProviderRequest()) {
        if ($this->IsIgnoreNoPrefixCredentialProvider()) {
          $supplemental_login_info = " (using Credential Provider, without forcing no prefix PIN)";
        } else {
          $force_no_prefix_pin = true;
          $supplemental_login_info = " (using Credential Provider)";
        }
      }

      $ldap_check_passed = FALSE;
      
      $calculated_token = "";
      if ("" == $original_input) {
          $original_input = $input;
      }
      $serial_number = mb_strtolower($serial,'UTF-8');
      if ($this->ReadUserData($user)) {
          $pin = $this->GetUserPin();
          $need_prefix = ($this->IsUserPrefixPin()) && (!$force_no_prefix_pin);

          if ($this->ReadTokenData($serial_number)) {
              $attributed_users = nullable_trim($this->GetTokenAttributedUsers());
              if ("" != nullable_trim($attributed_users)) {
                  if (FALSE === mb_strpos(','.$attributed_users.',', ','.$user.',')) {
                      $result = _MULTIOTP_TOKEN_ALREADY_ATTRIBUTED_; // ERROR: Token already attributed
                      $this->WriteLog("Error: Token ".$this->GetToken()." already attributed", FALSE, FALSE, $result, 'Token', $user);
                  }
                  // else $result = _MULTIOTP_ERROR_; // The token is already attributed to this user, stay neutral with the error
              } else {
                  $algorithm = $this->GetTokenAlgorithm();
                  $token_algo_suite = $this->GetTokenAlgoSuite();

                  $seed = $this->GetTokenSeed();
                  $seed_bin = hex2bin($seed);
                  $private_id = $this->GetUserTokenPrivateId();
                  $digits = $this->GetTokenNumberOfDigits();
                  $time_interval = $this->GetTokenTimeInterval();
                  $last_event = $this->GetTokenLastEvent();
                  $delta_time = $this->GetTokenDeltaTime();
                  $last_login = $this->GetTokenLastLogin();
                  $error_counter = $this->GetTokenErrorCounter();

                  $now_epoch = time();

                  $input_to_check = $input;
                  $interval = (0 >= $time_interval)?1:$time_interval;

                  $input_is_empty = ("" == $input_to_check);
                  if (mb_strlen($input_to_check) < 3) {
                      $input_to_check.= "!!!! <3 digits !!!!";
                  }

                  $now_steps         = intval($now_epoch / $interval);
                  $time_window       = $this->GetMaxTimeWindow();
                  $step_window       = intval($time_window / $interval);
                  $event_window      = $this->GetMaxEventWindow();
                  $time_sync_window  = $this->GetMaxTimeResyncWindow();
                  $step_sync_window  = intval($time_sync_window / $interval);
                  $event_sync_window = $this->GetMaxEventResyncWindow();
                  $last_login_step   = intval($last_login / $interval);
                  $delta_step        = $delta_time / $interval;
                      
                  $ldap_check_passed = FALSE;
                  $ldap_to_check = '!LDAP_FALSE!';
                  if (($need_prefix) && ($this->IsUserRequestLdapPasswordEnabled())) {
                      if ($input_to_check != "") {
                          $ldap_to_check = mb_substr($input_to_check, 0, mb_strlen($input_to_check) - $digits);
                          if ("" != $ldap_to_check) {
                              if ($this->CheckUserLdapPassword($this->GetUserSynchronizedDn(), $ldap_to_check)) {
                                  $ldap_check_passed = TRUE;
                                  // TODO Write a specific message in the log
                                  if ($this->IsCacheLdapHash()) {
                                      $this->SetUserLdapHashCache(nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check))));
                                  }
                              } elseif ($this->IsCacheLdapHash()) {
                                  if (!$this->IsLdapServerReachable()) {
                                      if ($this->GetVerboseFlag()) {
                                          $this->WriteLog("Debug: *user LDAP password checked in the cache", FALSE, FALSE, 8888, 'Debug', "");
                                      }
                                      if ($this->GetUserLdapHashCache() === nullable_bin2hex($this->NtPasswordHashHash($this->NtPasswordHash($ldap_to_check)))) {
                                          $ldap_check_passed = TRUE;
                                          // TODO Write a specific message in the log
                                      }
                                  } else {
                                      $ldap_check_passed = FALSE;
                                      $ldap_to_check = '!LDAP_FALSE!';
                                      $this->ResetUserLdapHashCache();
                                      if ($this->GetVerboseFlag()) {
                                          $this->WriteLog("Debug: *user LDAP password false, hash cache cleared", FALSE, FALSE, 8888, 'Debug', "");
                                      }
                                  }
                              } else { // If the LdapHash cache is disabled
                                  $ldap_check_passed = FALSE;
                                  $ldap_to_check = '!LDAP_FALSE!';
                                  $this->ResetUserLdapHashCache();
                                  if ($this->IsLdapServerReachable()) {
                                      $this->WriteLog("Error: User $real_user LDAP password false", FALSE, FALSE, _MULTIOTP_LDAP_AUTH_ERROR_, 'User');
                                  } else {
                                      $this->WriteLog("Error: User $real_user verification failed, unreachable LDAP/AD server(s)", FALSE, FALSE, _MULTIOTP_LDAP_CONNECTION_ERROR_, 'User');
                                  }
                              }
                          }
                      }
                      if (!$ldap_check_passed) {
                          $this->WriteLog("Error: authentication failed".$supplemental_login_info." for user ".$this->GetUser(), FALSE, FALSE, $result, 'User');
                          $input_to_check = "LDAP_FAILED_".$input_to_check.'_LDAP_FAILED';
                          $result = _MULTIOTP_LDAP_AUTH_ERROR_;
                      }
                  }

                  switch (mb_strtolower($algorithm,'UTF-8')) {
                      case 'motp':
                          $max_steps = 2 * $step_sync_window;
                          $check_step = 1;
                          do {
                              $additional_step = (1 - (2 * ($check_step % 2))) * intval($check_step/2);
                              $pure_calculated_token = $this->ComputeMotp($seed.$pin, $now_steps+$additional_step+$delta_step, $digits);
                              $calculated_token = $pure_calculated_token;
                              
                              if (($need_prefix) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                                  $code_confirmed_without_pin = $calculated_token;
                                  $code_confirmed = $calculated_token;
                                  $input_to_check = mb_substr($input_to_check, -mb_strlen($code_confirmed));                            
                                  if (!$ldap_check_passed) {
                                      $input_to_check.= '_BAD_LDAP_CHECK';
                                  }
                                  $this->SetLastClearOtpValue($original_input);
                              } else {
                                  if ($need_prefix) {
                                      $calculated_token = $pin.$calculated_token;
                                  }
                                  
                                  $code_confirmed_without_pin = $pure_calculated_token;
                                  $code_confirmed = $calculated_token;
                                  $this->SetLastClearOtpValue($original_input);
                                  if ("" != $this->GetChapPassword()) {
                                      $code_confirmed_without_pin = mb_strtolower($this->CalculateChapPassword($code_confirmed_without_pin),'UTF-8');
                                      $code_confirmed = mb_strtolower($this->CalculateChapPassword($code_confirmed),'UTF-8');
                                  } elseif ("" != $this->GetMsChapResponse()) {
                                      $code_confirmed_without_pin = mb_strtolower($this->CalculateMsChapResponse($code_confirmed_without_pin),'UTF-8');
                                      $code_confirmed = mb_strtolower($this->CalculateMsChapResponse($code_confirmed),'UTF-8');
                                  } elseif ("" != $this->GetMsChap2Response()) {
                                      $code_confirmed_without_pin = mb_strtolower($this->CalculateMsChap2Response($user, $code_confirmed_without_pin),'UTF-8');
                                      $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($user, $code_confirmed),'UTF-8');
                                  }
                              }

                              if (($input_to_check === $code_confirmed) || ($input_to_check === $code_confirmed_without_pin)) {
                                  if (($now_steps+$additional_step+$delta_step) > $last_login_step) {
                                      $this->SetTokenLastLogin(($now_steps+$additional_step+$delta_step) * $interval);
                                      $this->SetTokenDeltaTime(($additional_step+$delta_step) * $interval);
                                      $this->SetTokenErrorCounter(0);
                                      $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct token
                                  } else {
                                      $result = _MULTIOTP_TOKEN_REUSED_; // ERROR: this token has already been used
                                  }
                              } else {
                                  $check_step++;
                              }
                          } while (($check_step < $max_steps) && ($result >= 90));
                          break;
                      case 'hotp';
                          $max_steps = $event_sync_window;
                          $check_step = 1;
                          do {
                              $pure_calculated_token = $this->GenerateOathHotp($seed_bin,$last_event+$check_step,$digits,$token_algo_suite);
                              $calculated_token = $pure_calculated_token;
                              
                              if (($need_prefix) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                                  $code_confirmed_without_pin = $calculated_token;
                                  $code_confirmed = $calculated_token;
                                  $input_to_check = mb_substr($input_to_check, -mb_strlen($code_confirmed));                            
                                  if (!$ldap_check_passed) {
                                      $input_to_check.= '_BAD_LDAP_CHECK';
                                  }
                                  $this->SetLastClearOtpValue($original_input);
                              } else {
                                  if ($need_prefix) {
                                      $calculated_token = $pin.$calculated_token;
                                  }
                                  
                                  $code_confirmed = $calculated_token;
                                  $this->SetLastClearOtpValue($original_input);
                                  if ("" != $this->GetChapPassword()) {
                                      $code_confirmed = mb_strtolower($this->CalculateChapPassword($code_confirmed),'UTF-8');
                                  } elseif ("" != $this->GetMsChapResponse()) {
                                      $code_confirmed = mb_strtolower($this->CalculateMsChapResponse($code_confirmed),'UTF-8');
                                  } elseif ("" != $this->GetMsChap2Response()) {
                                      $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($user, $code_confirmed),'UTF-8');
                                  }
                              }

                              if ($input_to_check === $code_confirmed) {
                                  $this->SetTokenLastLogin($now_epoch);
                                  $this->SetTokenLastEvent($last_event+$check_step);
                                  $this->SetTokenErrorCounter(0);
                                  $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct token
                              } else {
                                  $check_step++;
                              }
                          } while (($check_step < $max_steps) && ($result >= 90));
                          break;
                      case 'yubicootp':
                          $yubikey_class = new MultiotpYubikey();
                          $bad_precheck = FALSE;
                          if (($need_prefix) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                              if (!$ldap_check_passed) {
                                  $input_to_check.= '_BAD_LDAP_CHECK';
                                  $bad_precheck = TRUE;
                              }
                              $this->SetLastClearOtpValue($original_input);
                          } else {
                              if ($need_prefix) {
                                  if ($pin != mb_substr($input_to_check, 0, mb_strlen($pin))) {
                                      $this->SetLastClearOtpValue($original_input);
                                      $input_to_check.= '_BAD_PREFIX';
                                      $bad_precheck = TRUE;
                                  }
                              }
                          }

                          if (!$bad_precheck) {
                              // Check only the last 32 digits, the first 12 are the serial number
                              // $uid = nullable_bin2hex(mb_substr($decrypted_part,  0, 6));
                              $result = $yubikey_class->CheckYubicoOtp(mb_substr($input_to_check, -32),
                                                                       $seed,
                                                                       $last_event,
                                                                       $private_id);
                          }
                          if (_MULTIOTP_AUTH_OK_ == $result) {
                              $calculated_token = $input_to_check;
                              $this->SetTokenLastLogin($now_epoch);
                              $this->SetTokenLastEvent($yubikey_class->GetYubicoOtpLastCount());
                              $this->SetTokenErrorCounter(0);
                              $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct token
                          }
                          break;
                      case 'totp';
                          $max_steps = 2 * $step_sync_window;
                          $check_step = 1;
                          do {
                              $additional_step = (1 - (2 * ($check_step % 2))) * intval($check_step/2);
                              $pure_calculated_token = $this->GenerateOathHotp($seed_bin,$now_steps+$additional_step+$delta_step,$digits,$token_algo_suite);
                              $calculated_token = $pure_calculated_token;
                              
                              if (($need_prefix) && ($input_to_check != "") && ($this->IsUserRequestLdapPasswordEnabled())) {
                                  $code_confirmed_without_pin = $calculated_token;
                                  $code_confirmed = $calculated_token;
                                  $input_to_check = mb_substr($input_to_check, -mb_strlen($code_confirmed));                            
                                  if (!$ldap_check_passed) {
                                      $input_to_check.= '_BAD_LDAP_CHECK';
                                  }
                                  $this->SetLastClearOtpValue($original_input);
                              } else {
                                  if ($need_prefix) {
                                      $calculated_token = $pin.$calculated_token;
                                  }

                                  $code_confirmed = $calculated_token;
                                  $this->SetLastClearOtpValue($original_input);
                                  if ("" != $this->GetChapPassword()) {
                                      $code_confirmed = mb_strtolower($this->CalculateChapPassword($code_confirmed),'UTF-8');
                                  } elseif ("" != $this->GetMsChapResponse()) {
                                      $code_confirmed = mb_strtolower($this->CalculateMsChapResponse($code_confirmed),'UTF-8');
                                  } elseif ("" != $this->GetMsChap2Response()) {
                                      $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($user, $code_confirmed),'UTF-8');
                                  }
                              }

                              if ($input_to_check === $code_confirmed) {
                                  if (($now_steps+$additional_step+$delta_step) > $last_login_step) {
                                      $this->SetTokenLastLogin(($now_steps+$additional_step+$delta_step) * $interval);
                                      $this->SetTokenDeltaTime(($additional_step+$delta_step) * $interval);
                                      $this->SetTokenErrorCounter(0);
                                      $result = _MULTIOTP_AUTH_OK_; // OK: This is the correct token
                                  } else {
                                      $result = _MULTIOTP_TOKEN_REUSED_; // ERROR: this token has already been used
                                  }
                              } else {
                                  $check_step++;
                              }
                          } while (($check_step < $max_steps) && ($result >= 90));
                          break;
                      default:
                          if ($this->GetVerboseFlag()) {
                            $this->WriteLog("Debug: *Invalid algorithm (SelfRegisterHardwareToken): $algorithm", FALSE, FALSE, 8888, 'Debug', "");
                          }
                          $result = _MULTIOTP_BAD_ALGORITHM_; // ERROR: Invalid algorithm
                  }

                  if ($result >= 90) {
                      if ($this->GetVerboseFlag()) {
                          if ((mb_strlen($input_to_check) === mb_strlen($calculated_token))) {
                              $this->WriteLog("Info: *(authentication typed by the user: ".$input_to_check.")", FALSE, FALSE, $result, 'User', $user);
                          } else {
                              $result = _MULTIOTP_AUTH_TOKEN_LENGTH_ERROR_;
                              $this->WriteLog("Info: *(authentication typed by the user is ".mb_strlen($input_to_check)." chars long instead of ".mb_strlen($calculated_token)." chars", FALSE, FALSE, $result, 'User', $user);
                          }
                      }
                      if ($this->IsDeveloperMode()) {
                        $this->WriteLog("Developer: *authentication typed by the user is $input_to_check", FALSE, FALSE, 8888, 'Debug', "");
                      }
                  }
                  
                  if (_MULTIOTP_AUTH_OK_ == $result) {
                      $this->AddTokenAttributedUsers($user);
                      if (!$this->WriteTokenData()) {
                          $result = _MULTIOTP_DATA_UPDATE_ERROR_; // ERROR: Unable to write the changes in the file
                          $this->WriteLog("Error: Unable to write the changes in the file for the token ".$this->GetToken(), FALSE, FALSE, $result, 'Token', $user);
                      } else {
                          $this->SetUserTokenSerialNumber($serial_number);
                          $this->SetUserAlgorithm($this->GetTokenAlgorithm());
                          $this->SetUserTokenAlgoSuite($this->GetTokenAlgoSuite());
                          $this->SetUserTokenSeed($this->GetTokenSeed());
                          $this->SetUserTokenPrivateId($this->GetTokenPrivateId());
                          $this->SetUserTokenNumberOfDigits($this->GetTokenNumberOfDigits());
                          $this->SetUserTokenTimeInterval($this->GetTokenTimeInterval());
                          $this->SetUserTokenLastEvent($this->GetTokenLastEvent());
                          $this->SetUserTokenDeltaTime($this->GetTokenDeltaTime());
                          $this->SetUserTokenLastLogin($this->GetTokenLastLogin());
                          $this->SetUserErrorCounter(0);
                          if (!$this->WriteUserData()) {
                              $result = _MULTIOTP_DATA_UPDATE_ERROR_; // ERROR: Unable to write the changes in the file
                              $this->WriteLog("Error: Unable to write the changes in the file for the user ".$this->GetUser(), FALSE, FALSE, $result, 'System', "");
                          } else {
                              $this->WriteLog("OK: token ".$this->GetToken()." successfully attributed to user ".$this->GetUser(), FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'User');
                          }
                      }
                  }
              }
          } else {
              $result = _MULTIOTP_TOKEN_MISSING_; // ERROR: Token doesn't exist
              $this->WriteLog("Error: Token ".$this->GetToken()." does not exist", FALSE, FALSE, $result, 'Token');
          }
      } else {
          $result = _MULTIOTP_USER_MISSING_; // ERROR: User doesn't exist
          $this->WriteLog("Error: User ".$this->GetUser()." does not exist", FALSE, FALSE, $result, 'User');
      }
      return $result;
  } // End of SelfRegisterHardwareToken


  function ImportTokensFile(
      $file,
      $original_name = "",
      $cipher_password = "",
      $key_mac = ""
  ) {
      if (!file_exists($file)) {
          $result = FALSE;
      } else {
          $data1000 = @file_get_contents($file, FALSE, NULL, 0, 1000);
          $file_name = ("" != $original_name)?$original_name:$file;
          if (FALSE !== mb_strpos(mb_strtolower($data1000,'UTF-8'), mb_strtolower('"urn:ietf:params:xml:ns:keyprov:pskc"','UTF-8'))) {
              $result = $this->ImportTokensFromPskc($file, $cipher_password, $key_mac);
          } elseif (FALSE !== mb_strpos(mb_strtolower($data1000,'UTF-8'), mb_strtolower('LOGGING START','UTF-8'))) {
              $result = $this->ImportYubikeyTraditional($file);
          } elseif ((FALSE !== mb_strpos(mb_strtolower($data1000,'UTF-8'), mb_strtolower('AUTHENEXDB','UTF-8'))) && ('.sql' == mb_strtolower(mb_substr($file_name, -4),'UTF-8'))) {
              $result = $this->ImportTokensFromAuthenexSql($file);
          } elseif ((FALSE !== mb_strpos(mb_strtolower($data1000,'UTF-8'), mb_strtolower('SafeWord Authenticator Records','UTF-8'))) && ('.dat' == mb_strtolower(mb_substr($file_name, -4),'UTF-8'))) {
              $result = $this->ImportTokensFromAlpineDat($file);
          } elseif (FALSE !== mb_strpos(mb_strtolower($data1000,'UTF-8'), mb_strtolower('<ProductName>eTPass','UTF-8'))) {
          // elseif (('.xml' == mb_strtolower(mb_substr($file_name, -4),'UTF-8')) && (FALSE !== mb_strpos(mb_strtolower($file_name,'UTF-8'), 'alpine')))
              $result = $this->ImportTokensFromAlpineXml($file);
          } elseif ('.xml' == mb_strtolower(mb_substr($file_name, -4),'UTF-8')) {
              $result = $this->ImportTokensFromXml($file);
          } else {
              $result = $this->ImportTokensFromCsv($file);
          }
      }
      return $result;
  }


  function DecodeCipherValue(
      $encrypted_tree,
      $cipher_array,
      $integer_value = FALSE
  ) {
      $passphrase = $cipher_array['Password'];
      $Secret = "";
      $cipher_aes = new Crypt_AES();

      $encryption_method_tag = (isset($encrypted_tree->{$cipher_array['xenc_ns'].'encryptionmethod'})?$cipher_array['xenc_ns']:"").'encryptionmethod';
      $encryption_method_algorithm_url = isset($encrypted_tree->{$encryption_method_tag}[0]->tagAttrs["algorithm"])?($encrypted_tree->{$encryption_method_tag}[0]->tagAttrs["algorithm"]):"";
      $encryption_method_algorithm = (FALSE !== mb_strpos($encryption_method_algorithm_url,'#aes128-cbc'))?'aes128':((FALSE !== mb_strpos($encryption_method_algorithm_url,'#kw-aes128'))?'kw-ases128':"");
      $cipher_data_tag = (isset($encrypted_tree->{$cipher_array['xenc_ns'].'cipherdata'})?$cipher_array['xenc_ns']:"").'cipherdata';
      $cipher_value_tag = (isset($encrypted_tree->{$cipher_data_tag}[0]->{$cipher_array['xenc_ns'].'ciphervalue'})?$cipher_array['xenc_ns']:"").'ciphervalue';
      $cipher_value = isset($encrypted_tree->{$cipher_data_tag}[0]->{$cipher_value_tag}[0]->tagData)?($encrypted_tree->{$cipher_data_tag}[0]->{$cipher_value_tag}[0]->tagData):"";

      if ("" != $passphrase) {
          for ($tries = 0; $tries < 3; $tries++) {
              if ("" == $cipher_array['KeyDerivationMethodAlgorithm']) {
                  $cipher_aes->setKey($passphrase);
                  $Secret = (mb_substr($cipher_aes->decrypt(base64_decode($cipher_value)),16));
                  if ("" == $Secret) {
                      $cipher_aes->setKey(hex2bin(preg_replace("/[^A-Fa-f0-9]/", "", $passphrase)));
                      $Secret = (mb_substr($cipher_aes->decrypt(base64_decode($cipher_value)),16));
                  }
              } elseif ('pkcs5' == $cipher_array['KeyDerivationMethodAlgorithm']) {
                  $cipher_aes->setPassword($passphrase, 'pbkdf2', 'sha1', $cipher_array['Salt'], $cipher_array['IterationCount'], $cipher_array['KeyLength']);
                  $Secret = (mb_substr($cipher_aes->decrypt(base64_decode($cipher_value)),16));
              }
              if ("" != $Secret) {
                  break;
              } elseif (0 == $tries) {
                  $passphrase = nullable_trim($passphrase);
              } elseif (1 == $tries) {
                  if ((0 === mb_strpos($passphrase, '0x')) && (0 == (mb_strlen($passphrase) % 2))) {
                      $passphrase = hex2bin(mb_substr($passphrase, 2));
                  }
              }
          }
      }
      if (("" != $Secret) && ($integer_value)) {
          $value = 0;
          for( $i = 0; $i < mb_strlen($Secret); $i++ ) {
              $value = ($value << 8) | ord($Secret[$i]);
          }
          $Secret = $value;
      }
      return $Secret;
  }

  
  function ImportTokensFromPskc(
      $pskc_file,
      $cipher_password = "",
      $keymac = ""
  ) {
      $this->ResetLastImportedTokensArray();
      $result = TRUE;
      if (!file_exists($pskc_file)) {
          $this->WriteLog("Error: Tokens definition file ".$pskc_file." doesn't exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_FILE_, 'Token', "");
          $result = FALSE;
      } else {
          //Get the XML document loaded into a variable
          $sXmlData = @file_get_contents($pskc_file);
                  
          //Set up the parser object
          $xml = new MultiotpXmlParser($sXmlData, TRUE);

          //Parse it !
          $xml->Parse();

          $ds_ns = "";
          $pkcs5_ns = "";
          $pskc_ns = "";
          $xenc_ns = "";
          $xenc11_ns = "";

          if (isset($xml->document)) {
              $keycontainer = $xml->document;
              foreach ($keycontainer->tagAttrs as $attribute_key => $attribute_value) {
                  if ('http://www.w3.org/2000/09/xmldsig#' == $attribute_value) {
                      $ds_ns = mb_substr($attribute_key,mb_strpos($attribute_key,':')+1);
                      $ds_ns.= ("" != $ds_ns)?':':"";
                  }
                  if ('http://www.rsasecurity.com/rsalabs/pkcs/schemas/pkcs-5v2-0#' == $attribute_value) {
                      $pkcs5_ns = mb_substr($attribute_key,mb_strpos($attribute_key,':')+1);
                      $pkcs5_ns.= ("" != $pkcs5_ns)?'_':"";
                  }
                  if ('urn:ietf:params:xml:ns:keyprov:pskc' == $attribute_value) {
                      $pskc_ns = mb_substr($attribute_key,mb_strpos($attribute_key,':')+1);
                      $pskc_ns.= ("" != $pskc_ns)?'_':"";
                  }
                  if ('http://www.w3.org/2001/04/xmlenc#' == $attribute_value) {
                      $xenc_ns = mb_substr($attribute_key,mb_strpos($attribute_key,':')+1);
                      $xenc_ns.= ("" != $xenc_ns)?'_':"";
                  }
                  if ('http://www.w3.org/2009/xmlenc11#' == $attribute_value) {
                      $xenc11_ns = mb_substr($attribute_key,mb_strpos($attribute_key,':')+1);
                      $xenc11_ns.= ("" != $xenc11_ns)?'_':"";
                  }
              }

              $CipherArray = [];
              $CipherArray['Password'] = $cipher_password;
              $CipherArray['xenc_ns'] = $xenc_ns;

              $EncryptionKey_tag = (isset($keycontainer->{$pskc_ns.'encryptionkey'})?$pskc_ns:"").'encryptionkey';
              $DerivedKey_tag = (isset($keycontainer->{$EncryptionKey_tag}[0]->{$xenc11_ns.'derivedkey'})?$xenc11_ns:"").'derivedkey';
              $KeyDerivationMethod_tag = (isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$xenc11_ns.'keyderivationmethod'})?$xenc11_ns:"").'keyderivationmethod';
              $KeyDerivationMethodAlgorithmUrl = isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->tagAttrs["algorithm"])?($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->tagAttrs["algorithm"]):"";
              $CipherArray['KeyDerivationMethodAlgorithm'] = ((FALSE !== mb_strpos($KeyDerivationMethodAlgorithmUrl,'#pbkdf2'))?'pkcs5':"");
              // http://www.rsasecurity.com/rsalabs/pkcs/schemas/pkcs-5v2-0#pbkdf2
              // http://www.rsasecurity.com/rsalabs/pkcs/schemas/pkcs-5#pbkdf2
              $CipherArray['Salt'] = "";
              $CipherArray['IterationCount'] = 0;
              $CipherArray['KeyLength'] = 0;
              if ('pkcs5' == $CipherArray['KeyDerivationMethodAlgorithm']) {

                  $search_tag = 'pbkdf2_params';
                  if (isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pkcs5_ns.$search_tag})) {
                      $search_tag = $pkcs5_ns.$search_tag;
                  } elseif (isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$xenc11_ns.$search_tag})) {
                      $search_tag = $xenc11_ns.$search_tag;
                  }
                  $pbkdf2_params_tag = $search_tag;

                  $search_tag = 'salt';
                  if (isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$pkcs5_ns.$search_tag})) {
                      $search_tag = $pkcs5_ns.$search_tag;
                  } elseif (isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$xenc11_ns.$search_tag})) {
                      $search_tag = $xenc11_ns.$search_tag;
                  }
                  $salt_tag = $search_tag;
 
                  $search_tag = 'specified';
                  if (isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$salt_tag}[0]->{$pkcs5_ns.$search_tag})) {
                      $search_tag = $pkcs5_ns.$search_tag;
                  } elseif (isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$salt_tag}[0]->{$xenc11_ns.$search_tag})) {
                      $search_tag = $xenc11_ns.$search_tag;
                  }
                  $salt_specified_tag = $search_tag;

                  $search_tag = 'iterationcount';
                  if (isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$pkcs5_ns.$search_tag})) {
                      $search_tag = $pkcs5_ns.$search_tag;
                  } elseif (isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$xenc11_ns.$search_tag})) {
                      $search_tag = $xenc11_ns.$search_tag;
                  }
                  $iterationcount_tag = $search_tag;

                  $search_tag = 'keylength';
                  if (isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$pkcs5_ns.$search_tag})) {
                      $search_tag = $pkcs5_ns.$search_tag;
                  } elseif (isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$xenc11_ns.$search_tag})) {
                      $search_tag = $xenc11_ns.$search_tag;
                  }
                  $keylength_tag = $search_tag;

                  $CipherArray['Salt'] = base64_decode(isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$salt_tag}[0]->{$salt_specified_tag}[0]->tagData)?($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$salt_tag}[0]->{$salt_specified_tag}[0]->tagData):"");
                  $CipherArray['IterationCount'] = intval(isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$iterationcount_tag}[0]->tagData)?($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$iterationcount_tag}[0]->tagData):0);
                  $CipherArray['KeyLength'] = intval(isset($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$keylength_tag}[0]->tagData)?($keycontainer->{$EncryptionKey_tag}[0]->{$DerivedKey_tag}[0]->{$KeyDerivationMethod_tag}[0]->{$pbkdf2_params_tag}[0]->{$keylength_tag}[0]->tagData):0);
              }
              
              $search_tag = 'keypackage';
              if (isset($keycontainer->{$pskc_ns.'keypackage'})) {
                  $search_tag = $pskc_ns.$search_tag;
              } elseif (isset($keycontainer->{$pkcs5_ns.'keypackage'})) {
                  $search_tag = $pkcs5_ns.$search_tag;
              } elseif (isset($keycontainer->{$xenc11_ns.'keypackage'})) {
                  $search_tag = $xenc11_ns.$search_tag;
              }
              $KeyPackage_tag = $search_tag;
              // Extract each key
              foreach ($keycontainer->{$KeyPackage_tag} as $keypackage_key => $keypackage) {
                  $DeviceInfo_tag = (isset($keypackage->{$pskc_ns.'deviceinfo'})?$pskc_ns:"").'deviceinfo';
                  
                  $Manufacturer_tag = (isset($keypackage->{$DeviceInfo_tag}[0]->{$pskc_ns.'manufacturer'})?$pskc_ns:"").'manufacturer';
                  $Manufacturer = (isset($keypackage->{$DeviceInfo_tag}[0]->{$Manufacturer_tag}[0]->tagData)?($keypackage->{$DeviceInfo_tag}[0]->{$Manufacturer_tag}[0]->tagData):"");
                  
                  $SerialNo_tag = (isset($keypackage->{$DeviceInfo_tag}[0]->{$pskc_ns.'serialno'})?$pskc_ns:"").'serialno';
                  $SerialNo = (isset($keypackage->{$DeviceInfo_tag}[0]->{$SerialNo_tag}[0]->tagData)?($keypackage->{$DeviceInfo_tag}[0]->{$SerialNo_tag}[0]->tagData):"");

                  $Model_tag = (isset($keypackage->{$DeviceInfo_tag}[0]->{$pskc_ns.'model'})?$pskc_ns:"").'model';
                  $Model = (isset($keypackage->{$DeviceInfo_tag}[0]->{$Model_tag}[0]->tagData)?($keypackage->{$DeviceInfo_tag}[0]->{$Model_tag}[0]->tagData):"");
                  
                  $IssueNo_tag = (isset($keypackage->{$DeviceInfo_tag}[0]->{$pskc_ns.'issueno'})?$pskc_ns:"").'issueno';
                  $IssueNo = (isset($keypackage->{$DeviceInfo_tag}[0]->{$IssueNo_tag}[0]->tagData)?($keypackage->{$DeviceInfo_tag}[0]->{$IssueNo_tag}[0]->tagData):"");
                  
                  $CryptoModuleInfo_tag = (isset($keypackage->{$pskc_ns.'cryptomoduleinfo'})?$pskc_ns:"").'cryptomoduleinfo';
                  
                  $CryptoId_tag = (isset($keypackage->{$CryptoModuleInfo_tag}[0]->{$pskc_ns.'id'})?$pskc_ns:"").'id';
                  $CryptoId = (isset($keypackage->{$CryptoModuleInfo_tag}[0]->{$CryptoId_tag}[0]->tagData)?($keypackage->{$CryptoModuleInfo_tag}[0]->{$CryptoId_tag}[0]->tagData):"");

                  $Key_tag = (isset($keypackage->{$pskc_ns.'key'})?$pskc_ns:"").'key';
                  
                  $AlgorithmUrl = isset($keypackage->{$Key_tag}[0]->tagAttrs["algorithm"])?($keypackage->{$Key_tag}[0]->tagAttrs["algorithm"]):"";
                  $Algorithm = (FALSE !== mb_strpos($AlgorithmUrl,'hotp'))?'hotp':((FALSE !== mb_strpos($AlgorithmUrl,'totp'))?'totp':"");
                  // $Algorithm = (FALSE !== mb_strpos($AlgorithmUrl,'hotp'))?'hotp':((FALSE !== mb_strpos($AlgorithmUrl,'totp'))?'totp':((FALSE !== mb_strpos($AlgorithmUrl,'ocra'))?'ocra':""));

                  $KeyId = isset($keypackage->{$Key_tag}[0]->tagAttrs["id"])?($keypackage->{$Key_tag}[0]->tagAttrs["id"]):"";
                  
                  $Issuer_tag = (isset($keypackage->{$Key_tag}[0]->{$pskc_ns.'issuer'})?$pskc_ns:"").'issuer';
                  $Issuer = (isset($keypackage->{$Key_tag}[0]->{$Issuer_tag}[0]->tagData)?($keypackage->{$Key_tag}[0]->{$Issuer_tag}[0]->tagData):"");
                  
                  $AlgorithmParameters_tag = (isset($keypackage->{$Key_tag}[0]->{$pskc_ns.'algorithmparameters'})?$pskc_ns:"").'algorithmparameters';
                  
                  $Suite_tag = (isset($keypackage->{$Key_tag}[0]->{$AlgorithmParameters_tag}[0]->{$pskc_ns.'suite'})?$pskc_ns:"").'suite';
                  $Suite = (isset($keypackage->{$Key_tag}[0]->{$AlgorithmParameters_tag}[0]->{$Suite_tag}[0]->tagData)?($keypackage->{$Key_tag}[0]->{$AlgorithmParameters_tag}[0]->{$Suite_tag}[0]->tagData):'HMAC-SHA1');
                  $ResponseFormat_tag = (isset($keypackage->{$Key_tag}[0]->{$AlgorithmParameters_tag}[0]->{$pskc_ns.'responseformat'})?$pskc_ns:"").'responseformat';
                  $Length = isset($keypackage->{$Key_tag}[0]->{$AlgorithmParameters_tag}[0]->{$ResponseFormat_tag}[0]->tagAttrs["length"])?($keypackage->{$Key_tag}[0]->{$AlgorithmParameters_tag}[0]->{$ResponseFormat_tag}[0]->tagAttrs["length"]):0;
                  $Encoding = isset($keypackage->{$Key_tag}[0]->{$AlgorithmParameters_tag}[0]->{$ResponseFormat_tag}[0]->tagAttrs["encoding"])?($keypackage->{$Key_tag}[0]->{$AlgorithmParameters_tag}[0]->{$ResponseFormat_tag}[0]->tagAttrs["encoding"]):'DECIMAL';
                  
                  $Data_tag = (isset($keypackage->{$Key_tag}[0]->{$pskc_ns.'data'})?$pskc_ns:"").'data';
                  
                  $Secret_tag = (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$pskc_ns.'secret'})?$pskc_ns:"").'secret';
                  $SecretPlainValue_tag = (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Secret_tag}[0]->{$pskc_ns.'plainvalue'})?$pskc_ns:"").'plainvalue';
                  $Secret = base64_decode(isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Secret_tag}[0]->{$SecretPlainValue_tag}[0]->tagData)?($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Secret_tag}[0]->{$SecretPlainValue_tag}[0]->tagData):"");
                  $EncryptedValue_tag = (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Secret_tag}[0]->{$pskc_ns.'encryptedvalue'})?$pskc_ns:"").'encryptedvalue';
                  if (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Secret_tag}[0]->{$EncryptedValue_tag}[0])) {
                      $SecretEncryptedPath = $keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Secret_tag}[0]->{$EncryptedValue_tag}[0];
                      $Secret = $this->DecodeCipherValue($SecretEncryptedPath, $CipherArray);
                  }
                  
                  $Counter_tag = (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$pskc_ns.'counter'})?$pskc_ns:"").'counter';
                  $CounterPlainValue_tag = (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Counter_tag}[0]->{$pskc_ns.'plainvalue'})?$pskc_ns:"").'plainvalue';
                  $Counter = intval(isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Counter_tag}[0]->{$CounterPlainValue_tag}[0]->tagData)?($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Counter_tag}[0]->{$CounterPlainValue_tag}[0]->tagData):0);
                  $EncryptedValue_tag = (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Counter_tag}[0]->{$pskc_ns.'encryptedvalue'})?$pskc_ns:"").'encryptedvalue';
                  if (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Counter_tag}[0]->{$EncryptedValue_tag}[0])) {
                    $CounterEncryptedPath = $keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Counter_tag}[0]->{$EncryptedValue_tag}[0];
                    $Counter = intval($this->DecodeCipherValue($CounterEncryptedPath, $CipherArray, TRUE));
                  }

                  $Time_tag = (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$pskc_ns.'time'})?$pskc_ns:"").'time';
                  $TimePlainValue_tag = (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Time_tag}[0]->{$pskc_ns.'plainvalue'})?$pskc_ns:"").'plainvalue';
                  $Time = intval(isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Time_tag}[0]->{$TimePlainValue_tag}[0]->tagData)?($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Time_tag}[0]->{$TimePlainValue_tag}[0]->tagData):"");
                  $EncryptedValue_tag = (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Time_tag}[0]->{$pskc_ns.'encryptedvalue'})?$pskc_ns:"").'encryptedvalue';
                  if (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Time_tag}[0]->{$EncryptedValue_tag}[0])) {
                    $TimeEncryptedPath = $keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$Time_tag}[0]->{$EncryptedValue_tag}[0];
                    $Time = intval($this->DecodeCipherValue($TimeEncryptedPath, $CipherArray, TRUE));
                  }
                  
                  $TimeInterval_tag = (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$pskc_ns.'timeinterval'})?$pskc_ns:"").'timeinterval';
                  $TimeIntervalPlainValue_tag = (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$TimeInterval_tag}[0]->{$pskc_ns.'plainvalue'})?$pskc_ns:"").'plainvalue';
                  $TimeInterval = intval(isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$TimeInterval_tag}[0]->{$TimeIntervalPlainValue_tag}[0]->tagData)?($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$TimeInterval_tag}[0]->{$TimeIntervalPlainValue_tag}[0]->tagData):_MULTIOTP_DEFAULT_TOTP_INTERVAL_);
                  $EncryptedValue_tag = (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$TimeInterval_tag}[0]->{$pskc_ns.'encryptedvalue'})?$pskc_ns:"").'encryptedvalue';
                  if (isset($keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$TimeInterval_tag}[0]->{$EncryptedValue_tag}[0])) {
                    $TimeIntervalEncryptedPath = $keypackage->{$Key_tag}[0]->{$Data_tag}[0]->{$TimeInterval_tag}[0]->{$EncryptedValue_tag}[0];
                    $TimeInterval = intval($this->DecodeCipherValue($TimeIntervalEncryptedPath, $CipherArray, TRUE));
                  }

                  $Policy_tag = (isset($keypackage->{$Key_tag}[0]->{$pskc_ns.'policy'})?$pskc_ns:"").'policy';

                  $PINPolicy_tag = (isset($keypackage->{$Key_tag}[0]->{$Policy_tag}[0]->{$pskc_ns.'pinpolicy'})?$pskc_ns:"").'pinpolicy';
                  $PINPolicyAttributes = isset($keypackage->{$Key_tag}[0]->{$Policy_tag}[0]->{$PINPolicy_tag}[0]->tagAttrs[0])?($keypackage->{$Key_tag}[0]->{$Policy_tag}[0]->{$PINPolicy_tag}[0]->tagAttrs):"";

                  $keyusage_tag = (isset($keypackage->{$Key_tag}[0]->{$Policy_tag}[0]->{$pskc_ns.'keyusage'})?$pskc_ns:"").'keyusage';
                  $keyusage = isset($keypackage->{$Key_tag}[0]->{$Policy_tag}[0]->{$keyusage_tag}[0]->tagData)?($keypackage->{$Key_tag}[0]->{$Policy_tag}[0]->{$keyusage_tag}[0]->tagData):"";

                  $SerialNumber = (("" == $SerialNo)?$KeyId:$SerialNo);

                  /*
                  if ((FALSE !== mb_strpos($KeyId, $SerialNo)) && (mb_strlen($KeyId) > mb_strlen($SerialNo))) {
                      $SerialNo = $KeyId;
                  }
                  */

                  if (("" == $keyusage) || ("OTP" == $keyusage)) {
                      if ((('hotp' == $Algorithm) || ('totp' == $Algorithm)) && ("" != $SerialNo) && ("" != $Secret)) {
                          $this->SetToken($SerialNumber);
                          $this->SetTokenDescription(nullable_trim(nullable_trim($Manufacturer.' '.$Model).' '.$SerialNo));
                          $this->SetTokenManufacturer($Manufacturer);
                          $this->SetTokenModel($Model);
                          $this->SetTokenIssueNo($IssueNo);
                          $this->SetTokenIssuer($Issuer);
                          $this->SetTokenKeyId($keyusage);
                          $this->SetTokenKeyUsage($KeyId);
                          $this->SetTokenSerialNo($SerialNo);
                          $this->SetTokenSerialNumber($SerialNumber);
                          $this->SetTokenKeyAlgorithm($AlgorithmUrl);
                          $this->SetTokenAlgorithm($Algorithm);
                          $this->SetTokenAlgoSuite($Suite);
                          $this->SetTokenOtp("TRUE");
                          $this->SetTokenFormat($Encoding);
                          $this->SetTokenNumberOfDigits($Length);
                          if ($Counter >= 0) {
                              $this->SetTokenLastEvent($Counter-1);
                          } else {
                              $this->SetTokenLastEvent(0);
                          }
                          $this->SetTokenDeltaTime($Time);
                          $this->SetTokenTimeInterval($TimeInterval);
                          $this->SetTokenSeed(nullable_bin2hex($Secret));
                          
                          if ($this->CheckTokenExists("", false)) {
                              $this->WriteLog("Info: Token ".$this->GetToken()." already exists", FALSE, FALSE, _MULTIOTP_TOKEN_EXISTS_, 'Token', "");
                          } else {
                              $result = $this->WriteTokenData() && $result;
                              $this->AddLastImportedToken($this->GetToken());
                              $this->WriteLog("Info: Token with SerialNo ".$SerialNo." successfully imported", FALSE, FALSE, _MULTIOTP_TOKEN_IMPORTED_, 'Token', "");
                          }
                          if ($this->GetVerboseFlag()) {
                              $full_token_data = "";
                              foreach ($this->_token_data as $key => $value) {
                                  if ("" != $value) {
                                      $full_token_data = $full_token_data."  Token ".$SerialNo." - ".$key.": ".$value."\n";
                                  }
                              }
                              $this->WriteLog("Debug: *".$full_token_data, FALSE, FALSE, 8888, 'Debug', "");
                          }
                      } elseif (('hotp' == $Algorithm) || ('totp' == $Algorithm)) {
                          $result = FALSE;
                          $this->WriteLog("Info: Token with SerialNo ".$SerialNo." failed during importation", FALSE, FALSE, _MULTIOTP_TOKEN_IMPORT_ERROR_, 'Token', "");
                      }
                  }
              }
          }
      }
      return $result;
  }


  /*
   * YubiKey traditional format log file (csv)
   * (https://github.com/Yubico/yubikey-personalization-gui/blob/master/src/yubikeylogger.cpp)
   *  0 eventType: "Yubico OTP"|"OATH-HOTP"|"Static Password: Scan Code"|"Static Password"|"Challenge-Response: Yubico OTP"|"Challenge-Response: HMAC-SHA1"
   *  1 timestampLocal: "dd.mm.yyyy hh:ii"
   *  2 configSlot: 1|2
   *  3 pubIdTxt: cbdefghijkln
   *  4 pvtIdTxt: 1234567890ab
   *  5 secretKeyTxt: 1234567890abcdef1234567890abcdef
   *  6 currentAccessCodeTxt: ""|"xxxxxxxxxxxx"
   *  7 newAccessCodeTxt: ""|"xxxxxxxxxxxx"
   *  8 oathFixedModhex1: 0|1 (First byte in fixed part sent as modhex, OATH only)
   *  9 oathFixedModhex2: 0|1 (First two bytes in fixed part sent as modhex, OATH only)
   * 10 oathFixedModhex: 0|1 (Fixed part sent as modhex, OATH only)
   * 11 hotpDigits: 0|6|8
   * 12 oathMovingFactorSeed: 0|nnnn
   * 13 strongPw1: 0|1 (Static Password - Upper and lower case)
   * 14 strongPw2: 0|1 (Static Password - Alphanumeric)
   * 15 sendRef: 0|1 (Static Password - Send ! as prefix)
   * 16 chalBtnTrig: 0|1 (Challenge-Response - challenge requires button trigger)
   * 17 hmacLT64 1|0 (Challenge-Response: HMAC-SHA1 - 1: variable input, 0: fixed 64 byte input)
  */
  function ImportYubikeyTraditional($yubikey_file) {
      $result = TRUE;
      $imported_tokens = 0;
      $this->ResetTokenArray();
      $this->ResetLastImportedTokensArray();
      if (!file_exists($yubikey_file)) {
          $this->WriteLog("Error: YubiKeys log file ".$yubikey_file." doesn't exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_FILE_, 'Token', "");
          $result = FALSE;
      } else {
          //Get the document loaded into a variable
          if ($file_handler = @fopen($yubikey_file, "rt")) {
              flock($file_handler, LOCK_SH);

              $yubikey_class = new MultiotpYubikey();
              
              while (!feof($file_handler)) {
                  $line = nullable_trim(fgets($file_handler));

                  $line = str_replace(';',"\t", $line);
                  $line = str_replace(',',"\t", $line);

                  $line_array = explode("\t", $line);

                  if (count($line_array) >= 18) {
                      $token_algo_suite = 'AES-128';
                      $manufacturer = "Yubico";
                      switch (nullable_trim($line_array[0])) {
                          case 'Yubico OTP':
                              $algorithm = 'yubicootp';
                              break;
                          case 'OATH-HOTP':
                              $algorithm = 'hotp';
                              break;
                          default:
                              $algorithm = "";
                      }
                      $esn = nullable_trim($line_array[3]); // modhex
                      if (('hotp' == $algorithm) && (0 == intval($line_array[10]))) {
                          if (1 == intval($line_array[8])) {
                              $esn = mb_substr(nullable_trim($line_array[3]),0,2).$yubikey_class->ModHexToHex(mb_substr(nullable_trim($line_array[3]),2));
                          } elseif (1 == intval($line_array[9])) {
                              $esn = mb_substr(nullable_trim($line_array[3]),0,4).$yubikey_class->ModHexToHex(mb_substr(nullable_trim($line_array[3]),4));
                          } else {
                              $esn = $yubikey_class->ModHexToHex(nullable_trim($line_array[3]));
                          }
                      }
                      $private_id = "";
                      $seed = nullable_trim($line_array[5]);
                      $interval_or_event = intval($line_array[12]);

                      if ('hotp' == $algorithm) {
                          $digits = intval($line_array[11]);
                          $next_event = $interval_or_event;
                          $time_interval = 0;
                      } elseif ('yubicootp' == $algorithm) {
                          $private_id = nullable_trim($line_array[4]);
                          if ("000000000000" == $private_id) {
                              $private_id = "";
                          }
                          $digits = 32;
                          $next_event = 0;
                          $time_interval = 0;
                      }
                      
                      if ("" != $algorithm) {
                          $this->SetToken($esn);
                          $this->SetTokenPrivateId($private_id);
                          $this->SetTokenDescription(nullable_trim($manufacturer.' '.$esn));
                          $this->SetTokenManufacturer($manufacturer);
                          $this->SetTokenSerialNumber($esn);
                          $this->SetTokenSeed($seed);
                          $this->SetTokenAlgorithm($algorithm);
                          $this->SetUserTokenAlgoSuite($token_algo_suite);
                          $this->SetTokenNumberOfDigits($digits);
                          $this->SetTokenLastEvent($next_event - 1);
                          $this->SetTokenTimeInterval($time_interval);

                          $imported_tokens++;
                          
                          if ("" == $esn) {
                              $this->WriteLog("Error: A token doesn't have any serial number", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_FILE_, 'Token', "");
                          } elseif (!$this->IsValidAlgorithm($algorithm)) {
                              $this->WriteLog("Error: The algorithm ".$algorithm." is not recognized", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_FILE_, 'Token', "");
                          } elseif ($this->CheckTokenExists("", false)) {
                              $this->WriteLog("Info: Token ".$this->GetToken()." already exists", FALSE, FALSE, _MULTIOTP_TOKEN_EXISTS_, 'Token', "");
                          } else {
                              $result = $this->WriteTokenData() && $result;
                              $this->AddLastImportedToken($this->GetToken());
                              $this->WriteLog("Info: Token ".$this->GetToken()." successfully imported", FALSE, FALSE, _MULTIOTP_TOKEN_IMPORTED_, 'Token', "");
                          }
                          $this->ResetTokenArray();
                      }
                  }
              }
              fclose($file_handler);
          }
      }
      if (0 == $imported_tokens) {
          $result = FALSE;
      }
      return $result;
  }


  function ImportTokensFromCsv(
      $csv_file
  ) {
      $result = TRUE;
      $imported_tokens = 0;
      $this->ResetTokenArray();
      $this->ResetLastImportedTokensArray();
      if (!file_exists($csv_file)) {
          $this->WriteLog("Error: Tokens definition file ".$csv_file." doesn't exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_FILE_, 'Token', "");
          $result = FALSE;
      } else {
          //Get the document loaded into a variable
          if ($file_handler = @fopen($csv_file, "rt")) {
              flock($file_handler, LOCK_SH);
              while (!feof($file_handler)) {
                  $line = nullable_trim(fgets($file_handler));

                  $line = str_replace(';',"\t", $line);
                  $line = str_replace(',',"\t", $line);

                  $line_array = explode("\t", $line);

                  if (count($line_array) >= 6) {
                      $esn               = nullable_trim($line_array[0]);
                      $manufacturer      = $line_array[1];
                      $algorithm         = mb_strtolower($line_array[2],'UTF-8');
                      $seed              = $line_array[3];
                      $digits            = $line_array[4];
                      $interval_or_event = intval($line_array[5]);
                      
                      if ('hotp' == $algorithm) {
                          $next_event = $interval_or_event;
                          $time_interval = 0;
                      } else {
                          $next_event = 0;
                          $time_interval = $interval_or_event;
                          if ('motp' == $algorithm) {
                              $time_interval = 10;
                          }
                      }

                      $this->SetToken($esn);
                      $this->SetTokenDescription(nullable_trim($manufacturer.' '.$esn));
                      $this->SetTokenManufacturer($manufacturer);
                      $this->SetTokenSerialNumber($esn);
                      $this->SetTokenSeed($seed);
                      $this->SetTokenAlgorithm($algorithm);
                      $this->SetTokenNumberOfDigits($digits);
                      $this->SetTokenLastEvent($next_event - 1);
                      $this->SetTokenTimeInterval($time_interval);

                      $imported_tokens++;
                      
                      if ("" == $esn) {
                          $this->WriteLog("Error: A token doesn't have any serial number", FALSE, FALSE, _MULTIOTP_TOKEN_IMPORT_ERROR_, 'Token', "");
                          $result = FALSE;
                      } elseif (!$this->IsValidAlgorithm($algorithm)) {
                          $this->WriteLog("Error: The algorithm ".$algorithm." is not recognized", FALSE, FALSE, _MULTIOTP_BAD_ALGORITHM_, 'Token', "");
                          $result = FALSE;
                      } elseif ($this->CheckTokenExists("", false)) {
                          $this->WriteLog("Info: Token ".$this->GetToken()." already exists", FALSE, FALSE, _MULTIOTP_TOKEN_EXISTS_, 'Token', "");
                          $result = FALSE;
                      } else {
                          $result = $this->WriteTokenData() && $result;
                          $this->AddLastImportedToken($this->GetToken());
                          $this->WriteLog("Info: Token ".$this->GetToken()." successfully imported", FALSE, FALSE, _MULTIOTP_TOKEN_IMPORTED_, 'Token', "");
                          $result = FALSE;
                      }
                      $this->ResetTokenArray();
                  }
              }
              fclose($file_handler);
          } else {
              $this->WriteLog("Error: Tokens definition file ".$csv_file." cannot be read", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_, 'Token', "");
              $result = FALSE;
          }
      }
      if (0 == $imported_tokens) {
          $result = FALSE;
      }
      return $result;
  }


  function ImportTokensFromXml(
      $xml_file
  ) {
      $this->ResetLastImportedTokensArray();
      $result = TRUE;
      if (!file_exists($xml_file)) {
          $this->WriteLog("Error: Tokens definition file ".$xml_file." doesn't exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_, 'Token', "");
          $result = FALSE;
      } else {
          // http://tools.ietf.org/html/draft-hoyer-keyprov-pskc-algorithm-profiles-00
          
          //Get the XML document loaded into a variable
          $sXmlData = @file_get_contents($xml_file);

          //Set up the parser object
          $xml = new MultiotpXmlParser($sXmlData);

          //Parse it !
          $xml->Parse();

          // Array of key types
          $key_types = [];
          
          if (isset($xml->document->keyproperties)) {
              foreach ($xml->document->keyproperties as $keyproperty) {
                  $id = (isset($keyproperty->tagAttrs['xml:id'])?$keyproperty->tagAttrs['xml:id']:"");
                  
                  if ("" != $id) {
                      $key_types[$id]['id'] = $id;
                      $key_types[$id]['issuer'] = (isset($keyproperty->issuer[0]->tagData)?$keyproperty->issuer[0]->tagData:"");
                      $key_types[$id]['keyalgorithm'] = (isset($keyproperty->tagAttrs['keyalgorithm'])?$keyproperty->tagAttrs['keyalgorithm']:"");
                      $pos = strrpos($key_types[$id]['keyalgorithm'], "#");
                      $key_types[$id]['algorithm'] = (($pos === false)?"":mb_strtolower(mb_substr($key_types[$id]['keyalgorithm'], $pos+1),'UTF-8'));
                      $key_types[$id]['otp'] = (isset($keyproperty->usage[0]->tagAttrs['otp'])?$keyproperty->usage[0]->tagAttrs['otp']:"");
                      $key_types[$id]['format'] = (isset($keyproperty->usage[0]->responseformat[0]->tagAttrs['format'])?$keyproperty->usage[0]->responseformat[0]->tagAttrs['format']:"");
                      $key_types[$id]['length'] = (isset($keyproperty->usage[0]->responseformat[0]->tagAttrs['length'])?$keyproperty->usage[0]->responseformat[0]->tagAttrs['length']:-1);
                      $key_types[$id]['counter'] = (isset($keyproperty->data[0]->counter[0]->plainvalue[0]->tagData)?$keyproperty->data[0]->counter[0]->plainvalue[0]->tagData:-1);
                      $key_types[$id]['time'] = (isset($keyproperty->data[0]->time[0]->plainvalue[0]->tagData)?$keyproperty->data[0]->time[0]->plainvalue[0]->tagData:-1);
                      $key_types[$id]['timeinterval'] = (isset($keyproperty->data[0]->timeinterval[0]->plainvalue[0]->tagData)?$keyproperty->data[0]->timeinterval[0]->plainvalue[0]->tagData:-1);
                      $key_types[$id]['suite'] = (isset($keyproperty->data[0]->suite[0]->plainvalue[0]->tagData)?$keyproperty->data[0]->suite[0]->plainvalue[0]->tagData:"");
                  }
              }
          }
          
          if (isset($xml->document->device)) {
              foreach ($xml->document->device as $device) {
                  $keyid = (isset($device->key[0]->tagAttrs['keyid'])?$device->key[0]->tagAttrs['keyid']:"");
                  if ("" != $keyid) {
                      $this->ResetTokenArray();
                      $keyproperties = "";
                      $manufacturer = "";
                      $serialno = "";
                      $issuer = "";
                      $keyalgorithm = "";
                      $algorithm = "";
                      $otp = "";
                      $format = "";
                      $length = 0;
                      $counter = -1;
                      $time = 0;
                      $timeinterval = 0;
                      $secret = "";
                      $suite = "";
                      
                      if (isset($device->key[0]->tagAttrs['keyproperties'])) {
                          $keyproperties = $device->key[0]->tagAttrs['keyproperties'];
                          if (isset($key_types[$keyproperties])) {
                              foreach ($key_types[$keyproperties] as $key => $value) {
                                  $$key = $value;
                              }
                          }
                      }
                      
                      $manufacturer = (isset($device->deviceinfo[0]->manufacturer[0]->tagData)?$device->deviceinfo[0]->manufacturer[0]->tagData:$manufacturer);
                      $serialno = (isset($device->deviceinfo[0]->serialno[0]->tagData)?$device->deviceinfo[0]->serialno[0]->tagData:$serialno);

                      $issuer = (isset($device->key[0]->issuer[0]->tagData)?$device->key[0]->issuer[0]->tagData:$issuer);
                      
                      if (isset($device->key[0]->tagAttrs['keyalgorithm'])) {
                          $keyalgorithm = $device->key[0]->tagAttrs['keyalgorithm'];
                          $pos = strrpos($keyalgorithm, "#");
                          $algorithm = (($pos === false)?$algorithm:mb_strtolower(mb_substr($keyalgorithm, $pos+1),'UTF-8'));
                      }
                      
                      $otp = (isset($device->key[0]->usage[0]->tagAttrs['otp'])?$device->key[0]->usage[0]->tagAttrs['otp']:$otp);
                      $format = (isset($device->key[0]->usage[0]->responseformat[0]->tagAttrs['format'])?$device->key[0]->usage[0]->responseformat[0]->tagAttrs['format']:$format);
                      $length = (isset($device->key[0]->usage[0]->responseformat[0]->tagAttrs['length'])?$device->key[0]->usage[0]->responseformat[0]->tagAttrs['length']:$length);
                      $counter = (isset($device->key[0]->data[0]->counter[0])?$device->key[0]->data[0]->counter[0]->plainvalue[0]->tagData:$counter);
                      $time = (isset($device->key[0]->data[0]->time[0])?$device->key[0]->data[0]->time[0]->plainvalue[0]->tagData:$time);
                      $timeinterval = (isset($device->key[0]->data[0]->timeinterval[0])?$device->key[0]->data[0]->timeinterval[0]->plainvalue[0]->tagData:$timeinterval);
                      $suite = (isset($device->key[0]->data[0]->suite[0])?$device->key[0]->data[0]->suite[0]->plainvalue[0]->tagData:$suite);
                      
                      if (isset($device->key[0]->data[0]->secret[0]->plainvalue[0]->tagData)) {
                          $secret = nullable_bin2hex(base64_decode($device->key[0]->data[0]->secret[0]->plainvalue[0]->tagData));
                      }

                      if ("" == nullable_trim($serialno)) {
                          $serialno = nullable_trim($keyid);
                      }
                      $this->SetToken($serialno);
                      $this->SetTokenDescription(nullable_trim($manufacturer.' '.$keyid));
                      $this->SetTokenManufacturer($manufacturer);
                      $this->SetTokenIssuer($issuer);
                      $this->SetTokenSerialNumber($serialno);
                      $this->SetTokenKeyAlgorithm($keyalgorithm);
                      $this->SetTokenAlgorithm($algorithm);
                      $this->SetTokenAlgoSuite($suite);
                      $this->SetTokenOtp($otp);
                      $this->SetTokenFormat($format);
                      $this->SetTokenNumberOfDigits($length);
                      if ($counter >= 0) {
                          $this->SetTokenLastEvent($counter-1);
                      } else {
                          $this->SetTokenLastEvent(0);
                      }
                      $this->SetTokenDeltaTime($time);
                      $this->SetTokenTimeInterval($timeinterval);
                      $this->SetTokenSeed($secret);
                      
                      if ($this->CheckTokenExists("", false)) {
                          $this->WriteLog("Error: Token ".$this->GetToken()." already exists", FALSE, FALSE, _MULTIOTP_TOKEN_EXISTS_, 'Token', "");
                      } else {
                          $result = $this->WriteTokenData() && $result;
                          $this->AddLastImportedToken($this->GetToken());
                          $this->WriteLog("Info: Token with keyid ".$keyid." successfully imported", FALSE, FALSE, _MULTIOTP_TOKEN_IMPORTED_, 'Token', "");
                      }
                      if ($this->GetVerboseFlag()) {
                          $full_token_data = "";
                          foreach ($this->_token_data as $key => $value) {
                              if ("" != $value) {
                                  $full_token_data = $full_token_data."  Token ".$keyid." - ".$key.": ".$value."\n";
                              }
                          }
                          $this->WriteLog("Debug: *".$full_token_data, FALSE, FALSE, 8888, 'Debug', "");
                      }
                  }
              }
          }
      }
      return $result;
  }    


  function ImportTokensFromAlpineXml(
      $xml_file
  ) {
      $this->ResetLastImportedTokensArray();
      $result = TRUE;
      if (!file_exists($xml_file)) {
          $this->WriteLog("Error: Tokens definition file ".$xml_file." doesn't exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_FILE_, 'Token', "");
          $result = FALSE;
      } else {
          $sXmlData = @file_get_contents($xml_file);

          //Set up the parser object
          $xml = new MultiotpXmlParser($sXmlData);

          //Parse it !
          $xml->Parse();

          // Array of key types
          $key_types = [];
          if (isset($xml->document->token)) {
              foreach ($xml->document->token as $token) {
                  $serial = (isset($token->tagAttrs['serial'])?$token->tagAttrs['serial']:"");
                  if ("" != $serial) {
                      $this->ResetTokenArray();
                      $manufacturer = 'SafeWord';
                      $serialno = $serial;
                      $issuer = 'SafeWord';
                      $algorithm = 'HOTP';
                      $length = 6;
                      $counter = 0;
                      $time = 0;
                      $timeinterval = 0;
                      $secret = "";
                      
                      if (isset($token->applications[0]->application[0]->seed[0]->tagData)) {
                          $secret = $token->applications[0]->application[0]->seed[0]->tagData;
                      }
                      $this->SetToken($serialno);
                      $this->SetTokenDescription(nullable_trim($manufacturer.' '.$serialno));
                      $this->SetTokenManufacturer($manufacturer);
                      $this->SetTokenSerialNumber($serialno);
                      $this->SetTokenIssuer($issuer);
                      $this->SetTokenAlgorithm($algorithm);
                      $this->SetTokenNumberOfDigits($length);
                      $this->SetTokenLastEvent($counter-1);
                      $this->SetTokenDeltaTime($time);
                      $this->SetTokenTimeInterval($timeinterval);
                      $this->SetTokenSeed($secret);
                      
                      if ($this->CheckTokenExists("", false)) {
                          $this->WriteLog("Error: Token ".$this->GetToken()." already exists", FALSE, FALSE, _MULTIOTP_TOKEN_EXISTS_, 'Token', "");
                      } else {
                          $result = $this->WriteTokenData() && $result;
                          $this->AddLastImportedToken($this->GetToken());
                          
                          $this->WriteLog("Info: Token with serial number ".$serialno." successfully imported", FALSE, FALSE, _MULTIOTP_TOKEN_IMPORTED_, 'Token', "");
                      }
                  }
              }
          }
      }
      return $result;
  }    


  function ImportTokensFromAlpineDat(
      $data_file
  ) {
      $ProductName = "";
      $this->ResetTokenArray();
      $this->ResetLastImportedTokensArray();
      $result = TRUE;
      if (!file_exists($data_file)) {
          $this->WriteLog("Error: Tokens definition file ".$data_file." doesn't exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_FILE_, 'Token', "");
          $result = FALSE;
      } else {
          // SafeWord Authenticator Records
          
          //Get the document loaded into a variable
          if ($file_handler = @fopen($data_file, "rt")) {
              flock($file_handler, LOCK_SH);

              $line = nullable_trim(fgets($file_handler));
              
              $reference_header       = "SafeWord Authenticator Records";
              $reference_manufacturer = "SafeWord";
              
              if (FALSE !== mb_strpos(mb_strtolower($line,'UTF-8'), mb_strtolower($reference_header,'UTF-8'))) {
                  $manufacturer = $reference_manufacturer;
              
                  while (!feof($file_handler)) {
                      $line = nullable_trim(fgets($file_handler));
                      $line_array = explode(":",$line,2);
                      $line_array[0] = nullable_trim($line_array[0]);
                      $line_array[1] = nullable_trim((isset($line_array[1])?$line_array[1]:""));

                      switch (mb_strtolower($line_array[0],'UTF-8')) {
                          case '# ===== safeword authenticator records $version':
                          case 'dn':
                              break;
                          case 'objectclass':
                              break;
                          case 'sccauthenticatorid':
                              $sccAuthenticatorId = $line_array[1];
                              $this->SetToken($sccAuthenticatorId);
                              $this->SetTokenDescription(nullable_trim($manufacturer.' '.$sccAuthenticatorId));
                              $this->SetTokenSerialNumber($sccAuthenticatorId);
                              break;
                          case 'scctokentype':
                              $sccTokenType = $line_array[1];
                              break;
                          case 'scctokendata':
                              $sccTokenData = $line_array[1];
                              $data_array = explode(";",$sccTokenData);
                              foreach ($data_array as $data_one) {
                                  $attribute_array = explode("=",$data_one,2);
                                  $attribute_array[0] = nullable_trim($attribute_array[0]);
                                  $attribute_array[1] = nullable_trim((isset($attribute_array[1])?$attribute_array[1]:""));
                                  switch (mb_strtolower($attribute_array[0],'UTF-8')) {
                                      case 'scckey':
                                          $sccKey = $attribute_array[1];
                                          $this->SetTokenSeed($sccKey); // 9C29B16121DB61E9D7216CB90016C45677B39009BBF825B5
                                          break;
                                      case 'sccMode':
                                          $sccMode = $attribute_array[1]; // E
                                          break;
                                      case 'sccpwlen':
                                          $sccPwLen = $attribute_array[1]; // 6
                                          $this->SetTokenNumberOfDigits($sccPwLen);
                                          break;
                                      case 'sccver':
                                          $sccVer = $attribute_array[1]; // 00000205
                                          break;
                                      case 'sccseq':
                                          $sccSeq = $attribute_array[1];
                                          $this->SetTokenLastEvent($sccSeq-1); // 0001
                                          break;
                                      case 'casemodel':
                                          $CaseModel = $attribute_array[1]; // 00000005
                                          break;
                                      case 'productiondate':
                                          $ProductionDate = $attribute_array[1]; // 07/28/2010
                                          break;
                                      case 'prtoductname':
                                      case 'productname':
                                          $ProductName = $attribute_array[1]; // eTPass 6.10
                                          break;
                                  }
                              }
                              break;
                          case 'sccsignature':
                              if ($this->CheckTokenExists("", false)) {
                                  $this->WriteLog("Error: Token ".$this->GetToken()." already exists", FALSE, FALSE, _MULTIOTP_TOKEN_EXISTS_, 'Token', "");
                              } else {
                                  $this->SetTokenManufacturer($manufacturer);
                                  $this->SetTokenIssuer($manufacturer);
                                  $this->SetTokenAlgorithm('HOTP');
                                  $result = $this->WriteTokenData() && $result;
                                  $this->AddLastImportedToken($this->GetToken());
                                  $this->WriteLog("Info: Token ".$this->GetToken()." successfully imported", FALSE, FALSE, _MULTIOTP_TOKEN_IMPORTED_, 'Token', "");
                              }
                              $this->ResetTokenArray();
                              break;
                      }
                  }
              }
              fclose($file_handler);
          }
      }
      return $result;
  }


  function ImportTokensFromAuthenexSql(
      $data_file
  ) {
      $ProductName = "";
      $this->ResetTokenArray();
      $this->ResetLastImportedTokensArray();
      $result = TRUE;
      if (!file_exists($data_file)) {
          $this->WriteLog("Error: Tokens definition file ".$data_file." doesn't exist", FALSE, FALSE, _MULTIOTP_TOKEN_MISSING_FILE_, 'Token', "");
          $result = FALSE;
      } else {
          // Authenex Authenticator Records
          
          //Get the document loaded into a variable
          if ($file_handler = @fopen($data_file, "rt")) {
              flock($file_handler, LOCK_SH);
          
              $line = nullable_trim(fgets($file_handler));
              
              $reference_header       = "AUTHENEXDB";
              $reference_manufacturer = "Authenex";
              
              if (FALSE !== mb_strpos(mb_strtolower($line,'UTF-8'), mb_strtolower($reference_header,'UTF-8'))) {
                  $manufacturer = $reference_manufacturer;
                  
                  while (!feof($file_handler)) {
                      $line = nullable_trim(fgets($file_handler));

                      if (FALSE !== mb_strpos(mb_strtoupper($line,'UTF-8'), 'INSERT INTO OTP')) {
                          $token_array = [];
                          $line_array = explode("(",$line,3);
                          $token_line = str_replace(")",",",$line_array[2]);
                          $token_array = explode(",",$token_line);
                          if (isset($token_array[1])) {
                              $esn  = preg_replace('#\W#', "", $token_array[0]);
                              $seed = preg_replace('#\W#', "", $token_array[1]);
                              $this->SetToken($esn);
                              $this->SetTokenDescription(nullable_trim($manufacturer.' '.$esn));
                              $this->SetTokenManufacturer($manufacturer);
                              $this->SetTokenSerialNumber($esn);
                              $this->SetTokenSeed($seed);
                              $this->SetTokenAlgorithm('HOTP');
                              $this->SetTokenNumberOfDigits(6);
                              $this->SetTokenLastEvent(-1);
                          }
                          if ($this->CheckTokenExists("", false)) {
                              $this->WriteLog("Error: Token ".$this->GetToken()." already exists", FALSE, FALSE, _MULTIOTP_TOKEN_EXISTS_, 'Token', "");
                          } else {
                              $result = $this->WriteTokenData() && $result;
                              $this->AddLastImportedToken($this->GetToken());
                              $this->WriteLog("Info: Token ".$this->GetToken()." successfully imported", FALSE, FALSE, _MULTIOTP_TOKEN_IMPORTED_, 'Token', "");
                          }
                          $this->ResetTokenArray();
                      }
                  }
              }
              fclose($file_handler);
          }
      }
      return $result;
  }


  /****************************
   ****************************
   ****************************
   ***   DEVICES HANDLING   ***
   ****************************
   ****************************
   ****************************/
  /**
   * @brief   Create a new device
   *
   * @param   array/string $id                           New unique array, or unique id of the device
   * @param   string  $description
   * @param   string  $device_secret
   * @param   string  $ip_or_fqdn
   * @param   string  $subnet
   * @param   string  $shortname
   * @param   booldan $with_radius_update
   * @param   int     $challenge_response_enabled
   * @param   string  $text_token_challenge
   * @param   int     $sms_challenge_enabled
   * @param   string  $text_sms_challenge
   * @param   int     $cache_result_enabled
   * @param   int     $cache_timeout
   * @param   int     $dont_send_groups
   *
   * New unique array parameter
   *   string  id                          Unique id of the device
   *   string  description
   *   string  device_secret
   *   string  ip_or_fqdn
   *   string  subnet
   *   string  shortname
   *   booelan with_radius_update
   *   int     challenge_response_enabled
   *   string  text_token_challenge
   *   int     sms_challenge_enabled
   *   string  text_sms_challenge
   *   int     cache_result_enabled
   *   int     cache_timeout
   *   int     dont_send_groups
   *
   * @retval  boolean                      Result of the operation
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.0.2.5
   * @date    2016-10-03
   * @since   2013-12-23
   */
  function CreateDevice($id_array = array('id' => ""),
                        $description = "",
                        $device_secret = "",
                        $ip_or_fqdn = "",
                        $subnet = "",
                        $shortname = "",
                        $with_radius_update = TRUE,
                        $challenge_response_enabled = 0,
                        $text_token_challenge = "",
                        $sms_challenge_enabled = 0,
                        $text_sms_challenge = "",
                        $cache_result_enabled = 0,
                        $cache_timeout = 3600,
                        $dont_send_groups = 0
  ) {
    $result = FALSE;
    $device_id = (is_array($id_array)?(isset($id_array['id'])?$id_array['id']:""):$id_array);
    if (((0 == intval($device_id)) && (strlen($device_id) <= 1)) || ("" == $device_id)) {
        $device_id = bigdec2hex((time()-mktime(1,1,1,1,1,2000)).mt_rand(10000,99999));
    }
    if (!$this->ReadDeviceData($device_id, TRUE)) {
      $this->SetDevice($device_id);
      if (is_array($id_array)) {
        if (isset($id_array['id'])) { unset($id_array['id']); }
        foreach ($id_array as $key => $value) {
          $this->_device_data[$key] = $value;
        }
      } else { // backward compatibility
        $this->_device_data['description'] = $description;
        $this->_device_data['device_secret'] = $device_secret;
        $this->_device_data['ip_or_fqdn'] = $ip_or_fqdn;
        $this->_device_data['subnet'] = $subnet;
        $this->_device_data['shortname'] = $shortname;
        $this->_device_data['with_radius_update'] = $with_radius_update;
        $this->_device_data['challenge_response_enabled'] = $challenge_response_enabled;
        $this->_device_data['text_token_challenge'] = $text_token_challenge;
        $this->_device_data['sms_challenge_enabled'] = $sms_challenge_enabled;
        $this->_device_data['text_sms_challenge'] = $text_sms_challenge;
        $this->_device_data['cache_result_enabled'] = $cache_result_enabled;
        $this->_device_data['cache_timeout'] = $cache_timeout;
        $this->_device_data['dont_send_groups'] = $dont_send_groups;
      }
      
      if (is_valid_ipv4($this->_device_data['ip_or_fqdn'])) {
        if ($this->IsDeveloperMode()) {
          $this->WriteLog("Developer: IP address to check is " . $this->_device_data['ip_or_fqdn'] . " with subnet " . $this->_device_data['subnet'], FALSE, FALSE, 8888, 'System', "");
        }
        $cidr_mask = mask2cidr($this->_device_data['subnet']);
        $device_ip_network = (ip2long($this->_device_data['ip_or_fqdn']) >> (32-$cidr_mask));
        $this->_device_data['ip_or_fqdn'] = long2ip($device_ip_network << (32-$cidr_mask));
        if ($this->IsDeveloperMode()) {
          $this->WriteLog("Developer: Validated IP address is " . $this->_device_data['ip_or_fqdn'], FALSE, FALSE, 8888, 'System', "");
        }
      }
      
      $result = $this->WriteDeviceData($this->_device_data['with_radius_update']);
    }
    return $result;
  }    


  function DeleteDdns(
      $ddns = "",
      $no_error_info = FALSE
  ) {
      $result = FALSE;
      
      // First, we delete the ddns file if the backend is files or when migration is enabled
      if (('files' == $this->GetBackendType()) || ($this->GetMigrationFromFile())) {
          $ddns_filename = $ddns.'.db';
          if (!file_exists($this->GetDdnsFolder().$ddns_filename)) {
              if (!$no_error_info) {
                  $this->WriteLog("Error: Unable to delete ddns ".$ddns.", database file ".$this->GetDdnssFolder().$ddns_filename." does not exist", FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
              }
          } else {
              $result = unlink($this->GetDdnsFolder().$ddns_filename);
              if ($result) {
                  if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Info: *Ddns ".$ddns." successfully deleted", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Ddns', "");
                  }
              } else {
                  if (!$no_error_info) {
                      $this->WriteLog("Error: Unable to delete ddns ".$ddns, FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'Ddns', "");
                  }
              }
          }
      }

      if ($this->GetBackendTypeValidated()) {
          switch ($this->_config_data['backend_type']) {
              case 'mysql':
                  $ddns_escaped = escape_mysql_string($ddns);
                  if ($this->OpenMysqlDatabase()) {
                      if ("" != $this->_config_data['sql_ddns_table']) {
                          $sQuery  = "DELETE FROM `".$this->_config_data['sql_ddns_table']."` WHERE `ddns_id` = '".$ddns_escaped."'";
                          
                          if (is_object($this->_mysqli)) {
                              if (!($rResult = $this->_mysqli->query($sQuery))) {
                                  if (!$no_error_info) {
                                      $this->WriteLog("Error: Could not delete ddns ".$ddns.": ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
                                  }
                              } else {
                                  $num_rows = $this->_mysqli->affected_rows;
                              }
                          } elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete ddns ".$ddns.": ".mysql_error(), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
                              }
                          } else {
                              $num_rows = mysql_affected_rows($this->_mysql_database_link);
                          }
                          
                          if (0 == $num_rows) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete ddns ".$ddns.". Ddns does not exist", FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *Ddns ".$ddns." successfully deleted", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Ddns', "");
                              }
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $ddns_escaped = pg_escape_string($this->_pgsql_database_link, $ddns);
                      if ("" != $this->_config_data['sql_ddns_table']) {
                          $sQuery  = "DELETE FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_ddns_table']."\" WHERE \"ddns_id\" = '".$ddns_escaped."'";
                          
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete ddns ".$ddns.": ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
                              }
                          } else {
                              $num_rows = pg_affected_rows($rResult);
                          }
                          
                          if (0 == $num_rows) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete ddns ".$ddns.". Ddns does not exist", FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *Ddns ".$ddns." successfully deleted", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Ddns', "");
                              }
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              default:
                  // Nothing to do if the backend type is unknown
                  break;
          }                        
      }
      if ($result) {
          $this->TouchFolder('data',
                             'Ddns',
                             $ddns,
                             TRUE,
                             "DeleteDdns");
      }
      return $result;
  }


  function ReadDdnsData(
      $hostname,
      $alternate_file = ""
  ) {
      $result = [];
      $result["exists"] = FALSE;
      $result["username"] = "";
      $result["password"] = "";
      $result["ip"] = "";
      $result["tag"] = "";
      $result['alternate_password'] = "";

      $filename = strtolower($hostname.".db");

      if (file_exists($alternate_file)) {
        // Extract the alternate password
        if ($file_handler = @fopen($alternate_file, "rt")) {
          flock($file_handler, LOCK_SH);
          while (!feof($file_handler)) {
            $line = str_replace(chr(10), "", str_replace(chr(13), "", fgets($file_handler)));
            $line_array = explode("=",$line,2);
            if (('#' != substr($line, 0, 1)) && (';' != substr($line, 0, 1)) && ("" != nullable_trim($line)) && (isset($line_array[1]))) {
              if ("radius_secret" == $line_array[0]) {
                $result['alternate_password'] = $line_array[1];
                break;
              }
            }
          }
          fclose($file_handler);
        }
      }

      if (file_exists($this->GetDdnsFolder().$filename)) {
        $result["exists"] = TRUE;
        if ($file_handler = @fopen($this->GetDdnsFolder().$filename, "rt")) {
          flock($file_handler, LOCK_SH);
          $first_line = nullable_trim(fgets($file_handler));
          
          while (!feof($file_handler)) {
            $line = str_replace(chr(10), "", str_replace(chr(13), "", fgets($file_handler)));
            $line_array = explode("=",$line,2);
            if (('#' != substr($line, 0, 1)) && (';' != substr($line, 0, 1)) && ("" != nullable_trim($line)) && (isset($line_array[1]))) {
              if ("" != $line_array[0]) {
                $result[strtolower($line_array[0])] = $line_array[1];
              }
            }
          }
          fclose($file_handler);
        }
      } elseif ($result['alternate_password'] != "") {
          $result["exists"] = true;
          $result["username"] = substr($hostname, 0, strpos($hostname.'.', '.'));
          $result["password"] = $result['alternate_password'];
          $result["ip"] = "0.0.0.0";
      }
      return $result;
  }


  function WriteDdnsData(
      $data_array,
      $hostname,
      $write_ddns_data_array = []
  ) {
      if ("" == nullable_trim($hostname)) {
          $result = false;
      } else {
          if (isset($data_array['exists'])) { unset($data_array['exists']); }
          $result = $this->WriteData(array_merge(array('item'               => 'Ddns',
                                                       'table'              => 'ddns',
                                                       'folder'             => $this->GetDdnsFolder(),
                                                       'data_array'         => $data_array,
                                                       'force_file'         => true,
                                                       'id_field'           => 'ddns_id',
                                                       'id_value'           => nullable_trim($hostname)
                                                      ), $write_ddns_data_array));
      }
      return $result;
  }


  function GetDdnsList()
  {
      return $this->GetList('ddns_id', 'sql_ddns_table', $this->GetDdnsFolder());
  }


  function ReadDeviceData(
      $device_id = "",
      $create = FALSE,
      $ignore_missing = FALSE
  ) {
      // We initialize the local encryption check variable
      $local_encryption_check = false;

      $this->ResetDeviceArray();
      if ("" != $device_id) {
          $this->SetDevice($device_id);
      }
      $result = FALSE;
      
      // We initialize the encryption hash to empty
      $this->_device_data['encryption_hash'] = "";
      
      // First, we read the user file if the backend is files or when migration is enabled
      if (('files' == $this->GetBackendType()) || ($this->GetMigrationFromFile())) {
          $device_filename = mb_strtolower($this->GetDevice(),'UTF-8').'.db';
          if (!file_exists($this->GetDevicesFolder().$device_filename)) {
              if ((!$create) && (!$ignore_missing)) {
                  $this->WriteLog("Error: database file ".$this->GetDevicesFolder().$device_filename." for device ".$this->_device." does not exist", FALSE, FALSE, _MULTIOTP_DEVICE_MISSING_, 'System', "", 3);
              }
          } else {
              if ($file_handler = @fopen($this->GetDevicesFolder().$device_filename, "rt")) {
                  flock($file_handler, LOCK_SH);
                  $first_line = nullable_trim(fgets($file_handler));
                  
                  while (!feof($file_handler)) {
                      $line = nullable_trim(fgets($file_handler));
                      $line_array = explode("=",$line,2);
                      if (":" == mb_substr($line_array[0], -1)) {
                          $line_array[0] = mb_substr($line_array[0], 0, mb_strlen($line_array[0]) -1);
                          $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                          $local_encryption_check = $this->_encryption_check;
                      }
                      $line_array[1] = str_replace("<<CRLF>>",chr(10),isset($line_array[1]) ? $line_array[1] : "");
                      if ("" != nullable_trim($line_array[0])) {
                          $this->_device_data[mb_strtolower($line_array[0],'UTF-8')] = $line_array[1];
                      }
                  }
                  
                  fclose($file_handler);
                  $result = TRUE;

                  if (("" != $this->_device_data['encryption_hash']) && ($local_encryption_check)) {
                      if ($this->_device_data['encryption_hash'] != $this->CalculateControlHash($this->GetEncryptionKey())) {
                          $this->_device_data['encryption_hash'] = "ERROR";
                          $this->WriteLog("Error: the device information encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "");
                          $result = FALSE;
                      }
                  }
              }
          }
      }

      // And now, we override the values if another backend type is defined
      if ($this->GetBackendTypeValidated()) {
          switch ($this->_config_data['backend_type']) {
              case 'mysql':
                  $device_escaped = escape_mysql_string($this->_device);
                  if ($this->OpenMysqlDatabase()) {
                      if ("" != $this->_config_data['sql_devices_table']) {
                          $sQuery  = "SELECT * FROM `".$this->_config_data['sql_devices_table']."` WHERE `device_id` = '".$device_escaped."'";
                          $aRow = NULL;
                          
                          if (is_object($this->_mysqli)) {
                              if (!($rResult = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: SQL query error ($sQuery) : ".nullable_trim($this->_mysqli->error).' ', TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  $aRow = $rResult->fetch_assoc();
                              }
                          } else {
                              if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                                  $this->WriteLog("Error: SQL query error ($sQuery) : ".mysql_error(), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  $aRow = mysql_fetch_assoc($rResult);
                              }
                          }

                          if (NULL != $aRow) {
                              $result = FALSE;
                              foreach ($aRow as $key => $value) {
                                  $in_the_schema = FALSE;
                                  foreach ($this->_sql_tables_schema['devices'] as $valid_key => $valid_format) {
                                      if ($valid_key == $key) {
                                          $in_the_schema = TRUE;
                                          if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                            $value = "";
                                          }
                                          break;
                                      }
                                  }
                                  if (($in_the_schema) && ($key != 'device_id')) {
                                      if (('ENC:' == mb_substr($value,0,4)) && (':ENC' == mb_substr($value,-4))) {
                                          $value = mb_substr($value,4);
                                          $value = mb_substr($value,0,mb_strlen($value)-4);
                                          $this->_device_data[$key] = $this->Decrypt($key,$value,$this->GetEncryptionKey());
                                          $local_encryption_check = $this->_encryption_check;
                                      } else {
                                          $this->_device_data[$key] = $value;
                                      }
                                      $this->_device_data[$key] = str_replace("<<CRLF>>",chr(10),$this->_device_data[$key]);
                                  } elseif ((!$in_the_schema) && ('unique_id' != $key)  && $this->GetVerboseFlag()) {
                                      $this->WriteLog("Warning: *The key ".$key." is not in the devices database schema", FALSE, FALSE, _MULTIOTP_SYSTEM_KEY_NOT_IN_SCHEMA_ERROR_, 'System', "", 3);
                                  }
                                  $result = TRUE;
                              }
                              if (0 == count($aRow) && !$create) {
                                  $this->WriteLog("Error: SQL database entry for device ".$this->_device." does not exist", FALSE, FALSE, _MULTIOTP_DEVICE_MISSING_, 'System', "", 3);
                              }
                          }
                      }
                      if (("" != $this->_device_data['encryption_hash']) && ($local_encryption_check)) {
                          if ($this->_device_data['encryption_hash'] != $this->CalculateControlHash($this->GetEncryptionKey())) {
                              $this->_device_data['encryption_hash'] = "ERROR";
                              $this->WriteLog("Error: the devices mysql encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "");
                              $result = FALSE;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $device_escaped = pg_escape_string($this->_pgsql_database_link, $this->_device);
                      if ("" != $this->_config_data['sql_devices_table']) {
                          $sQuery  = "SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_devices_table']."\" WHERE \"device_id\" = '".$device_escaped."'";
                          $aRow = NULL;
                          
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: SQL query error ($sQuery) : ".pg_last_error($this->_pgsql_database_link), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                              $result = FALSE;
                          } else {
                              $aRow = pg_fetch_assoc($rResult);
                          }

                          if (NULL != $aRow) {
                              $result = FALSE;
                              foreach ($aRow as $key => $value) {
                                  $in_the_schema = FALSE;
                                  foreach ($this->_sql_tables_schema['devices'] as $valid_key => $valid_format) {
                                      if ($valid_key == $key) {
                                          $in_the_schema = TRUE;
                                          if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                            $value = "";
                                          }
                                          break;
                                      }
                                  }
                                  if (($in_the_schema) && ($key != 'device_id')) {
                                      if (('ENC:' == mb_substr($value,0,4)) && (':ENC' == mb_substr($value,-4))) {
                                          $value = mb_substr($value,4);
                                          $value = mb_substr($value,0,mb_strlen($value)-4);
                                          $this->_device_data[$key] = $this->Decrypt($key,$value,$this->GetEncryptionKey());
                                          $local_encryption_check = $this->_encryption_check;
                                      } else {
                                          $this->_device_data[$key] = $value;
                                      }
                                      $this->_device_data[$key] = str_replace("<<CRLF>>",chr(10),$this->_device_data[$key]);
                                  } elseif ((!$in_the_schema) && ('unique_id' != $key)  && $this->GetVerboseFlag()) {
                                      $this->WriteLog("Warning: *The key ".$key." is not in the devices database schema", FALSE, FALSE, _MULTIOTP_SYSTEM_KEY_NOT_IN_SCHEMA_ERROR_, 'System', "", 3);
                                  }
                                  $result = TRUE;
                              }
                              if (0 == count($aRow) && !$create) {
                                  $this->WriteLog("Error: SQL database entry for device ".$this->_device." does not exist", FALSE, FALSE, _MULTIOTP_DEVICE_MISSING_, 'System', "", 3);
                              }
                          }
                      }
                      if (("" != $this->_device_data['encryption_hash']) && ($local_encryption_check)) {
                          if ($this->_device_data['encryption_hash'] != $this->CalculateControlHash($this->GetEncryptionKey())) {
                              $this->_device_data['encryption_hash'] = "ERROR";
                              $this->WriteLog("Error: the devices pgsql encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "");
                              $result = FALSE;
                          }
                      }
                  }
                  break;
              default:
              // Nothing to do if the backend type is unknown
                  break;
          }
      }
      return $result;
  } // ReadDeviceData


  // The first parameter is an array (new) with various options or a boolean with_radius_update (old)
  function WriteDeviceData(
      $write_device_data_array = true
  ) {
      if (is_array($write_device_data_array)) {
        if (!isset($write_device_data_array['with_radius_update'])) {
          $write_device_data_array['with_radius_update'] = TRUE;
        }
      } else {
        $temp_array = [];
        $temp_array['with_radius_update'] = $write_device_data_array;
        $write_device_data_array = $temp_array;
      }

      if ("" == nullable_trim($this->GetDevice())) {
          $result = false;
      } else {
          $result = $this->WriteData(array_merge(array('item'               => 'Device',
                                                       'table'              => 'devices',
                                                       'folder'             => $this->GetDevicesFolder(),
                                                       'data_array'         => $this->_device_data,
                                                       'force_file'         => false,
                                                       'id_field'           => 'device_id',
                                                       'id_value'           => $this->GetDevice()
                                                      ), $write_device_data_array));
      }
      return $result;
  }


  function SetDevice(
      $device
  ) {
      $this->ResetDeviceArray();
      $this->_device = mb_strtolower($device,'UTF-8');
      $this->ReadDeviceData("", TRUE); // First parameter empty, otherwise it will loop with SetDevice !
  }


  function GetDevice() {
      return mb_strtolower($this->_device,'UTF-8');
  }


  function IsDeviceDontSendGroups(
      $device = ""
  ) {
      if($device != "") {
          $this->SetDevice($device);
      }
      return (1 == (isset($this->_device_data['dont_send_groups'])?$this->_device_data['dont_send_groups']:false));
  }


  function SetDeviceDontSendGroups(
      $first_param,
      $second_param = "*-*"
  ) {
      $value = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $this->SetDevice($first_param);
          $value = $second_param;
      }
      $this->_device_data['dont_send_groups'] = intval($value);

      return $value;
  }


  function GetDeviceDontSendGroups(
      $device = ""
  ) {
      if($device != "") {
          $this->SetDevice($device);
      }
      return intval($this->_device_data['dont_send_groups']);
  }


  function IsDeviceCacheResultEnabled(
      $device = ""
  ) {
      if($device != "") {
          $this->SetDevice($device);
      }
      return (1 == (isset($this->_device_data['cache_result_enabled'])?$this->_device_data['cache_result_enabled']:false));
  }


  function SetDeviceCacheResultEnabled(
      $first_param,
      $second_param = "*-*"
  ) {
      $value = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $this->SetDevice($first_param);
          $value = $second_param;
      }
      $this->_device_data['cache_result_enabled'] = intval($value);

      return $value;
  }


  function GetDeviceCacheResultEnabled(
      $device = ""
  ) {
      if($device != "") {
          $this->SetDevice($device);
      }
      return intval($this->_device_data['cache_result_enabled']);
  }


  function IsDeviceForceNoPrefixEnabled(
      $device = ""
  ) {
      if($device != "") {
          $this->SetDevice($device);
      }
      return (1 == (isset($this->_device_data['force_no_prefix_pin'])?$this->_device_data['force_no_prefix_pin']:false));
  }


  function SetDeviceForceNoPrefixEnabled(
      $first_param,
      $second_param = "*-*"
  ) {
      $value = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $this->SetDevice($first_param);
          $value = $second_param;
      }
      $this->_device_data['force_no_prefix_pin'] = intval($value);

      return $value;
  }


  function GetDeviceForceNoPrefixEnabled(
      $device = ""
  ) {
      if($device != "") {
          $this->SetDevice($device);
      }
      return intval($this->_device_data['force_no_prefix_pin']);
  }


  function SetDeviceCacheTimeout(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetDevice($first_param);
          $result = $second_param;
      }
      $this->_device_data['cache_timeout'] = intval($result);

      return $result;
  }


  function GetDeviceCacheTimeout(
      $device = ""
  ) {
      if ($device != "") {
          $this->SetDevice($device);
      }
      return intval($this->_device_data['cache_timeout']);
  }


  function SetDeviceDescription(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetDevice($first_param);
          $result = $second_param;
      }
      $this->_device_data['description'] = $result;

      return $result;
  }


  function GetDeviceDescription(
      $device_description_array = []
  ) {
      if (!is_array($device_description_array)) {
        // Backward compatibility
        $device = $device_description_array;
      } else {
        $device = isset($device_description_array['device'])?$device_description_array['device']:"";
      }

      if($device != "") {
          $this->SetDevice($device);
      }
      return $this->_device_data['description'];
  }


  function SetDeviceShortname(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetDevice($first_param);
          $result = $second_param;
      }
      $this->_device_data['shortname'] = $result;

      return $result;
  }


  function GetDeviceShortname(
      $device = ""
  ) {
      if ($device != "") {
          $this->SetDevice($device);
      }
      return $this->_device_data['shortname'];
  }


  function SetDeviceIpOrFqdn(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetDevice($first_param);
          $result = $second_param;
      }
      $this->_device_data['ip_or_fqdn'] = $result;

      return $result;
  }


  function GetDeviceIpOrFqdn(
      $device = ""
  ) {
      if($device != "") {
          $this->SetDevice($device);
      }
      return $this->_device_data['ip_or_fqdn'];
  }


  function SetDeviceSubnet(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetDevice($first_param);
          $result = $second_param;
      }
      $this->_device_data['subnet'] = $result;

      return $result;
  }


  function GetDeviceSubnet(
      $device = ""
  ) {
      if ($device != "") {
          $this->SetDevice($device);
      }
      return $this->_device_data['subnet'];
  }


  function SetDeviceSecret(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetDevice($first_param);
          $result = $second_param;
      }
      $this->_device_data['device_secret'] = $result;

      return $result;
  }


  function GetDeviceSecret(
      $device = ""
  ) {
      if($device != "") {
          $this->SetDevice($device);
      }
      return $this->_device_data['device_secret'];
  }


  function SetDeviceChallengeEnabled(
      $first_param,
      $second_param = "*-*"
  ) {
      $value = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $this->SetDevice($first_param);
          $value = $second_param;
      }
      $this->_device_data['challenge_response_enabled'] = intval($value);

      return $value;
  }


  function GetDeviceChallengeEnabled(
      $device = ""
  ) {
      if($device != "") {
          $this->SetDevice($device);
      }
      return intval($this->_device_data['challenge_response_enabled']);
  }


  function SetDeviceTextTokenChallenge(
      $first_param,
      $second_param = "*-*"
  ) {
      $value = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $this->SetDevice($first_param);
          $value = $second_param;
      }
      $this->_device_data['text_token_challenge'] = $value;

      return $value;
  }


  function GetDeviceTextTokenChallenge(
      $device = ""
  ) {
      if($device != "") {
          $this->SetDevice($device);
      }
      return $this->_device_data['text_token_challenge'];
  }


  function SetDeviceSmsChallengeEnabled(
      $first_param,
      $second_param = "*-*"
  ) {
      $value = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $this->SetDevice($first_param);
          $value = $second_param;
      }
      $this->_device_data['sms_challenge_enabled'] = intval($value);

      return $value;
  }


  function GetDeviceSmsChallengeEnabled(
      $device = ""
  ) {
      if($device != "") {
          $this->SetDevice($device);
      }
      return intval($this->_device_data['sms_challenge_enabled']);
  }


  function SetDeviceTextSmsChallenge(
      $first_param,
      $second_param = "*-*"
  ) {
      $value = "";
      if ($second_param == "*-*") {
          $value = $first_param;
      } else {
          $this->SetDevice($first_param);
          $value = $second_param;
      }
      $this->_device_data['text_sms_challenge'] = $value;

      return $value;
  }


  function GetDeviceTextSmsChallenge(
      $device = ""
  ) {
      if ($device != "") {
          $this->SetDevice($device);
      }
      return $this->_device_data['text_sms_challenge'];
  }


  function DeleteDevice(
      $device = "",
      $no_error_info = FALSE
  ) {
      if ("" != $device) {
          $this->SetDevice($device);
      }
      
      $result = FALSE;
      
      // First, we delete the device file if the backend is files or when migration is enabled
      if (('files' == $this->GetBackendType()) || ($this->GetMigrationFromFile())) {
          $device_filename = $this->GetDevice().'.db';
          if (!file_exists($this->GetDevicesFolder().$device_filename)) {
              if (!$no_error_info) {
                  $this->WriteLog("Error: Unable to delete device ".$this->_device.", database file ".$this->GetDevicesFolder().$device_filename." does not exist", FALSE, FALSE, _MULTIOTP_DEVICE_MISSING_, 'System', "");
              }
          } else {
              $result = unlink($this->GetDevicesFolder().$device_filename);
              if ($result) {
                  if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Info: *Device ".$this->_device." successfully deleted", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Device', "");
                  }
              } else {
                  if (!$no_error_info) {
                      $this->WriteLog("Error: Unable to delete device ".$this->_device, FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'Device', "");
                  }
              }
          }
      }

      if ($this->GetBackendTypeValidated()) {
          switch ($this->_config_data['backend_type']) {
              case 'mysql':
                  $device_escaped = escape_mysql_string($this->_device);
                  if ($this->OpenMysqlDatabase()) {
                      if ("" != $this->_config_data['sql_devices_table']) {
                          $sQuery  = "DELETE FROM `".$this->_config_data['sql_devices_table']."` WHERE `device_id` = '".$device_escaped."'";
                          
                          if (is_object($this->_mysqli)) {
                              if (!($rResult = $this->_mysqli->query($sQuery))) {
                                  if (!$no_error_info) {
                                      $this->WriteLog("Error: Could not delete device ".$this->_device.": ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
                                  }
                              } else {
                                  $num_rows = $this->_mysqli->affected_rows;
                              }
                          } elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete device ".$this->_device.": ".mysql_error(), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
                              }
                          } else {
                              $num_rows = mysql_affected_rows($this->_mysql_database_link);
                          }
                          
                          if (0 == $num_rows) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete device ".$this->_device.". Device does not exist", FALSE, FALSE, _MULTIOTP_DEVICE_MISSING_, 'System', "");
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *Device ".$this->_device." successfully deleted", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Device', "");
                              }
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $device_escaped = pg_escape_string($this->_pgsql_database_link, $this->_device);
                      if ("" != $this->_config_data['sql_devices_table']) {
                          $sQuery  = "DELETE FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_devices_table']."\" WHERE \"device_id\" = '".$device_escaped."'";
                          
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete device ".$this->_device.": ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
                              }
                          } else {
                              $num_rows = pg_affected_rows($rResult);
                          }
                          
                          if (0 == $num_rows) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete device ".$this->_device.". Device does not exist", FALSE, FALSE, _MULTIOTP_DEVICE_MISSING_, 'System', "");
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *Device ".$this->_device." successfully deleted", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Device', "");
                              }
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              default:
                  // Nothing to do if the backend type is unknown
                  break;
          }                        
      }
      if ($result) {
          $this->TouchFolder('data',
                             'Device',
                             $this->GetDevice(),
                             TRUE,
                             "DeleteDevice");
      }
      return $result;
  }


  function GetDevicesList()
  {
      return $this->GetList('device_id', 'sql_devices_table', $this->GetDevicesFolder());
  }
  
  
  function GetDevicesCount()
  {
      if (($this->IsCacheData()) && (intval($this->ReadCacheValue('devices_count')) >= 0))
      {
          $devices_count = intval($this->ReadCacheValue('devices_count'));
      }
      else
      {
          $devices_count = 0;
          switch ($this->GetBackendType())
          {
              case 'mysql':
                  if ($this->OpenMysqlDatabase())
                  {
                      $sQuery  = "SELECT device_id FROM `".$this->_config_data['sql_devices_table']."` ";
                      
                      if (is_object($this->_mysqli))
                      {
                          if (!($result = $this->_mysqli->query($sQuery)))
                          {
                              $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              $result = FALSE;
                          }
                          else
                          {
                              while ($aRow = $result->fetch_assoc())
                              {
                                  $devices_count++;
                              }
                          }
                      }
                      else
                      {
                          if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link)))
                          {
                              $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          }
                          else
                          {
                              while ($aRow = mysql_fetch_assoc($rResult))
                              {
                                  $devices_count++;
                              }                         
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase())
                  {
                      $sQuery  = "SELECT \"device_id\" FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_devices_table']."\" ";
                      
                      if (!($rResult = pg_query($sQuery, $this->_pgsql_database_link)))
                      {
                          $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                      }
                      else
                      {
                          while ($aRow = pg_fetch_assoc($rResult))
                          {
                              $devices_count++;
                          }                         
                      }
                  }
                  break;
              case 'files':
              default:
                  if ($devices_handle = @opendir($this->GetDevicesFolder()))
                  {
                      while ($file = readdir($devices_handle))
                      {
                          if ((mb_substr($file, -3) == ".db") && ($file != '.db'))
                          {
                              $devices_count++;
                          }
                      }
                      closedir($devices_handle);
                  }
          }
          if (($this->IsCacheData()) && ($devices_count >= 0))
          {
              $this->WriteCacheValue('devices_count', $devices_count);
              $this->WriteCacheData();
          }
      }
      return $devices_count;
  }



  /***************************
   ***************************
   ***************************
   ***   GROUPS HANDLING   ***
   ***************************
   ***************************
   ***************************/

  function CreateGroup(
      $id = "",
      $name = "",
      $description = ""
  ) {
      if ("" != nullable_trim($name)) {
          $group_id = $id;
          if (("" == $group_id) || ('0' == $group_id)) {
              $group_id = bigdec2hex((time()-mktime(1,1,1,1,1,2000)).mt_rand(10000,99999));
          }
          if ($this->CheckGroupExists($group_id)) {
              return FALSE; // ERROR: group already exists.
          } else {
              $this->SetGroup($group_id);
              $this->SetGroupName(nullable_trim($name));
              $this->SetGroupDescription($description);
              return $this->WriteGroupData();
          }
      } else {
          return TRUE;
      }
  }    


  function ReadGroupData(
      $group_id = "",
      $create = FALSE
  ) {
      // We initialize the local encryption check variable
      $local_encryption_check = false;

      $this->ResetGroupArray();
      if ("" != $group_id) {
          $this->SetGroup($group_id);
      }
      $result = FALSE;
      
      // We initialize the encryption hash to empty
      $this->_group_data['encryption_hash'] = "";
      
      // First, we read the user file if the backend is files or when migration is enabled
      if (('files' == $this->GetBackendType()) || ($this->GetMigrationFromFile())) {
          $group_filename = $this->EncodeFileId($this->GetGroup(), FALSE).'.db';
          if (!file_exists($this->GetGroupsFolder().$group_filename)) {
              $group_filename = $this->EncodeFileId($this->GetGroup(), FALSE, TRUE).'.db';
          }
          if (!file_exists($this->GetGroupsFolder().$group_filename)) {
              if (!$create) {
                  $this->WriteLog("Error: database file ".$this->GetGroupsFolder().$group_filename." for group ".$this->_group." does not exist", FALSE, FALSE, _MULTIOTP_GROUP_MISSING_, 'System', "", 3);
              }
          } else {
              if ($file_handler = @fopen($this->GetGroupsFolder().$group_filename, "rt")) {
                  flock($file_handler, LOCK_SH);
                  $first_line = nullable_trim(fgets($file_handler));
                  
                  while (!feof($file_handler)) {
                      $line = nullable_trim(fgets($file_handler));
                      $line_array = explode("=",$line,2);
                      if (":" == mb_substr($line_array[0], -1)) {
                          $line_array[0] = mb_substr($line_array[0], 0, mb_strlen($line_array[0]) -1);
                          $line_array[1] = $this->Decrypt($line_array[0],$line_array[1],$this->GetEncryptionKey());
                          $local_encryption_check = $this->_encryption_check;
                      }
                      $line_array[1] = str_replace("<<CRLF>>",chr(10),isset($line_array[1]) ? $line_array[1] : "");
                      if ("" != nullable_trim($line_array[0])) {
                          $this->_group_data[mb_strtolower($line_array[0],'UTF-8')] = $line_array[1];
                      }
                  }
                  
                  fclose($file_handler);
                  $result = TRUE;

                  if (("" != $this->_group_data['encryption_hash']) && ($local_encryption_check)) {
                      if ($this->_group_data['encryption_hash'] != $this->CalculateControlHash($this->GetEncryptionKey())) {
                          $this->_group_data['encryption_hash'] = "ERROR";
                          $this->WriteLog("Error: the group information encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "");
                          $result = FALSE;
                      }
                  }
              }
          }
      }

      // And now, we override the values if another backend type is defined
      if ($this->GetBackendTypeValidated()) {
          switch ($this->_config_data['backend_type']) {
              case 'mysql':
                  $group_escaped = escape_mysql_string($this->_group);
                  if ($this->OpenMysqlDatabase()) {
                      if ("" != $this->_config_data['sql_groups_table']) {
                          $sQuery  = "SELECT * FROM `".$this->_config_data['sql_groups_table']."` WHERE `group_id` = '".$group_escaped."'";
                          $aRow = NULL;
                          
                          if (is_object($this->_mysqli)) {
                              if (!($rResult = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: SQL query error ($sQuery) : ".nullable_trim($this->_mysqli->error).' ', TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  $aRow = $rResult->fetch_assoc();
                              }
                          } else {
                              if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                                  $this->WriteLog("Error: SQL query error ($sQuery) : ".mysql_error(), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                                  $result = FALSE;
                              } else {
                                  $aRow = mysql_fetch_assoc($rResult);
                              }
                          }

                          if (NULL != $aRow) {
                              $result = FALSE;
                              foreach ($aRow as $key => $value) {
                                  $in_the_schema = FALSE;
                                  foreach ($this->_sql_tables_schema['groups'] as $valid_key => $valid_format) {
                                      if ($valid_key == $key) {
                                          $in_the_schema = TRUE;
                                          if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                            $value = "";
                                          }
                                          break;
                                      }
                                  }
                                  if (($in_the_schema) && ($key != 'group_id')) {
                                      if (('ENC:' == mb_substr($value,0,4)) && (':ENC' == mb_substr($value,-4))) {
                                          $value = mb_substr($value,4);
                                          $value = mb_substr($value,0,mb_strlen($value)-4);
                                          $this->_group_data[$key] = $this->Decrypt($key,$value,$this->GetEncryptionKey());
                                          $local_encryption_check = $this->_encryption_check;
                                      } else {
                                          $this->_group_data[$key] = $value;
                                      }
                                      $this->_group_data[$key] = str_replace("<<CRLF>>",chr(10),$this->_group_data[$key]);
                                  } elseif ((!$in_the_schema) && ('unique_id' != $key)  && $this->GetVerboseFlag()) {
                                      $this->WriteLog("Warning: *The key ".$key." is not in the groups database schema", FALSE, FALSE, _MULTIOTP_SYSTEM_KEY_NOT_IN_SCHEMA_ERROR_, 'System', "", 3);
                                  }
                                  $result = TRUE;
                              }
                              if (0 == count($aRow) && !$create) {
                                  $this->WriteLog("Error: SQL database entry for group ".$this->_group." does not exist", FALSE, FALSE, _MULTIOTP_GROUP_MISSING_, 'System', "", 3);
                              }
                          }
                      }
                      if (("" != $this->_group_data['encryption_hash']) && ($local_encryption_check)) {
                          if ($this->_group_data['encryption_hash'] != $this->CalculateControlHash($this->GetEncryptionKey())) {
                              $this->_group_data['encryption_hash'] = "ERROR";
                              $this->WriteLog("Error: the groups mysql encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "");
                              $result = FALSE;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $group_escaped = pg_escape_string($this->_pgsql_database_link, $this->_group);
                      if ("" != $this->_config_data['sql_groups_table']) {
                          $sQuery  = "SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_groups_table']."\" WHERE \"group_id\" = '".$group_escaped."'";
                          $aRow = NULL;
                      
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: SQL query error ($sQuery) : ".pg_last_error($this->_pgsql_database_link), TRUE, FALSE, _MULTIOTP_SYSTEM_SQL_QUERY_ERROR_, 'System', "", 3);
                              $result = FALSE;
                          } else {
                              $aRow = pg_fetch_assoc($rResult);
                          }

                          if (NULL != $aRow) {
                              $result = FALSE;
                              foreach ($aRow as $key => $value) {
                                  $in_the_schema = FALSE;
                                  foreach ($this->_sql_tables_schema['groups'] as $valid_key => $valid_format) {
                                      if ($valid_key == $key) {
                                          $in_the_schema = TRUE;
                                          if (((substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 4) == 'TEXT') || (substr(mb_strtoupper($valid_format, 'UTF-8'), 0, 7) == 'VARCHAR')) && is_null($value)) {
                                            $value = "";
                                          }
                                          break;
                                      }
                                  }
                                  if (($in_the_schema) && ($key != 'group_id')) {
                                      if (('ENC:' == mb_substr($value,0,4)) && (':ENC' == mb_substr($value,-4))) {
                                          $value = mb_substr($value,4);
                                          $value = mb_substr($value,0,mb_strlen($value)-4);
                                          $this->_group_data[$key] = $this->Decrypt($key,$value,$this->GetEncryptionKey());
                                          $local_encryption_check = $this->_encryption_check;
                                      } else {
                                          $this->_group_data[$key] = $value;
                                      }
                                      $this->_group_data[$key] = str_replace("<<CRLF>>",chr(10),$this->_group_data[$key]);
                                  } elseif ((!$in_the_schema) && ('unique_id' != $key)  && $this->GetVerboseFlag()) {
                                      $this->WriteLog("Warning: *The key ".$key." is not in the groups database schema", FALSE, FALSE, _MULTIOTP_SYSTEM_KEY_NOT_IN_SCHEMA_ERROR_, 'System', "", 3);
                                  }
                                  $result = TRUE;
                              }
                              if (0 == count($aRow) && !$create) {
                                  $this->WriteLog("Error: SQL database entry for group ".$this->_group." does not exist", FALSE, FALSE, _MULTIOTP_GROUP_MISSING_, 'System', "", 3);
                              }
                          }
                      }
                      if (("" != $this->_group_data['encryption_hash']) && ($local_encryption_check)) {
                          if ($this->_group_data['encryption_hash'] != $this->CalculateControlHash($this->GetEncryptionKey())) {
                              $this->_group_data['encryption_hash'] = "ERROR";
                              $this->WriteLog("Error: the groups pgsql encryption key is not matching", FALSE, FALSE, _MULTIOTP_ENCRYPTION_ERROR_, 'System', "");
                              $result = FALSE;
                          }
                      }
                  }
                  break;
              default:
              // Nothing to do if the backend type is unknown
                  break;
          }
      }
      return $result;
  }


  function WriteGroupData(
    $write_group_data_array = []
  ) {
      if ("" == nullable_trim($this->GetGroup())) {
          $result = false;
      } else {
          $result = $this->WriteData(array_merge(array('item'               => 'Group',
                                                       'table'              => 'groups',
                                                       'folder'             => $this->GetGroupsFolder(),
                                                       'data_array'         => $this->_group_data,
                                                       'force_file'         => false,
                                                       'id_field'           => 'group_id',
                                                       'id_value'           => $this->GetGroup()
                                                      ), $write_group_data_array));
      }
      return $result;
  }


  function SetGroup(
      $group
  ) {
      $this->ResetGroupArray();
      $this->_group = mb_strtolower($group,'UTF-8');
      $this->ReadGroupData("", TRUE); // First parameter empty, otherwise it will loop with SetGroup !
  }


  function GetGroup()
  {
      return mb_strtolower($this->_group,'UTF-8');
  }


  function SetGroupDescription(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = $first_param;
      } else {
          $this->SetGroup($first_param);
          $result = $second_param;
      }
      $result = $this->EncodeForBackend($result);
      $this->_group_data['description'] = $result;

      return $result;
  }


  function SetGroupName(
      $first_param,
      $second_param = "*-*"
  ) {
      $result = "";
      if ($second_param == "*-*") {
          $result = nullable_trim($first_param);
      } else {
          $this->SetGroup($first_param);
          $result = nullable_trim($second_param);
      }
      $this->_group_data['name'] = $result;

      return $result;
  }


  function GetGroupDescription(
      $group = ""
  ) {
      if($group != "") {
          $this->SetGroup($group);
      }
      return $this->_group_data['description'];
  }


  function GetGroupName(
      $group = ""
  ) {
      if($group != "") {
          $this->SetGroup($group);
      }
      return $this->_group_data['name'];
  }


  function DeleteGroup(
      $group = "",
      $no_error_info = FALSE
  ) {
      if ("" != $group) {
          $this->SetGroup($group);
      }
      
      $result = FALSE;
      
      // First, we delete the group file if the backend is files or when migration is enabled
      if (('files' == $this->GetBackendType()) || ($this->GetMigrationFromFile())) {
          $group_filename = $this->EncodeFileId($this->GetGroup(), FALSE).'.db';
          if (!file_exists($this->GetGroupsFolder().$group_filename)) {
              $group_filename = $this->EncodeFileId($this->GetGroup(), FALSE, TRUE).'.db';
          }
          if (!file_exists($this->GetGroupsFolder().$group_filename)) {
              if (!$no_error_info) {
                  $this->WriteLog("Error: Unable to delete group ".$this->_group.", database file ".$this->GetGroupsFolder().$group_filename." does not exist", FALSE, FALSE, _MULTIOTP_GROUP_MISSING_, 'System', "");
              }
          } else {
              $result = unlink($this->GetGroupsFolder().$group_filename);
              if ($result) {
                  if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Info: *Group ".$this->_group." successfully deleted", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Group', "");
                  }
              } else {
                  if (!$no_error_info) {
                      $this->WriteLog("Error: Unable to delete group ".$this->_group, FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'Group', "");
                  }
              }
          }
      }

      if ($this->GetBackendTypeValidated()) {
          switch ($this->_config_data['backend_type']) {
              case 'mysql':
                  $group_escaped = escape_mysql_string($this->_group);
                  if ($this->OpenMysqlDatabase()) {
                      if ("" != $this->_config_data['sql_groups_table']) {
                          $sQuery  = "DELETE FROM `".$this->_config_data['sql_groups_table']."` WHERE `group_id` = '".$group_escaped."'";
                          
                          if (is_object($this->_mysqli)) {
                              if (!($rResult = $this->_mysqli->query($sQuery))) {
                                  if (!$no_error_info) {
                                      $this->WriteLog("Error: Could not delete group ".$this->_group.": ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
                                  }
                              } else {
                                  $num_rows = $this->_mysqli->affected_rows;
                              }
                          } elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete group ".$this->_group.": ".mysql_error(), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
                              }
                          } else {
                              $num_rows = mysql_affected_rows($this->_mysql_database_link);
                          }
                          
                          if (0 == $num_rows) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete group ".$this->_group.". Group does not exist", FALSE, FALSE, _MULTIOTP_GROUP_MISSING_, 'Group', "");
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *Group ".$this->_group." successfully deleted", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Group', "");
                              }
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $group_escaped = pg_escape_string($this->_pgsql_database_link, $this->_group);
                      if ("" != $this->_config_data['sql_groups_table']) {
                          $sQuery  = "DELETE FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_groups_table']."\" WHERE \"group_id\" = '".$group_escaped."'";
                          
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete group ".$this->_group.": ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_DATA_UPDATE_ERROR_, 'System', "");
                              }
                          } else {
                              $num_rows = pg_affected_rows($rResult);
                          }
                          
                          if (0 == $num_rows) {
                              if (!$no_error_info) {
                                  $this->WriteLog("Error: Could not delete group ".$this->_group.". Group does not exist", FALSE, FALSE, _MULTIOTP_GROUP_MISSING_, 'Group', "");
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *Group ".$this->_group." successfully deleted", FALSE, FALSE, _MULTIOTP_OPERATION_DONE_, 'Group', "");
                              }
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              default:
                  // Nothing to do if the backend type is unknown
                  break;
          }                        
      }
      if ($result) {
          $this->TouchFolder('data',
                             'Group',
                             $this->GetGroup(),
                             TRUE,
                             "DeleteGroup");
      }
      return $result;
  }


  // Check if group exists
  function CheckGroupExists(
      $group = ""
  ) {
      $check_group = ("" != $group)?$group:$this->GetGroup();
      $result = FALSE;

      if ("" != nullable_trim($check_group)) {
          if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data['sql_groups_table'])) || ('files' == $this->GetBackendType())) {
              switch ($this->GetBackendType()) {
                  case 'mysql':
                      $check_group_escaped = escape_mysql_string($check_group);
                      if ($this->OpenMysqlDatabase()) {
                          $sQuery  = "SELECT * FROM `".$this->_config_data['sql_groups_table']."` WHERE `group_id` = '{$check_group_escaped}'";
                          
                          if (is_object($this->_mysqli)) {
                              if (!($rResult = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              } else {
                                  $num_rows = $rResult->num_rows;
                              }
                          } elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              $num_rows = mysql_num_rows($this->_mysql_database_link);
                          }
                          
                          if (0 == $num_rows) {
                              $this->WriteLog("Error: Group ".$group.". does not exist", FALSE, FALSE, _MULTIOTP_GROUP_MISSING_, 'System', "", 3);
                              $result = FALSE;
                          } else {
                              $result = TRUE;
                          }
                      }
                      break;
                  case 'pgsql':
                      if ($this->OpenPGSQLDatabase()) {
                          $check_group_escaped = pg_escape_string($this->_pgsql_database_link, $check_group);
                          $sQuery  = "SELECT * FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_groups_table']."\" WHERE \"group_id\" = '{$check_group_escaped}'";
                          
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              $num_rows = pg_num_rows($rResult);
                          }
                          
                          if (0 == $num_rows) {
                              $this->WriteLog("Error: Group ".$group.". does not exist", FALSE, FALSE, _MULTIOTP_GROUP_MISSING_, 'System', "", 3);
                              $result = FALSE;
                          } else {
                              $result = TRUE;
                          }
                      }
                      break;
                  case 'files':
                  default:
                      $group_filename = $this->EncodeFileId($check_group, FALSE).'.db';
                      if (!file_exists($this->GetGroupsFolder().$group_filename)) {
                          $group_filename = $this->EncodeFileId($check_group, FALSE, TRUE).'.db';
                      }
                      $result = file_exists($this->GetGroupsFolder().$group_filename);
                      break;
              }
          }
      }
      return $result;
  }


  function GetGroupsList()
  {
      return $this->GetList('group_id', 'sql_groups_table', $this->GetGroupsFolder());
  }


  function GetList(
      $raw_id,
      $table_name,
      $folder
  ) {
      $list = "";
      $list_array = [];
      if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data[$table_name])) || ('files' == $this->GetBackendType())) {
          switch ($this->GetBackendType()) {
              case 'mysql':
                  if ($this->OpenMysqlDatabase()) {
                      $sQuery = "SELECT `".$raw_id."` FROM `".$this->_config_data[$table_name]."`";
                      $sQuery.= " ORDER BY `".$raw_id."` ASC";
                      if (is_object($this->_mysqli)) {
                          if (!($result = $this->_mysqli->query($sQuery))) {
                              $this->WriteLog("Error: Unable to access the database: ".nullable_trim($this->_mysqli->error), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                              $result = FALSE;
                          } else {
                              while ($aRow = $result->fetch_assoc()) {
                                  if ("" != $aRow[$raw_id]) {
                                      $list.= (("" != $list)?"\t":"").$aRow[$raw_id];
                                  }
                              }
                          }
                      } else {
                          if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                              $this->WriteLog("Error: Unable to access the database: ".mysql_error(), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                          } else {
                              while ($aRow = mysql_fetch_assoc($rResult)) {
                                  if ("" != $aRow[$raw_id]) {
                                      $list.= (("" != $list)?"\t":"").$aRow[$raw_id];
                                  }
                              }                         
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $sQuery = "SELECT \"".$raw_id."\" FROM \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data[$table_name]."\"";
                      $sQuery.= " ORDER BY \"".$raw_id."\" ASC";
                      if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                          $this->WriteLog("Error: Unable to access the database: ".pg_last_error($this->_pgsql_database_link), FALSE, FALSE, _MULTIOTP_SYSTEM_SQL_ERROR_, 'System', "", 3);
                      } else {
                          while ($aRow = pg_fetch_assoc($rResult)) {
                              if ("" != $aRow[$raw_id]) {
                                  $list.= (("" != $list)?"\t":"").$aRow[$raw_id];
                              }
                          }                         
                      }
                  }
                  break;
              case 'files':
              default:
                  if ($file_handle = @opendir($folder)) {
                      while ($file = readdir($file_handle)) {
                          if ((mb_substr($file, -3) == ".db") && ($file != '.db')) {
                              array_push($list_array, mb_substr($file,0,-3));
                          }
                      }
                      sort($list_array);
                      foreach ($list_array as $one_list) {
                          $list.= (("" != $list)?"\t":"").$this->DecodeFileId($one_list);
                      }
                      closedir($file_handle);
                  }
          }
      }
      return $list;
  }


  function GetHardwareType(
    $return_array = false
  ) {
    $type       = (!empty($this->_hardware_array["type"]) ? $this->_hardware_array["type"] : "");
    $family     = (!empty($this->_hardware_array["family"]) ? $this->_hardware_array["family"] : "");
    $model      = (!empty($this->_hardware_array["model"]) ? $this->_hardware_array["model"] : "");
    $hardware   = (!empty($this->_hardware_array["hardware"]) ? $this->_hardware_array["hardware"] : "");
    $cpu_speed  = (!empty($this->_hardware_array["cpu_speed"]) ? $this->_hardware_array["cpu_speed"] : "");
    $os_running = (!empty($this->_hardware_array["os_running"]) ? $this->_hardware_array["os_running"] : "");
    
    if (
      (empty($type)) ||
      (empty($family)) ||
      (empty($model)) ||
      (empty($hardware)) ||
      (empty($cpu_speed)) ||
      (empty($os_running))
    ) {
      // Hardware detection (2023-11-23, enhanced 2025-11-11)
      $type   = '';
      $family = '';
      $model  = '';
      $hardware  = '';
      $cpu_speed  = '';
      $os_running = str_replace(' '. $this->GetSystemName(), "", php_uname());

      // Is it a Windows platform (for development only) ?
      if (mb_strtolower(mb_substr(PHP_OS, 0, 3),'UTF-8') === 'win') {
        $type   = 'DVP';
        $family = 'WIN';
        $model = $this->ReadBootCacheValue("model");
        if (is_null($model)) {
          $model  = 'Windows device';
          $cmd = 'powershell -Command "(Get-CimInstance Win32_ComputerSystem).Model"';
          $output = nullable_trim(shell_exec($cmd));
          if (!empty($output)) {
            $model = $output;
          }
          $this->WriteBootCacheValue("model", $model);
        }
        $hardware = $this->ReadBootCacheValue("hardware");
        if (is_null($hardware)) {
          $cmd = 'powershell -Command "(Get-CimInstance Win32_Processor).Name"';
          $output = nullable_trim(shell_exec($cmd));
          if (!empty($output)) {
            $hardware = $output;
          }
          $this->WriteBootCacheValue("hardware", $hardware);
        }
        $cpu_speed = $this->ReadBootCacheValue("cpu_speed");
        if (is_null($cpu_speed)) {
          $cmd = 'powershell -Command "(Get-CimInstance Win32_Processor).MaxClockSpeed"';
          $output = nullable_trim(shell_exec($cmd));
          if (!empty($output)) {
            $cpu_speed  = intval($output); // "{$output} MHz"
          }
          $this->WriteBootCacheValue("cpu_speed", $cpu_speed);
        }
      } else {
        $model  = 'Linux device';
        exec("cat /proc/cpuinfo | grep --color=never -i Model", $output);
        foreach ($output as $line) {
          $line.= "  ";
          if (preg_match("/^Model\sname\s*:\s*(.*)/i", $line)) {
            preg_match_all("/^Model\sname\s*:\s*(.*)/i", $line, $result_array, PREG_SET_ORDER);
            if (isset($result_array[0][1])) {
              $model = nullable_trim($result_array[0][1]);
              break;
            }
          }
        }
        exec("cat /proc/cpuinfo | grep --color=never -i Hardware", $output);
        foreach ($output as $line) {
          $line.= "  ";
          if (preg_match("/^Hardware\s*:\s*(.*)/i", $line)) {
            preg_match_all("/^Hardware\s*:\s*(.*)/i", $line, $result_array, PREG_SET_ORDER);
            if (isset($result_array[0][1])) {
              $hardware = nullable_trim($result_array[0][1]);
              break;
            }
          }
        }
        if (empty($hardware) && is_readable('/sys/class/dmi/id/product_name')) {
          $hardware = nullable_trim(file_get_contents('/sys/class/dmi/id/product_name'));
          $sys_vendor = is_readable('/sys/class/dmi/id/sys_vendor') ? nullable_trim(file_get_contents('/sys/class/dmi/id/sys_vendor')) : '';
          if (stripos($hardware, 'kvm') !== false || stripos($sys_vendor, 'qemu') !== false) {
            $hardware.= ' (KVM)';
          } elseif (stripos($hardware, 'virtualbox') !== false || stripos($sys_vendor, 'oracle') !== false) {
            $hardware.= ' (VirtualBox)';
          } elseif (stripos($hardware, 'vmware') !== false) {
            $hardware.= ' (VMware)';
          } elseif (stripos($hardware, 'microsoft') !== false) {
            $hardware.= ' (Hyper-V)';
          }
        }
        if (empty($hardware)) {
          if (is_readable('/proc/cpuinfo')) {
            $cpuinfo = file_get_contents('/proc/cpuinfo');
            if (preg_match('/^model name\s*:\s*(.+)$/mi', $cpuinfo, $matches)) {
              $hardware = nullable_trim($matches[1]);
              if (!empty($hardware)) {
                if (stripos($cpuinfo, 'hypervisor') !== false) {
                  $hardware.= ' (virtual)';
                }
              }
            }
          }
        }
        if (empty($hardware)) {
          $hardware = nullable_trim(shell_exec('uname -m'));
        }        
        exec("/usr/bin/lscpu | grep --color=never -i MHz", $output);
        foreach ($output as $line) {
          $line.= "  ";
          if (preg_match("/^CPU max MHz\s*:\s*(.*)/i", $line)) {
            preg_match_all("/^CPU max MHz\s*:\s*(.*)/i", $line, $result_array, PREG_SET_ORDER);
            if (isset($result_array[0][1])) {
              $cpu_speed = intval(nullable_trim($result_array[0][1]));
              break;
            }
          } elseif (preg_match("/^CPU\sMHz\s*:\s*(.*)/i", $line)) {
            preg_match_all("/^CPU\sMHz\s*:\s*(.*)/i", $line, $result_array, PREG_SET_ORDER);
            if (isset($result_array[0][1])) {
              $cpu_speed = intval(nullable_trim($result_array[0][1]));
              break;
            }
          } elseif (preg_match("/.*CPU.*\sMHz\s*:\s*(.*)/i", $line)) {
            preg_match_all("/.*CPU.*\sMHz\s*:\s*(.*)/i", $line, $result_array, PREG_SET_ORDER);
            if (isset($result_array[0][1])) {
              $cpu_speed = intval(nullable_trim($result_array[0][1]));
              break;
            }
          }
        }
        if (0 == intval($cpu_speed)) {
          exec("cat /proc/cpuinfo | grep --color=never -i cpu", $output);
          foreach ($output as $line) {
            $line.= "  ";
            if (preg_match("/^cpu MHz\s*:\s*(.*)/i", $line)) {
              preg_match_all("/^cpu MHz\s*:\s*(.*)/i", $line, $result_array, PREG_SET_ORDER);
              if (isset($result_array[0][1])) {
                $cpu_speed = intval(nullable_trim($result_array[0][1]));
                break;
              }
            }
          }
        }
        if (false !== mb_stripos($model,'Raspberry Pi 5')) {
          $type = 'RP5';
          $family = 'RPI';
        } elseif (false !== mb_stripos($model,'Raspberry Pi 4')) {
          $type = 'RP4';
          $family = 'RPI';
        } elseif (false !== mb_stripos($model,'Raspberry Pi 3 Model B Plus')) {
          $type = 'RP3+';
          $family = 'RPI';
        } elseif (false !== mb_stripos($model,'Raspberry Pi 3')) {
          $type = 'RP3';
          $family = 'RPI';
        } elseif (false !== mb_stripos($model,'Raspberry Pi 2')) {
          $type = 'RP2';
          $family = 'RPI';
        } elseif (false !== mb_stripos($model,'Raspberry Pi')) {
          $type = 'RP';
          $family = 'RPI';
        } elseif ((false !== mb_stripos($os_running, 'armv8')) || (FALSE !== mb_stripos($os_running, 'aarch64'))) {
          if ((FALSE !== mb_stripos($hardware, 'bcm27')) || (FALSE !== mb_stripos($hardware, 'bcm28'))) {
            if (false !== mb_stripos($cpu_speed, '1500')) {
              $type = 'RP4';
              $family = 'RPI';
            } elseif (false !== mb_stripos($cpu_speed, '1400')) {
              $type = 'RP3+';
              $family = 'RPI';
            } else {
              $type = 'RP3';
              $family = 'RPI';
            }
          } else {
              $type = 'ARM';
              $family = 'ARM';
          }
        } elseif (FALSE !== mb_stripos($os_running, 'armv7l')) {
          if ((FALSE !== mb_stripos($hardware, 'bcm27')) || (FALSE !== mb_stripos($hardware, 'bcm28'))) {
            if (false !== mb_stripos($cpu_speed, '1500')) {
              $type = 'RP4';
              $family = 'RPI';
            } elseif (false !== mb_stripos($cpu_speed, '1400')) {
              $type = 'RP3+';
              $family = 'RPI';
            } elseif (false !== mb_stripos($cpu_speed, '1200')) {
              $type = 'RP3';
              $family = 'RPI';
            } else {
              $type = 'RP2';
              $family = 'RPI';
            }
          } else {
            $type = 'BBB'; // Beaglebone Black (Generic AM33XX and others)
            $family = 'ARM';
          }
        } elseif (FALSE !== mb_stripos($os_running, 'armv6l')) {
            $type = 'RPI';
            $family = 'RPI';
        // Is it a virtual appliance and/or a Linux Debian edition
        } elseif (FALSE !== mb_stripos($os_running, 'debian')) {
            $type = 'VAP';
            $family = 'VAP';
        }
      }
      // End of hardware detection (2023-11-23)
    }

    $this->_hardware_array = ["type"       => (empty($type) ? "unknown" : $type),
                              "family"     => (empty($family) ? "unknown" : $family),
                              "model"      => (empty($model) ? "unknown" : $model),
                              "hardware"   => (empty($hardware) ? "unknown" : $hardware),
                              "cpu_speed"  => (empty($cpu_speed) ? "unknown" : $cpu_speed),
                              "os_running" => (empty($os_running) ? "unknown" : $os_running),
                             ];

    if ($return_array) {
      return $this->_hardware_array;
    } else {
      return $type;
    }
  }


  function GetRaspberryPiSerialNumber()
  {
      $serial = "";
      exec("cat /proc/cpuinfo | grep --color=never -i Serial", $output);
      foreach ($output as $line) {
          $line.= "  ";
          if (preg_match("/^Serial\s*:\s*(.*)/", $line)) {
              preg_match_all("/^Serial\s*:\s*(.*)/", $line, $result_array, PREG_SET_ORDER);
              if (isset($result_array[0][1])) {
                  $serial = mb_strtoupper(nullable_trim($result_array[0][1]),'UTF-8');
                  break;
              }
          }
      }
      return $serial;
  }


  function ReadUserDataOnServer(
      $user
  ) {
      $result = _MULTIOTP_SERVER_ANSWER_FORMAT_ERROR_;

      $server_challenge = 'MOSH'.md5($this->GetEncryptionKey().time().mt_rand(100000,999999));
      $this->SetServerChallenge($server_challenge);

      $xml_data = <<<EOL
*XmlVersion*
<multiOTP version="4.0" xmlns="http://www.sysco.ch/namespaces/multiotp">
<ServerChallenge>*ServerChallenge*</ServerChallenge>
<ReadUserData>
  <UserId>*UserId*</UserId>
</ReadUserData>
</multiOTP>
EOL;
      $xml_data = str_replace('*XmlVersion*', '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>', $xml_data);
      $xml_data = str_replace('*ServerChallenge*', $this->Encrypt('ServerChallenge', $server_challenge, $this->GetServerSecret()), $xml_data);
      $xml_data = str_replace('*UserId*', $user, $xml_data);
      
      $xml_urls = $this->GetServerUrl();
      $xml_timeout = $this->GetServerTimeout();
      $xml_data_encoded = urlencode($xml_data);
      
      $response = $this->PostHttpDataXmlRequest($xml_data_encoded, $xml_urls, $xml_timeout);

      if (FALSE !== $response) {
          if ($this->_xml_dump_in_log) {
              $this->WriteLog("Info: Host returned the following answer: $response", FALSE, FALSE, 8888, 'Debug', "");
          }
          
          if (FALSE !== mb_strpos($response,'<multiOTP')) {
              $error_code = _MULTIOTP_ERROR_;
              
              //Set up the parser object
              $xml = new MultiotpXmlParser($response);

              //Parse it !
              $xml->Parse();

              if (isset($xml->document->errorcode[0])) {
                  $server_password = (isset($xml->document->serverpassword[0]) ? ($xml->document->serverpassword[0]->tagData) : "");
                  
                  if ($server_password != md5('ReadUserData'.$this->GetServerSecret().$this->GetServerChallenge())) {
                      $error_code = _MULTIOTP_SERVER_AUTH_FAILED_;
                  } else {
                      $error_code = (isset($xml->document->errorcode[0]) ? intval($xml->document->errorcode[0]->tagData) : _MULTIOTP_ERROR_);
                  }
                  $error_description = (isset($xml->document->errordescription[0])?($xml->document->errordescription[0]->tagData):$this->GetErrorText($error_code));

                  if ($this->_xml_dump_in_log) {
                      $this->WriteLog("Info: Host returned the following result: $error_code ($error_description)", FALSE, FALSE, $error_code, 'Debug', "");
                  }
              }
              
              if ((_MULTIOTP_OPERATION_DONE_ == intval($error_code)) && (isset($xml->document->user[0]))) {
                  $result = (isset($xml->document->user[0]->userdata[0])?($xml->document->user[0]->userdata[0]->tagData):"");
              } else {
                  $this->WriteLog("Error: Host answers with the following error code: $error_code ($error_description)", FALSE, FALSE, _MULTIOTP_OPERATION_CANCELLED_, 'Client-Server', "", 3);
                  $result = intval($error_code);
              }
          } else {
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Error: *Host sent an incorrect answer: $response", FALSE, FALSE, _MULTIOTP_OPERATION_CANCELLED_, 'Client-Server', "", 3);
              }
          }
      }
      return $result;
  }


  function CheckUserExistsOnServer(
      $user = ""
  ) {
      $result = _MULTIOTP_SERVER_ANSWER_FORMAT_ERROR_;
      
      $server_challenge = 'MOSH'.md5($this->GetEncryptionKey().time().mt_rand(100000,999999));
      $this->SetServerChallenge($server_challenge);

      $xml_data = <<<EOL
*XmlVersion*
<multiOTP version="4.0" xmlns="http://www.sysco.ch/namespaces/multiotp">
<ServerChallenge>*ServerChallenge*</ServerChallenge>
<CheckUserExists>
  <UserId>*UserId*</UserId>
</CheckUserExists>
</multiOTP>
EOL;
      $xml_data = str_replace('*XmlVersion*', '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>', $xml_data);
      $xml_data = str_replace('*ServerChallenge*', $this->Encrypt('ServerChallenge', $server_challenge, $this->GetServerSecret()), $xml_data);
      $xml_data = str_replace('*UserId*', $user, $xml_data);
      
      $xml_urls = $this->GetServerUrl();
      $xml_timeout = $this->GetServerTimeout();
      $xml_data_encoded = urlencode($xml_data);
      
      $response = $this->PostHttpDataXmlRequest($xml_data_encoded, $xml_urls, $xml_timeout);

      if (FALSE !== $response) {
          if ($this->_xml_dump_in_log) {
              $this->WriteLog("Info: Host returned the following answer: $response", FALSE, FALSE, 8888, 'Debug', "");
          }
          
          if (FALSE !== mb_strpos($response,'<multiOTP')) {
              $error_code = _MULTIOTP_ERROR_;
              
              //Set up the parser object
              $xml = new MultiotpXmlParser($response);

              //Parse it !
              $xml->Parse();

              if (isset($xml->document->errorcode[0])) {
                  $server_password = (isset($xml->document->serverpassword[0])?($xml->document->serverpassword[0]->tagData):"");
                  
                  if ($server_password != md5('CheckUserExists'.$this->GetServerSecret().$this->GetServerChallenge())) {
                      $error_code = _MULTIOTP_SERVER_AUTH_FAILED_;
                  } else {
                      $error_code = (isset($xml->document->errorcode[0]) ? intval($xml->document->errorcode[0]->tagData) : _MULTIOTP_ERROR_);
                  }
                  $error_description = (isset($xml->document->errordescription[0]) ? ($xml->document->errordescription[0]->tagData) : $this->GetErrorText($error_code));

                  if ($this->_xml_dump_in_log) {
                      $this->WriteLog("Info: Host returned the following result: $error_code ($error_description).", FALSE, FALSE, $error_code, 'Debug', "");
                  }
              }
              
              // User doesn't exist: 21 - User exists = 22
              $result = intval($error_code);
          } else {
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Error: *Host sent an incorrect answer: $response", FALSE, FALSE, 8888, 'Client-Server', "");
              }
          }
      }
      return $result;
  }


  function CheckUserTokenOnServer(
      $user,
      $password,
      $auth_method = "PAP",
      $id= "",
      $challenge = "",
      $response2 = ""
  ) {
      $result = _MULTIOTP_SERVER_ANSWER_FORMAT_ERROR_;
      
      // We initialize the local encryption check variable
      $local_encryption_check = false;

      $server_challenge = 'MOSH'.md5($this->GetEncryptionKey().time().mt_rand(100000,999999));
      $this->SetServerChallenge($server_challenge);

      switch (mb_strtoupper($auth_method,'UTF-8')) {
          case 'CHAP':
              $chap_id        = $id;
              $chap_challenge = $challenge;
              $chap_password  = $password;
              $chap_hash      = "";
              break;
          case 'MS-CHAP':
              $ms_chap_id        = $id;
              $ms_chap_challenge = $challenge;
              $ms_chap_response  = $password;
              $chap_hash      = "";
              break;
          case ' MS-CHAPV2':
              $ms_chap_id        = $id;
              $ms_chap_challenge = $challenge;
              $ms_chap_response  = $password;
              $ms_chap2_response = $response2;
              $chap_hash      = "";
              break;
          case 'PAP':
          default:
              /*
              $chap_id        = "";
              $chap_challenge = md5(time());
              $chap_password  = $password;
              */
              $chap_id        = nullable_bin2hex(chr(mt_rand(0, 255)));
              $chap_challenge = md5(time());
              $chap_password  = $this->CalculateChapPassword($password, $chap_id, $chap_challenge);
              $chap_hash      = $this->Encrypt('ChapHash', $password, $chap_id.$server_challenge.$chap_id);
              break;
      }

      $xml_data = <<<EOL
*XmlVersion*
<multiOTP version="4.0" xmlns="http://www.sysco.ch/namespaces/multiotp">
<ServerChallenge>*ServerChallenge*</ServerChallenge>
<CredentialProviderRequest>*CredentialProviderRequest*</CredentialProviderRequest>
<CheckUserToken>
  <UserId>*UserId*</UserId>
  <Chap>
      <ChapId>*ChapId*</ChapId>
      <ChapChallenge>*ChapChallenge*</ChapChallenge>
      <ChapPassword>*ChapPassword*</ChapPassword>
      <ChapHash>*ChapHash*</ChapHash>
  </Chap>
  <CacheLevel>*CacheLevel*</CacheLevel>
</CheckUserToken>
</multiOTP>
EOL;
      $xml_data = str_replace('*XmlVersion*', '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>', $xml_data);
      $xml_data = str_replace('*ServerChallenge*', $this->Encrypt('ServerChallenge', $server_challenge, $this->GetServerSecret()), $xml_data);
      $xml_data = str_replace('*CredentialProviderRequest*', strval(($this->GetCredentialProviderMode()) ? 1 : 0), $xml_data);
      $xml_data = str_replace('*UserId*', $user, $xml_data);
      $xml_data = str_replace('*ChapId*', $chap_id, $xml_data);
      $xml_data = str_replace('*ChapChallenge*', $chap_challenge, $xml_data);
      $xml_data = str_replace('*ChapPassword*', $chap_password, $xml_data);
      $xml_data = str_replace('*ChapHash*', $chap_hash, $xml_data);
      $xml_data = str_replace('*CacheLevel*', $this->GetServerCacheLevel(), $xml_data);
      
      $xml_urls = $this->GetServerUrl();
      $xml_timeout = $this->GetServerTimeout();
      $xml_data_encoded = urlencode($xml_data);

      // $this->WriteLog("Debug: Host received the following request: $xml_data", FALSE, FALSE, 8888, 'Debug', "");
      
      $response = $this->PostHttpDataXmlRequest($xml_data_encoded, $xml_urls, $xml_timeout);

      if (FALSE !== $response) {
          if ($this->_xml_dump_in_log) {
              $this->WriteLog("Debug: Host returned the following answer: $response", FALSE, FALSE, 8888, 'Debug', "");
          }

          if (FALSE !== mb_strpos($response,'<multiOTP')) {
              $result = _MULTIOTP_ERROR_;
              $error_code = _MULTIOTP_ERROR_;
              
              //Set up the parser object
              $xml = new MultiotpXmlParser($response);

              //Parse it !
              $xml->Parse();

              if (isset($xml->document->errorcode[0])) {
                  $server_password = (isset($xml->document->serverpassword[0])?($xml->document->serverpassword[0]->tagData):"");
                  
                  if ($server_password != md5('CheckUserToken'.$this->GetServerSecret().$this->GetServerChallenge())) {
                      $error_code = _MULTIOTP_SERVER_AUTH_FAILED_;
                  } else {
                      $error_code = (isset($xml->document->errorcode[0]) ? intval($xml->document->errorcode[0]->tagData) : _MULTIOTP_ERROR_);
                  }
                  $error_description = (isset($xml->document->errordescription[0])?($xml->document->errordescription[0]->tagData):$this->GetErrorText(intval($error_code)));
                  $result = intval($error_code);

                  if ($this->_xml_dump_in_log) {
                      $this->WriteLog("Info: Host returned the following result: $result ($error_description).", FALSE, FALSE, $result, 'Debug', "");
                  }
              }

              if ((intval(0) == intval($error_code)) && (isset($xml->document->cache[0]))) {
                  if (isset($xml->document->cache[0]->user[0])) {
                      foreach ($xml->document->cache[0]->user as $one_user) {
                          $current_user = isset($one_user->tagAttrs['userid'])?$one_user->tagAttrs['userid']:"";
                          if ("" != $current_user) {
                              $current_user_data = isset($one_user->userdata[0])?$one_user->userdata[0]->tagData:"";
                              if ("" != $current_user_data) {
                                  $this->SetUser($current_user);
                                  $this->_user_data['encryption_hash'] = $this->CalculateControlHash($this->GetEncryptionKey());
                                  $current_user_array = explode("\n",$current_user_data);

                                  foreach ($current_user_array as $one_line) {
                                      $line = nullable_trim($one_line);
                                      $line_array = explode("=",$line,2);
                                      if (":" == mb_substr($line_array[0], -1)) {
                                          $line_array[0] = mb_substr($line_array[0], 0, mb_strlen($line_array[0]) -1);
                                          $line_array[1] = $this->Decrypt($line_array[0], $line_array[1], $this->GetServerSecret());
                                          $local_encryption_check = $this->_encryption_check;
                                      }
                                      $line_array[1] = str_replace("<<CRLF>>",chr(10),isset($line_array[1]) ? $line_array[1] : "");
                                      if ("" != nullable_trim($line_array[0])) {
                                          if ('encryption_hash' != mb_strtolower($line_array[0],'UTF-8')) {
                                              $this->_user_data[mb_strtolower($line_array[0],'UTF-8')] = $line_array[1];
                                          }
                                      }
                                  }
                                  $this->WriteUserData();
                              }
                          }
                      }
                  }
              }
          } else {
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Error: *Host sent an incorrect answer: $response", FALSE, FALSE, 8888, 'Client-Server', "");
              }
          }
      }
      $this->SetUser($user);
      return $result;
  }


  /**
   * @brief   Pure PHP standalone HTTP request function
   *
   * @param   string  $xml_data           Complete data to be posted (or 'tryurlonly' if we don't want any error handling)
   *          string  $xml_urls           Urls where to post, post to the next one in case of an error (separated by ;)
   *          string  $xml_timeout        Timeout before changing to the next server
   *          string  $xml_urls_splitter  String splitter between two Urls (default is ;)
   * @retval  string  Content received from the server (must contain <multiOTP to be valid)
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.0.5.0
   * @date    2017-08-01
   * @since   2010-07-18
   *
   * Better SSL support starting with 5.0.2.5
   */
  function PostHttpDataXmlRequest(
      $xml_data,
      $xml_urls,
      $xml_timeout = 3,
      $xml_urls_splitter = ";",
      $no_multiotp_validity_check = FALSE,
      $prefix_data = "data=",
      $content_type = "application/x-www-form-urlencoded",
      $accept = ""
  ) {
      $this->SetLastHttpStatus(999);
      $result = FALSE;
      if ("mouid" == substr($xml_data, 0, 5)) {
        $content_to_post = $xml_data;
        $no_multiotp_validity_check = TRUE;
      } elseif ('tryurlonly' != $xml_data) {
        $content_to_post = $prefix_data.$xml_data;
      }
      else {
        $content_to_post = $prefix_data;
        $no_multiotp_validity_check = TRUE;
      }
      
      // Generic cleaner of multiple URLs
      $cleaned_xml_urls = nullable_trim(str_replace(" ",$xml_urls_splitter,str_replace(",",$xml_urls_splitter,str_replace(";",$xml_urls_splitter,$xml_urls))));
      $xml_url = explode($xml_urls_splitter,$cleaned_xml_urls);
      
      foreach ($xml_url as $xml_url_one) {
          $server_to_ban = mb_substr($xml_url_one, 0, mb_strpos($xml_url_one."?", "?"));
          $skip = $this->IsTemporaryBadServer($server_to_ban);
          
          if (!$skip) {
              $port = 80;

              $pos = mb_strpos($xml_url_one, '://');
              if (FALSE === $pos) {
                  $protocol = "";
              } else {
                  switch (mb_strtolower(mb_substr($xml_url_one,0,$pos),'UTF-8')) {
                      case 'https':
                      case 'ssl':
                          $protocol = 'ssl://';
                          $port = 443;
                          break;
                      case 'tls':
                          $protocol = 'tls://';
                          $port = 443;
                          break;
                      default:
                          $protocol = "";
                          break;
                  }
                  
                  $xml_url_one = mb_substr($xml_url_one,$pos+3);
              }
              
              $pos = mb_strpos($xml_url_one, '/');
              if (FALSE === $pos) {
                  $host = $xml_url_one;
                  $url = '/';
              } else {
                  $host = mb_substr($xml_url_one,0,$pos);
                  $url = mb_substr($xml_url_one,$pos); // And not +1 as we want the / at the beginning
              }
              
              $pos = mb_strpos($host, ':');
              if (FALSE !== $pos) {
                  $port = mb_substr($host,$pos+1);
                  $host = mb_substr($host,0,$pos);
              }
              
              $errno = 0;
              $errdesc = 0;
              if (function_exists("stream_socket_client")) {
                  $sslContext = stream_context_create($this->_default_ssl_context);
                  $fp = @stream_socket_client($protocol.$host.":".$port, $errno, $errdesc, $xml_timeout, STREAM_CLIENT_CONNECT, $sslContext);
              } else {
                  $fp = @fsockopen($protocol.$host, $port, $errno, $errdesc, $xml_timeout);
              }
              if (FALSE !== $fp) {
                  $info['timed_out'] = FALSE;
                  fputs($fp, "POST ".$url." HTTP/1.0\r\n");
                  if (!empty($accept)) {
                  fputs($fp, "Accept: $accept\r\n");
                  }
                  fputs($fp, "Content-Type: $content_type\r\n");
                  fputs($fp, "Content-Length: ".mb_strlen($content_to_post)."\r\n");
                  fputs($fp, "User-Agent: multiOTP\r\n");
                  fputs($fp, "Host: ".$host."\r\n");
                  fputs($fp, "\r\n");
                  fputs($fp, $content_to_post);
                  fputs($fp, "\r\n");

                  // If not in CP mode, at least 20 seconds before the stream timeout (the server can be reached if we are here)
                  $stream_timeout = $xml_timeout;
                  if (!$this->GetCredentialProviderMode()) {
                      if ($stream_timeout < 20) {
                          $stream_timeout = 20;
                      }
                  }
                  stream_set_blocking($fp, TRUE);
                  stream_set_timeout($fp, $stream_timeout);
                  $info = stream_get_meta_data($fp); 
          
                  $reply = "";
                  $last_length = 0;
                  while ((!feof($fp)) && ((!$info['timed_out']) || ($last_length != mb_strlen($reply)))) {
                      $last_length = mb_strlen($reply);
                      $reply.= fgets($fp, 1024);
                      $info = stream_get_meta_data($fp);
                      // No flush, as we are not dislaying anything in the process
                      // @ob_flush(); // Avoid notice if any (if the buffer is empty and therefore cannot be flushed)
                      // flush(); 
                  }
                  fclose($fp);

                  if ($info['timed_out']) {
                      if ('tryurlonly' != $xml_data) {
                          $this->WriteLog("Warning: timeout after $xml_timeout seconds for $protocol$host:$port with a result code of $errno ($errdesc).", FALSE, FALSE, 8888, 'Client-Server', "");
                      }
                  } else {
                      $pos = mb_strpos(mb_strtolower($reply,'UTF-8'), "\r\n\r\n");
                      $header = mb_substr($reply, 0, $pos);
                      $answer = mb_substr($reply, $pos + 4);
                      $header_array = explode(" ", $header."   ");
                      $status = intval($header_array[1]);

                      $this->SetLastHttpStatus($status);

                      $result = $answer;

                      if (($errno > 0) && ('tryurlonly' != $xml_data)) {
                          $this->WriteLog("Info: $protocol$host:$port returns a resultcode of $errno ($errdesc).", FALSE, FALSE, 8888, 'Client-Server', "");
                      }
                      if ((FALSE !== mb_strpos($result, '<multiOTP')) || $no_multiotp_validity_check) {
                          break; // Break of the foreach loop
                      }
                  }
                  // If we are here, something was bad with the actual server
                  $this->AddTemporaryBadServer($server_to_ban, time());
                  $log_info = "Info: temporary adding $server_to_ban to the list of banned servers, content not recognized (".mb_substr($result, 0, 80)."...)";
                  if ($this->_xml_dump_in_log) {
                      $log_info.= ": ".$result;
                  }

                  $this->WriteLog($log_info, FALSE, FALSE, 8888, 'Client-Server', "");
              } else {
                  $this->AddTemporaryBadServer($server_to_ban, time());
                  $this->WriteLog("Warning: Host $protocol$host on port $port not reached before a timeout of $xml_timeout seconds.", FALSE, FALSE, 8888, 'Client-Server', "");
              }
          } else {
              // This server has been skipped
              $this->WriteLog("Info: temporary skipping $xml_url_one due to previous timeout or inconsistent response.", FALSE, FALSE, 8888, 'Client-Server', "");
              $result = "";
          }
      } // foreach

      if ((FALSE === mb_strpos($result,'<multiOTP')) && ('tryurlonly' != $xml_data) && (!$no_multiotp_validity_check)) {
          $this->_servers_last_timeout = time();

          if ($this->_xml_dump_in_log) {
              $this->WriteLog("Debug: timeout detected.", FALSE, FALSE, 8888, 'Debug', "");
          }
      }
      return $result;
  }


  // SOAP functions
  function SoapOpenotpNormalLogin(
      $username = "",
      $domain = "",
      $ldapPassword = "",
      $otpPassword = "",
      $client = "",
      $source = "",
      $settings = "",
      $options = ""
  ) {
      // First check if the user/device is challenge enabled (if yes, check only the ldapPassword, if correct, return 10)
      $error_code = intval($this->CheckUserToken($username, $ldapPassword.$otpPassword));
      switch ($error_code) {
          case 0:
              $result['code'] = 1; // success
              break;
          case 10:
              $result['code'] = 2; // challenge
              break;
          default:
              $result['code'] = 0; // failure
              break;
      }

      $result['message'] = $this->GetErrorText($error_code); // Server reply, or message of the challenge
      if ($error_code > 10) {
          $result['message'].= " (".$error_code.")";
      }
      $result['session'] = ""; // for challenge, session ID
      $result['data']    = ""; // contains the ReplyData set in the LDAP user or group settings
      $result['timeout'] = 0;  // for challenge, remaining timeout to send challenge response
      $result['otpChallenge'] = "";  // PIN,TOKEN,SMS
      $result['u2fChallenge'] = "";  // U2F challenges in JSON format
      return $result;
  }

  
  function SoapOpenotpSimpleLogin(
      $username = "",
      $domain = "",
      $anyPassword = "",
      $client = "",
      $source = "",
      $settings = ""
  ) {
      // First check if the user/device is challenge enabled (if yes, check the static password, and if correct, return 10)
      $error_code = intval($this->CheckUserToken($username, $anyPassword));
      switch ($error_code) {
          case 0:
              $result['code'] = 1; // success
              break;
          case 10:
              $result['code'] = 2; // challenge
              break;
          default:
              $result['code'] = 0; // failure
              break;
      }

      $result['message'] = $this->GetErrorText($error_code); // Server reply, or message of the challenge
      if ($error_code > 10) {
          $result['message'].= " (".$error_code.")";
      }
      $result['session'] = ""; // for challenge, session ID
      $result['data']    = ""; // contains the ReplyData set in the LDAP user or group settings
      $result['timeout'] = 0;  // for challenge, remaining timeout to send challenge response
      return $result;
  }


  function SoapOpenotpChallenge(
      $username = "",
      $domain = "",
      $session = "",
      $otpPassword = "",
      $u2fResponse = ""
  ) {
      $error_code = intval($this->CheckUserToken($username, $otpPassword));
      switch ($error_code) {
          case 0:
              $result['code'] = 1; // success
              break;
          default:
              $result['code'] = 0; // failure
              break;
      }

      $result['message'] = $this->GetErrorText($error_code); // Server reply, or message of the challenge
      if ($error_code > 0) {
          $result['message'].= " (".$error_code.")";
      }
      $result['data']    = ""; // contains the ReplyData set in the LDAP user or group settings
      return $result;
  }

  
  function SoapOpenotpStatus()
  {
      $eol = chr(13).chr(10);
      $result['status'] = true;
      $result['message'] = "Server: ".$this->GetFullVersionInfo().$eol;
      if (isset($_SERVER["SERVER_ADDR"])) {
          $result['message'].= "Listener: ".$_SERVER["SERVER_ADDR"].(isset($_SERVER["SERVER_PORT"])?":".$_SERVER["SERVER_PORT"]:"").$eol;
      }
      if (isset($_SERVER["SERVER_PROTOCOL"])) {
          $result['message'].= "Protocol: ".$_SERVER["SERVER_PROTOCOL"];
      }
      if ("" != (isset($_SERVER["HTTPS"])?$_SERVER["HTTPS"]:"")) {
          $result['message'].= " (SSL)";
      } else {
          $result['message'].= " (no SSL)";
      }
      $result['message'].= $eol;
      $result['message'].= "Uptime: ".$this->GetUptime(false).$eol;
      $memory_limit = ini_get("memory_limit");
      if ('M' == mb_substr($memory_limit,-1)) {
          $memory_limit = intval(mb_substr($memory_limit,0,mb_strlen($memory_limit)-1)) * 1024 * 1024;
      } elseif ('K' == mb_substr($memory_limit,-1)) {
          $memory_limit = intval(mb_substr($memory_limit,0,mb_strlen($memory_limit)-1)) * 1024;
      }
      $result['message'].= "Memory: ".$memory_limit.$eol;
      // $result['message'].= "Total Requests: ".$this->TotalRequests().$eol;
      // $result['message'].= "Active Requests: ".$this->ActiveRequests()." (unlimited)".$eol;
      return $result;
  }


  /*
   * Call a REST service, REST authentication is done as for Amazon services
   * (http://docs.aws.amazon.com/AWSECommerceService/latest/DG/RequestAuthenticationArticle.html)
   * (http://randomdrake.com/2009/07/27/amazon-aws-api-rest-authentication-for-php-5/)
   */

  function CallApi(
      $call_array = array("script_uri" => "",
                          "secret"     => "",
                          "post_data"  => "")
  ) {
      $script_uri = isset($call_array["script_uri"]) ? $call_array["script_uri"] : "";
      $secret     = isset($call_array["secret"])     ? $call_array["secret"]     : "";
      $post_data  = isset($call_array["post_data"])  ? $call_array["post_data"]  : "";

      // Get a nice array of elements to work with
      $uri_elements = parse_url($script_uri);

      // Grab our request elements
      $scheme  = isset($uri_elements['scheme']) ? $uri_elements['scheme'] : 'http';
      $port    = isset($uri_elements['port'])   ? $uri_elements['port']   : "";
      $request = isset($uri_elements['query'])  ? $uri_elements['query']  : "";
      $host    = isset($uri_elements['host'])   ? $uri_elements['host']   : "";
      $path    = isset($uri_elements['path'])   ? $uri_elements['path']   : "";
   
      // Throw them into an array
      parse_str($request, $parameters);
      // $parameters = $_GET;

      if (isset($parameters['Signature'])) {
          unset($parameters['Signature']);
      }
      if (isset($parameters['Timestamp'])) {
          unset($parameters['Timestamp']);
      }
      $parameters['Timestamp'] = gmdate("Y-m-d\TH:i:s\Z");

      ksort($parameters);

      $request_array = [];
      // Create our new request
      foreach ($parameters as $parameter => $value) {
          // We need to be sure we properly encode the value of our parameter
          $parameter = str_replace("%7E", "~", rawurlencode($parameter));
          $value = str_replace("%7E", "~", rawurlencode($value));
          $request_array[] = $parameter . '=' . $value;
      }   

      // Put our & symbol at the beginning of each of our request variables and put it in a string
      $new_request = implode('&', $request_array);

      // Create our signature string
      $signature_string = "GET\n{$host}\n{$path}\n{$new_request}";
   
      $secret_key = $secret;
      if ("" == $secret_key) {
          $secret_key = $this->GetServerSecret();
      }

      // Create our signature using hash_hmac
      $signature = urlencode(base64_encode(hash_hmac('sha256', $signature_string, $secret_key, TRUE)));

      // Return our new request
      $url_request = "{$scheme}://{$host}".(("" != $port) ? ":{$port}" : "")."{$path}?{$new_request}&Signature={$signature}";

      // echo "DEBUG: Signature: $signature ($signature_string) $new_request\n<br />";

      if ($this->GetVerboseFlag()) {
          $this->WriteLog("Debug: *CallApi $script_uri (secret: $secret_key, signature string: $signature_string, full URL: $url_request)", FALSE, FALSE, 8888, 'Debug', "");
      }

      $api_result = $this->PostHttpDataXmlRequest(
        $post_data,                // $xml_data
        $url_request,              // $xml_urls
        $this->GetServerTimeout(), // $xml_timeout
        "\t",                      // $xml_urls_splitter
        TRUE                       // No multiOTP validity check
      );

      // echo $api_result;
      // return (FALSE !== mb_strpos($api_result, 'result_code'));
      return $api_result;
  }


  // The XmlServer is called from the client side using PostHttpDataXmlRequest function
  function XmlServer($data)
  {
      // $this->WriteLog("Info: Host received the following request: $data", FALSE, FALSE, 8888, 'Debug', "");

      $remote_ip = isset($_SERVER['REMOTE_ADDR'])?$_SERVER['REMOTE_ADDR']:"";

      $cache_data      = "";
      $command_name    = "";
      $error_code      = _MULTIOTP_SERVER_REQUEST_FORMAT_ERROR_;
      $server_password = "";
      $user_data       = "";
      $user_info       = "";
      $user_password   = "";

      $cache_data_template = <<<EOL
      <Cache>
      *UserInCache*</Cache>
EOL;

      $user_template = <<<EOL
          <User UserId="*UserId*">
              <UserData>*UserData*</UserData>
          </User>
EOL;

      $xml_data = <<<EOL
*XmlVersion*
<multiOTP version="4.0" xmlns="http://www.sysco.ch/namespaces/multiotp">
<DebugCode>*Command*</DebugCode>
<ServerPassword>*ServerPassword*</ServerPassword>
<ErrorCode>*ErrorCode*</ErrorCode>
<ErrorDescription>*ErrorDescription*</ErrorDescription>
*UserInfo**Cache*</multiOTP>
EOL;
      $xml_data = str_replace('*XmlVersion*', '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>', $xml_data);
      
      if (FALSE !== mb_strpos($data,'<multiOTP')) {
          if ($this->_xml_dump_in_log) {
              $this->WriteLog("Info: Host answer is correctly formatted.", FALSE, FALSE, 8888, 'Debug', "");
              $this->WriteLog("Info: Host received the following request: $data", FALSE, FALSE, 8888, 'Debug', "");
          }
          
          //Set up the parser object
          $xml = new MultiotpXmlParser($data);

          //Parse it !
          $xml->Parse();

          $server_challenge = $this->Decrypt('ServerChallenge', (isset($xml->document->serverchallenge[0])?($xml->document->serverchallenge[0]->tagData):""),$this->GetServerSecret($remote_ip));

          $this->SetCredentialProviderRequest(isset($xml->document->credentialproviderrequest[0])?intval($xml->document->credentialproviderrequest[0]->tagData):0);

          if ($this->GetVerboseFlag()) {
              $this->WriteLog("Info: *Value for IsCredentialProviderRequest: ".$this->IsCredentialProviderRequest(), FALSE, FALSE, 8888, 'CredentialProviderRequest', "");
          }

          if (isset($xml->document->checkusertoken[0])) {
              $command_name = 'CheckUserToken';
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Info: *CheckUserToken server request.", FALSE, FALSE, 8888, 'Server-Client', "");
              }
              $user_id = (isset($xml->document->checkusertoken[0]->userid[0])?($xml->document->checkusertoken[0]->userid[0]->tagData):"");
              $chap_id = (isset($xml->document->checkusertoken[0]->chap[0]->chapid[0])?($xml->document->checkusertoken[0]->chap[0]->chapid[0]->tagData):'00');
              $chap_challenge = (isset($xml->document->checkusertoken[0]->chap[0]->chapchallenge[0])?($xml->document->checkusertoken[0]->chap[0]->chapchallenge[0]->tagData):"");
              $chap_password = (isset($xml->document->checkusertoken[0]->chap[0]->chappassword[0])?($xml->document->checkusertoken[0]->chap[0]->chappassword[0]->tagData):"");

              $chap_hash = (isset($xml->document->checkusertoken[0]->chap[0]->chaphash[0])?($xml->document->checkusertoken[0]->chap[0]->chaphash[0]->tagData):"");
              if ("" != $chap_hash) {
                  $chap_hash = $this->Decrypt('ChapHash', $chap_hash, $chap_id.$server_challenge.$chap_id);
              }
              
              $cache_level = (isset($xml->document->checkusertoken[0]->cachelevel[0])?($xml->document->checkusertoken[0]->cachelevel[0]->tagData):0);
              if ($cache_level > $this->GetServerCacheLevel()) {
                  $cache_level = $this->GetServerCacheLevel();
              }
              
              $error_code = _MULTIOTP_SERVER_AUTH_FAILED_;

              if ('MOSH' == mb_substr($server_challenge, 0, 4)) {
              // Ok, the challenge is encoded with the correct server secret
                  if ("" != $chap_hash) {
                      $this->SetChapId("");
                      $this->SetChapChallenge("");
                      $this->SetChapPassword("");
                      $user_password = $chap_hash;
                  } elseif ("" == $chap_id) {
                      $this->SetChapId("");
                      $this->SetChapChallenge("");
                      $this->SetChapPassword("");
                      $user_password = $chap_password;
                  } else {
                      $this->SetChapId($chap_id);
                      $this->SetChapChallenge($chap_challenge);
                      $this->SetChapPassword($chap_password);
                  }
                  
                  if (!$this->CheckUserExists($user_id)) {
                      $error_code = _MULTIOTP_USER_MISSING_; // ERROR: User doesn't exist
                  } else {
                      $error_code = intval($this->CheckUserToken($user_id, $user_password, "", FALSE, FALSE, FALSE, TRUE)); // do_not_check_on_server = TRUE;
                      
                      $now_epoch = time();
                      $cache_lifetime = $this->GetServerCacheLifetime();
                      if ($cache_lifetime > $this->GetUserCacheLifetime()) {
                          $cache_lifetime = $this->GetUserCacheLifetime();
                          if ($this->GetVerboseFlag()) {
                              $this->WriteLog("Info: *Cache lifetime lowered to $cache_lifetime for the user $user_id", FALSE, FALSE, 8888, 'Server-Client', "");
                          }
                      }

                      if ($cache_level > $this->GetUserCacheLevel()) {
                          $cache_level = $this->GetUserCacheLevel();
                          if ($this->GetVerboseFlag()) {
                              $this->WriteLog("Info: *Cache level lowered to $cache_level for the user $user_id", FALSE, FALSE, 8888, 'Server-Client', "");
                          }
                      }
                      
                      if ((0 < $cache_level) && (0 == intval($error_code))) {
                          if ($this->GetVerboseFlag()) {
                              $this->WriteLog("Info: *Cache level is set to $cache_level", FALSE, FALSE, 8888, 'Server-Client', "");
                          }
                          
                          foreach ($this->_user_data as $key => $value) {
                              if ("" != nullable_trim($key)) {
                                // CheckUserToken will return all attributes, included encrypted ones, but only if user is authenticated successfully
                                  if ('encryption_hash' != $key) {
                                      $user_data.= mb_strtolower($key,'UTF-8');
                                      if ('autolock_time' == $key) {
                                          if (0 < $cache_lifetime) {
                                              if (($value == 0) || ($value > ($now_epoch + $cache_lifetime))) {
                                                  $value = ($now_epoch + $cache_lifetime);
                                              }
                                          }
                                      }
                                      $value = $this->Encrypt($key, $value, $this->GetServerSecret($remote_ip));
                                      $user_data = $user_data.":";
                                      $user_data = $user_data."=".$value;
                                      $user_data.= "\n";
                                  }
                              }
                          }

                          $cache_user = "";
                          $one_cache_user = str_replace('*UserId*', $user_id, $user_template);
                          $one_cache_user = str_replace('*UserData*', $user_data, $one_cache_user);
                          $cache_user .= $one_cache_user;
                          
                          $cache_data = str_replace('*UserInCache*', $cache_user, $cache_data_template);
                      }
                  }
              }
          } // End of CheckUserToken
          elseif (isset($xml->document->readuserdata[0])) {
              $command_name = 'ReadUserData';
              $user_id = (isset($xml->document->readuserdata[0]->userid[0])?($xml->document->readuserdata[0]->userid[0]->tagData):'NO_USER_DETECTED!');
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Info: *ReadUserData server request for $user_id", FALSE, FALSE, 8888, 'Server-Client', "");
              }

              $cache_level = (isset($xml->document->readuserdata[0]->cachelevel[0])?($xml->document->readuserdata[0]->cachelevel[0]->tagData):0);
              if ($cache_level > $this->GetServerCacheLevel()) {
                  $cache_level = $this->GetServerCacheLevel();
              }

              $error_code = _MULTIOTP_SERVER_AUTH_FAILED_;

              if ('MOSH' == mb_substr($server_challenge, 0, 4)) {
                  // Ok, the challenge is encoded with the correct server secret
                  $error_code = _MULTIOTP_USER_MISSING_; // ERROR: User doesn't exist

                  if ($this->ReadUserData($user_id, FALSE, TRUE)) {
                      // $no_server_check = TRUE;
                      $error_code = _MULTIOTP_OPERATION_DONE_;
                      
                      // Preparing caching code
                      $now_epoch = time();
                      $cache_lifetime = $this->GetServerCacheLifetime();
                      if ($cache_lifetime > $this->GetUserCacheLifetime()) {
                          $cache_lifetime = $this->GetUserCacheLifetime();
                          if ($this->GetVerboseFlag()) {
                              $this->WriteLog("Info: *Cache lifetime lowered to $cache_lifetime for the user $user_id", FALSE, FALSE, 8888, 'Server-Client', "");
                          }
                      }

                      if ($cache_level > $this->GetUserCacheLevel()) {
                          $cache_level = $this->GetUserCacheLevel();
                          if ($this->GetVerboseFlag()) {
                              $this->WriteLog("Info: *Cache level lowered to $cache_level for the user $user_id", FALSE, FALSE, 8888, 'Server-Client', "");
                          }
                      }
                      // End of preparing caching code
                      
                      foreach ($this->_user_data as $key => $value) {
                          if ("" != nullable_trim($key)) {
                              // ReadUserData will only return non-encrypted attributes
                              if (('encryption_hash' != $key) &&
                                  (!((FALSE !== mb_strpos(mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'), mb_strtolower('*'.$key.'*','UTF-8'))) ||
                                     ("*all*" == mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8')))
                                  )
                                 ) {
                                  if ('autolock_time' == $key) {
                                      if (0 < $cache_lifetime) {
                                          if (($value == 0) || ($value > ($now_epoch + $cache_lifetime))) {
                                              $value = ($now_epoch + $cache_lifetime);
                                          }
                                      }
                                  }
                                  $user_data.= mb_strtolower($key,'UTF-8');
                                  $value = $this->Encrypt($key, $value, $this->GetServerSecret($remote_ip));
                                  $user_data = $user_data.":";
                                  $user_data = $user_data."=".$value;
                                  $user_data.= "\n";
                              }
                          }
                      }
                     
                      // Caching for without2fa algorithm only
                      if ((0 < $cache_level) && ("without2fa" == mb_strtolower($this->_user_data['algorithm'],'UTF-8'))) {
                          if ($this->GetVerboseFlag()) {
                              $this->WriteLog("Info: *Cache level is set to $cache_level", FALSE, FALSE, 8888, 'Server-Client', "");
                          }

                          $cache_user = "";
                          $one_cache_user = str_replace('*UserId*', $user_id, $user_template);
                          $one_cache_user = str_replace('*UserData*', $user_data, $one_cache_user);
                          $cache_user .= $one_cache_user;
                          
                          // $cache_data = str_replace('*UserInCache*', $cache_user, $cache_data_template);
                      }
                      // End of caching for without2fa

                      $user_info = str_replace('*UserId*', $user_id, $user_template);
                      $user_info = str_replace('*UserData*', $user_data, $user_info);
                  }
              }
          } // End of ReadUserData
          elseif (isset($xml->document->checkuserexists[0])) {
              $command_name = 'CheckUserExists';
              $user_id = (isset($xml->document->checkuserexists[0]->userid[0])?($xml->document->checkuserexists[0]->userid[0]->tagData):'NO_USER_DETECTED!');
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Info: *CheckUserExists server request for $user_id with challenge $server_challenge", FALSE, FALSE, 8888, 'Server-Client', "");
              }

              $cache_level = (isset($xml->document->checkuserexists[0]->cachelevel[0])?($xml->document->checkuserexists[0]->cachelevel[0]->tagData):0);
              if ($cache_level > $this->GetServerCacheLevel()) {
                  $cache_level = $this->GetServerCacheLevel();
              }

              $error_code = _MULTIOTP_SERVER_AUTH_FAILED_;

              if ('MOSH' == mb_substr($server_challenge, 0, 4)) {
                  // Ok, the challenge is encoded with the correct server secret
                  $error_code = _MULTIOTP_USER_MISSING_; // ERROR: User doesn't exist

                  if ($this->CheckUserExists($user_id, TRUE)) {
                      // $no_server_check = TRUE;
                      $error_code = _MULTIOTP_USER_EXISTS_;
                      
                      // Preparing caching code
                      $now_epoch = time();
                      $cache_lifetime = $this->GetServerCacheLifetime();
                      if ($cache_lifetime > $this->GetUserCacheLifetime()) {
                          $cache_lifetime = $this->GetUserCacheLifetime();
                          if ($this->GetVerboseFlag()) {
                              $this->WriteLog("Info: *Cache lifetime lowered to $cache_lifetime for the user $user_id", FALSE, FALSE, 8888, 'Server-Client', "");
                          }
                      }

                      if ($cache_level > $this->GetUserCacheLevel()) {
                          $cache_level = $this->GetUserCacheLevel();
                          if ($this->GetVerboseFlag()) {
                              $this->WriteLog("Info: *Cache level lowered to $cache_level for the user $user_id", FALSE, FALSE, 8888, 'Server-Client', "");
                          }
                      }
                      // End of preparing caching code
                      
                      // Reading caching info
                      foreach ($this->_user_data as $key => $value) {
                          if ("" != nullable_trim($key)) {
                              // ReadUserData will only return non-encrypted attributes
                              if (('encryption_hash' != $key) &&
                                  (!((FALSE !== mb_strpos(mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8'), mb_strtolower('*'.$key.'*','UTF-8'))) ||
                                     ("*all*" == mb_strtolower($this->GetAttributesToEncrypt(),'UTF-8')))
                                  )
                                 ) {
                                  if ('autolock_time' == $key) {
                                      if (0 < $cache_lifetime) {
                                          if (($value == 0) || ($value > ($now_epoch + $cache_lifetime))) {
                                              $value = ($now_epoch + $cache_lifetime);
                                          }
                                      }
                                  }
                                  $user_data.= mb_strtolower($key,'UTF-8');
                                  $value = $this->Encrypt($key, $value, $this->GetServerSecret($remote_ip));
                                  $user_data = $user_data.":";
                                  $user_data = $user_data."=".$value;
                                  $user_data.= "\n";
                              }
                          }
                      }
                      // End of reading caching info

                      // Caching for without2fa algorithm only
                      if ((0 < $cache_level) && ("without2fa" == mb_strtolower($this->_user_data['algorithm'],'UTF-8'))) {
                          if ($this->GetVerboseFlag()) {
                              $this->WriteLog("Info: *Cache level is set to $cache_level", FALSE, FALSE, 8888, 'Server-Client', "");
                          }

                          $cache_user = "";
                          $one_cache_user = str_replace('*UserId*', $user_id, $user_template);
                          $one_cache_user = str_replace('*UserData*', $user_data, $one_cache_user);
                          $cache_user .= $one_cache_user;
                          
                          // $cache_data = str_replace('*UserInCache*', $cache_user, $cache_data_template);
                      }
                      // End of caching for without2fa
                      
                  }
              }
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Info: *CheckUserExists intermediate error code: $error_code", FALSE, FALSE, 8888, 'Server-Client', "");
              }
          } // End of CheckUserExists
          
          $server_password = md5($command_name.$this->GetServerSecret($remote_ip).$server_challenge);
          
      } elseif ($this->GetVerboseFlag()) {
          $this->WriteLog("Info: *Server received the following request: $data", FALSE, FALSE, 8888, 'Server-Client', "");
      }
      
      $error_description = $this->GetErrorText($error_code);
      
      if ($this->IsDeveloperMode()) {
        $this->WriteLog("Developer: *Server secret used for command ".$command_name." with error code result ".$error_code.": ".$this->GetServerSecret($remote_ip), FALSE, FALSE, 8888, 'Server-Client', "");
      }
      
      $xml_data = str_replace('*Command*', $command_name, $xml_data);
      $xml_data = str_replace('*ServerPassword*', $server_password, $xml_data);
      $xml_data = str_replace('*ErrorCode*', intval($error_code), $xml_data);
      $xml_data = str_replace('*ErrorDescription*', $error_description, $xml_data);
      $xml_data = str_replace('*UserInfo*', $user_info, $xml_data);
      $xml_data = str_replace('*Cache*', $cache_data, $xml_data);

      /****************************************
       * WE REALLY DO NOT WANT TO BE CACHED !!!
       ****************************************/
      header("Expires: " . gmdate("D, d M Y H:i:s") . " GMT");
      header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
      header("Cache-Control: no-store, no-cache, must-revalidate");
      header("Cache-Control: post-check=0, pre-check=0", false);
      header("Pragma: no-cache");
      header("Content-type: text/xml");

      if ($this->_xml_dump_in_log) {
        $this->WriteLog("Info: Server sent the following answer: $xml_data", FALSE, FALSE, 8888, 'Debug', "");
      }
      echo $xml_data;
  }


  // This method is a stub that calls the QRcode library with the good pathes
  // If $file_name = "binary", send binary content without header
  
  function qrcode(
    $qrcode_array = "",
    $file_name = "",
    $image_type = "P",
    $ecc_level = "Q",
    $module_size = 4,
    $version = 0,
    $structure_m = 0,
    $structure_n = 0,
    $parity = 0,
    $original_data = ""
  ) {
    if (is_array($qrcode_array)) {
      $data = isset($qrcode_array['data'])?$qrcode_array['data']:"";
      $file_name = isset($qrcode_array['file_name'])?$qrcode_array['file_name']:"";
      $format = isset($qrcode_array['format'])?$qrcode_array['format']:'png';
      $symbology = isset($qrcode_array['symbology'])?$qrcode_array['symbology']:'qr-q';
      $options = isset($qrcode_array['options'])?$qrcode_array['options']:[];
    } else {
      // Backward compatibility
      $data = $qrcode_array;
      $format = ('P' == strtoupper(mb_substr($image_type.' ', 0, 1))) ? 'png' : 'jpeg';
      $symbology = 'qr-'.strtolower($ecc_level);
      $options = [];
    }
    
    $return_binary = ('binary' == strtolower(nullable_trim($file_name)));

    $generator = new barcode_generator();
    
    if ("" == $file_name) {
      $generator->output_image($format, $symbology, $data, $options);
      $result = 100;
    } else {
      if ('svg' == strtolower(preg_replace('/[^A-Za-z0-9]/', "", $format))) {
        $result = $generator->render_svg($symbology, $data, $options);
        if (!$return_binary) {
          file_put_contents($file_name, $result);
          $result = 100;
        }
      } else {
        if ($return_binary) {
          ob_start();
          $file_name = null;
        }
        switch (strtolower(preg_replace('/[^A-Za-z0-9]/', "", $format))) {
          case 'png':
            $image = $generator->render_image($symbology, $data, $options);
            imagepng($image, $file_name);
            $result = imagesx($image);
            imagedestroy($image);
            break;
          case 'jpg': case 'jpe': case 'jpeg':
            $image = $generator->render_image($symbology, $data, $options);
            imagejpeg($image, $file_name);
            $result = imagesx($image);
            imagedestroy($image);
            break;
          case 'gif':
            $image = $generator->render_image($symbology, $data, $options);
            imagegif($image, $file_name);
            $result = imagesx($image);
            imagedestroy($image);
            break;
          default:
            $generator = null;
            return false;
        }
        if ($return_binary) {
          $result = ob_get_clean();          
        }
      }
    }
    
    $generator = null;

    ob_start();
    
  if (("" != nullable_trim($file_name)) && (!$return_binary) && ("" != $this->GetLinuxFileMode())) {
      if (file_exists($file_name)) {
        @chmod($file_name, octdec($this->GetLinuxFileMode()));
      }
    }
    return $result;
  }


  function ApiGetDashboardArray() {
    $level = "success";
    $message = "OK";
    $d = [];
    $d["available_algo"]     = $this->GetAlgorithmsList(true);
    $d["backend"]            = $this->GetBackendType();
    $d["copyright"]          = $this->GetCopyright();
    $d["current_date_time"]  = $this->GetCurrentDateTime();
    $d["current_timestamp"]  = time();
    $d["current_timezone"]   = date('T').' (GMT'.date('O').')';
    $d["default_password"]   = $this->CheckAdminPassword(_MULTIOTP_DEFAULT_PASSWORD_);
    $d["disk_free_space"]    = disk_free_space($this->GetUsersFolder());
    $d["disk_total_space"]   = disk_total_space($this->GetUsersFolder());
    $d["locked_users"]       = $this->GetLockedUsersList(true);
    if (empty($d["locked_users"])) {
      $d["locked_users"] = null;
      $d["locked_users_count"] = null;
    } else {
      $d["locked_users_count"] = count($d["locked_users"]);
    }
    $d["ram_free_space"]     = ram_free_space();
    $d["ram_total_space"]    = ram_total_space();
    $d["short_model_name"]   = $this->GetShortModelName();
    $d["system_name"]        = $this->GetSystemName();
    $d["system_uptime"]      = $this->GetUptime(false);

    if (!is_null($d["default_password"]) && $d["default_password"]) {
      $level = "error";
      $message = "defaultPasswordUsed";
    } elseif (!is_null($d["locked_users_count"]) && ($d["locked_users_count"] > 0)) {
      $level = "warning";
      $message = "lockedUsersToCheck";
    }

    $answer = ["status" => true, "level" => $level, "message" => $message, "data" => ["dashboard" => $d]];

    return $answer;
  }


  function ApiGetMonitorArray(
    $data_array = []
  ) {
    $page = intval(isset($data_array['page'])?$data_array['page']:1);
    if ($page < 1) {
      $page = 1;
    }
    $itemsPerPage = intval(isset($data_array['itemsPerPage'])?$data_array['itemsPerPage']:25);
    if ($itemsPerPage < 10) {
      $itemsPerPage = 10;
    }
    $maxlines = $page * $itemsPerPage;
    $total_items = 0;
    $search_filter = [];

    // Name of the form search field / name of the database field
    $implemented_filters = [
      "priority" => "severity",
      "user"     => "user",
      "source"   => "source",
      "category" => "category",
      "message"  => "logentry",
    ];

    $search = (!empty($data_array['search'])) ? $data_array['search'] : [];

    foreach($implemented_filters as $one_search_key => $one_search_record) {
      if (!empty($search[$one_search_key])) {
        $search_filter[$one_search_key] = $one_search_record;
      }
    }

    $formatting_array = array("Debug: ", "Error: ", "Info: ", "Ok: ");

    $index = 0;
    $lines = [];
    $log_array = [];

    if ((!$this->IsLogForcedInFile()) && ('mysql' == $this->GetBackendType())) {
        if ($this->OpenMysqlDatabase()) {
            $where = "";
            foreach($search_filter as $one_search_key => $one_search_record) {
              if (!empty($search[$one_search_key])) {
                if (" " == $search[$one_search_key]) { // Field is Empty ?
                  $sec_where = "TRIM($one_search_record) = ''";  
                } else {
                  $one_filter_array = explode(" ", $search[$one_search_key]);
                  $sec_where = "";
                  foreach($one_filter_array as $noesc_one_sub_filter) {
                    $one_sub_filter = nullable_trim(escape_mysql_string($noesc_one_sub_filter));
                    if (!empty($one_sub_filter) && ("!" != $one_sub_filter)) {
                      $negation = '';
                      if ("!" == mb_substr($one_sub_filter, 0, 1)) {
                        $one_sub_filter = mb_substr($one_sub_filter, 1);
                        $negation = 'NOT ';
                      }
                      $sec_where.= !empty($sec_where) ? " AND " : "";
                      $sec_where.= "(`$one_search_record` $negation LIKE '%$one_sub_filter%')";
                    }
                  }
                }
                if (!empty($sec_where)) {
                  $where.= empty($where) ? "" : " AND ";
                  $where.= "(".$sec_where.")";
                }
              }
            }
            $where = (!empty($where) ? " WHERE " : "") . $where;

            $sQueryCount = "SELECT COUNT(*) AS counter FROM `".$this->_config_data['sql_log_table']."` ".
              $where.
              ";";
            // $sQuery  = "SELECT *, (@counter := @counter + 1) AS 'counter' FROM `".$this->_config_data['sql_log_table']."`, (SELECT @counter := 0) AS reset_counter ORDER BY datetime DESC, counter DESC LIMIT ".$maxlines.";"; // datetime DESC only
            $sQuery = "SELECT * FROM `".$this->_config_data['sql_log_table']."` ".
              $where.
              "ORDER BY datetime DESC, create_time DESC, unique_id DESC ".
              "LIMIT $itemsPerPage OFFSET ". intval(($page-1) * $itemsPerPage) . "";
            if (is_object($this->_mysqli)) {
                if (!($rResultCount = $this->_mysqli->query($sQueryCount))) {
                    $this->WriteLog(array('text'       => "Error: Unable to access the database: ".nullable_trim($this->_mysqli->error),
                                          'error_code' => _MULTIOTP_SYSTEM_SQL_ERROR_,
                                          'category'   => 'System',
                                          'user'       => '',
                                          'overwrite_severity' => 3)
                                    );
                } else {
                    if ($aRow = $rResultCount->fetch_assoc()) {
                        $total_items = $aRow['counter'];
                    }
                }
                if (!($rResult = $this->_mysqli->query($sQuery))) {
                    $this->WriteLog(array('text'       => "Error: Unable to access the database: ".nullable_trim($this->_mysqli->error),
                                          'error_code' => _MULTIOTP_SYSTEM_SQL_ERROR_,
                                          'category'   => 'System',
                                          'user'       => '',
                                          'overwrite_severity' => 3)
                                    );
                } else {
                    while ($aRow = $rResult->fetch_assoc()) {
                        foreach ($formatting_array as $one_format) {
                            if (0 === mb_strpos(mb_strtoupper($aRow['logentry'],'UTF-8'), mb_strtoupper($one_format,'UTF-8'))) {
                                $aRow['logentry'] = ucfirst(mb_substr($aRow['logentry'], mb_strlen($one_format)));
                            }
                        }
                        $log_array[] = ["dateTime" => htmlentities($aRow['datetime']),
                                        "priority" => htmlentities($aRow['severity']),
                                        "user"     => htmlentities($aRow['user']),
                                        "category" => htmlentities($aRow['category']),
                                        "message"  => htmlentities($aRow['logentry']),
                                        "source"   => htmlentities($aRow['source']),
                                      ];
                    }                         
                }
            } else {
                if (!($rResultCount = mysql_query($sQueryCount, $this->_mysql_database_link))) {
                    $this->WriteLog(array('text'       => "Error: Unable to access the database: ".mysql_error(),
                                          'error_code' => _MULTIOTP_SYSTEM_SQL_ERROR_,
                                          'category'   => 'System',
                                          'user'       => '',
                                          'overwrite_severity' => 3)
                                    );
                } else {
                    if ($aRow = mysql_fetch_assoc($rResultCount)) {
                        $total_items = $aRow['counter'];
                    }
                }
                if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                $this->WriteLog(array('text'       => "Error: Unable to access the database: ".mysql_error(),
                                      'error_code' => _MULTIOTP_SYSTEM_SQL_ERROR_,
                                      'category'   => 'System',
                                      'user'       => '',
                                      'overwrite_severity' => 3)
                                );
                } else {
                    while ($aRow = mysql_fetch_assoc($rResult)) {
                        foreach ($formatting_array as $one_format) {
                            if (0 === mb_strpos(mb_strtoupper($aRow['logentry'],'UTF-8'), mb_strtoupper($one_format,'UTF-8'))) {
                                $aRow['logentry'] = ucfirst(mb_substr($aRow['logentry'], mb_strlen($one_format)));
                            }
                        }
                        $log_array[] = ["dateTime" => htmlentities($aRow['datetime']),
                                        "priority" => htmlentities($aRow['severity']),
                                        "user"     => htmlentities($aRow['user']),
                                        "category" => htmlentities($aRow['category']),
                                        "message"  => htmlentities($aRow['logentry']),
                                        "source"   => htmlentities($aRow['source']),
                                      ];
                    }                         
                }
            }
        }
        //mysql_close($log_link);
    } elseif ((!$this->IsLogForcedInFile()) && ('pgsql' == $this->GetBackendType())) {
        if ($this->OpenPGSQLDatabase())
        {
            $where = "";
            foreach($search_filter as $one_search_key => $one_search_record) {
              if (!empty($search[$one_search_key])) {
                if (" " == $search[$one_search_key]) {  // Field is Empty ?
                  $sec_where = "BTRIM($one_search_record) = ''";
                } else {
                  $one_filter_array = explode(" ", $search[$one_search_key]);
                  $sec_where = "";
                  foreach($one_filter_array as $noesc_one_sub_filter) {
                    $one_sub_filter = nullable_trim(escape_mysql_string($noesc_one_sub_filter));
                    if (!empty($one_sub_filter) && ("!" != $one_sub_filter)) {
                      $negation = '';
                      if ("!" == mb_substr($one_sub_filter, 0, 1)) {
                        $one_sub_filter = mb_substr($one_sub_filter, 1);
                        $negation = 'NOT ';
                      }
                      $sec_where.= !empty($sec_where) ? " AND " : "";
                      $sec_where.= "($one_search_record $negation ILIKE '%$one_sub_filter%')";
                    }
                  }
                }
                if (!empty($sec_where)) {
                  $where.= empty($where) ? "" : " AND ";
                  $where.= "(".$sec_where.")";
                }
              }
            }
            $where = (!empty($where) ? " WHERE " : "") . $where;

            $sQueryCount = "SELECT COUNT(*) AS counter FROM ".$this->_config_data['sql_schema'].".".$this->_config_data['sql_log_table']." ".
              $where.
              ";";
            if (!($rResultCount = pg_query($this->_pgsql_database_link, $sQueryCount))) {
                $this->WriteLog(array('text'       => "Error: Unable to access the database: ".nullable_trim(pg_last_error($this->_pgsql_database_link)),
                                      'error_code' => _MULTIOTP_SYSTEM_SQL_ERROR_,
                                      'category'   => 'System',
                                      'user'       => '',
                                      'overwrite_severity' => 3)
                                );
            } elseif ($aRow = pg_fetch_assoc($rResultCount)) {
                $total_items = $aRow['counter'];
            }
            $sQuery = "SELECT * FROM ".$this->_config_data['sql_schema'].".".$this->_config_data['sql_log_table']." ".
              $where.
              "ORDER BY datetime DESC, create_time DESC, unique_id DESC ".
              "LIMIT $itemsPerPage OFFSET ". intval(($page-1) * $itemsPerPage) . "";
            if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                $this->WriteLog(array('text'       => "Error: Unable to access the database: ".nullable_trim(pg_last_error($this->_pgsql_database_link)),
                                      'error_code' => _MULTIOTP_SYSTEM_SQL_ERROR_,
                                      'category'   => 'System',
                                      'user'       => '',
                                      'overwrite_severity' => 3)
                                );
            } else {
                while ($aRow = pg_fetch_assoc($rResult)) {
                    foreach ($formatting_array as $one_format) {
                        if (0 === mb_strpos(mb_strtoupper($aRow['logentry'],'UTF-8'), mb_strtoupper($one_format,'UTF-8'))) {
                            $aRow['logentry'] = ucfirst(mb_substr($aRow['logentry'], mb_strlen($one_format)));
                        }
                    }
                    $log_array[] = ["dateTime" => htmlentities($aRow['datetime']),
                                    "priority" => htmlentities($aRow['severity']),
                                    "user"     => htmlentities($aRow['user']),
                                    "category" => htmlentities($aRow['category']),
                                    "message"  => htmlentities($aRow['logentry']),
                                    "source"   => htmlentities($aRow['source']),
                                   ];
                }
            }
        }
    } else {
        foreach (array('.9','.8','.7','.6','.5','.4','.3','.2','.1','') as $postfix_file) {
            if (file_exists($this->GetLogFolder().$this->GetLogFileName().$postfix_file)) {
              if ($log_file_handle = @fopen($this->GetLogFolder().$this->GetLogFileName().$postfix_file,"r")) {
                flock($log_file_handle, LOCK_SH);
                while (!feof($log_file_handle)) {
                  $one_line = nullable_trim(fgets($log_file_handle));
                  if ('' != $one_line) {
                    $one_array = explode("\t", $one_line);
                    if (5 <= count($one_array)) { // Should be at least 5 elements, but who knows...
                      for ($i=0; $i < count($one_array); $i++) {
                        $one_array[$i] = htmlentities($one_array[$i]);
                      }
                      foreach ($formatting_array as $one_format) {
                        if (0 === mb_strpos(mb_strtoupper($one_array[4],'UTF-8'), mb_strtoupper($one_format,'UTF-8'))) {
                          $one_array[4] = ucfirst(mb_substr($one_array[4], mb_strlen($one_format)));
                        }
                      }
                      $one_line_array = ["dateTime" => $one_array[0],
                                         "priority" => $one_array[1],
                                         "user"     => $one_array[2],
                                         "category" => $one_array[3],
                                         "message"  => $one_array[4],
                                         "source"   => empty($one_array[7]) ? "" : $one_array[7],
                                        ];
                      $add_one = true;
                      foreach($search_filter as $one_search_key => $one_search_record) {
                        if (!empty($search[$one_search_key])) {
                          if (" " == $search[$one_search_key]) { // Field is Empty ?
                            if (!empty(nullable_trim($one_line_array[$one_search_key]))) {
                              $add_one = false;
                            }
                          } else {
                            $one_filter_array = explode(" ", $search[$one_search_key]);
                            foreach($one_filter_array as $one_sub_filter) {
                              if (!empty(nullable_trim($one_sub_filter) && ("!" != nullable_trim($one_sub_filter)))) {
                                if ("!" == mb_substr(nullable_trim($one_sub_filter), 0, 1)) {
                                  $one_sub_filter = mb_substr(nullable_trim($one_sub_filter), 1);
                                  if (false !== mb_stripos($one_line_array[$one_search_key], nullable_trim($one_sub_filter))) {
                                    $add_one = false;
                                  }
                                } else {
                                  if (false === mb_stripos($one_line_array[$one_search_key], nullable_trim($one_sub_filter))) {
                                    $add_one = false;
                                  }
                                }
                              }
                            }
                          }
                        }
                      }

                      if ($add_one) {
                        $lines[$index] = $one_line_array;
                        $index = ($index + 1) % $maxlines;
                        $total_items++;
                      }
                    }
                  }
                }
                fclose($log_file_handle);
              }
            } // if file exists
        } // foreach
        $part1 = array_slice($lines,    $index);
        $part2 = array_slice($lines, 0, $index);
        $ordered_lines = array_merge($part1, $part2);
        $temp_array = array_reverse($ordered_lines);

        for ($i = (($page-1) * $itemsPerPage); $i < ($page * $itemsPerPage); $i++) {
          if (isset($temp_array[$i])) {
            $log_array[] = $temp_array[$i];
          }
        }
    }
    return ["total" => $total_items,
            "logs"  => $log_array,
           ];
  }


  function ApiGetUsersArray(
    $data_array = []
  ) {
    $page = intval(isset($data_array['page']) ? $data_array['page'] : 1);
    if ($page < 1) {
      $page = 1;
    }
    $itemsPerPage = intval(isset($data_array['itemsPerPage']) ? $data_array['itemsPerPage'] : 25);
    if ($itemsPerPage < 10) {
      $itemsPerPage = 10;
    }
    $add_fields = empty($data_array['add_fields']) ? [] : $data_array['add_fields'];

    $fields = !empty($data_array['fields']) ? $data_array['fields'] : [
                                                                       'user',
                                                                       'description',
                                                                       'display_name',
                                                                       'email',
                                                                       'group',
                                                                       'desactivated',
                                                                       'locked',
                                                                       'sms',
                                                                       'synchronized',
                                                                       'synchronized_channel',
                                                                       'synchronized_server',
                                                                       'synchronized_time',
                                                                       'token_serial',
                                                                       'synchronized_dn',
                                                                       'error_counter',
                                                                       'last_error'
                                                                      ];
    if (is_array($fields)) {
      $fields = array_merge($fields, $add_fields);
    }

    // Name of the form search field / name of the database field
    $implemented_filters = [
      "user"         => "user",
      "description"  => "description",
      "group"        => "group",
      "display_name" => "display_name",
      "email"        => "email",
      "desactivated" => "desactivated",
      "locked"       => "locked",
      "token"        => "token_serial",
    ];

    $total_items = 0;
    $search_filter = [];

    $now_epoch = time();

    $search = (!empty($data_array['search'])) ? $data_array['search'] : [];

    foreach($implemented_filters as $one_search_key => $one_search_record) {
      if (!empty($search[$one_search_key])) {
        $search_filter[$one_search_key] = $one_search_record;
      }
    }

    $index = 0;
    $item_array = [];

    $fields_text = "";
    $fields_separator = "";
    $sql_attribute_quote = "";
    $sql_string_quote = "";
    
    $table_name = 'sql_users_table';
    $folder = $this->GetUsersFolder();
    $user_array = [];

    if ((($this->GetBackendTypeValidated()) && ("" != $this->_config_data[$table_name])) || ('files' == $this->GetBackendType())) {
      switch ($this->GetBackendType()) {
        case 'mysql':
          $sql_attribute_quote = "`";
          $sql_string_quote = "'";
          foreach ($fields as $one_field) {
            $fields_text.= $fields_separator.$sql_attribute_quote.$one_field.$sql_attribute_quote;
            $fields_separator = ',';
          }
          if ($this->OpenMysqlDatabase()) {
            $where = "";
            foreach($search_filter as $one_search_key => $one_search_record) {
              if (!empty($search[$one_search_key])) {
                if (" " == $search[$one_search_key]) { // Field is Empty ?
                  $sec_where = "TRIM($one_search_record) = {$sql_string_quote}{$sql_string_quote}";  
                } else {
                  $one_filter_array = explode(" ", $search[$one_search_key]);
                  $sec_where = "";
                  foreach($one_filter_array as $noesc_one_sub_filter) {
                    $one_sub_filter = nullable_trim(escape_mysql_string($noesc_one_sub_filter));
                    if (!empty($one_sub_filter) && ("!" != $one_sub_filter)) {
                      $negation = "";
                      if ("!" == mb_substr($one_sub_filter, 0, 1)) {
                        $one_sub_filter = mb_substr($one_sub_filter, 1);
                        $negation = "NOT ";
                      }
                      $sec_where.= !empty($sec_where) ? " AND " : "";
                      $sec_where.= "({$sql_attribute_quote}$one_search_record{$sql_attribute_quote} $negation LIKE {$sql_string_quote}%$one_sub_filter%{$sql_string_quote})";
                    }
                  }
                }
                if (!empty($sec_where)) {
                  $where.= empty($where) ? "" : " AND ";
                  $where.= "(".$sec_where.")";
                }
              }
            }
            $where = (!empty($where) ? " WHERE " : "") . $where;

            $sQueryCount = "SELECT COUNT(*) AS counter FROM {$sql_attribute_quote}".$this->_config_data[$table_name]."{$sql_attribute_quote} ".
              $where.
              ";";
            $sQuery = "SELECT ".$fields_text." FROM {$sql_attribute_quote}".$this->_config_data[$table_name]."{$sql_attribute_quote} ".
              $where.
              "ORDER BY user ASC ".
              "LIMIT $itemsPerPage OFFSET ". intval(($page-1) * $itemsPerPage) . "";
            if (is_object($this->_mysqli)) {
              if (!($rResultCount = $this->_mysqli->query($sQueryCount))) {
                $this->WriteLog(array('text'       => "Error: Unable to access the database: ".nullable_trim($this->_mysqli->error),
                                      'error_code' => _MULTIOTP_SYSTEM_SQL_ERROR_,
                                      'category'   => 'System',
                                      'user'       => '',
                                      'overwrite_severity' => 3)
                                );
              } else {
                if ($aRow = $rResultCount->fetch_assoc()) {
                  $total_items = $aRow['counter'];
                }
              }
              if (!($rResult = $this->_mysqli->query($sQuery))) {
                $this->WriteLog(array('text'       => "Error: Unable to access the database: ".nullable_trim($this->_mysqli->error),
                                      'error_code' => _MULTIOTP_SYSTEM_SQL_ERROR_,
                                      'category'   => 'System',
                                      'user'       => '',
                                      'overwrite_severity' => 3)
                               );
              } else {
                while ($aRow = $rResult->fetch_assoc()) {
                  if (isset($aRow['user'])) {
                    $user_array = array('id'                   => $aRow['user'],
                                        'user'                 => $aRow['user'],
                                        'description'          => $aRow['description'],
                                        'display_name'         => $aRow['display_name'],
                                        'email'                => $aRow['email'],
                                        'enabled'              => (0 == $aRow['desactivated']),
                                        'group'                => $aRow['group'],
                                        'locked'               => (1 == $aRow['locked']),
                                        'sms'                  => $aRow['sms'],
                                        'synchronized'         => (1 == $aRow['synchronized']),
                                        'synchronized_channel' => $aRow['synchronized_channel'],
                                        'synchronized_server'  => $aRow['synchronized_server'],
                                        'synchronized_time'    => $aRow['synchronized_time'],
                                        'token'                => (isset($aRow['token_serial']) ? $aRow['token_serial'] : ""),
                                        'synchronized_dn'      => $aRow['synchronized_dn'],
                                        'error_counter'        => $aRow['error_counter'],
                                        'last_error'           => (isset($aRow['last_error']) ? $aRow['last_error'] : 0)
                                        );
                    $user_array['delayed_account'] = (($user_array['error_counter'] >= $this->GetMaxDelayedFailures()) && ($now_epoch < ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime())));
                    $user_array['delayed_time'] = 0;
                    $user_array['delayed_finished'] = 0;
                    if ($user_array['delayed_account']) {
                      $user_array['delayed_time'] = ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime()) - $now_epoch;
                      $user_array['delayed_finished'] = ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime());
                      $user_array['locked'] = true;
                    }
                    $item_array[] = $user_array;
                  }
                }                         
              }
            } else {
              if (!($rResultCount = mysql_query($sQueryCount, $this->_mysql_database_link))) {
                $this->WriteLog(array('text'       => "Error: Unable to access the database: ".mysql_error(),
                                      'error_code' => _MULTIOTP_SYSTEM_SQL_ERROR_,
                                      'category'   => 'System',
                                      'user'       => '',
                                      'overwrite_severity' => 3)
                                );
              } else {
                if ($aRow = mysql_fetch_assoc($rResultCount)) {
                  $total_items = $aRow['counter'];
                }
              }
              if (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
              $this->WriteLog(array('text'       => "Error: Unable to access the database: ".mysql_error(),
                                    'error_code' => _MULTIOTP_SYSTEM_SQL_ERROR_,
                                    'category'   => 'System',
                                    'user'       => '',
                                    'overwrite_severity' => 3)
                              );
              } else {
                while ($aRow = mysql_fetch_assoc($rResult)) {
                  if (isset($aRow['user'])) {
                    $user_array = array('id'                   => $aRow['user'],
                                        'user'                 => $aRow['user'],
                                        'description'          => $aRow['description'],
                                        'display_name'         => $aRow['display_name'],
                                        'email'                => $aRow['email'],
                                        'enabled'              => (0 == $aRow['desactivated']),
                                        'group'                => $aRow['group'],
                                        'locked'               => (1 == $aRow['locked']),
                                        'sms'                  => $aRow['sms'],
                                        'synchronized'         => (1 == $aRow['synchronized']),
                                        'synchronized_channel' => $aRow['synchronized_channel'],
                                        'synchronized_server'  => $aRow['synchronized_server'],
                                        'synchronized_time'    => $aRow['synchronized_time'],
                                        'token'                => (isset($aRow['token_serial']) ? $aRow['token_serial'] : ""),
                                        'synchronized_dn'      => $aRow['synchronized_dn'],
                                        'error_counter'        => $aRow['error_counter'],
                                        'last_error'           => (isset($aRow['last_error']) ? $aRow['last_error'] : 0)
                                        );
                    $user_array['delayed_account'] = (($user_array['error_counter'] >= $this->GetMaxDelayedFailures()) && ($now_epoch < ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime())));
                    $user_array['delayed_time'] = 0;
                    $user_array['delayed_finished'] = 0;
                    if ($user_array['delayed_account']) {
                      $user_array['delayed_time'] = ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime()) - $now_epoch;
                      $user_array['delayed_finished'] = ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime());
                      $user_array['locked'] = true;
                    }
                    $item_array[] = $user_array;
                  }
                }                         
              }
            }
          }
          break;
        case 'pgsql':
          $sql_attribute_quote = "\"";
          $sql_string_quote = "'";
          foreach ($fields as $one_field) {
            $fields_text.= $fields_separator.$sql_attribute_quote.$one_field.$sql_attribute_quote;
            $fields_separator = ',';
          }
          if ($this->OpenPGSQLDatabase()) {
            $where = "";
            foreach($search_filter as $one_search_key => $one_search_record) {
              if (!empty($search[$one_search_key])) {
                if (" " == $search[$one_search_key]) { // Field is Empty ?
                  $sec_where = "BTRIM($one_search_record) = {$sql_string_quote}{$sql_string_quote}";  
                } else {
                  $one_filter_array = explode(" ", $search[$one_search_key]);
                  $sec_where = "";
                  foreach($one_filter_array as $noesc_one_sub_filter) {
                    $one_sub_filter = nullable_trim(escape_mysql_string($noesc_one_sub_filter));
                    if (!empty($one_sub_filter) && ("!" != $one_sub_filter)) {
                      $negation = '';
                      if ("!" == mb_substr($one_sub_filter, 0, 1)) {
                        $one_sub_filter = mb_substr($one_sub_filter, 1);
                        $negation = 'NOT ';
                      }
                      $sec_where.= !empty($sec_where) ? " AND " : "";
                      $sec_where.= "({$sql_attribute_quote}$one_search_record{$sql_attribute_quote} $negation ILIKE {$sql_string_quote}%$one_sub_filter%{$sql_string_quote})";
                    }
                  }
                }
                if (!empty($sec_where)) {
                  $where.= empty($where) ? "" : " AND ";
                  $where.= "(".$sec_where.")";
                }
              }
            }
            $where = (!empty($where) ? " WHERE " : "") . $where;

            $sQueryCount = "SELECT COUNT(*) AS {$sql_attribute_quote}counter{$sql_attribute_quote} FROM {$sql_attribute_quote}".$this->_config_data['sql_schema']."{$sql_attribute_quote}.{$sql_attribute_quote}".$this->_config_data[$table_name]."{$sql_attribute_quote} ".
              $where.
              ";";
            $sQuery = "SELECT ".$fields_text." FROM {$sql_attribute_quote}".$this->_config_data['sql_schema']."{$sql_attribute_quote}.{$sql_attribute_quote}".$this->_config_data[$table_name]."{$sql_attribute_quote} ".
              $where.
              "ORDER BY {$sql_attribute_quote}user{$sql_attribute_quote} ASC ".
              "LIMIT $itemsPerPage OFFSET ". intval(($page-1) * $itemsPerPage) . "";
            if (!($rResultCount = pg_query($this->_pgsql_database_link, $sQueryCount))) {
              $this->WriteLog(array('text'       => "Error: Unable to access the database: ".nullable_trim(pg_last_error($this->_pgsql_database_link)),
                                    'error_code' => _MULTIOTP_SYSTEM_SQL_ERROR_,
                                    'category'   => 'System',
                                    'user'       => '',
                                    'overwrite_severity' => 3)
                              );
            } elseif ($aRow = pg_fetch_assoc($rResultCount)) {
              $total_items = $aRow['counter'];
            }
            if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
              $this->WriteLog(array('text'       => "Error: Unable to access the database: ".nullable_trim(pg_last_error($this->_pgsql_database_link)),
                                    'error_code' => _MULTIOTP_SYSTEM_SQL_ERROR_,
                                    'category'   => 'System',
                                    'user'       => '',
                                    'overwrite_severity' => 3)
                              );
            } else {
              while ($aRow = pg_fetch_assoc($rResult)) {
                if (isset($aRow['user'])) {
                  $user_array = ['id'                   => $aRow['user'],
                                 'user'                 => $aRow['user'],
                                 'description'          => $aRow['description'],
                                 'display_name'         => $aRow['display_name'],
                                 'email'                => $aRow['email'],
                                 'enabled'              => (0 == $aRow['desactivated']),
                                 'group'                => $aRow['group'],
                                 'locked'               => (1 == $aRow['locked']),
                                 'sms'                  => $aRow['sms'],
                                 'synchronized'         => (1 == $aRow['synchronized']),
                                 'synchronized_channel' => $aRow['synchronized_channel'],
                                 'synchronized_server'  => $aRow['synchronized_server'],
                                 'synchronized_time'    => $aRow['synchronized_time'],
                                 'token'                => (isset($aRow['token_serial']) ? $aRow['token_serial'] : ""),
                                 'synchronized_dn'      => $aRow['synchronized_dn'],
                                 'error_counter'        => $aRow['error_counter'],
                                 'last_error'           => (isset($aRow['last_error']) ? $aRow['last_error'] : 0),
                                 ];
                  $user_array['delayed_account'] = (($user_array['error_counter'] >= $this->GetMaxDelayedFailures()) && ($now_epoch < ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime())));
                  $user_array['delayed_time'] = 0;
                  $user_array['delayed_finished'] = 0;
                  if ($user_array['delayed_account']) {
                    $user_array['delayed_time'] = ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime()) - $now_epoch;
                    $user_array['delayed_finished'] = ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime());
                    $user_array['locked'] = true;
                  }
                  $item_array[] = $user_array;
                }
              }
            }
          }
          break;
        case 'files':
        default:
          if (!$handle  = @opendir($folder)) {
            $result = false;
            return $result;
          } else {
            $full_array = [];
            do {
              $file = readdir($handle);
              if ((".db" == (mb_substr($file, -3)) && ('.db' != $file))) {
                $add_one = true;
                $user = $this->DecodeFileId(mb_substr($file,0,-3));
                $this->SetUser($user);
                $user_array = ['id'                   => $user,
                               'user'                 => $user,
                               'description'          => $this->GetUserDescription(),
                               'display_name'         => $this->GetUserDisplayName(),
                               'email'                => $this->GetUserEmail(),
                               'enabled'              => (1 == $this->GetUserActivated()),
                               'group'                => $this->GetUserGroup(),
                               'locked'               => (1 == $this->GetUserLocked()),
                               'sms'                  => $this->GetUserSms(),
                               'synchronized'         => (1 == $this->GetUserSynchronized()),
                               'synchronized_channel' => $this->GetUserSynchronizedChannel(),
                               'synchronized_server'  => $this->GetUserSynchronizedServer(),
                               'synchronized_time'    => $this->GetUserSynchronizedTime(),
                               'token'                => $this->GetUserTokenSerialNumber(),
                               'synchronized_dn'      => $this->GetUserSynchronizedDn(),
                               'error_counter'        => $this->GetUserErrorCounter(),
                               'last_error'           => $this->GetUserTokenLastError()
                              ];

                $user_array['delayed_account'] = (($user_array['error_counter'] >= $this->GetMaxDelayedFailures()) && ($now_epoch < ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime())));
                $user_array['delayed_time'] = 0;
                $user_array['delayed_finished'] = 0;
                if ($user_array['delayed_account']) {
                  $user_array['delayed_time'] = ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime()) - $now_epoch;
                  $user_array['delayed_finished'] = ((isset($user_array['last_error']) ? $user_array['last_error'] : 0) + $this->GetMaxDelayedTime());
                $user_array['locked'] = true;
                }

                if (!empty($search_filter)) {
                  foreach($search_filter as $one_search_key => $one_search_record) {
                    if (!empty($search[$one_search_key])) {
                      if (" " == $search[$one_search_key]) { // Field is Empty ?
                        if (!empty(nullable_trim($user_array[$one_search_key]))) {
                          $add_one = false;
                        }
                      } else {
                        $one_filter_array = explode(" ", $search[$one_search_key]);
                        foreach($one_filter_array as $one_sub_filter) {
                          if (!empty(nullable_trim($one_sub_filter) && ("!" != nullable_trim($one_sub_filter)))) {
                            if ("!" == mb_substr(nullable_trim($one_sub_filter), 0, 1)) {
                              $one_sub_filter = mb_substr(nullable_trim($one_sub_filter), 1);
                              if (false !== mb_stripos($user_array[$one_search_key], nullable_trim($one_sub_filter))) {
                                $add_one = false;
                              }
                            } else {
                              if (false === mb_stripos($user_array[$one_search_key], nullable_trim($one_sub_filter))) {
                                $add_one = false;
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
                if ($add_one) {
                  $full_array[] = $user_array;
                  $total_items++;
                  usort($full_array, function($a, $b) {
                    return strcasecmp($a["user"], $b["user"]);
                  });
                }
              }
            } while (false !== $file);
            for($i = intval(($page-1) * $itemsPerPage); $i < min($page * $itemsPerPage, $total_items); $i++) {
              if (!empty($full_array[$i])) {
                $item_array[] = $full_array[$i];
              }
            }
          }
      }
    }
    return ["total" => $total_items,
            "users" => $item_array,
           ];
  }


  function ApiLogout() {
    $_SESSION = [];
    if (ini_get("session.use_cookies"))
    {
      // Set secure to true and cookie to httponly true
      $params = session_get_cookie_params();
      $cookie_secure = (!$this->IsDebugOption()) && (!$this->IsDeveloperMode() && ('DVP' != $this->GetHardwareType()));
      $cookie_httponly = true;
      setcookie(session_name(), '', time() - 42000,
                $params["path"], $params["domain"],
                $cookie_secure, $cookie_httponly
                );
    }
    session_destroy();
    session_start();
    return true;
  }


  function ApiLogin(
    $data_array = []
  ) {
    $result = false;
    $username = mb_substr(empty($data_array['username']) ? '' : $data_array['username'], 0, 255);
    $hashedPassword = mb_substr(empty($data_array['hashedPassword']) ? '' : $data_array['hashedPassword'], 0, 255);

    if ('admin' == $username) {
      $result = $this->CheckAdminPasswordHashWithRandomSalt($hashedPassword);
    } elseif ($this->ReadUserData($username)) {
      if ($this->IsUserAdminWithOtp()) {
        $result = (0 === $this->CheckToken($password));
      }
    }
    
    if ($result) {
      $_SESSION['authenticated']       = true;
      $_SESSION['authenticated_admin'] = true;
      $_SESSION['authenticated_user']  = $username;
      
      /* And we change the random_salt, so even a second login
        *   with the same previous hash is not accepted         */
      $_SESSION['random_salt'] = mb_substr(md5(time()."@".rand(100000,999999)),0,12);
      $this->SetRandomSalt($_SESSION['random_salt']);
      $this->WriteLog(array('text'       => "Info: $username successfully logged in on console",
                            'error_code' => _MULTIOTP_OPERATION_DONE_,
                            'category'   => 'Authentication',
                            'user'       => $username)
                      );
    } else {
      // No authentication, we empty the session table but we keep the random_salt if any
      $random_salt = (empty($_SESSION['random_salt']) ? mb_substr(md5(time()."@".rand(100000,999999)),0,12) : $_SESSION['random_salt']);
      $_SESSION = [];
      $_SESSION['random_salt'] = $random_salt;
      $this->WriteLog(array('text'       => "Warning: $username login denied on console",
                            'error_code' => _MULTIOTP_ERROR_,
                            'category'   => 'Authentication',
                            'user'       => $username)
                      );

      // Email alert to the admin address if any
      if (("" != $username) && ("" != $this->GetSmtpServer()) && ("" != $this->GetEmailAdminAddress())) {
        $content = "<h2 color=\"#ff0000\">Security alert<h2>";
        $content.= "<br />\n";
        $content.= "Login denied on the console, tried from the IP address ".$this->GetSourceIp()." with the username \"$username\".";

        $email = $this->GetEmailAdminAddress();
        $email_from = $this->GetSmtpSender();
        if ('' == $email_from) {
            $email_from = $email;
        }

        // Default subject
        $subject = $this->GetClassName()." - security alert";

        $email_result = $this->SendEmail(array('email'    => $email,
                                               'subject'  => $subject,
                                               'content'  => $content,
                                               'priority' => 'High'));
        
        if (true !== $email_result) {
          $this->WriteLog("Error: failed to send security alert email to $email", FALSE, FALSE, _MULTIOTP_EMAIL_SEND_ERROR_, 'Authentication', '');
        }
      }
    }
    return $result;
  }


  /**************************
   **************************
   *** API/VUEJS pre-auth ***
   **************************
   **************************/
  function ApiVueJsPreAuth($hash_salt = ""): void {
    $params = session_get_cookie_params();
    $cookie_secure = (!$this->IsDebugOption()) && (!$this->IsDeveloperMode() && ('DVP' != $this->GetHardwareType()));
    $cookie_httponly = true;
    
    session_set_cookie_params(
      $params["lifetime"],
      $params["path"], $params["domain"],
      $cookie_secure, $cookie_httponly
    );

    $previous_session_name = session_name("cookid");
    session_start();

    if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > ($this->GetSessionTimeout()))) {
      $_SESSION = [];
      if (ini_get("session.use_cookies")) {
        // Set secure to true and cookie to httponly true
        setcookie(
          session_name(), '', time() - 42000,
          $params["path"], $params["domain"],
          $cookie_secure, $cookie_httponly
        );
      }
      session_destroy();
      session_start();
    }

    $_SESSION['LAST_ACTIVITY'] = time();

    if (empty($_SESSION['authenticated'])) {
      $_SESSION['authenticated']       = false;
      $_SESSION['authenticated_admin'] = false;
      $_SESSION['authenticated_user']  = '';
    }

    // This is the session authentication temporary salt
    if (empty($_SESSION['random_salt'])) {
      $_SESSION['random_salt'] = mb_substr(md5(time()."@".rand(100000,999999)),0,12);
      $_SESSION['ajax_encryption'] = md5($_SESSION['random_salt'].$this->GetConfigAttribute('admin_password_hash').$_SESSION['random_salt']);
    }

    /****************************************
     * WE REALLY DO NOT WANT TO BE CACHED !!!
     ****************************************/
    header("Expires: " . gmdate("D, d M Y H:i:s") . " GMT");
    header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
    header("Cache-Control: no-store, no-cache, must-revalidate");
    header("Cache-Control: post-check=0, pre-check=0", false);
    header("Pragma: no-cache");

    // Set the shared secret between front and backend
    $this->SetHashSalt(empty($hash_salt) ? $this->GetHashSalt() : $hash_salt);
    
    // Set the temporary salt for this session
    $this->SetRandomSalt($_SESSION['random_salt']);

    if ('' == $this->GetConfigAttribute('admin_password_hash')) {
      $this->SetAdminPassword(_MULTIOTP_DEFAULT_PASSWORD_);
      $this->WriteConfigData();
    }
  }


  /*****************
   * API/VUEJS
   * 
   * @param string $data
   * @param bool $data
   * 
   * @return string|void
   */
  function ApiVueJs(
    $data = "",
    $as_result = false
  ) {
    $http = ["status" => 200, "message" => "OK"];
    $answer = ["status" => true, "level" => "success", "message" => "OK"];
    $data_array = json_decode($data, true);
    $c = (!empty($data_array["c"])) ? $data_array["c"] : "";

    // Always available commands
    switch ($c) {

      case "getSalt":
        $answer["data"] = ["auth" => ["salt" => $this->GetRandomSalt()]];
        break;

      case "login":
        $answer["data"] = ["auth" => ["login" => $this->ApiLogin($data_array)]];
        break;

      case "logout":
        $answer["data"] = ["auth" => ["logout" => $this->ApiLogout(), "model" => $this->GetModelName()]];
        break;

      default:

        if ((isset($_SESSION['authenticated']) && $_SESSION['authenticated'])) {
          // Authenticated available commands
          switch ($c) {

            case "getDashboard":
              $answer = $this->ApiGetDashboardArray();
              break;

            case "getMonitor":
              $answer["data"] = ["monitor" => $this->ApiGetMonitorArray($data_array)];
              break;

            case "getUsers":
              $answer["data"] = ["users" => $this->ApiGetUsersArray($data_array)];
              break;

            default:
              $http = ["status" => 404, "message" => "No valid command"];
              $answer = ["status" => false, "level" => "error", "message" => "unknownCommand"];
              break;

          }
          $answer["timeout"] = intval($this->GetSessionTimeout());
        } else {
          // Unauthenticated only commands
          switch ($c) {

            default:
              $http = ["status" => 401, "message" => "Unauthorized"];
              $answer = ["status" => false, "level" => "error", "message" => "unauthorized"];
              break;
          }
        }
    }
    if ($as_result) {
      return json_encode($answer);
    } else {
      header (((substr(php_sapi_name(), 0, 3) == 'cgi') ? "Status:" : "HTTP/1.1") . " ".$http["status"]." ".$http["message"]);
      echo json_encode($answer);
      exit();
    }
  }

}

//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
//                                                                  //
// The source codes of the next classes are not directly related to //
//  multiOTP but they are needed for extended functionalities.      //
//                                                                  //
// They are inserted directly in the class file to eliminitate any  //
//  include or require problem.                                     //
//                                                                  //
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

require_once('contrib/MultiotpSms.php'); // External contribution

require_once('contrib/MultiotpTools.php'); // External contribution

/*******************************************************************
 * PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY 2.1 (LGPLv2.1) *
 * Scott Barnett                                                   *
 * http://adldap.sourceforge.net/                                  *
 *******************************************************************/
require_once('contrib/MultiotpAdLdap.php'); // External contribution

/*************************************************
 * barcode.php (MIT License)                     *
 * Generate barcodes from a single PHP file      *
 * Copyright (c) 2016-2018 Kreative Software     *
 * https://github.com/kreativekorp/barcode       *
 *************************************************/
require_once('contrib/barcode.php'); // External contribution

/************************************************
 * status_bar.php (2010) (FREE "AS IS")         *
 * dealnews.com, Inc.                           *
 * http://brian.moonspot.net/status_bar.php.txt *
 ************************************************/
require_once('contrib/MultiotpShowStatus.php'); // External contribution

/*****************************************
 * PHP Syslog class 1.1.2 (FREE "AS IS") *
 * André Liechti                         *
 * http://developer.sysco.ch/php/        *
 *****************************************/
require_once('contrib/MultiotpSyslog.php'); // External contribution

/*******************************************
 * XML Parser Class 1.3.0 (LGPLv3)         *
 * Adam A. Flynn                           *
 * http://www.criticaldevelopment.net/xml/ *
 *******************************************/
require_once('contrib/MultiotpXmlParser.php'); // External contribution

/*****************************************
 * MultiotpYubikey Class (LGPLv3)        *
 * André Liechti                         *
 * https://www.multiotp.net/              *
 *****************************************/
require_once('contrib/MultiotpYubikey.php'); // External contribution

/*************************************
 * phpseclib 1.0.6 (MIT License)     *
 * MMVI Jim Wigginton                *
 * http://phpseclib.sourceforge.net/ *
 *************************************/

// This function is redefined here as the phpseclib files are flat saved for this project.
if (!function_exists('phpseclib_resolve_include_path')) {
    function phpseclib_resolve_include_path($filename)
    {
        return $filename;
    }
}

if (!function_exists('crypt_random_string')) {
  require_once('contrib/Random.php'); // External contribution
}
if (!class_exists('Math_BigInteger')) {
  require_once('contrib/BigInteger.php'); // External contribution
}
if (!class_exists('Crypt_Base')) {
  require_once('contrib/Base.php'); // External contribution
}
if (!class_exists('Crypt_Hash')) {
  require_once('contrib/Hash.php'); // External contribution
}
if (!class_exists('Crypt_Rijndael')) {
  require_once('contrib/Rijndael.php'); // External contribution
}
if (!class_exists('Crypt_AES')) {
  require_once('contrib/AES.php'); // External contribution
}
if (!class_exists('Crypt_DES')) {
  require_once('contrib/DES.php'); // External contribution
}
if (!class_exists('Crypt_TripleDES')) {
  require_once('contrib/TripleDES.php'); // External contribution
}
if (!class_exists('Crypt_RSA')) {
  require_once('contrib/RSA.php'); // External contribution
}
if (!class_exists('Crypt_Blowfish')) {
  require_once('contrib/Blowfish.php'); // External contribution
}
if (!class_exists('Crypt_Twofish')) {
  require_once('contrib/Twofish.php'); // External contribution
}
if (!class_exists('Crypt_RC4')) {
  require_once('contrib/RC4.php'); // External contribution
}
if (!class_exists('Net_SSH2')) {
  require_once('contrib/SSH2.php'); // External contribution
}
if (!class_exists('Net_SFTP')) {
  require_once('contrib/SFTP.php'); // External contribution
}
if (!class_exists('Net_SFTP_Stream')) {
  require_once('contrib/Stream.php'); // External contribution
}
// if (!class_exists('Element')) {
//   require_once('contrib/Element.php'); // External contribution
// }
if (!class_exists('File_ASN1')) {
  require_once('contrib/ASN1.php'); // External contribution
}
if (!class_exists('File_X509')) {
  require_once('contrib/X509.php'); // External contribution
}

/********************************************************
* XPertMailer package 4.0.5 (LGPLv2.1)                 *
* Tanase Laurentiu Iulian                              *
* http://xpertmailer.sourceforge.net/                  *
********************************************************/
if (!constant_defined('DISPLAY_XPM4_ERRORS')) define('DISPLAY_XPM4_ERRORS', FALSE);
if (version_compare(phpversion(), '5', '>=')) {
  if (!class_exists('FUNC5')) {
    require_once('contrib/FUNC5.php'); // External contribution
  }

  if (!class_exists('MIME5')) {
    require_once('contrib/MIME5.php'); // External contribution
  }

  if (!class_exists('SMTP5')) {
    require_once('contrib/SMTP5.php'); // External contribution
  }

  if (!class_exists('MAIL5')) {
    require_once('contrib/MAIL5.php'); // External contribution
  }
  class MAIL extends MAIL5 { }
} else {
  if (!class_exists('FUNC4')) {
    require_once('contrib/FUNC4.php'); // External contribution
  }

  if (!class_exists('MIME4')) {
    require_once('contrib/MIME4.php'); // External contribution
  }

  if (!class_exists('SMTP4')) {
    require_once('contrib/SMTP4.php'); // External contribution
  }

  if (!class_exists('MAIL4')) {
    require_once('contrib/MAIL4.php'); // External contribution
  }
  class MAIL extends MAIL4 { }
}

/*******************************************************
* NuSOAP - PHP Web Services Toolkit 1.123 (LGPLv2.1)   *
* NuSphere Corporation                                 *
* http://sourceforge.net/projects/nusoap/              *
********************************************************/
if (!class_exists('nusoap_base')) {
  require_once('contrib/nusoap.php'); // External contribution
}

/*******************************************************
* Based on phpWebSocketServer project                  *
* https://github.com/napengam/phpWebSocketServer       *
********************************************************/
if (!class_exists('websocketCore')) {
  require_once('contrib/phpWebSocketServer/websocketCore.php'); // External contribution
}

class multiotpwebsocket extends websocketCore {
  function __construct($Address, $timeout = _MULTIOTP_DEFAULT_PUSH_TIMEOUT_) {
    $this->timeout = $timeout;
    parent::__construct($Address);
  }

  final function setTimeout($timeout) {
    $this->timeout = $timeout;
  }
}

?>