PHP Classes

File: multiotp.php

Recommend this page to a friend!
  Classes of André Liechti  >  multiOTP PHP class  >  multiotp.php  >  Download  
File: multiotp.php
Role: Application script
Content type: text/plain
Description: Command line tool (merged from the special header and the class)
Class: multiOTP PHP class
Authenticate and manage OTP strong user tokens
Author: By
Last change: New release 5.8.8.4
ENH: Better docker support (also for Synology)
ENH: Add Raspberry Pi Bullseye 11.0 support
New release 5.8.7.0
FIX: Token "Without2FA" where not working all time with LDAP users
ENH: Embedded Windows nginx edition updated to version 1.21.6
ENH: Embedded Windows PHP edition updated to version 7.4.29
ENH: New MariaDB/MySQL indexes handling during schema creation and schema updates
ENH: Enhanced internal tests
ENH: Telnyx SMS provider support
ENH: PHP 7.4 deprecated code cleaned
ENH: Email token is now supported for Credential Provider
ENH: In CLI check, if username doesn't exist, it try automatically a shorter domain name step by step
ENH: Enhanced multiOTP Credential Provider support
ENH: VM version 011 support
(Debian Bullseye 11.0, PHP 7.4, FreeRADIUS 3.0.21, Nginx 1.18.0)
ENH: Removed multicast support on the network card
5.8.2.9 Added compatibility with new multiOTP Credential Provider
New release 5.8.2.3
FIX: Dockerfile updated (php-bcmath added)
URL typo
New release 5.8.2.1
ENH: eDirectory LDAP server support (set the LDAP server type value to 4)
ENH: Raspberry content also in source file
New release 5.8.1.9
FIX: Cookie privacy (httponly and secure) backported to previous virtual appliances
ENH: Weak SSL ciphers disabled
ENH: Better Docker support
ENH: Better log handling
New release 5.8.1.1
FIX: In some cases, the HOTP/TOTP was not well computed
New release 5.8.1.0
FIX: Too many ReadConfigData loop during initialization
FIX: Better unicode handling, multibyte fonctions also for mb_substr()
FIX: A device file was searched with the name of the FreeRADIUS Client-Shortname
ENH: Enhanced Web GUI accounts list (green=AD/LDAP synced, orange = delayed, red=locked)
ENH: -sync-delete-retention-days= option is set by default to 30 days
ENH: VM version 010 support (Debian Buster 10.5, PHP 7.3, FreeRADIUS 3.0.17)
ENH: MySQL optimization
ENH: Enhanced windows command line scripts (automatic administrator level)
ENH: New -sync-delete-retention-days= option in order to purge inexistent AD/LDAP users (SetSyncDeleteRetentionDays and GetSyncDeleteRetentionDays method)
ENH: Raspberry Pi 4B support
ENH: New unified distribution
ENH: Debian Buster 10.5 support
ENH: Enhanced PHP 7.3 support
ENH: Better mysqli support for alternate connection port
NOT -> NOTE
New release 5.6.1.5
FIX: Separated configuration/statistics storage handling
FIX: IsTemporaryBadServer function (thanks to brownowski on GitHub)
ENH: Better PHP 7.3 support
ENH: Base32 encoder/decoder new implementation
ENH: During WriteConfigData, loop on the current values, and check with the old values
ENH: Enhanced internal tests
ENH: Give an info if time based token is probably out of sync (in a window 10 time bigger)
(for example for hardware tokens not used for a long time)
ENH: Modifications for Debian 10.x (buster) binary images support (64 bits)
ENH: Enhanced error messages, more log information
ENH: In debug mode, display an error if logfile cannot be written
ENH: Global Access-Challenge support
ENH: New QRcode library used (without external files dependency)
ENH: New Raspberry images support for Raspberry Pi 1B/1B+/2B/3B/3B+
Fix IsTemporaryBadServer function.
- return missing
- logic incorrect
Date: 4 months ago
Size: 3,076,865 bytes
 

Contents

Class file image Download
#!/usr/bin/php
<?php
/* multiOTP command line version (all-in-one) */
/**
 * @file  multiotp.cli.header.php
 * @brief Command line implementation of the multiOTP PHP class.
 *
 * multiOTP PHP CLI header - Strong two-factor authentication PHP class
 * https://www.multiotp.net
 *
 * Visit http://forum.multiotp.net/ for additional support.
 *
 * Donation are always welcome! Please check https://www.multiOTP.net
 * and you will find the magic button ;-)
 *
 * If the name of this file is multiotp.php, it means that it is already
 * the result of the merge of the two files multiotp.cli.header.php and
 * multiotp.class.php
 *
 * The MultiOTP PHP CLI header is simply merged with the MultiOTP PHP
 * class in order to provide an authentication command line script.
 *
 * This script can be used as an external authentication provider with at
 * least the following RADIUS servers:
 *  - 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)
 *  - FreeRADIUS, a free Radius server implementation for Linux
 *    and *nix environments (http://freeradius.org)
 *  - WinRADIUS, the FreeRADIUS implementation ported for Windows
 *    (http://winradius.eu/)
 *
 * For Windows, you can also use the multiotp.exe file provided, which is
 * an embedded PHP interpreter together with the result of the merge.
 *
 * PHP 5.3.0 or higher is supported.
 *
 * @author    Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
 * @version   5.8.8.4
 * @date      2022-05-08
 * @since     2010-06-08
 * @copyright (c) 2010-2022 SysCo systemes de communication sa
 * @copyright GNU Lesser General Public License
 *
 *//*
 *
 * LICENCE
 *
 *   Copyright (c) 2010-2022 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 PHP class
 *
 *   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/>.
 *
 *
 * Command line usage
 *
 *   Type multiotp -help to have the full description of the options,
 *    and have a look at the readme.txt file for enhanced explanations
 *
 *
 * Return codes
 *
 *   0 OK: Token accepted
 *
 *   9 INFO: Access Challenge returned back to the client
 *  10 INFO: Access Challenge returned back to the client
 *
 *  11 INFO: User successfully created or updated
 *  12 INFO: User successfully deleted
 *  13 INFO: User PIN code successfully changed
 *  14 INFO: Token has been resynchronized successfully
 *  15 INFO: Tokens definition file successfully imported
 *  16 INFO: QRcode successfully created
 *  17 INFO: UrlLink successfully created
 *  18 INFO: SMS code request received
 *  19 INFO: Requested operation successfully done
 *
 *  20 ERROR: User blacklisted
 *  21 ERROR: User doesn't exist
 *  22 ERROR: User already exists
 *  23 ERROR: Invalid algorithm
 *  24 ERROR: Token locked (too many tries)
 *  25 ERROR: Token delayed (too many tries, but still a hope in a few minutes)
 *  26 ERROR: The time based token has already been used
 *  27 ERROR: Resynchronization of the token has failed
 *  28 ERROR: Unable to write the changes in the file
 *  29 ERROR: Token doesn't exist
 *
 *  30 ERROR: At least one parameter is missing
 *  31 ERROR: Tokens definition file doesn't exist
 *  32 ERROR: Tokens definition file not successfully imported
 *  33 ERROR: Encryption hash error, encryption key is not the same
 *  34 ERROR: Linked user doesn't exist
 *  35 ERROR: User not created
 *  36 ERROR: Token doesn't exist
 *  37 ERROR: Token already attributed
 *  38 ERROR: User is deactivated
 *  39 ERROR: Requested operation aborted
 *
 *  40 ERROR: SQL query error
 *  41 ERROR: SQL error
 *  42 ERROR: They key is not in the table schema
 *  43 ERROR: SQL entry cannot be updated
 *
 *  50 ERROR: QRcode not created
 *  51 ERROR: UrlLink not created (no provisionable client for this protocol)
 *  58 ERROR: File is missing
 *  59 ERROR: Bad restore configuration password
 *
 *  60 ERROR: No information on where to send SMS code
 *  61 ERROR: SMS code request received, but an error occured during transmission
 *  62 ERROR: SMS provider not supported
 *  63 ERROR: This SMS code has expired
 *  64 ERROR: Cannot resent an SMS code right now
 *  69 ERROR: Failed to send email
 *
 *  70 ERROR: Server authentication error
 *  71 ERROR: Server request is not correctly formatted
 *  72 ERROR: Server answer is not correctly formatted
 *  79 ERROR: AD/LDAP connection error
 *
 *  80 ERROR: Server cache error
 *  81 ERROR: Cache too old for this user, account autolocked
 *  82 ERROR: User not allowed for this device
 *  88 ERROR: Device is not defined as a HA slave
 *  89 ERROR: Device is not defined as a HA master
 *
 *  93 ERROR: Authentication failed (time based token probably out of sync)
 *  94 ERROR: API request error
 *  95 ERROR: API authentication failed
 *  96 ERROR: Authentication failed (CRC error)
 *  97 ERROR: Authentication failed (wrong private id)
 *  98 ERROR: Authentication failed (wrong token length)
 *  99 ERROR: Authentication failed (and other possible unknown errors)
 *
 *
 * Radius integration examples
 *
 *   Example 1 (FreeRADIUS 2.x under Linux or Windows, new fashion)
 *
 *     Have a look in the readme file, everything is explained.
 *
 *
 *   Example 2 (FreeRADIUS under Linux or Windows, old fashion)
 *
 *     Define a DEFAULT entry in the /etc/freeradius/users file like this:
 *     DEFAULT Auth-Type = Accept
 *     Exec-Program-Wait = "/usr/local/bin/multiotp.php %{User-Name} %{User-Password}",
 *     Fall-Through = Yes,
 *     Reply-Message = "Hello, %{User-Name}"
 *
 *
 *   Example 3 (TekRADIUS or TekRADIUS LT under Windows)
 *
 *     TekRADIUS supports a Default Username to be used when a matching user
 *     profile cannot be found for an incoming RADIUS authentication request.
 *     So a quick and easy way is to create in the TekRADIUS Manager a User
 *     named 'Default' that belongs to the existing 'Default' Group.
 *     Then add to this Default user the following attribute :
 *     Check  External-Executable  C:\multitop\multiotp.exe %ietf|1% %ietf|2%
 *
 *
 * External files created
 *
 *   Users database files in the subfolder called users
 *   Tokens database files in the subfolder called tokens
 *
 *
 * External file needed
 *
 *   Users database files in the subfolder called users
 *   Tokens database files in the subfolder called tokens
 *
 *
 * Special issues
 *
 *   If you need specific developements concerning strong authentication,
 *   do not hesistate to contact us per email at info@multiotp.net.
 *
 *
 * Users feedbacks and comments
 *
 * 2018-08-25 Muzammel (PK)
 *   Thanks for your questions about the client/server process,
 *    which has been enhanced based on the exchange we had.
 *
 * 2018-07-31 Sergey, Kiev (UA)
 *   Thanks for your questions regarding -restore-config in the command line version.
 *   The restore function has been corrected
 *
 * 2018-02-13 Jonathan Garber (via GitHub)
 *   Thanks for your feedback about various issues.
 *
 * 2017-11-22 vak255 (via GitHub)
 *   Thanks for your feedback about a bad handled unicode issue.
 *   All strtoXXX and strpos have been changed to the the multibyte version.
 *
 * 2017-06-11 Richard Green
 *   Thanks for your proposal about specific LDAPTLS configuration values to be moved in the config parameters.
 *
 * 2017-04-19 Frank van der Aa, Vanboxtel BV (NL)
 *   Thanks a lot for your valuable implementation suggestion about PostgreSQL.
 *   The proposed code has been adapted and integrated in the project.
 *
 * 2017-02-14 Frank van der Aa, Vanboxtel BV (NL)
 *   Thanks for your proposal about GetList() method sorted output.
 *
 * 2017-02-09 Frank van der Aa, Vanboxtel BV (NL)
 *   Thanks for your debug about lockedlistarray[], the proposed
 *   GetDelayedUsersList() method and the delayed users display on the web GUI.
 *
 * 2017-02-02 Stefan Kügler, SerNet GmbH (DE)
 *   Thanks for your feedback on the last edition.
 *
 * 2017-01-24 Jean-François Perillo, Kudelski Security (CH)
 *   As proposed by Jean-François, requested LDAP password for synchronized users can be overwritten.
 *
 * 2017-01-05 Stefan Kügler, SerNet GmbH (DE)
 *   Thanks for your feedbacks on the last beta edition.
 *
 * 2017-01-04 Frank van der Aa, Vanboxtel (NL)
 *   Thanks for your feedback concerning leading zeros that can be omitted for the OTP or the PIN.
 *   This has been fixed for the next 5.0.3.4 release.
 *
 * 2016-12-07, 2016-12-01 Stefan Kügler, SerNet GmbH (DE)
 *   Thanks for your feedbacks on the last beta edition.
 *
 * 2016-12-02, Jim Bailey (USA)
 *   Thanks for your feedbacks with some features proposals.
 *
 * 2016-11-25 SKB Kontur (RU)
 *   Thanks for your appreciated $$$ donation.
 *
 * 2016-11-23 Serg Avtukhovich, SKB Kontur (RU)
 *   Serg had some issues with large Active Directory. He did the beta tests for several improvements.
 *
 * 2016-11-10 SerNet GmbH (DE)
 *   MANY thanks for your appreciated $$$ sponsorship for new implemented features proposed by Stefan Kügler.
 *
 * 2016-04-18 Serg Avtukhovich, SKB Kontur (RU)
 *   Serg had some strange problems when using multiOTP in client/server mode.
 *   After some trials, we fix the issue with the server when "log on display" is activated.
 *
 * 2015-12-20 Svetoslav Mateev, STS Soft (BG)
 *   Thanks for your appreciated $$$ donation.
 *
 * 2015-12-18 Sam Leach, Warwickshire County Council (UK)
 *   Sam informed us that a huge AD/LDAP organizational unit (100'000 users)
 *   crashed the sync process. This has been internally reproduced and corrected.
 *
 * 2015-08-10 Edward Kovarski (CA)
 *   Edward informed us that some special chars in the LDAP/AD group name
 *    was killing the SyncLdapUsers process. This has been corrected.
 *
 * 2015-07-14 Pierre-Nicolas Paradis, SherWeb (FR)
 *   Pierre-Nicolas informed us that it was still not possible to change
 *    the admin password using the web GUI. This has been corrected.
 *
 * 2015-06-23 Jun Li (CN)
 *   As proposed by Jun Li, launching the command line version without
 *    enough parameters returns now a 30 error code (instead of 19).
 *    Side effect is that -help is now required to display help page.
 *
 * 2015-06-02 Jean-François Perillo, Kudelski Security (CH)
 *   As proposed by Jean-François, token length error information has been
 *    added in the regular log and the autoresync is now enabled by default.
 *
 * 2015-06-02 Sébastien Charlier, Thesis SA (CH)
 * 2015-03-09 Martin
 *   Martin and Sébastien informed us that passwords containing the minus sign are not accepted.
 *
 * 2015-02-16 Sylvain Maret, Kudelski Security (CH)
 *   Sylvain informed us that Gemalto PSKC file don't provide the time interval for TOTP tokens.
 *   RFC default value (30 seconds) is now set by default if no time interval is given.
 *
 * 2015-01-27 Thomas Klute, ingenit GmbH & Co. KG (DE)
 *   Thanks Thomas for you feedback concerning a potential exploit with dots and slashes in a username.
 *   Even if no information can be extracted using this method, it's always good to patch this kind of weakness.
 *
 * 2015-01-08 Markus Arnoldi, LEWA Attendorn GmbH (DE)
 *   Useful comments about prefix PIN handling, documentation has been enhanced.
 *   Two new command line options are now available (fastcreatenopin and fastecreatewithpin)
 *
 * 2014-12-22 Sajid Hameed, Network Places Ltd (UK)
 *   Questions about users lockout, documentation has been enhanced.
 *   Three command line options information has been added in the documentation.
 *
 * 2014-12-15 Steve Jacot-Guillarmod, Swissdotnet SA (CH)
 *   Thanks Steve for your valuable feedback about LDAP sync and groups
 *   handling with a specific Synology OpenLDAP server implementation.
 *
 * 2014-11-04 Yubico Inc. (USA) / Yubico AB (S) / Yubico Ltd. (UK)
 *   BIG THANKS to the Yubico team which provides us several YubiKeys for the
 *   workshop organized during the Application Security Forum in Yverdon-les-Bains (Switzerland).
 *   Starting with version 4.3.0.0, YubiKeys (both Yubico OTP and HOTP) are now also supported and easy to import.
 *   (simply import the YubiKey traditional format log file)
 *
 * 2014-10-13 Adam Twardowski, Choopa LLC (USA)
 *   Thanks Adam for your valuable feedback concerning a bug with the NT_KEY generation if prefix PIN is enabled.
 *   Adam discovered the bug and fixed it when he configured pptpd with
 *   FreeRADIUS in order to set up a PPTP VPN with strong authentication.
 *
 * 2014-06-17 Stefan Kügler, SerNet GmbH (DE)
 *   Stefan proposes to add Active Directory msRADIUSFramedIPAddress attribute
 *   synchronization in order to distribute the Framed-IP-Address to a user.
 *
 * 2014-04-04 Stefan Kügler, SerNet GmbH (DE)
 * 2014-04-01 Daniel Särnström, Donator AB (SE)
 *   Daniel & Stefan asks some info in order to import tokens without a know format.
 *   Good question, multiOTP supports now importation of tokens from CSV file.
 *
 * 2014-04-02 Prashant Kumar, Alscient (UK)
 *   Prash is playing with FreeRADIUS and VPN (PPTP with MPPE). This requires radius to send MPPE keys.
 *   Interesting feedback, multiOTP provides now NT_KEY, like the ntlm_auth external helper.
 *
 * 2014-03-31 Alex Tasikas (GR)
 *   Thanks Alex for your valuable feedback concerning some bugs in LDAP support.
 *
 * 2014-03-25 Prashant Kumar, Alscient (UK)
 *   As proposed by Prash, we have added the possibility to modify the list of attributes to encrypt.
 *
 * 2014-03-17 Arthur de Jong, West Consulting (NL)
 *   Arthur gave some feedbacks concerning distributing the source code in the
 *    "preferred form of the work for making modifications".
 *
 * 2014-03-14 Soeren Malchow, MCON (DE)
 *   Thanks for your feedback concerning a bug in the SQL request for the log table.
 *
 * 2014-01-27 Henk van der Helm (NL)
 *   MANY thanks for your appreciated $$$ donation.
 *
 * 2014-01-19 Erik Nylund (FI)
 *   Thanks four your feedback concerning specific parameters order in QRCode for Microsoft Authenticator
 *
 * 2014-01-14 Sylvain Maret, Kudelski Security (CH)
 *   Thanks for your feedback concerning possible zero division in the ComputeOathTruncate method.
 *    Method has been altered in order to be more compatible with almost any PHP version.
 *   Thanks also for the suggestion to resync without the prefix PIN. Both are supported now.
 *
 * 2014-01-08/09  Cheng Shao-Pin (CN)
 *   Thanks for your feedback concerning possible missing JSON extension in old PHP distribution
 *    and possible image functions incompatibilities with some PHP versions during QRcode generation.
 *   Thanks also for your appreciated $ donation.
 *
 * 2014-01-08  Cheng Shao-Pin (CN) and Daniel Särnström, Donator AB (SE)
 *   Thanks for your feedback concerning md5.js missing in the distribution.
 *
 * 2013-12-20 Rico Zeiss, Hermann Wegener GmbH & Co. KG (DE)
 *   MANY thanks for your appreciated $$$ sponsorship to support us to add MS-CHAP and MS-CHAPv2 in a next release.
 *
 * 2013-12-18 Xavier Céspedes (ES)
 *   Thanks to Xavier who noticed a problem with the hex2bin() function duringthe scratch password generation.
 *   In the meantime, the GetUserScratchPasswordsList() function has been improved and fixed and is in the 4.1 release.
 *
 * 2013-09-20 Sean Butler-Lee (IE)
 *   Thanks a lot for announcing a bug with the GetUserScratchPasswordsArray() method.
 *
 * 2013-08-22,26 Frank Bongrand (FR)
 *   Thanks a lot for valuable feedbacks concerning some minor bugs in 4.0.4 and 4.0.6
 *
 * 2013-08-21 Henk van der Helm (NL)
 *   Thanks a lot for a valuable feedback concerning some minor bugs in 4.0.4
 *
 * 2013-08-15 Donator AB (SE)
 *   MANY thanks for your appreciated $$$ sponsorship to support us to add self-registration in a next release.
 *
 * 2013-08-13 Daniel Särnström, Donator AB (SE)
 *   Daniel proposed to add self-registration and pskc v12 with encrypted data support (OATH compliant).
 * 
 * 2013-07-25 Dominik Pretzsch from Last Squirrel IT (DE)
 *   After some discussions with Dominik, integration of the client/server support in the basic library
 *
 * 2013-07-23 Stefan Kügler (DE) (again ;-)
 *   Stefan proposed to add the possibility to show the log, which is especially convenient for MySQL log.
 *   He proposed also to be able to call an external program to send SMS.
 *
 * 2013-07-11 Stefan Kügler (DE)
 *   Stefan proposed to add a lock and unlock option for the user.
 *
 * 2013-06-19 SerNet GmbH (DE)
 *   MANY thanks for your appreciated $$$ sponsorship after we implemented some features proposed by Stefan Kügler.
 *
 * 2013-06-13 Henk van der Helm (NL) (again ;-)
 *   Henk proposed to be able to have a specific description for the software token.
 *   (we use the already existing user description attribute)
 *
 * 2013-06-01 Stefan Irion (CH)
 *   Thanks for your appreciated $$ donation.
 *
 * 2013-05-14 Henk van der Helm (NL)
 *   Henk asked to support also the provider IntelliSMS. Thanks for the $$ sponsorship!
 *
 * 2013-05-03 Stefan Kügler (DE)
 *   Stefan proposed to lower the default max_time_window to 600 seconds.
 *
 * 2013-03-04 Alan DeKok (CA)
 *   Alan proposed in the freeradius mailing-list to put a prefix to be able to handle the
 *   debug info by the freeradius server.
 *
 * 2012-11-28  Gareth Thomas
 *   Thanks for your appreciated $$ donation.
 *
 * 2012-03-16 Nicolas Goralski (LU)
 *   Nicolas proposed an enhancement in order to support PAM. Thanks also for the $$ sponsorship!
 *     (with the -checkpam option in the command line edition)
 *
 * 2011-05-19 Fabiano Domeniconi (CH)
 *   Fabiano found old info in the samples, CheckToken() is not boolean anymore! Samples fixed.
 *
 * 2011-04-24 Steven Roddis (AU)
 *   Steven asked for more examples. Thanks to Steven for the $ donation ;-)
 *
 * 2010-09-15 Jasper Pol (NL)
 *   Jasper has added an initial MySQL backend support
 *
 * 2010-09-13 Brenno Hiemstra (NL)
 *   Brenno reported bad extra spaces after the #!/usr/bin/php in the Linux version of multiotp.php
 *
 * 2010-08-20 C. Christophi, BirdNet (CH)
 *   Documentation enhancement proposal for the TekRADIUS part, thanks !
 *
 * 2010-07-19 SysCo/al (CH)
 *   Well, as requested by some users, the new "class" design is done, enjoy !
 *
 *
 * Change Log
 *
 *   2022-04-11 5.8.6.0 SysCo/al ENH: Telnyx SMS provider support
 *                               ENH: PHP 7.4 deprecated code cleaned
 *                               ENH: In CLI check, if username doesn't exist, it try automatically a shorter domain name step by step
 *   2022-01-14 5.8.5.1 SysCo/al ENH: Embedded Windows nginx edition updated to version 1.21.4
 *   2021-11-18 5.8.3.2 SysCo/al ENH: Enhanced multiOTP Credential Provider support
 *   2021-09-14 5.8.3.0 SysCo/al ENH: VM version 011 support
 *                                    (Debian Bullseye 11.0, PHP 7.4, FreeRADIUS 3.0.21, Nginx 1.18.0)
 *                               ENH: Removed multicast support on the network card
 *   2021-08-19 5.8.2.9 SysCo/al ENH: Added compatibility with new multiOTP Credential Provider (5.8.2 and further)
 *   2021-05-19 5.8.2.3 SysCo/al FIX: Dockerfile updated (php-bcmath added)
 *   2021-04-08 5.8.2.1 SysCo/al ENH: eDirectory LDAP server support (set the LDAP server type to 4)
 *   2021-02-12 5.8.1.0 SysCo/al FIX: Minor fixes
 *   2020-12-11 5.8.0.6 SysCo/al ENH: Some new commands added/updated, like sync-delete-retention-days
 *   2019-10-23 5.6.1.4 SysCo/al FIX: Separated configuration/statistics storage handling
 *   2019-10-22 5.6.1.3 SysCo/al ENH: Better PHP 7.3 support
 *                               ENH: Base32 encoder/decoder new implementation
 *                               ENH: During WriteConfigData, loop on the current values, and check with the old values
 *                               ENH: Enhanced internal tests
 *   2019-09-02 5.5.0.3 SysCo/al ENH: Give an info if time based token is probably out of sync (in a window 10 time bigger)
 *                                    (for example for hardware tokens not used for a long time)
 *   2019-03-29 5.4.1.8 SysCo/al ENH: Challenge-Response support
 *   2019-01-24 5.4.1.5 SysCo/al FIX: If any, clean specific NTP DHCP option at every reboot
 *   2019-01-07 5.4.1.1 SysCo/al ENH: Raspberry Pi 3B+ support
 *   2018-11-13 5.4.0.2 SysCo/al ENH: Import of PSKC definition files with binary decoding key file
 *                               ENH: added new sms providers (clickatell2, nexmo, nowsms, smseagle, swisscom, custom)
 *   2018-08-26 5.3.0.3 SysCo/al FIX: Restore configuration has been fixed in the command line edition
 *   2018-08-21 5.3.0.0 SysCo/al ENH: help text enhanced, without2fa option added
 *   2018-07-16 5.2.0.2 SysCo/al ENH: new commande line option ldap-users-dn
 *   2018-03-16 5.1.1.1 SysCo/al FIX: command line -set error for ldap-pwd and prefix-pin
 *   2018-02-26 5.1.0.6 SysCo/al ENH: Regular registry entries are now used directly from the Credential Provider.
 *   2018-02-19 5.1.0.3 SysCo/al ENH: Credential Provider multiOTPOptions registry entry is used if available
 *   2017-11-10 5.0.6.0 SysCo/al New -cp option (Credential Provider mode)
 *   2017-05-29 5.0.4.5 SysCo/al PostgreSQL support, based on source code provided by Frank van der Aa
 *   2017-02-21 5.0.3.6 SysCo/al Seed can now be given in Base32 format
 *   2017-02-03 5.0.3.5 SysCo/al -user-info fixed and replaced by a call to the GetUserInfo method
 *   2017-01-24 5.0.3.4 SysCo/al It's now possible to do several commands at once with the CLI edition
 *                               Some new commands added
 *                               Commands -user-info and -ldap-user-info enhanced
 *                               Commands -lock and -unlock return now 19 (instead of 99)
 *   2016-11-14 5.0.3.0 SysCo/al Better SSL support
 *                               Some new commands added
 *   2016-11-04 5.0.2.6 SysCo/al Better SSL support
 *                               Specific LDAP/AD attribute used as the synchronised account name can be defined
 *                               Implementing new library options
 *                               Additional error information sent back in the Reply-Message attribute
 *                                  (the debug prefix must be set to Reply-Message = )
 *                               Backup configuration file can now be restored in commercial version without any change
 *   2016-08-02 5.0.1.4 SysCo/al Command -network-info added
 *                               More debug information
 *   2015-07-18 4.3.2.6 SysCo/al Minor fixes
 *   2015-07-15 4.3.2.5 SysCo/al Calling multiotp CLI without parameter returns now error code 30 (instead of 19)
 *   2015-06-24 4.3.2.4 SysCo/al multiotp_account automatic support
 *   2015-06-10 4.3.2.3 SysCo/al Enhancements for the Dev(Talks): demo
 *   2015-06-09 4.3.2.2 SysCo/al Additional CLI features (fastcreatenopin, fastcreatewithpin)
 *                               Initialize-backend process enhanced
 *                               Resync during authentication (autoresync) is now better handled in the class directly
 *   2014-12-09 4.3.1.0 SysCo/al MULTIOTP_PATH environment variable support
 *                               CLI local proxy mode support added to speed up the command line
 *                               Scratch password need also the prefix PIN if it's activated
 *                               OTP with integrated serial numbers better supported (in PAP)
 *                               Generic LDAP support (no more only Microsoft AD compatible LDAP)
 *                               Raspberry Pi edition has the local proxy mode activated to speed up the process
 *   2014-11-04 4.3.0.0 SysCo/al Command -lockeduserslist added
 *                               Resynchronization is now done with ResyncToken() method instead of CheckToken()
 *                      SysCo/yj Changing examples : %message -> %msg; Added " around parameter sms-api-id in the example
 *   2014-06-12 4.2.4.3 SysCo/al Bug fix concerning aspsms provider
 *   2014-04-13 4.2.4.2 SysCo/al Minor fixes
 *   2014-04-06 4.2.4.1 SysCo/al Fixed bug concerning LDAP handling
 *                               NT_KEY support added (for FreeRADIUS further handling)
 *                               Tokens CSV import (serial_number;manufacturer;algorithm;seed;digits;interval_or_event)
 *                               When a user is deleted, the token(s) attributed to this user is/are unassigned
 *                               New option -user-info added
 *   2014-03-30 4.2.4   SysCo/al Fixed bug concerning MySQL handling and mysqli support added
 *                               Enhanced SetAttributesToEncrypt function
 *                               New implementation for some external classes
 *                               Generated QRcode are better
 *                               LOT of new QA tests, more than 60 different tests (including PHP class and command line versions)
 *                               Enhanced documentation
 *   2014-03-13 4.2.3   SysCo/al Updated examples
 *   2014-03-03 4.2.2   SysCo/al Cleaned some non-interpreted TekRADIUS variables (for old TeKRADIUS releases)
 *                               Some values can now go back to TekRADIUS
 *   2014-02-07 4.2.0   SysCo/al MS-CHAP and MS-CHAPv2 fully supported
 *   2014-01-21 4.1.2   SysCo/al Direct call of class methods using -call-method
 *   2014-01-20 4.1.1   SysCo/al Minor fixes
 *   2013-12-23 4.1.0   SysCo/al Some modifications in order to correctly handle the class methods
 *                               It is now possible to activate or deactivate a user
 *                               Encrypted pskc files are now supported
 *   2013-08-30 4.0.7   SysCo/al GetScriptFolder() was still buggy sometimes, thanks Frank for the feedback
 *                               File mode of the created QRcode file is also changed base on GetLinuxFileMode()
 *   2013-08-25 4.0.6   SysCo/al base32_encode() is now RFC compliant with uppercases
 *                               GetUserTokenQrCode() and GetTokenQrCode() where buggy
 *                               GetScriptFolder() use now __FILE__ if the full path is included
 *                               When doing a check in the CLI header, @... is automatically removed from the
 *                                username if the user doesn't exist, and the check is done on the clean name
 *                               Added a lot of tests to enhance release quality
 *   2013-08-21 4.0.5   SysCo/al Fixed the check of the cache lifetime
 *                               Added a temporary server blacklist during the same instances
 *                               Default server timeout is now set to 1 second
 *   2013-08-20 4.0.4   SysCo/al Added an optional group attribute for the user
 *                                (which will be send with the Radius Filter-Id option)
 *                               Added scratch passwords generation (if the token is lost)
 *                               Automatic database schema upgrade using method UpgradeSchemaIfNeeded()
 *                               Added client/server support with local cache
 *                               Added CHAP authentication support (PAP is of course still supported)
 *                               The encryption key is now a parameter of the class constructor
 *                               The method SetEncryptionKey('MyPersonalEncryptionKey') IS DEPRECATED
 *                               The method DefineMySqlConnection IS DEPRECATED
 *                               Full MySQL support, including tables creation (see example and SetSqlXXXX methods)
 *                               Added email, sms and seed_password to users attributes
 *                               Added sms support (aspsms, clickatell, intellisms, custom, exec)
 *                               Added prefix support for debug mode (in order to send Reply-Message := to Radius)
 *                               Added a lot of new methods to handle easier the users and the tokens
 *                               General speedup by using available native functions for hash_hmac and others
 *                               Default max_time_window has been lowered to 600 seconds (thanks Stefan for suggestion)
 *                               Integrated Google Authenticator support with integrated base 32 seed handling
 *                               Integrated QRcode generator library (from Y. Swetake)
 *                               General options in an external configuration file
 *                               Comments have been reformatted and enhanced for automatic documentation
 *                               Development process enhanced, source code reorganized, external contributions are
 *                                added automatically at the end of the library after an internal build release
 *   2011-10-25 3.9.2   SysCo/al Improved get_script_dir() for Linux/Windows compatibility
 *   2011-09-15 3.9.1   SysCo/al Some quick fixes concerning multiple users
 *   2011-09-13 3.9.0   SysCo/al Adding support for account with multiple users
 *   2011-07-06 3.2.0   SysCo/al Encryption hash handling with additional error message 33
 *                                (if the key has changed)
 *                               Adding more examples
 *                               Adding generic user with multiple account
 *                                (Real account name is combined: "user" and "account password")
 *                               Adding log options, now default doesn't log token value anymore
 *                               Debugging MySQL backend support for the token handling
 *                               Fixed automatic detection of \ or / for script path detection
 *   2010-12-19 3.1.1   SysCo/al Better MySQL backend support, including in CLI version
 *   2010-09-15 3.1.0   SysCo/al Removed bad extra spaces in the multiotp.php file for Linux
 *                               MySQL backend support
 *   2010-09-02 3.0.0   SysCo/al Adding tokens handling support, including importing XML tokens definition file
 *                                (http://tools.ietf.org/html/draft-hoyer-keyprov-pskc-algorithm-profiles-00)
 *                               Enhanced flat database file format (multiotp is still compatible with old formats)
 *                               Internal method SetDataReadFlag renamed to SetUserDataReadFlag
 *                               Internal method GetDataReadFlag renamed to GetUserDataReadFlag
 *   2010-08-21 2.0.4   SysCo/al Enhancement in order to use an alternate php "compiler" for Windows command line
 *                               Documentation enhancement
 *   2010-08-18 2.0.3   SysCo/al Minor notice fix, define timezone if not defined (for embedded command line)
 *                               If user doesn't exist, do not create the related flat file after a check
 *   2010-07-21 2.0.2   SysCo/al Fix to create correctly the folders "users" and "log" if needed
 *   2010-07-19 2.0.1   SysCo/al Adding more information in the help text
 *   2010-07-19 2.0.0   SysCo/al New design using a class and a cli header stub
 *   2010-06-15 1.1.5   SysCo/al Adding OATH/TOTP support
 *   2010-06-15 1.1.4   SysCo/al Project renamed to multiotp to avoid overlapping
 *   2010-06-08 1.1.3   SysCo/al Typo in script folder detection
 *   2010-06-08 1.1.2   SysCo/al Typo in variable name
 *   2010-06-08 1.1.1   SysCo/al Status bar during resynchronization
 *   2010-06-08 1.1.0   SysCo/al Fix in the example, distribution not compressed
 *   2010-06-07 1.0.0   SysCo/al Initial implementation
 *
 *********************************************************************/

global $argc;
global $argv;

if (!isset($multiotp)) {
	  /**

 * @file  multiotp.php
 * @brief Command line version (all-in-one) of the multiOTP PHP class.
 *
 * @mainpage
 *
 * multiOTP PHP command line (all-in-one) - 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, 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 5.3.0 or higher is supported.
 *
 * @author    Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
 * @version   5.8.8.4
 * @date      2022-05-08
 * @since     2010-06-08
 * @copyright (c) 2010-2022 SysCo systemes de communication sa
 * @copyright GNU Lesser General Public License
 *
 *//*
 *
 * LICENCE
 *
 *   Copyright (c) 2010-2022 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
 *
 *
 * Users valuable feedbacks and comments
 *
 * 2021-04-08 Derek Kenny (CA)
 *   Thanks for your valuable support in order to support eDirectory LDAP server
 *
 * 2018-08-25 Muzammel (PK)
 *   Thanks for your questions about the client/server process,
 *    which has been enhanced based on the exchange we had.
 *
 * 2018-07-31 Sergey, Kiev (UA)
 *   Thanks for your questions regarding -restore-config in the command line version.
 *   The restore function has been corrected
 *
 * 2018-02-13 Jonathan Garber (via GitHub)
 *   Thanks for your feedback about various issues.
 *
 * 2017-11-22 vak255 (via GitHub)
 *   Thanks for your feedback about a bad handled unicode issue.
 *   All strtoXXX and strpos have been changed to the the multibyte version.
 *
 * 2017-06-11 Richard Green
 *   Thanks for your proposal about specific LDAPTLS configuration values to be moved in the config parameters.
 *
 * 2017-04-19 Frank van der Aa, Vanboxtel BV (NL)
 *   Thanks a lot for your valuable implementation suggestion about PostgreSQL.
 *   The proposed code has been adapted and integrated in the project.
 *
 * 2017-02-14 Frank van der Aa, Vanboxtel BV (NL)
 *   Thanks for your proposal about GetList() method sorted output.
 *
 * 2017-02-09 Frank van der Aa, Vanboxtel BV (NL)
 *   Thanks for your debug about lockedlistarray[], the proposed
 *   GetDelayedUsersList() method and the delayed users display on the web GUI.
 *
 * 2017-02-02 Stefan Kügler, SerNet GmbH (DE)
 *   Thanks for your feedback on the last edition.
 *
 * 2017-01-24 Jean-François Perillo, Kudelski Security (CH)
 *   As proposed by Jean-François, requested LDAP password for synchronized users can be overwritten.
 *
 * 2017-01-05 Stefan Kügler, SerNet GmbH (DE)
 *   Thanks for your feedbacks on the last beta edition.
 *
 * 2017-01-04 Frank van der Aa, Vanboxtel (NL)
 *   Thanks for your feedback concerning leading zeros that can be omitted for the OTP or the PIN.
 *   This has been fixed for the next 5.0.3.4 release.
 *
 * 2016-12-07, 2016-12-01 Stefan Kügler, SerNet GmbH (DE)
 *   Thanks for your feedbacks on the last beta edition.
 *
 * 2016-12-02, Jim Bailey (USA)
 *   Thanks for your feedbacks with some features proposals.
 *
 * 2016-11-25 SKB Kontur (RU)
 *   Thanks for your appreciated $$$ donation.
 *
 * 2016-11-23 Serg Avtukhovich, SKB Kontur (RU)
 *   Serg had some issues with large Active Directory. He did the beta tests for several improvements.
 *
 * 2016-11-10 SerNet GmbH (DE)
 *   MANY thanks for your appreciated $$$ sponsorship for new implemented features proposed by Stefan Kügler.
 *
 * 2016-04-18 Serg Avtukhovich, SKB Kontur (RU)
 *   Serg had some strange problems when using multiOTP in client/server mode.
 *   After some trials, we fix the issue with the server when "log on display" is activated.
 *
 * 2015-12-20 Svetoslav Mateev, STS Soft (BG)
 *   Thanks for your appreciated $$$ donation.
 *
 * 2015-12-18 Sam Leach, Warwickshire County Council (UK)
 *   Sam informed us that a huge AD/LDAP organizational unit (100'000 users)
 *   crashed the sync process. This has been internally reproduced and corrected.
 *
 * 2015-08-10 Edward Kovarski (CA)
 *   Edward informed us that some special chars in the LDAP/AD group name
 *    was killing the SyncLdapUsers process. This has been corrected.
 *
 * 2015-07-14 Pierre-Nicolas Paradis, SherWeb (FR)
 *   Pierre-Nicolas informed us that it was still not possible to change
 *    the admin password using the web GUI. This has been corrected.
 *
 * 2015-06-23 Jun Li (CN)
 *   As proposed by Jun Li, launching the command line version without
 *    enough parameters returns now a 30 error code (instead of 19).
 *    Side effect is that -help is now required to display help page.
 *
 * 2015-06-02 Jean-François Perillo, Kudelski Security (CH)
 *   As proposed by Jean-François, token length error information has been
 *    added in the regular log and the autoresync is now enabled by default.
 *
 * 2015-06-02 Sébastien Charlier, Thesis SA (CH)
 * 2015-03-09 Martin
 *   Martin and Sébastien informed us that passwords containing the minus sign are not accepted.
 *
 * 2015-02-16 Sylvain Maret, Kudelski Security (CH)
 *   Sylvain informed us that Gemalto PSKC file don't provide the time interval for TOTP tokens.
 *   RFC default value (30 seconds) is now set by default if no time interval is given.
 *
 * 2015-01-27 Thomas Klute, ingenit GmbH & Co. KG (DE)
 *   Thanks Thomas for you feedback concerning a potential exploit with dots and slashes in a username.
 *   Even if no information can be extracted using this method, it's always good to patch this kind of weakness.
 *
 * 2015-01-08 Markus Arnoldi, LEWA Attendorn GmbH (DE)
 *   Useful comments about prefix PIN handling, documentation has been enhanced.
 *   Two new command line options are now available (fastcreatenopin and fastecreatewithpin)
 *
 * 2014-12-22 Sajid Hameed, Network Places Ltd (UK)
 *   Questions about users lockout, documentation has been enhanced.
 *   Three command line options information has been added in the documentation.
 *
 * 2014-12-15 Steve Jacot-Guillarmod, Swissdotnet SA (CH)
 *   Thanks Steve for your valuable feedback about LDAP sync and groups
 *   handling with a specific Synology OpenLDAP server implementation.
 *
 * 2014-11-04 Yubico Inc. (USA) / Yubico AB (S) / Yubico Ltd. (UK)
 *   BIG THANKS to the Yubico team which provides us several YubiKeys for the
 *   workshop organized during the Application Security Forum in Yverdon-les-Bains (Switzerland).
 *   Starting with version 4.3.0.0, YubiKeys (both Yubico OTP and HOTP) are now also supported and easy to import.
 *   (simply import the YubiKey traditional format log file)
 *
 * 2014-10-13 Adam Twardowski, Choopa LLC (USA)
 *   Thanks Adam for your valuable feedback concerning a bug with the NT_KEY generation if prefix PIN is enabled.
 *   Adam discovered the bug and fixed it when he configured pptpd with
 *   FreeRADIUS in order to set up a PPTP VPN with strong authentication.
 *
 * 2014-06-17 Stefan Kügler, SerNet GmbH (DE)
 *   Stefan proposes to add Active Directory msRADIUSFramedIPAddress attribute
 *   synchronization in order to distribute the Framed-IP-Address to a user.
 *
 * 2014-04-04 Stefan Kügler, SerNet GmbH (DE)
 * 2014-04-01 Daniel Särnström, Donator AB (SE)
 *   Daniel & Stefan asks some info in order to import tokens without a know format.
 *   Good question, multiOTP supports now importation of tokens from CSV file.
 *
 * 2014-04-02 Prashant Kumar, Alscient (UK)
 *   Prash is playing with FreeRADIUS and VPN (PPTP with MPPE). This requires radius to send MPPE keys.
 *   Interesting feedback, multiOTP provides now NT_KEY, like the ntlm_auth external helper.
 *
 * 2014-03-31 Alex Tasikas (GR)
 *   Thanks Alex for your valuable feedback concerning some bugs in LDAP support.
 *
 * 2014-03-25 Prashant Kumar, Alscient (UK)
 *   As proposed by Prash, we have added the possibility to modify the list of attributes to encrypt.
 *
 * 2014-03-17 Arthur de Jong, West Consulting (NL)
 *   Arthur gave some feedbacks concerning distributing the source code in the
 *    "preferred form of the work for making modifications".
 *
 * 2014-03-14 Soeren Malchow, MCON (DE)
 *   Thanks for your feedback concerning a bug in the SQL request for the log table.
 *
 * 2014-01-27 Henk van der Helm (NL)
 *   MANY thanks for your appreciated $$$ donation.
 *
 * 2014-01-19 Erik Nylund (FI)
 *   Thanks four your feedback concerning specific parameters order in QRCode for Microsoft Authenticator
 *
 * 2014-01-14 Sylvain Maret, Kudelski Security (CH)
 *   Thanks for your feedback concerning possible zero division in the ComputeOathTruncate method.
 *    Method has been altered in order to be more compatible with almost any PHP version.
 *   Thanks also for the suggestion to resync without the prefix PIN. Both are supported now.
 *
 * 2014-01-08/09  Cheng Shao-Pin (CN)
 *   Thanks for your feedback concerning possible missing JSON extension in old PHP distribution
 *    and possible image functions incompatibilities with some PHP versions during QRcode generation.
 *   Thanks also for your appreciated $ donation.
 *
 * 2014-01-08  Cheng Shao-Pin (CN) and Daniel Särnström, Donator AB (SE)
 *   Thanks for your feedback concerning md5.js missing in the distribution.
 *
 * 2013-12-20 Rico Zeiss, Hermann Wegener GmbH & Co. KG (DE)
 *   MANY thanks for your appreciated $$$ sponsorship to support us to add MS-CHAP and MS-CHAPv2 in a next release.
 *
 * 2013-12-18 Xavier Céspedes (ES)
 *   Thanks to Xavier who noticed a problem with the hex2bin() function duringthe scratch password generation.
 *   In the meantime, the GetUserScratchPasswordsList() function has been improved and fixed and is in the 4.1 release.
 *
 * 2013-09-20 Sean Butler-Lee (IE)
 *   Thanks a lot for announcing a bug with the GetUserScratchPasswordsArray() method.
 *
 * 2013-08-22,26 Frank Bongrand (FR)
 *   Thanks a lot for valuable feedbacks concerning some minor bugs in 4.0.4 and 4.0.6
 *
 * 2013-08-21 Henk van der Helm (NL)
 *   Thanks a lot for a valuable feedback concerning some minor bugs in 4.0.4
 *
 * 2013-08-15 Donator AB (SE)
 *   MANY thanks for your appreciated $$$ sponsorship to support us to add self-registration in a next release.
 *
 * 2013-08-13 Daniel Särnström, Donator AB (SE)
 *   Daniel proposed to add self-registration and pskc v12 with encrypted data support (OATH compliant).
 * 
 * 2013-07-25 Dominik Pretzsch from Last Squirrel IT (DE)
 *   After some discussions with Dominik, integration of the client/server support in the basic library
 *
 * 2013-07-23 Stefan Kügler (DE) (again ;-)
 *   Stefan proposed to add the possibility to show the log, which is especially convenient for MySQL log.
 *   He proposed also to be able to call an external program to send SMS.
 *
 * 2013-07-11 Stefan Kügler (DE)
 *   Stefan proposed to add a lock and unlock option for the user.
 *
 * 2013-06-19 SerNet GmbH (DE)
 *   MANY thanks for your appreciated $$$ sponsorship after we implemented some features proposed by Stefan Kügler.
 *
 * 2013-06-13 Henk van der Helm (NL) (again ;-)
 *   Henk proposed to be able to have a specific description for the software token.
 *   (we use the already existing user description attribute)
 *
 * 2013-06-01 Stefan Irion (CH)
 *   Thanks for your appreciated $$ donation.
 *
 * 2013-05-14 Henk van der Helm (NL)
 *   Henk asked to support also the provider IntelliSMS. Thanks for the $$ sponsorship!
 *
 * 2013-05-03 Stefan Kügler (DE)
 *   Stefan proposed to lower the default max_time_window to 600 seconds.
 *
 * 2013-03-04 Alan DeKok (CA)
 *   Alan proposed in the freeradius mailing-list to put a prefix to be able to handle the
 *   debug info by the freeradius server.
 *
 * 2012-11-28  Gareth Thomas
 *   Thanks for your appreciated $$ donation.
 *
 * 2012-03-16 Nicolas Goralski (LU)
 *   Nicolas proposed an enhancement in order to support PAM. Thanks also for the $$ sponsorship!
 *     (with the -checkpam option in the command line edition)
 *
 * 2011-05-19 Fabiano Domeniconi (CH)
 *   Fabiano found old info in the samples, CheckToken() is not boolean anymore! Samples fixed.
 *
 * 2011-04-24 Steven Roddis (AU)
 *   Steven asked for more examples. Thanks to Steven for the $ donation ;-)
 *
 * 2010-09-15 Jasper Pol (NL)
 *   Jasper has added an initial MySQL backend support
 *
 * 2010-09-13 Brenno Hiemstra (NL)
 *   Brenno reported bad extra spaces after the #!/usr/bin/php in the Linux version of multiotp.php
 *
 * 2010-08-20 C. Christophi, BirdNet (CH)
 *   Documentation enhancement proposal for the TekRADIUS part, thanks !
 *
 * 2010-07-19 SysCo/al (CH)
 *   Well, as requested by some users, the new "class" design is done, enjoy !
 *
 *
 * Change Log
 *
 *   2022-04-28 5.8.7.0 SysCo/al ENH: PHP 7.4 deprecated code cleaned
 *                               ENH: Embedded Windows nginx edition updated to version 1.21.6
 *                               ENH: Embedded Windows PHP edition updated to version 7.4.29
 *                               ENH: New MariaDB/MySQL indexes handling during schema creation and schema updates
 *                               ENH: Enhanced internal tests
 *   2022-04-14 5.8.6.1 SysCo/al FIX: Token "Without2FA" where not working all time with LDAP users
 *                               ENH: Telnyx SMS provider support
 *                               ENH: PHP 7.4 deprecated code cleaned
 *                               ENH: Email token is now supported for Credential Provider
 *                               ENH: In CLI check, if username doesn't exist, it try automatically a shorter domain name step by step
 *   2022-01-14 5.8.5.1 SysCo/al ENH: Embedded Windows nginx edition updated to version 1.21.4
 *   2021-11-18 5.8.3.2 SysCo/al ENH: Enhanced multiOTP Credential Provider support
 *   2021-09-14 5.8.3.0 SysCo/al ENH: VM version 011 support
 *                                    (Debian Bullseye 11.0, PHP 7.4, FreeRADIUS 3.0.21, Nginx 1.18.0)
 *                               ENH: Removed multicast support on the network card
 *   2021-08-19 5.8.2.9 SysCo/al ENH: Added compatibility with new multiOTP Credential Provider (5.8.2 and further)
 *   2021-06-04 5.8.2.4 SysCo/al ENH: Detect Credential Provider Request and force the no prefix option
 *   2021-04-08 5.8.2.1 SysCo/al ENH: eDirectory LDAP server support (set the LDAP server type value to 4)
 *   2021-03-25 5.8.1.9 SysCo/al FIX: Cookie privacy (httponly and secure) backported to previous virtual appliances
 *                               ENH: Cookie privacy (httponly and secure) are now handled in the application directly
 *                               ENH: Weak SSL ciphers disabled
 *                               ENH: Better Docker support
 *                               ENH: Better log handling
 *   2021-03-21 5.8.1.2 SysCo/al ENH: Test (1 == GetUserPrefixPin()) replaced by IsUserPrefixPin()
 *   2021-03-14 5.8.1.1 SysCo/al FIX: In some cases, the HOTP/TOTP was not well computed
 *   2021-02-12 5.8.1.0 SysCo/al ENH: Enhanced Web GUI accounts list (green=AD/LDAP synced, orange = delayed, red=locked)
 *   2020-12-11 5.8.0.7 SysCo/al ENH: -sync-delete-retention-days= option is set by default to 30 days
 *   2020-12-11 5.8.0.6 SysCo/al ENH: VM version 010 support  (Debian Buster 10.5, PHP 7.3, FreeRADIUS 3.0.17)
 *                               ENH: MySQL optimization
 *                               ENH: Enhanced windows command line scripts (automatic administrator level)
 *   2020-09-20 5.8.0.2 SysCo/al ENH: New -sync-delete-retention-days= option in order to purge inexistent AD/LDAP users
 *                                    (SetSyncDeleteRetentionDays and GetSyncDeleteRetentionDays method)
 *   2020-08-31 5.8.0.0 SysCo/al ENH: Raspberry Pi 4B support
 *                               ENH: New unified distribution
 *                               ENH: Debian Buster 10.5 support
 *                               ENH: Enhanced PHP 7.3 support
 *                               ENH: Better mysqli support for alternate connection port
 *                               FIX: Too many ReadConfigData loop during initialization
 *                               FIX: Better unicode handling, multibyte fonctions also for mb_substr()
 *                               FIX: A device file was searched with the name of the FreeRADIUS Client-Shortname
 *   2019-10-23 5.6.1.4 SysCo/al FIX: Separated configuration/statistics storage handling
 *   2019-10-22 5.6.1.3 SysCo/al ENH: Better PHP 7.3 support
 *                               ENH: Base32 encoder/decoder new implementation
 *                               ENH: During WriteConfigData, loop on the current values, and check with the old values
 *                               ENH: Enhanced internal tests
 *   2019-09-02 5.5.0.3 SysCo/al ENH: Give an info if time based token is probably out of sync (in a window 10 time bigger)
 *                                    (for example for hardware tokens not used for a long time)
 *   2019-03-29 5.4.1.8 SysCo/al ENH: Enhanced error messages, more log information
 *                               ENH: In debug mode, display an error if logfile cannot be written
 *                               ENH: Global Access-Challenge support
 *   2019-01-30 5.4.1.7 SysCo/al FIX: IsTemporaryBadServer function (thanks to brownowski on GitHub)
 *                               ENH: New QRcode library used (without external files dependency)
 *   2019-01-25 5.4.1.6 SysCo/al FIX: If any, clean specific NTP DHCP option at every reboot
 *   2019-01-18 5.4.1.4 SysCo/al ENH: Modifications for Debian 9.x (stretch) binary images support
 *   2019-01-07 5.4.1.1 SysCo/al ENH: Raspberry Pi 3B+ support
 *   2018-11-13 5.4.0.2 SysCo/al ENH: Enigma Virtual Box updated to version 9.10 (to create the special all-in-one-file)
 *                      SysCo/al ENH: PHP 7.1.22 used in the one single file (only PHP < 7.2 is still compatible with Windows 7/2008)
 *                      SysCo/al ENH: Compatibility mode to Windows 7 automatically added for radiusd.exe during radius service installation
 *                      SysCo/al ENH: PHP display error flag is now set to off by default in the webservice under Windows
 *                      SysCo/al ENH: Import of PSKC definition files with binary decoding key file
 *                      SysCo/al ENH: Added Swisscom LA REST, Afilnet, Clickatell2, eCall, Nexmo,
 *                                    NowSMS, SMSEagle and custom SMS provider support
 *   2018-09-14 5.4.0.1 SysCo/al FIX: Values of SetUserCacheLevel(), GetUserCacheLevel(), SetUserCacheLifetime()
 *                                    and GetUserCacheLifetime() are not correctly initialized
 *                      SysCo/al ENH: Enigma Virtual Box updated to version 9.10 (to create the special all-in-one-file)
 *                      SysCo/al ENH: PHP 7.1.22 used in the one single file (only PHP < 7.2 is still compatible with Windows 7/2008)
 *                      SysCo/al ENH: Compatibility mode to Windows 7 automatically added for radiusd.exe during radius service installation
 *                      SysCo/al ENH: PHP display error flag is now set to off by default in the webservice under Windows
 *   2018-08-26 5.3.0.3 SysCo/al FIX: Better without2FA algorithm support
 *                               FIX: Restore configuration has been fixed in the command line edition
 *                               ENH: Cache-level and cache-lifetime can be set separately for each user
 *                               ENH: In client/server mode, only unencrypted user attributes are sent back to a successful client request
 *                               ENH: Enhanced monitoring
 *   2018-08-22 5.3.0.1 SysCo/al ENH: Monitoring fields added (create_host, create_time, last_update_host)
 *   2018-08-21 5.3.0.0 SysCo/al FIX: stream_timeout is no more pushed to 20 seconds in PostHttpDataXmlRequest if we are in Credential Provider mode
 *                               FIX: RemoveTokenFromUser() method corrected. Token administrative information corrected,
 *                                    new software token created for the user
 *                               ENH: Multiple semicolon separated "Users DN" supported for AD/LDAP synchronization
 *                               ENH: Additional debug messages for disabled users during synchronization
 *                               ENH: Enigma Virtual Box updated to version 9.00 (to create the special all-in-one-file)
 *                               ENH: PHP 7.2.8 used in the one single file
 *                               ENH: without2FA algorithm now available (useful to do 2FA only for some accounts and not for others)
 *   2018-07-16 5.2.0.2 SysCo/al ENH: Active Directory nested groups support
 *                                    (user1 in groupA, groupA in groupB, setting the OTP groups to "groupB" will add user1)
 *                               ENH: Enhanced AD/LDAP support for huge Microsoft Active Directory
 *                               ENH: "Base DN" and "Users DN" are now two different parameters ("Users DN" optional)
 *   2018-03-20 5.1.1.2 SysCo/al FIX: typo in the source code of the command line option for ldap-pwd and prefix-pin
 *                               ENH: Dockerfile available
 *   2018-03-05 5.1.0.8 SysCo/al FIX: Enigma Virtual Box updated to version 8.10 (to create the special all-in-one-file)
 *   2018-02-27 5.1.0.7 SysCo/al FIX: [Receive an OTP by SMS] link is now fixed for Windows 10
 *   2018-02-26 5.1.0.6 SysCo/al ENH: Credential Provider registry entries are now always used when calling multiOTP.exe
 *   2018-02-21 5.1.0.5 SysCo/al FIX: To avoid virus false positive alert, multiOTP.exe is NO more packaged in one single file
 *                                    using Enigma, a php folder is now included in the multiOTP folder
 *                               FIX: multiOTPOptions registry entry is now useless
 *   2018-02-21 5.1.0.4 SysCo/al ENH: Credential Provider registry entries are used if available
 *   2018-02-19 5.1.0.3 SysCo/al FIX: Better unicode handling, multibyte fonctions used when needed (mb_strtolower(), ...)
 *                               ENH: Expired AD/LDAP password support
 *                               ENH: multiOTP Credential Provider (for Windows) improvements
 *                                     (user@domain.name UPN support, default domain name supported and displayed, SMS request link)
 *                               ENH: "force_no_prefix_pin" option for devices (for example if the device is a
 *                                     computer with multiOTP credential Provider and AD/LDAP synced password)
 *   2017-11-04 5.0.5.6 SysCo/al Better FreeRADIUS 3.x documentation
 *                               New radius tag prefix configuration option
 *                               New multiple groups device option
 *                               Some notice corrections (if the array element doesn't exist)
 *                               A user cannot be created with a leading backslash (fixed in FastCreateUser and CreateUserFromToken)
 *   2017-09-29 5.0.5.2 SysCo/al The proposed mOTP generator for Android/iOS is now OTP Authenticator
 *                               New xml QRCode provisioning format for mOTP (compatible with OTP Authenticator)
 *   2017-09-08 5.0.5.0 SysCo/al NirSoft nircmd.exe tool removed from the distribution (false virus detection)
 *                               Multiple URLs separator for client/server config is still ";", but [space] and "," are accepted
 *                               New developer mode for some specific detailed logs during development process only
 *   2017-07-07 5.0.4.9 SysCo/al New methods: SetLdapTlsReqcert, GetLdapTlsReqcert, SetLdapTlsCipherSuite, GetLdapTlsCipherSuite
 *                                to change config parameters, instead of hard coded parameters (for SSL/TLS LDAP connection)
 *                               Fixed too much detailed information in the log when trying
 *                                to detect a token serial number for self-registration
 *   2017-06-06 5.0.4.8 SysCo/al Fixed SSL/TLS LDAP failed connection for PHP 7.x (GnuTLS TLS1.2 restriction removed for PHP 7.x)
 *   2017-06-02 5.0.4.6 SysCo/al Fixed a typo in the ReadCacheData method for PostgreSQL support
 *                               Important, under Linux, the config, devices, groups, tokens and users folders are now always
 *                                located in /etc/multiotp/. Please be sure to make the move when you are upgrading
 *                               Cleaned some ugly PHP warnings when the backend is not initialized
 *   2017-05-29 5.0.4.5 SysCo/al Restore configuration added in Web GUI
 *                               Fixed configuration file directory under Windows in Web GUI
 *                               Fixed path with spaces handling for the command line edition (thanks Scott for the feedback)
 *                               PostgreSQL support, based on source code provided by Frank van der Aa
 *                               Fixed file_get_contents issue with offset parameter in PHP 7.x
 *   2017-05-16 5.0.4.4 SysCo/al GetList() is now sorted with files backend
 *                               A replay during a defined delay (default 60 seconds) of the previous refused password is rejected,
 *                                but the error counter is not incremented (SetLastFailedWhiteDelay and GetLastFailedWhiteDelay)
 *                               A user cannot be created with a leading backslash
 *   2017-02-23 5.0.3.7 SysCo/al Group names are now always trimed to avoid blank spaces
 *                               SetLinuxFolderMode() and GetLinuxFolderMode() methods added
 *   2017-02-21 5.0.3.6 SysCo/al GetDelayedUsersList() method added
 *                               GetList() is now sorted with MySQL backend
 *                               RestoreConfiguration() method updated, system configuration data can be ignored
 *                               SetUserTokenSeed() and SetTokenSeed() methods accept now also base32 and raw binary
 *                               The full windows package has been fixed and cleaned
 *   2017-02-03 5.0.3.5 SysCo/al GetUserInfo() method added
 *                               ImportTokensFromCsv fixed when the file is not readable
 *                               Fix possible endless loop when opening a file that exists but without the right to read it
 *   2017-01-26 5.0.3.4 SysCo/al It's now possible to do several commands at once with the CLI edition
 *                               New overwrite_request_ldap_pwd option (enabled by default).
 *                                If overwrite is enabled, default_request_ldap_pwd value is forced during synchronization
 *                               Multiple groups per user is now supported (not all devices support multiple groups).
 *                                (radius reply attributor has been changed to += by default)
 *                               multiotp -delete-token command has been added in the CLI
 *                               -lock and -unlock command return now 19 (instead of 99) in the CLI
 *                               Better support of DialinIp functions in command line usage
 *                               New LDAP cache management to support huge AD/LDAP, with cache on disk (system temporary folder)
 *                               New PurgeLockFolder() and PurgeLdapCacheFolder() method
 *                               The default proposed TOTP/HOTP generator for Android/iOS is now FreeOTP Authenticator
 *                               Better Eastern European languages support
 *                               Multiple purpose tokens provisioning format PSKCV10,
 *                                like Gemalto e3050cL and t1050 tokens, is now supported.
 *                               Various bug fixes and enhancements when using the proxy mode.
 *   2016-11-14 5.0.3.0 SysCo/al Log messages are better categorized
 *                               The user dialin IP address is synchronized from the Active Directory msRADIUSFramedIPAddress attribute
 *                               New IP dialin methods : SetUserDialinIpAddress(), SetUserDialinIpMask(), SetDefaultDialinIpMask(),
 *                                GetUserDialinIpAddress(), GetUserDialinIpMask(), GetDefaultDialinIpMask()
 *                               If the user dialin IP address is defined, Framed-IP-Address
 *                                and Framed-IP-Mask are delivered in the RADIUS answer
 *                               Enhanced token importation process (to support binary encryption key in hexadecimal 0xAABBCC format)
 *   2016-11-04 5.0.2.6 SysCo/al Better log message for automatically or manually created objects
 *                               External packages update
 *                               New GetUserLastLogin() and SetUserLastLogin() methods
 *                               Backup configuration file can now be restored in commercial version without any changes
 *   2016-10-16 5.0.2.5 SysCo/al Better SSL support using context if available (for PHP >= 5.3)
 *                               New methods SetTouchFolder(), GetTouchFolder(), TouchFolder(), FolderTouched() to offer asynchronous capabilities
 *                               New methods added for SOAP service
 *                               Weekly anonymized stats added (can be disabled). Anonymized stats include the following information:
 *                                backend type, AD/LDAP used or not, OS version, PHP version, library version, number of accounts defined,
 *                                number of tokens defined. They are sent on the stats.multiotp.net FQDN which is hosted in Switzerland.
 *                               It's now possible to select a specific LDAP/AD attribute used as the synchronised account name
 *                                SetLdapSyncedUserAttribute(), GetLdapSyncedUserAttribute()
 *                               An account can be tested from the dashboard
 *                               Unified configuration backup and restore format (BackupConfiguration)
 *                               Better support of MS-CHAPv2 in the provided appliances
 *                               Cached requests supported (cached during a specific amount of time, useful for WebDAV authentication)
 *                                (device option cache_result_enabled)
 *                               A try on the previous accepted password is rejected, but the error counter is not incremented
 *                               ForceNoDisplayLog() method added, in order to be able to disable log on display in server mode
 *                               XML parsing error are more verbose
 *                               XmlServer is now sending XML response with the specific Content-type: text/xml
 *                               YubicoOTP private id check is now implemented
 *                               SSL AD/LDAP also supported with Windows 2012 server
 *                               SyncLdapUsers is now using a semaphore file in order to avoid concurrent process for large AD/LDAP sync
 *                                (tested with 1'000 groups, 100'000 users, 1'000 users in the LDAP sync group)
 *                               AD/LDAP additional log information
 *                               New GetNetworkInfo and SetNetworkInfo methods
 *                               Special chars support enhanced in LDAP class (as described in RFC4515)
 *                               The default ldap_group_cn_identifier is now cn instead of sAMAccountName
 *                               The first matching group defined in AD/LDAP group(s) filtering is now defined for the user
 *                                (this group is returned as the Filter-Id (11) option in a successful RADIUS answer)
 *                               Enhanced SMS support for Clickatell, SSL is now also working
 *                               Bug fix concerning QRcode generation for mOTP
 *                               Code fixes
 *                               New AssignTokenToUser() and RemoveTokenFromUser() methods
 *   2015-07-18 4.3.2.6 SysCo/al New ResetTempUserArray method (as we want to move away from global array in the near future)
 *                               For _user_data, default values are now extracted from the definition array
 *                               QRcode generation for mOTP (motp://[SITENAME]:[USERNAME]?secret=[SECRET-KEY])
 *   2015-07-15 4.3.2.5 SysCo/al Calling multiotp CLI without parameter returns now error code 30 (instead of 19)
 *   2015-06-24 4.3.2.4 SysCo/al multi_account automatic support
 *                               Scratch password generation (UTF)
 *   2015-06-10 4.3.2.3 SysCo/al Enhancements for the Dev(Talks): demo
 *   2015-06-09 4.3.2.2 SysCo/al Empty users are refused
 *                               TOTP time interval of imported tokens is set by default to 30s
 *                               More accuracy in the logged information
 *                               Refactoring backend methods, sharing code
 *                               Refactoring some ugly parts (!)
 *                               Documentation update concerning lockout functions and prefix PIN prefix
 *                               Special token entry 'Sms' is now also accepted, like 'SMS' or 'sms', to send an SMS token
 *                               The minus (-) in the prefix password is now supported (it was filtered to fix some rare user issues)
 *                               The autoresync option is now enabled by default
 *                               Resync during authentication (autoresync) is now better handled in the class directly
 *                               The server_cache_level is now set to 1 by default (instead of 0)
 *                               If the token length is not correct, it's now written in the log
 *                               Some LDAP messages are now only logged in debug mode
 *   2014-12-15 4.3.1.1 SysCo/al Better generic LDAP support
 *                                 - description sync done in the following order: description, gecos, displayName
 *                                 - memberOf is not always implemented, alternative method to sync users based on group names.
 *                                 - disabled account synchronization using shadowExpire or sambaAcctFlags
 *                               Better Active Directory support
 *                                 - accountExpires is now supported for synchronization
 *                                 - ms-DS-User-Account-Control-Computed (to handle locked out accounts, available since Windows 2003)
 *   2014-12-09 4.3.1.0 SysCo/al MULTIOTP_PATH environment variable support
 *                               CLI proxy added to speed up the command line
 *                               Scratch password need also the prefix PIN if it's activated
 *                               OTP with integrated serial numbers better supported (in PAP)
 *                               Generic LDAP support (instead of Microsoft AD support only)
 *                               Raspberry Pi edition has now a special proxy to speed up the command line
 *   2014-11-04 4.3.0.0 SysCo/al It's now possible to use the AD/LDAP password instead of the PIN code
 *                               Yubico OTP support, including keys import using the log file in Traditional format
 *                               qrcode() stub enhanced to check if the required folders are available
 *                               SyncLdapUsers completely redesigned
 *                                 - no more complete array in memory
 *                                 - MultiotpAdLdap class also enhanced accordingly
 *                                   - cached group_cn requests
 *                                   - cached recursive_groups requests
 *                                   - new "by element" functions
 *                               Demo mode support
 *                               Bug fix concerning the NT_KEY generation with enabled prefix PIN (thanks Adam)
 *                               ResyncToken() method added (instead of using CheckToken() method for synchronization)
 *   2014-06-12 4.2.4.3 SysCo/al Bug fix concerning aspsms provider
 *   2014-04-13 4.2.4.2 SysCo/al XML parsing consolidation, one library for the whole project
 *                               Fixed bug concerning tokens CSV import
 *   2014-04-06 4.2.4.1 SysCo/al Fixed bug concerning LDAP handling
 *                               NT_KEY support added (for FreeRADIUS further handling)
 *                               Tokens CSV import (serial_number;manufacturer;algorithm;seed;digits;interval_or_event)
 *                               When a user is deleted, the token(s) attributed to this user is/are unassigned
 *                               New option -user-info added
 *   2014-03-30 4.2.4   SysCo/al Fixed bug concerning MySQL handling and mysqli support added
 *                               Enhanced SetAttributesToEncrypt function
 *                               New implementation fo some external classes
 *                               Generated QRcode are better
 *                               LOT of new QA tests, more than 60 different tests (including PHP class and command line versions)
 *                               Enhanced documentation
 *   2014-03-13 4.2.3   SysCo/al Fixed bug for clear text password going back to TekRADIUS (PIN was always prefixed for mOTP)
 *                               Fixed bug when client/server mode is activated, but not working well
 *   2014-03-03 4.2.2   SysCo/al Better AD/LDAP integration
 *                               Web GUI is now complete for a simple usage, including hardware tokens import
 *                               Better template for provisioning information
 *                               Some values can now go back to TekRADIUS
 *                               If activated, prefix PIN is now also requested for SMS authentication
 *                               More information in the logs
 *                               Better list of the external packages used
 *   2014-02-14 4.2.1   SysCo/al AD/LDAP is now fully supported in order to create users based on AD/LDAP content
 *                                (with groups filtering)
 *   2014-02-07 4.2.0   SysCo/al MS-CHAP and MS-CHAPv2 are now supported
 *                                (md4 implementation added for PHP backward compatibility)
 *                               Enhanced LDAP configuration structure
 *                               Fixed bug during token attribution to users
 *                                (a "no name" token appeared sometimes)
 *   2014-01-20 4.1.1   SysCo/al md5.js was missing in the public distribution
 *                               Alternate json_encode function is defined if the JSON extension is not loaded
 *                               Fixed possible image functions incompatibilities with some PHP versions
 *                                during QRcode generation
 *                               As suggested by Sylvain, token resync doesn't need prefix PIN anymore
 *                                (but still accepted)
 *                               More verbosity in the logs in debug mode
 *                               Specific parameters order in QRCode for Microsoft Authenticator support
 *                                (thanks to Erik Nylund)
 *   2013-12-23 4.1.0   SysCo/al The open source edition of multiOTP is OATH certified ;-)
 *                                (that means full compatibility with any OATH tokens and encrypted PSKC import support)
 *                               Raspberry Pi nanocomputer is now fully supported
 *                               Basic web interface
 *                               Self-registration of hardware tokens is now possible
 *                                PAP mode: if self-registration is enabled, a user can register a non-attributed token by typing
 *                                [serial number][OTP] instead of [OTP]. If user has a prefix PIN, type [serial number][PIN][OTP])
 *                                PAP/CHAP mode: if self-registration is enabled, a user can register a non-attributed token by typing
 *                                [username:serialnumber] as the username and the [OTP] in the password field.
 *                                If user has a prefix PIN, [PIN][OTP] must be typed in the password field
 *                               Automatic resync/unlock option during authentication (PAP only). When the autoresync option
 *                                is enabled, any user can resync his token by typing [OTP1] [OTP2] in the password field. 
 *                                If user has a prefix PIN, he must type [PIN][OTP1] [PIN][OTP2].
 *                               Tokens with less than 3 characters are not accepted anymore in CheckToken()
 *                               Default Linux file mode is now set by default (0666 for created and changed files)
 *                               Error 28 is returned if the file is not writable, even after a successful login
 *                               Added GetUsersCount() function
 *                               Added GenerateSmsToken() function
 *                               Added Groups management functions
 *                               Added Tokens assignation functions
 *                               Added SetUserActivated(1|0) and GetUserActivated() function
 *                               Added SetUserSynchronized(1|0) and GetUserSynchronized() function
 *                               scratch_passwords is now a text field in the database
 *                               The third parameter of the Decrypt method is now mandatory
 *                               Some modifications in order to correctly handle the class methods
 *   2013-09-22 4.0.9   SysCo/al Fixed a bug in GetUserScratchPasswordsArray. If a user had no scratch password
 *                                and the implementation accepted blank password, it was accepted
 *                               Fixed a bug where scratch passwords generation used odd numbers of characters for hex2bin()
 *   2013-08-30 4.0.7   SysCo/al GetScriptFolder() was still buggy sometimes, thanks Frank for the feedback
 *                               File mode of the created QRcode file is also changed based on GetLinuxFileMode()
 *                               'sms' as the password to request an SMS token can now be sent in lower or uppercase
 *                               Added a description attribute for the tokens
 *   2013-08-25 4.0.6   SysCo/al base32_encode() is now RFC compliant with uppercases
 *                               GetUserTokenQrCode() and GetTokenQrCode() where buggy
 *                               GetScriptFolder() use now __FILE__ if the full path is included
 *                               When doing a check in the CLI header, @... is automatically removed from the
 *                                username if the user doesn't exist, and the check is done on the clean name
 *                               Added a lot of tests to enhance release quality
 *   2013-08-21 4.0.5   SysCo/al Fixed the check of the cache lifetime
 *                               Added a temporary server blacklist during the same instances
 *                               Default server timeout is now set to 1 second
 *   2013-08-20 4.0.4   SysCo/al Added an optional group attribute for the user
 *                                (which will be send with the Radius Filter-Id option)
 *                               Added scratch passwords generation (if the token is lost)
 *                               Automatic database schema upgrade using method UpgradeSchemaIfNeeded()
 *                               Added client/server support with local cache
 *                               Added CHAP authentication support (PAP is of course still supported)
 *                               The encryption key is now a parameter of the class constructor
 *                               The method SetEncryptionKey('MyPersonalEncryptionKey') is DEPRECATED
 *                               The method DefineMySqlConnection is DEPRECATED
 *                               Full MySQL support, including tables creation (see example and SetSqlXXXX methods)
 *                               Added email, sms and seed_password to users attributes
 *                               Added sms support (aspsms, clickatell, intellisms, exec)
 *                               Added prefix support for debug mode (in order to send Reply-Message := to Radius)
 *                               Added a lot of new methods to handle easier the users and the tokens
 *                               General speedup by using available native functions for hash_hmac and others
 *                               Default max_time_window has been lowered to 600 seconds (thanks Stefan for suggestion)
 *                               Integrated Google Authenticator support with integrated base 32 seed handling
 *                               Integrated QRcode generator library (from Y. Swetake)
 *                               General options in an external configuration file
 *                               Comments have been reformatted and enhanced for automatic documentation
 *                               Development process enhanced, source code reorganized, external contributions are
 *                                added automatically at the end of the library after an internal build release
 *   2011-10-25 3.9.2   SysCo/al Some quick fixes after intensive check
 *                               Improved get_script_dir() in CLI for Linux/Windows compatibility
 *   2011-09-15 3.9.1   SysCo/al Some quick fixes concerning multiple users
 *   2011-09-13 3.9.0   SysCo/al Added support for account with multiple users
 *   2011-07-06 3.2.0   SysCo/al Encryption hash handling with additional error message 33
 *                                (if the key has changed)
 *                               Added more examples
 *                               Added generic user with multiple account
 *                                (Real account name is combined: "user" and "account password")
 *                               Added log options, now default doesn't log token value anymore
 *                               Debugging MySQL backend support for the token handling
 *                               Fixed automatic detection of \ or / for script path detection
 *   2010-12-19 3.1.1   SysCo/al Better MySQL backend support, including in CLI version
 *   2010-09-15 3.1.0   SysCo/al Removed bad extra spaces in the multiotp.php file for Linux
 *                               MySQL backend support
 *   2010-09-02 3.0.0   SysCo/al Added tokens handling support
 *                                including importing XML tokens definition file
 *                                (http://tools.ietf.org/html/draft-hoyer-keyprov-pskc-algorithm-profiles-00)
 *                               Enhanced flat database file format (multiotp is still compatible with old versions)
 *                               Internal method SetDataReadFlag renamed to SetUserDataReadFlag
 *                               Internal method GetDataReadFlag renamed to GetUserDataReadFlag
 *   2010-08-21 2.0.4   SysCo/al Enhancement in order to use an alternate php "compiler" for Windows command line
 *                               Documentation enhancement
 *   2010-08-18 2.0.3   SysCo/al Minor notice fix
 *   2010-07-21 2.0.2   SysCo/al Fix to create correctly the folders "users" and "log" if needed
 *   2010-07-19 2.0.1   SysCo/al Foreach was not working well in PHP4, replaced at some places
 *   2010-07-19 2.0.0   SysCo/al New design using a class, mOTP support, cleaning of the code
 *   2010-06-15 1.1.5   SysCo/al Added OATH/TOTP support
 *   2010-06-15 1.1.4   SysCo/al Project renamed to multiotp to avoid overlapping
 *   2010-06-08 1.1.3   SysCo/al Typo in script folder detection
 *   2010-06-08 1.1.2   SysCo/al Typo in variable name
 *   2010-06-08 1.1.1   SysCo/al Status bar during resynchronization
 *   2010-06-08 1.1.0   SysCo/al Fix in the example, distribution not compressed
 *   2010-06-07 1.0.0   SysCo/al Initial implementation
 *********************************************************************/

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.8.8.4
 * @date      2022-05-08
 * @since     2010-07-18
 */
{
  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 $_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 $_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 $_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 $_users_folder;             // Folder where users definition files are stored
  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  

  /**
   * @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.8.8.4
   * @date      2022-05-08
   * @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.8.8.4'; // You should add a suffix for your changes (for example 5.0.3.2-andy-2016-10-XX)
        $this->_version = trim(mb_substr($temp_version, 8));
      }
      if (!isset($this->_date)) {
        $temp_date = '@date      2022-05-08'; // You should update the date with the date of your changes
        $this->_date = trim(mb_substr($temp_date, 8));
      }
      if (!isset($this->_copyright)) { $this->_copyright = base64_decode('KGMpIDIwMTAtMjAyMiBTeXNDbyBzeXN0ZW1lcyBkZSBjb21tdW5pY2F0aW9uIHNh'); }
      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 = 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_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_algorithm'           => "TEXT DEFAULT 'totp'",
          'default_dialin_ip_mask'      => "TEXT DEFAULT ''",
          '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 ''",
          '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 ''",
          // If ldap_group_attribute is empty, the default value used is "memberOf"
          '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
          '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",
          // 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",
          '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_request_ldap_pwd'  => "int(1) DEFAULT 1",
          'radius_error_reply_message'  => "int(1) DEFAULT 1",
          'radius_reply_attributor'     => "TEXT DEFAULT ' += '",
          'radius_reply_separator_hex'  => "TEXT DEFAULT '".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 ''",
          '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 7",
          'syslog_level'                => "int(10) DEFAULT 5",
          'syslog_port'                 => "int(10) DEFAULT 514",
          'syslog_server'               => "TEXT DEFAULT ''",
          '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 ''",
          '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(
          '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 ''",
          '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",
          // 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
          '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 ("" != trim($config_dir)) {
          $this->SetConfigFolder($config_dir, true, false);
      }
      
      $this->_parser_pointers         = array();
      
      $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
      $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->_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 = array();
      
      $this->_servers_temp_bad_list  = array();

      $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")) {
                  $temp_encryption_key = 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("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 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();
      }
      */
  }


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


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


  function GetLastFailedWhiteDelay()
  {
    return $this->_config_data['last_failed_white_delay'];
  }


  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'] = 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 = trim($this->_config_data['ldap_cache_folder']);
      if (("tempdir" == $folder) || ("" == 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 $this->_config_data['backend_encoding'];
  }


  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 $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 $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 $this->_config_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 = trim($value);
  }


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


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


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


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


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


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


  function GetLockFolder()
  {
    return 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 = trim($value);
  }


  function GetTouchFolder()
  {
    return 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().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'] = trim($value);
  }


  function GetUserLanguage($ignore_main_language = FALSE) {
    $language = trim($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'] = trim($value);
  }


  function GetLanguage() {
    return mb_strtolower(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")) {
        $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();
      }
    }
  }


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

    $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[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[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: Authentication failed (CRC error)";
    $this->_errors_text[97] = "ERROR: Authentication failed (wrong private id)";
    $this->_errors_text[98] = "ERROR: Authentication failed (wrong token length)";
    $this->_errors_text[99] = "ERROR: Authentication failed (and other possible unknown errors)";
  }


  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;
  }


  // 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 = 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         = array(),
      $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']: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']:array();
        $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   = array();
        $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) {
          $encryption_key = '';
      } elseif ('' == $encryption_key) {
          $encryption_key = $this->GetEncryptionKey();
      }

      $item_info = trim($item." ".$id_value);
      if ('group' == mb_strtolower($item,'UTF-8')) {
          $item_info = trim($item." ".(isset($data_array['name'])?$data_array['name']:$id_value));
      } elseif ('device' == mb_strtolower($item,'UTF-8')) {
          $item_info = 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;

              // 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) {
                  $file_handler = @fopen($folder.$filename,"ab+");
                  if (FALSE === $file_handler) {
                    usleep(500000); // Wait 0.5 seconds before retry to have more chance to do the job
                    $file_handler = @fopen($folder.$filename,"ab+");
                  }
                } else {
                  $file_handler = @fopen($folder.$filename, "wt"); // was wt
                  if (FALSE === $file_handler) {
                    usleep(500000); // Wait 0.5 seconds before retry to have more chance to do the job
                    $file_handler = @fopen($folder.$filename, "wt"); // was wt
                  }
                }
              } 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, 28, '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")) {
                    while(!feof($raw_fn))
                    {
                      $line.= mb_strtolower($key,'UTF-8');
                      $value = 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 ('' != 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 ($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()) && (!$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 (('' == 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 ".trim($this->_mysqli->error)." ".$sQuery,
                                                'file_only'  => TRUE,
                                                'error_code' => 40,
                                                'category'   => 'System',
                                                'user'       => '',
                                                'overwrite_severity' => 3)
                                         );
                      } else {
                          $num_rows = $result->num_rows;                                    
                      }
                  } elseif (!($result = @mysql_query($sQuery, $this->_mysql_database_link))) {
                      $this->WriteLog(array('text'       => "Error: SQL query error ($sQuery) : ".mysql_error(),
                                            'file_only'  => TRUE,
                                            'error_code' => 40,
                                            '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)) {
                          if (!($rResult = @$this->_mysqli->query($sQuery))) {
                              $this->WriteLog(array('text'       => "Error: SQL query error ".trim($this->_mysqli->error)." ".$sQuery,
                                                    'file_only'  => TRUE,
                                                    'error_code' => 40,
                                                    'category'   => 'System',
                                                    'user'       => '',
                                                    'overwrite_severity' => 3)
                                             );
                              $result = FALSE;
                          }
                      } elseif (!($rResult = @mysql_query($sQuery, $this->_mysql_database_link))) {
                          $this->WriteLog(array('text'       => "Error: SQL query error ($sQuery) : ".mysql_error(),
                                                'file_only'  => TRUE,
                                                'error_code' => 40,
                                                '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 ".trim($this->_mysqli->error)." ".$sQuery, TRUE, FALSE, 40, 'System', '', 3);
                          } elseif (0 == $this->_mysqli->affected_rows) {
                              $this->WriteLog("Error: SQL entry for ".$item_info." cannot be created or changed", FALSE, FALSE, 43, '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, 40, '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, 43, 'System', '', 3);
                          $result = FALSE;
                      } else {
                          $item_created = TRUE;
                      }
                  }
              }
          } elseif ((!$backup_format) && ('pgsql' == $this->GetBackendType()) && (!$file_only)) {
              $esc_id_value = pg_escape_string($id_value);
              if ($this->OpenPGSQLDatabase()) {
                  $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 (('' == 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($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(), TRUE, FALSE, 40, '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(), TRUE, FALSE, 40, '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(), TRUE, FALSE, 40, '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, 43, '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' => 19,
                                        'category'   => 'System',
                                        'user'       => '',
                                        'local_only' => 1)
                                 );
              }
              else {
                  $this->WriteLog(array('text'       => "Info: ".$item_info." manually created",
                                        'error_code' => 19,
                                        '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")) {
                  $first_line = 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)) && ("" != 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: ".trim($this->_mysqli->error)." ".$sQuery, TRUE, FALSE, 41, '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, 41, '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()." ".$sQuery, TRUE, FALSE, 41, '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 = 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;
  }


  // 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 = 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 = 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 = 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();
  }


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


  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 = 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 ($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/", 5);
      // if (FALSE !== mb_strpos($result_stats, "OK")) {
      // We have to upgrade the anonymous last update even if the answer id 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, "wt")) {
                  fwrite($write, $infoweb);
                  fclose($write);
                  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 = 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']:array();
    $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
    $content = $this->WriteConfigData($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 ('' != 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 ('' != 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 ('' != 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 ('' != 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 = 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'] : array();
    $ignore_files      = isset($rc_array['ignore_files'])      ? $rc_array['ignore_files'] : array();
    $rename_files      = isset($rc_array['rename_files'])      ? $rc_array['rename_files'] : array();
    $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 = array();
    }

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

    $type = '';
    $item = '';
    $id_value = '';
    $deleted = FALSE;
    $file_handler = FALSE;
    $data_array = array();
    $validity = TRUE;
    $result = TRUE;

    if (file_exists($backup_file)) {

      if ($backup_handler = @fopen($backup_file, "rt")) {
        $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, 59, '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 = 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, "wb"))) {
                        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 = 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)) && ("" != 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 = 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, 28, '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  $info                Information to log
   * @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
   * @retval  void
   *
   * @author  Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
   * @version 5.8.1
   * @date    2021-03-21
   * @since   2010-12-19
   *
   *   Severity values:
   *     0 Emergency: system is unusable
   *     1 Alert: action must be taken immediately
   *     2 Critical: critical conditions
   *     3 Error: error conditions
   *     4 Warning: warning conditions
   *     5 Notice: normal but significant condition (default value)
   *     6 Informational: informational messages
   *     7 Debug: debug-level messages
   */
  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
  ) {
      if (is_array($text_array)) {
        $text = isset($text_array['text'])?$text_array['text']:'';
        $file_only = isset($text_array['file_only'])?(TRUE === $text_array['file_only']):FALSE;
        $hide_on_display = isset($text_array['hide_on_display'])?(TRUE === $text_array['hide_on_display']):FALSE;
        $error_code = isset($text_array['error_code'])?intval($text_array['error_code']):9999;
        $category = isset($text_array['category'])?$text_array['category']:'*DEFAULT*';
        $user = isset($text_array['user'])?$text_array['user']:'*DEFAULT*';
        $overwrite_severity = isset($text_array['overwrite_severity'])?$text_array['overwrite_severity']:-1;
        $no_syslog = isset($text_array['no_syslog'])?(TRUE === $text_array['no_syslog']):FALSE;
        $local_only = isset($text_array['local_only'])?intval($text_array['local_only']):0;
      } else { // backward compatibility
        $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;
        // New parameters, only available in the array
        $local_only = 0;
      }

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

      if ('*DEFAULT*' != $user) {
          $user_log = $user;
      } else {
          $user_log = $this->GetUser();
      }
      
      if ('*DEFAULT*' != $category) {
          $category_log = $category;
      } else {
          $category_log = "Authentication"; // $this->_class;
      }

      // 0000 notice         (5)
      // 8888-9999 debug     (7)
      // 0001-0019 info      (6)
      // 0020-0099 warning   (4)
      // 0100-0199 error     (3)
      // 0200-0299 critical  (2)
      // 0300-0399 alert     (1)
      // 0400-0499 emergency (0)
      if (0 == intval($error_code)) {
          $severity = 5;
      } elseif (8888 <= intval($error_code)) {
          $severity = 7;
      } elseif (20 > intval($error_code)) {
          $severity = 6;
      } elseif (100 > intval($error_code)) {
          $severity = 4;
      } elseif (200 > intval($error_code)) {
          $severity = 3;
      } elseif (300 > intval($error_code)) {
          $severity = 2;
      } elseif (400 > intval($error_code)) {
          $severity = 1;
      } elseif (500 > intval($error_code)) {
          $severity = 0;
      } else {
          $severity = 3;
      }
      
      if ((intval($overwrite_severity) >= 0) && (intval($overwrite_severity) <= 7)) {
          $severity = intval($overwrite_severity);
      }
      
      switch ($severity) {
          case 0:
              $severity_txt = 'emergency';
              break;
          case 1:
              $severity_txt = 'alert';
              break;
          case 2:
              $severity_txt = 'critical';
              break;
          case 3:
              $severity_txt = 'error';
              break;
          case 4:
              $severity_txt = 'warning';
              break;
          case 5:
              $severity_txt = 'notice';
              break;
          case 6:
              $severity_txt = 'info';
              break;
          case 7:
              $severity_txt = 'debug';
              break;
          default:
              $severity_txt = 'error';
      }

      $post_text = "";
      $pre_text = "";
      if ("" != ($this->GetSourceIp().$this->GetSourceMac())) {
          $post_text.= "from ";
          if ("" != $this->GetSourceIp()) {
              $post_text.= $this->GetSourceIp().' ';
          }
          if ("" != $this->GetSourceMac()) {
              $post_text.= '['.$this->GetSourceMac().'] ';
          }
      }
      if ("" != ($this->GetCallingIp().$this->GetCallingMac())) {
          $post_text.= "for ";
          if ("" != $this->GetCallingIp()) {
              $post_text.= $this->GetCallingIp().' ';
          }
          if ("" != $this->GetCallingMac()) {
              $post_text.= '['.$this->GetCallingMac().'] ';
          }
      }
      $log_text = trim(trim($pre_text).' '.$text.' '.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_txt."\t".$user_log."\t".$category_log."\t".str_replace("\n", $this->IsDebugViaHtml()?"<br />":"; ", $log_text)."\t".$local_only."\t".$this->GetCreateHost();

      if (($this->GetDisplayLogFlag()) && (!$hide_on_display) && (!$this->GetNoDisplayLogFlag())) {
          $display_text = "\nLOG ".$log_datetime.' '.$severity_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 (("" != trim($this->GetSysLogServer())) && (!$this->IsSysLogServerBad()) && (!$no_syslog)) {
          if ($severity <= $this->GetSyslogLevel()) {
              $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 = 3; // 3 seconds timeout for udp connection
                  $syslog_severity_facility = $severity + 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);
                      $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, 99, 'System', '', -1, TRUE);
                          } else {
                              $this->WriteLog("Warning: Error with the Syslog server ".$this->GetSysLogServer(), FALSE, FALSE, 99, 'System', '', -1, TRUE);
                          }
                      }
                  }
              } else {
                  $this->EnableBadSysLogServer();
                  $this->WriteLog("Warning: resolution name error for the Syslog server ".$this->GetSysLogServer(), FALSE, FALSE, 99, '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'])
          ) {
              if ('mysql' == $this->GetBackendType()) {
                  if ($this->OpenMysqlDatabase()) {
                      $log_severity_escaped = escape_mysql_string($severity_txt);
                      $log_user_escaped = escape_mysql_string($user_log);
                      $log_category_escaped = escape_mysql_string($category_log);
                      $log_text_escaped = escape_mysql_string(mb_substr($log_text,0,255));
                      $log_create_host = escape_mysql_string($this->GetCreateHost());

                      $sQuery  = "INSERT INTO `".$this->_config_data['sql_log_table']."` (`datetime`,`severity`,`user`,`category`,`logentry`,`local_only`, `create_host`, `create_time`) VALUES ('".$log_datetime."','".$log_severity_escaped."','".$log_user_escaped."','".$log_category_escaped."','".$log_text_escaped."',".intval($local_only).",'".$log_create_host."', ".$log_time.")";
                      
                      if (is_object($this->_mysqli)) {
                          if (!($rResult = $this->_mysqli->query($sQuery))) {
                              $this->WriteLog("Error: SQL query error ($sQuery) : ".trim($this->_mysqli->error), TRUE, FALSE, 40, 'System', '', 3);
                          }
                      } elseif (!($rResult = mysql_query($sQuery, $this->_mysql_database_link))) {
                          $this->WriteLog("Error: SQL query error ($sQuery) : ".mysql_error(), TRUE, FALSE, 40, 'System', '', 3);
                      }
                  }
                  //mysql_close($log_link);
              } elseif ('pgsql' == $this->GetBackendType()) {
                  if ($this->OpenPGSQLDatabase()) {
                      $log_severity_escaped = pg_escape_string($severity_txt);
                      $log_user_escaped = pg_escape_string($user_log);
                      $log_category_escaped = pg_escape_string($category_log);
                      $log_text_escaped = pg_escape_string(mb_substr($log_text,0,255));
                      $log_create_host = pg_escape_string($this->GetCreateHost());

                      $sQuery  = "INSERT INTO \"".$this->_config_data['sql_schema']."\".\"".$this->_config_data['sql_log_table']."\" (\"datetime\",\"severity\",\"user\",\"category\",\"logentry\",\"local_only\",\"create_host\", \"create_time\") VALUES ('".$log_datetime."','".$log_severity_escaped."','".$log_user_escaped."','".$log_category_escaped."','".$log_text_escaped."',".intval($local_only).",'".$log_create_host."', ".$log_time.")";
                      
                      if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                          $this->WriteLog("Error: SQL query error ($sQuery) : ".pg_last_error(), TRUE, FALSE, 40, '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+")) {
                  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";
              }
          }
      }
  }


  function ShowLog(
      $as_result = FALSE
  ) {
      $result = "";
      if ('mysql' == $this->GetBackendType()) {
          if ($this->OpenMysqlDatabase()) {
              $sQuery  = "SELECT * FROM `".$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: ".trim($this->_mysqli->error), FALSE, FALSE, 41, 'System', '', 3);
                      $result = FALSE;
                  } else {
                      while ($aRow = $rResult->fetch_assoc()) {
                          if ($as_result) {
                              $result.= 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, 41, 'System', '', 3);
                  $result = FALSE;
              } else {
                  while ($aRow = mysql_fetch_assoc($rResult)) {
                      if ($as_result) {
                          $result.= trim($aRow['datetime'].' '.$aRow['user']).' '.$aRow['logentry']."\n";
                      }
                  }                         
              }
          }
          //mysql_close($log_link);
      } elseif ('pgsql' == $this->GetBackendType()) {
          if ($this->OpenPGSQLDatabase()) {
              $sQuery  = "SELECT * FROM \"".$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(), FALSE, FALSE, 41, 'System', '', 3);
                  $result = FALSE;
              } else {
                  while ($aRow = pg_fetch_assoc($rResult)) {
                      if ($as_result) {
                          $result.= trim($aRow['datetime'].' '.$aRow['user']).' '.$aRow['logentry']."\n";
                      }
                  }                         
              }
          }
          //pg_close($log_link);
      } elseif (file_exists($this->GetLogFolder().$this->GetLogFileName())) {
          if ($log_file_handle = @fopen($this->GetLogFolder().$this->GetLogFileName(),"r")) {
              while (!feof($log_file_handle)) {
                  if ($as_result) {
                      $result.= 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: ".trim($this->_mysqli->error), FALSE, FALSE, 41, '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, 41, '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(), FALSE, FALSE, 41, '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 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 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 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, 28, '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, 28, '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, 28, '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, 28, '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 = array();
          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 = trim($result_array[0][1]);
                      if ('0.0.0.0' != $temp) {
                          $ip = $temp;
                          break;
                      }
                  }
              }
          }
      } else { // Linux
          $output = array();
          // inet -> works for old (inet addr) and new (inet)
          exec("ifconfig eth0 | grep \"inet \" | grep -o -E '([[:xdigit:]]{1,3}\.){3}[[:xdigit:]]{1,3}'", $output);
          $ip = (isset($output[0])?$output[0]:'');
      }
      return $ip;
  }


  function GetNetworkInfo()
  {
      // These are demo values for the development
      $mode    = "";
      $ip      = "";
      $mask    = "";
      $gateway = "";
      $dns     = array();
      $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 = array();
          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 = 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 = trim($result_array[2][1]);
                      if ('0.0.0.0' != $temp) {
                          $gateway = $temp;
                          if (!isset($result_array[3][1])) {
                              $fixed_gateway = true;
                          }
                      }
                  }
              }
          }
          $output = array();
          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 = 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 = 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 = array();
          exec("netsh interface dump | find \"".$ip."\"", $output);
          foreach ($output as $line) {
              $line.= "  ";
              $result_array = 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 (trim($result_array[0][1]),"UTF-8","CP850");
                  $mode = "static";
                  break;
              }
          }
          if ("" == $interface_name) {
              $mode = "dhcp";
              $output = array();
              exec("netsh interface dump", $output);
              $ip4config = false;
              foreach ($output as $line) {
                  $line.= "  ";
                  if (0 === mb_strpos(trim($line),"pushd interface ipv4")) {
                      $ip4config = true;
                  } elseif ($ip4config && (0 === mb_strpos(trim($line),"popd"))) {
                      $ip4config = false;
                  }
                  if ($ip4config) {
                      $result_array = 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 (trim($result_array[0][1]),"UTF-8","CP850");
                          break;
                      }
                  }
              }
          }

          $dns_count = 0;
          $output = array();
          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 = 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] = trim($result_array[0][1]);
                      $dns_count++;
                  }
              }
          }
      } else {
          // Linux
          // eth -> works for old (eth0) and new (ether)
          $output = array();
          exec("grep -e \"^iface\seth0.*inet\s.*dhcp\" /etc/network/interfaces", $output);
          $mode = (false !== mb_strpos(mb_strtolower(isset($output[0])?$output[0]:'','UTF-8'), "dhcp"))?"dhcp":"static";
          
          $output = array();
          exec("ifconfig eth0 | grep \"eth\" | grep -o -E '([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}'", $output);
          $mac = mb_strtoupper(isset($output[0])?$output[0]:'','UTF-8');
          
          $output = array();
          exec("ifconfig eth0 | grep \"inet addr\" | grep -o -E '([[:xdigit:]]{1,3}\.){3}[[:xdigit:]]{1,3}'", $output);
          $ip = (isset($output[0])?$output[0]:'');
          $mask = (isset($output[2])?$output[2]:'');
          if ('' == $ip) {
              exec("ifconfig eth0 | grep \"inet \" | grep -o -E '([[:xdigit:]]{1,3}\.){3}[[:xdigit:]]{1,3}'", $output);
              $ip = (isset($output[0])?$output[0]:'');
              $mask = (isset($output[1])?$output[1]:'');
          }

          $output = array();
          exec("ip route show default | awk '/default/ {print $3}'", $output);
          $gateway = mb_strtoupper(isset($output[0])?$output[0]:'','UTF-8');

          $output = array();
          exec("cat /etc/resolv.conf | grep -o -E '([[:xdigit:]]{1,3}\.){3}[[:xdigit:]]{1,3}'", $output);
          $dns[0] = (isset($output[0])?$output[0]:'');
          $dns[1] = (isset($output[1])?$output[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
  ) {

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

          $this->WriteConfigData();
      }

      $result = false;
      if ('' != $ip) {
          $resolv_file = "/etc/resolv.conf";
          $resolv_tmp  = sys_get_temp_dir()."/multiotp_resolv_tmp";
          if (!($write = @fopen($resolv_tmp, "wt"))) {
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Error: *Temporary DNS information cannot be created", FALSE, FALSE, 8888, 'System', '');
              }
          } else {
              $domain_name = $this->GetDomainName();
              if ('' != $domain_name) {
                  fwrite($write, "domain ".$domain_name."\n");
                  fwrite($write, "search ".$domain_name."\n");
              }
              if ('' != $dns1) {
                  fwrite($write, "nameserver ".$dns1."\n");
              }
              if ('' != $dns2) {
                  fwrite($write, "nameserver ".$dns2."\n");
              }
              fclose($write);
              if ('' != $this->GetLinuxFileMode()) {
                  @chmod($resolv_tmp, octdec($this->GetLinuxFileMode()));
              }

              // Do not change the DNS servers in demo mode!
              if (!$this->IsDemoMode()) {
                  if (mb_strtolower(mb_substr(PHP_OS, 0, 3),'UTF-8') !== 'win') { // Currently only for non-windows machine
                      exec("sudo cp -f ".$resolv_tmp." ".$resolv_file, $output);
                  }
              }
          }
      }

      $interfaces_file = "/etc/network/interfaces";
      $interfaces_tmp  = sys_get_temp_dir()."/multiotp_interfaces_tmp";
      if (file_exists($interfaces_file)) {
          if (!($read = @fopen($interfaces_file, "rt"))) {
              if ($this->GetVerboseFlag()) {
                  $this->WriteLog("Error: *Interface configuration cannot be accessed", FALSE, FALSE, 8888, 'System', '');
              }
          } else {
              if (!($write = @fopen($interfaces_tmp, "wt"))) {
                  if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Error: *Temporary interface configuration cannot be created", FALSE, FALSE, 8888, 'System', '');
                  }
              } else {
                  $direct_write = true;
                  $inet_eth0 = false;
                  while(!feof($read)) {
                  // "iface", "mapping", "auto",  "allow-" and "source" eth0 (iface eth0)
                      $one_line = fgets($read);
                      if (preg_match("/^iface\seth0(.*)/", $one_line)) {
                          $direct_write = false;
                          $inet_eth0    = true;
                          if ('' != $ip) {
                              fwrite($write, "iface eth0 inet static\n");
                              fwrite($write, "\taddress ".$ip."\n");
                              fwrite($write, "\tnetmask ".$mask."\n");
                              fwrite($write, "\tgateway ".$gateway."\n");
                              fwrite($write, "\n");
                          } else {
                              fwrite($write, "iface eth0 inet dhcp\n");
                              fwrite($write, "\n");
                          }
                      } elseif ((0 === mb_strpos(trim($one_line),"allow-")) || 
                                (0 === mb_strpos(trim($one_line),"auto")) || 
                                (0 === mb_strpos(trim($one_line),"iface")) || 
                                (0 === mb_strpos(trim($one_line),"mapping")) || 
                                (0 === mb_strpos(trim($one_line),"source"))) {
                          $direct_write = true;
                      }
                      if ($direct_write) {
                          fwrite($write, $one_line); // $one_line includes \n
                      }
                  }
                  fclose($read);
                  fclose($write);
                  if ('' != $this->GetLinuxFileMode()) {
                      @chmod($interfaces_tmp, octdec($this->GetLinuxFileMode()));
                  }

                  // Do not change the IP in demo mode!
                  if (!$this->IsDemoMode()) {
                      exec("sudo cp -f ".$interfaces_tmp." ".$interfaces_file, $output);
                      $result = true;
                      if ($if_down_up) {
                          exec("sudo /sbin/ifdown eth0 > /dev/null 2>&1", $output);
                          sleep(1);
                          exec("sudo /sbin/ifup eth0 > /dev/null 2>&1", $output);
                      }
                  }
              }
          }
      } else {
          if ($this->GetVerboseFlag()) {
              $this->WriteLog("Error: *Interface configuration file cannot be found", FALSE, FALSE, 8888, 'System', '');
          }
      }
      return $result;
  }


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


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


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


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


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


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


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


  function GetHashSalt()
  {
      $salt = $this->_hash_salt;
      if ((("" == $salt) ||
           ("MySalt" == $salt) ||
           ("AjaxH@shS@lt" == $salt)
          ) &&
          ("" != $this->GetHashSaltFullPath()) &&
          file_exists($this->GetHashSaltFullPath())) {
          if ($hash_salt_file_handler = @fopen($this->GetHashSaltFullPath(), "rt")) {
              $temp = trim(fgets($hash_salt_file_handler));
              if ("" != $temp) {
                  $salt = $temp;
              }
              fclose($hash_salt_file_handler);
          }
      }
      return trim($salt);
  }


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


  function GetRandomSalt()
  {
      return 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 SetDebugOption(
      $value
  ) {
      $this->_config_data['debug'] = intval($value);
      if (1 == $this->_config_data['debug']) {
          $this->EnableVerboseLog();
      }
  }


  function GetDebugOption() {
      return $this->_config_data['debug'];
  }


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


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


  function GetDeveloperMode() {
      return $this->_config_data['developer_mode'];
  }


  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 $this->_config_data['scratch_passwords_digits'];
  }


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


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


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


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


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


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


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


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


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


  function GetSqlServer()
  {
      return 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;
          if ($select_row = $this->_mysqli->query($sql_query)) {
              $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: ".trim($this->_mysqli->error)." ".$sql_query, TRUE, FALSE, 40, 'System', '', 3);
                  $result = FALSE;
              }
          } else { //$select_row = $result->fetch_assoc();
              $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: ".trim($this->_mysqli->error)." ".$sql_query, TRUE, FALSE, 40, 'System', '', 3);
                  $result = FALSE;
              }
          }
      } elseif (NULL != $this->_mysql_database_link) {
          $sql_query = "SELECT `".$column."` FROM ".$table;
          if (($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, 40, 'System', '', 3);
                  $result = FALSE;
              }
          } elseif (!$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, 40, 'System', '', 3);
                  $result = FALSE;
              }
          }
      } elseif ($this->GetVerboseFlag()) {
          $this->WriteLog("Error: *The database link is down!", TRUE, FALSE, 41, '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";
                  }
                  $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.', '.trim($this->_mysqli->connect_error), TRUE, FALSE, 41, 'System', '', 3);
                      unset($this->_mysqli);
                      $this->_mysqli = NULL;
                  } else {
                      $result = TRUE;
                  }
              } 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, 41, 'System', '', 3);
              } else {
                  if (!mysql_select_db($this->_config_data['sql_database'])) {
                      $this->WriteLog("Error: Bad SQL database", TRUE, FALSE, 41, '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()." ".$sql_query, TRUE, FALSE, 40, '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()." ".$sql_query, TRUE, FALSE, 40, '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()." ".$sql_query, TRUE, FALSE, 40, '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()." ".$sql_query, TRUE, FALSE, 40, 'System', '', 3);
                      $result = FALSE;
                  }
              }
          }
      } elseif ($this->GetVerboseFlag()) {
          $this->WriteLog("Error: *The database link is down!", TRUE, FALSE, 41, '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'];
              if (!($this->_pgsql_database_link = pg_connect($pgsql_connect_string))) {
                  $this->WriteLog("Error: Bad SQL authentication parameters, ".pg_last_error(), TRUE, FALSE, 41, 'System', '', 3);
              } else {
                  $result = TRUE;
              }
          }
      }
      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), ".trim($this->_mysqli->error), TRUE, FALSE, 40, 'System', '', 3);
                              return 41;
                          }
                      } elseif (!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, 40, 'System', '', 3);
                          return 41;
                      }
                      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 = trim(mb_substr($valid_format, $pos + mb_strlen("DEFAULT")));
                            $row_format = 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(), TRUE, FALSE, 40, 'System', '', 3);
                          return 41;
                      }
                      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 = trim(mb_substr($valid_format, $pos + mb_strlen("DEFAULT")));
                            $row_format = 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(array(), true);
      }
      return 19;
  }


  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")) {
              $first_line = 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)) && ("" != 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, 33, '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: ".trim($this->_mysqli->error)." ".$sQuery, TRUE, FALSE, 41, '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, 41, '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, 33, '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()." ".$sQuery, TRUE, FALSE, 41, '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, 33, '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(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 = 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];
              }
          }
      }

      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")) {
                  $first_line = 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)) && ("" != 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: ".trim($this->_mysqli->error)." ".$sQuery, TRUE, FALSE, 41, '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, 41, '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()." ".$sQuery, TRUE, FALSE, 41, '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 = 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 (array() 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 ($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 = 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 ($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 = 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 = 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 = array();
      $this->_user_data = $this->ResetTempUserArray();

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


  function ResetTokenArray()
  {
      $this->_token_data = array();
      // 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 = 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 = array();
      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 = 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 = array();
      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 = 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 = array();
      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 = 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 = 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 GetUptime(
      $text_output = true
  ) {
      $uptime = '';
      if (file_exists('/proc/uptime')) {
          $file = @fopen('/proc/uptime', 'r');
          if ($file) {
              $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);
          }
      }
      else
      {
          $pagefile = 'C:\pagefile.sys';
          if (!is_file($pagefile)) {
              $pagefile = 'D:\pagefile.sys';
              if (!is_file($pagefile)) {
                  $pagefile = '';
              }
          }
          if ('' != $pagefile) {
              $gettime = (time() - filemtime($pagefile));
              $upsecs = $gettime;
              $days = floor($gettime / (24 * 3600));
              $gettime = $gettime - ($days * (24 * 3600));
              $hours = floor($gettime / (3600));
              $gettime = $gettime - ($hours * (3600));
              $minutes = floor($gettime / (60));
              $gettime = $gettime - ($minutes * 60);
              $seconds = $gettime; - ($seconds * 1);
              $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 = trim($value);
  }


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


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


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


  function SetSourceMac(
      $value
  ) {
      $this->_source_mac = $value;
  }


  function GetSourceMac()
  {
      return $this->_source_mac;
  }


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


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


  function SetCallingMac(
      $value
  ) {
      $this->_calling_mac = $value;
  }


  function GetCallingMac()
  {
      return $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 = 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 = bin2hex($temp);
      } elseif (32 != strlen($temp)) {
          $temp = '';
      }
      return strtoupper($temp); // ! THIS NON-MB strtoupper must stay as is !
  }


  function SetState(
      $value
  ) {
      if ((0 === mb_strpos($value, '0x')) && (0 == (mb_strlen($value) % 2))) {
          $value = hex2bin(mb_substr($value, 2));
      }
      $this->_state = 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 trim($value);
  }


  function GetSmsProvidersArray()
  {
      return $this->_sms_providers_array;
  }


  function GetSmsProvidersList()
  {
      $providers_list = '';
      foreach ($this->GetSmsProvidersArray() as $one_provider) {
          $providers_list.= (('' != $providers_list)?"\t":'');
          $providers_list.= $one_provider[1];
          $providers_list.= ('' != $one_provider[0])?' ('.$one_provider[0].')':'';
          $providers_list.= ('' != $one_provider[2])?', '.$one_provider[2]:'';
      }
      return $providers_list;
  }


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


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


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


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


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


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


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


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


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


  function GetSmsUsername()
  {
      return $this->_config_data['sms_userkey'];
  }


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


  function GetSmsContentSuccess()
  {
      return $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 $this->_config_data['sms_content_encoding'];
  }


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


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


  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 $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 $this->_config_data['overwrite_request_ldap_pwd'];
  }


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


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


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


  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 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 ('' != trim($value)) {
          $this->_config_data['ldap_language_attribute'] = trim($value);
      }
  }


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


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


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


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


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


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


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


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


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


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


  function GetLdapGroupCnIdentifier()
  {
    $value = $this->_config_data['ldap_group_cn_identifier'];
    if ("" == 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 && ('' == 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 && ('' == 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'] = trim($value);
  }


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


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


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


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

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

      $groups_array = 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(trim($one_group), $groups_array)) {
              if ('' != trim($one_group)) {
                  $this->CreateGroup('', trim($one_group), trim($one_group));
              }
          }
      }
  }


  function GetLdapInGroup()
  {
      return $this->_config_data['ldap_in_group'];
  }


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


  function GetLdapGroupAttribute()
  {
    $value = $this->_config_data['ldap_group_attribute'];
    if ("" == 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 $this->_config_data['ldap_port'];
  }


  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');
      } 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 $this->_config_data['ldap_server_type'];
  }


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


  function GetLdapTimeLimit()
  {
      return $this->_config_data['ldap_time_limit'];
  }


  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 $this->_config_data['ldap_hash_cache_time'];
  }


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


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


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


  function GetLdapTlsCipherSuite()
  {
    $value = $this->_config_data['ldaptls_cipher_suite'];
    if ("" == 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 ("" == trim($value)) {
          $value = "%s is your SMS-Code";
        }
      return ($value);
  }


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


  function GetSmsDigits()
  {
      return $this->_config_data['sms_digits'];
  }


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


  function GetSmsTimeout()
  {
      return $this->_config_data['sms_timeout'];
  }


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


  function GetEmailDigits()
  {
      return $this->_config_data['email_digits'];
  }


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


  function GetEmailCodeTimeout()
  {
      return $this->_config_data['email_code_timeout'];
  }


  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 ("" == trim($value)) {
        $value = "ClientServerSecret";
      }
      return ($value);
  }


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


  function GetServerType()
  {
      $value = $this->_config_data['server_type'];
      if ("" == 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 = trim(str_replace(",",";",str_replace(" ",";",$value)));
      $this->_config_data['server_url'] = $cleaned_server_url;
  }


  function GetServerUrl()
  {
      $cleaned_server_url = 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'] = trim($value);
  }


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


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


  function GetGlobalTextTokenChallenge() {
    $value = trim($this->_config_data['text_token_challenge']);
    if ("" == 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".bin2hex($id);
          } else {
              return "id0x".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 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'] = 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() {
      return (1 == $this->_config_data['radius_error_reply_message']);
  }


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


  function GetTimezone()
  {
      $timezone = 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(
      $value
  ) {
      $this->_config_data['smtp_auth'] = ((intval($value) > 0)?1:0);
  }


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


  function GetSmtpUsername()
  {
      return $this->_config_data['smtp_username'];
  }


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


  function GetSyslogFacility()
  {
      return $this->_config_data['syslog_facility'];
  }


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


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


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


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


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


  function GetSysLogServer()
  {
      return $this->_config_data['syslog_server'];
  }


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


  function EnableBadSysLogServer()
  {
    $this->_bad_syslog_server = TRUE;
  }


  /**
   * @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 // if ('HMAC-SHA1' == strtoupper($hash_algo)) // ! THIS NON-MB strtoupper must stay as is !
      {
          $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(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(bin2hex($mschap_response));
      }
      else
      {
          $result = 'Error: '.bin2hex($calculated_response).' instead of '.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(
      $user,
      $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(bin2hex($this->NtPasswordHashHash($hash)));

      /*
      $kr = hash_hmac('md5',
                      pack('H*',hash('md4', $hash)),
                      $this->Convert2Unicode(strtoupper($user).$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.$user)), 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(bin2hex($mschap2_response));
      }
      else
      {
          $result = 'Error: '.bin2hex($calculated_response).' instead of '.bin2hex($nt_response);
      }
      return $result;
  }


  function CheckMsChap2Response(
      $user,
      $secret,
      $domain = '',
      $hex_mschap_challenge = '',
      $hex_mschap2_response = ''
  ) {
      $result = $this->CalculateMsChap2Response($user, $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")); // ! 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
   * @return  boolean
   *
   *********************************************************************/
  function CreateUser($user_raw,
                      $prefix_pin_needed = -1,
                      $algorithm = 'totp',
                      $seed = '',
                      $pin = '',
                      $number_of_digits = 6,
                      $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
  ) {
      // A user cannot be created with one (or more) leading backslash
      $user = str_replace("\\", "", $user_raw);
      $result = FALSE;
      if ('' != 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($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, 21, 'User', '');
              } else {
                  $this->WriteLog("Error: User ".$user." already exists", FALSE, FALSE, 22, 'User', $user);
              }
          } else {
              $this->SetUser($user);
              $this->SetUserPrefixPin($request_prefix_pin);
              $this->SetUserRequestLdapPassword($request_ldap_pwd);
              $this->SetUserAlgorithm($algorithm);
              $this->SetUserTokenAlgoSuite($token_algo_suite);

              $the_pin = $pin;
              if ('' == $the_pin) {
                  $the_pin = mt_rand(1000,9999);
              }
              $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)?30:$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 = mt_rand(1000,9999);
                      }
                      $the_pin = mb_substr($the_pin, 0, 4);
                  }
              } else { // without2FA or unknown
                  $next_event = 0;
                  $time_interval = 0;
              }

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

              $this->SetUserEmail($this_email);
              $this->SetUserGroup(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, 21, 'User', '');
          }
          else
          {
              $this->WriteLog("Error: User ".$user." already exists", FALSE, FALSE, 22, 'User', $user);
          }
      }
      elseif (!$this->ReadTokenData($token))
      {
          $result = FALSE;
          $this->WriteLog("Error: information about token ".$token." for user $user cannot be accessed", FALSE, FALSE, 29, 'Token', $token);
      }
      else
      {
          $this->AddTokenAttributedUsers($user);
          if (!$this->WriteTokenData())
          {
              $result = 28; // 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 = mt_rand(1000,9999);
              }
              
              $this_email = trim($email);
              if (('' == $this_email) && (FALSE !== mb_strpos($user, '@')))
              {
                  $this_email = $user;
              }
              
              $this->SetUserPin($the_pin);
              $this->SetUserEmail($this_email);
              $this->SetUserGroup(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, 39, '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, 39, '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);

      if ('' != 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' : '';
      }

      // 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 = 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="[BCHIMOPTUY,]*'.mb_strtoupper($this->GetUserAlgorithm(),'UTF-8').'[BCHIMOPTUY,]*"}/i', 'IfMultiotpUserAlgorithm="'.mb_strtoupper($this->GetUserAlgorithm(),'UTF-8').'"}', $html);

      // Clean other algorithms info
      foreach (explode("\t",$this->GetAlgorithmsList()) as $algorithm_one) {
          if (mb_strtoupper($algorithm_one,'UTF-8') != mb_strtoupper($this->GetUserAlgorithm(),'UTF-8')) {
              $html = preg_replace('/<!--\s*\{\/IfMultiotpUserAlgorithm="[BCHIMOPTUY,]*'.mb_strtoupper($algorithm_one,'UTF-8').'[BCHIMOPTUY,]*"\}\s*-->/i', ' -- {/IfMultiotpUserAlgorithm="DELETE"} -->', $html);
              $html = preg_replace('/<!--\s*\{IfMultiotpUserAlgorithm="[BCHIMOPTUY,]*'.mb_strtoupper($algorithm_one,'UTF-8').'[BCHIMOPTUY,]*"\}\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('{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=(.*?)[\}\s]/';
      $regex_h='/\sheight=(.*?)[\}\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 = trim(str_replace('}', '', $w[1]));
                      }
                      if(preg_match($regex_h, $item, $values)) {
                          $h = str_replace('"', '', explode('=', $values[0],2));
                          $h = 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 = $this->GetUserTokenUrlLink($user, $descr, $qrcode_format);
                  $w = $code_width;
                  $h = $code_height;
                  if(!empty($item)) {
                      if(preg_match($regex_w, $item, $values)) {
                          $w = str_replace('"', '', explode('=', $values[0],2));
                          $w = trim(str_replace('}', '', $w[1]));
                      }
                      if(preg_match($regex_h, $item, $values)) {
                          $h = str_replace('"', '', explode('=', $values[0],2));
                          $h = trim(str_replace('}', '', $h[1]));
                      }
                      $html = str_replace($item, "<a id=\"QrCodeUserToken\" href=\"".$url."\" target=\"blank\"><img border=\"0\" width=\"".$w."\" height=\"".$h."\" src=\"data:image/png;base64,".base64_encode($this->qrcode($url, 'binary'))."\"></a>", $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();
          $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 {
                    $result = 'motp://'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.$q_seed;
                    // $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, 23, 'System', '');
          }
      } else {
          // $result = '';
      }
      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 {
                    $result = 'motp://'.$encoded_issuer_and_separator.rawurlencode($q_display_name).'?secret='.$q_seed;
                    // $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, 23, '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
   * @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
  ) {
      // A user cannot be created with one (or more) leading backslash
      $user = str_replace("\\", "", $user_raw);

      $result = FALSE;
      if ('' != trim($user))
      {
          if ($this->ReadUserData($user, TRUE, TRUE) || ('' == $user))
          {
              $this->WriteLog("Error: Unable to create the user ".$user." because it already exists", FALSE, FALSE, 22, '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($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 = mt_rand(1000,9999);
              }
              $this->SetUserPrefixPin($prefix_required);
              $this->SetUserTokenNumberOfDigits(6);
              $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 = 30;
              } 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 = mt_rand(1000,9999);
                  }
              } 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->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);
      if ('' != $user_encoded) {
          if ($user_encoded != $this->GetUser()) {
              $this->ResetUserArray();
              $this->_user = $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 !
              }
          }
      }
      else
      {
          $this->ResetUserArray();
      }
      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, 22, '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().": ".trim($this->_mysqli->error), FALSE, FALSE, 28, '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, 28, '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, 21, 'User');
                                  }
                                  else
                                  {
                                      $this->WriteLog("Info: User ".$this->GetUser()." successfully renamed to $the_new_user", FALSE, FALSE, 11, 'User');
                                      $result = TRUE;
                                  }
                              }
                          }
                          break;
                      case 'pgsql':
                          $esc_actual = pg_escape_string($this->GetUser());
                          $esc_new    = pg_escape_string($the_new_user);
                          if ($this->OpenPGSQLDatabase())
                          {
                              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(), FALSE, FALSE, 28, '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, 21, 'User');
                                  }
                                  else
                                  {
                                      $this->WriteLog("Info: User ".$this->GetUser()." successfully renamed to $the_new_user", FALSE, FALSE, 11, '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);
  }


  /// 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, FALSE, 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, FALSE, 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, FALSE, TRUE)) {
          $real_user = $cleaned_user;
        }
      // Try to clean a phone number
      } else {
        $clean_phone = $this->CleanPhoneNumber($user);
        if ($this->CheckUserExists($clean_phone, FALSE, FALSE, 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 ('' != trim($check_user)) {
          $server_result = -1;
          if ((!$no_server_check) && ('' != $this->GetServerUrl())) {
              $server_result = $this->CheckUserExistsOnServer($check_user);
              if (22 == $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 && (21 == $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: ".trim($this->_mysqli->error), FALSE, FALSE, 41, '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, 41, 'System', '', 3);
                          } else {
                              $num_rows = mysql_num_rows($this->_mysql_database_link);
                          }
                          
                          if (0 == $num_rows) {
                              if (!$no_error) {
                                  $this->WriteLog("Error: User ".$check_user." does not exist", FALSE, FALSE, 21, 'System', '');
                              }
                              $result = FALSE;
                          } else {
                              $result = TRUE;
                          }
                      }
                      break;
                  case 'pgsql':
                      $check_user_escaped = pg_escape_string($check_user);
                      if ($this->OpenPGSQLDatabase()) {
                          $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(), FALSE, FALSE, 41, '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, 21, '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, 19, '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, 19, '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, 21, 'User');
                  } else {
                      $this->WriteLog("Error: Unable to delete user ".$this->GetUser(), FALSE, FALSE, 29, 'User');
                  }
              }
          } else {
              $result = unlink($this->GetUsersFolder().$user_filename);
              if ($result) {
                  if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Info: *User ".$this->GetUser()." successfully deleted", FALSE, FALSE, 12, 'User');
                  }
              } elseif (!$this->GetMigrationFromFile()) {
                  if (!$no_error_info) {
                      $this->WriteLog("Error: Unable to delete user ".$this->GetUser(), FALSE, FALSE, 28, 'User');
                  }
              }
          }
      }

      if ($this->GetBackendTypeValidated()) {
          switch ($this->_config_data['backend_type']) {
              case 'mysql':
                  $user_escaped = escape_mysql_string($this->_user);
                  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().": ".trim($this->_mysqli->error), FALSE, FALSE, 28, '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, 28, '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, 21, 'User');
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *User ".$this->GetUser()." successfully deleted", FALSE, FALSE, 12, 'User');
                              }
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  $user_escaped = pg_escape_string($this->_user);
                  if ($this->OpenPGSQLDatabase()) {
                      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(), FALSE, FALSE, 28, '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, 21, 'User');
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *User ".$this->GetUser()." successfully deleted", FALSE, FALSE, 12, '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: ".trim($this->_mysqli->error), FALSE, FALSE, 41, '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, 41, '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(), FALSE, FALSE, 41, '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';
          if (!file_exists($this->GetUsersFolder().$user_filename)) {
              $user_filename = $this->EncodeFileId($array_user, $this->IsCaseSensitiveUsers(), TRUE).'.db';
          }

          if (!file_exists($this->GetUsersFolder().$user_filename)) {
              if (!$create) {
                  $this->WriteLog("Error: database file ".$this->GetUsersFolder().$user_filename." for user ".$array_user." does not exist", FALSE, FALSE, 21, 'System', '');
              }
          } else {
              $temp_user_array['multi_account'] = 0;
              $temp_user_array['time_interval'] = 0;

              if ($file_handler = @fopen($this->GetUsersFolder().$user_filename, "rt")) {
                  $first_line = 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']         = trim(fgets($file_handler));
                      $temp_user_array['user_pin']           = trim(fgets($file_handler));
                      $temp_user_array['number_of_digits']   = trim(fgets($file_handler));
                      $temp_user_array['last_event']         = intval(trim(fgets($file_handler))) - 1;
                      $temp_user_array['request_prefix_pin'] = intval(trim(fgets($file_handler)));
                      $temp_user_array['last_login']         = intval(trim(fgets($file_handler)));
                      $temp_user_array['error_counter']      = intval(trim(fgets($file_handler)));
                      $temp_user_array['locked']             = intval(trim(fgets($file_handler)));
                  } else {
                      while (!feof($file_handler)) {
                          $line = 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 ('' != trim($line_array[0])) {
                              $temp_user_array[mb_strtolower($line_array[0],'UTF-8')] = $line_array[1];
                          }
                      }
                  }
                  fclose($file_handler);
                  $result = true;
              }
              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, 33, '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) : ".trim($this->_mysqli->error).' ', TRUE, FALSE, 40, '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, 40, '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, 42, 'System', '', 3);
                                  }
                                  $result = true;
                              }
                              if(0 == count($aRow) && !$create) {
                                  $this->WriteLog("Error: SQL database entry for user ".$array_user." does not exist", FALSE, FALSE, 21, '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, 33, 'System', '');
                              $result = false;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  $array_user_escaped = pg_escape_string($array_user);
                  if ($this->OpenPGSQLDatabase()) {
                      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(), TRUE, FALSE, 40, '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 (($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, 42, 'System', '', 3);
                                  }
                                  $result = true;
                              }
                              if(0 == count($aRow) && !$create) {
                                  $this->WriteLog("Error: SQL database entry for user ".$array_user." does not exist", FALSE, FALSE, 21, '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, 33, 'System', '');
                              $result = false;
                          }
                      }
                  }
                  break;
              default:
              // Nothing to do if the backend type is 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 = 19;

              foreach ($server_array as $one_line)
              {
                  $line = 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 ('' != 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;
          }
      }

      $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 = 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 ('' == trim($this->GetUser())) {
          $result = false;
      } else {
          $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 = array();
    $users_array = explode("\t", $this->GetUsersList());
    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);
  }


  /**
   * @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: ".trim($this->_mysqli->error), FALSE, FALSE, 41, '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, 41, '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(), FALSE, FALSE, 41, '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")) {
                                  $first_line = 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 = 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' == trim($line_array[0]))
                                          {
                                              $error_counter = $line_array[1];
                                          }
                                          if ('last_error' == trim($line_array[0]))
                                          {
                                              $last_error = $line_array[1];
                                          }
                                          if ('desactivated' == trim($line_array[0]))
                                          {
                                              if (1 == (isset($line_array[1])?$line_array[1]:0))
                                              {
                                                  $desactivated = TRUE;
                                              }
                                          }
                                          if ('locked' == 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
  ) {

      // 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) 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: ".trim($this->_mysqli->error), FALSE, FALSE, 41, '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, 41, '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) 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(), FALSE, FALSE, 41, '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;
                              $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")) {
                                      $first_line = 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 = 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' == trim($line_array[0]))
                                              {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0))
                                                  {
                                                      $locked = TRUE;
                                                  }
                                              }
                                              if ('desactivated' == trim($line_array[0]))
                                              {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0))
                                                  {
                                                      $desactivated = TRUE;
                                                  }
                                              }
                                          }
                                      }
                                      fclose($file_handler);
                                      $users_count++;
                                      
                                      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 $locked_users_list;
  }


  function GetLockedUsersCount() {

      // 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)";
                          if (is_object($this->_mysqli)) {
                              if (!($result = $this->_mysqli->query($sQuery))) {
                                  $this->WriteLog("Error: Unable to access the database: ".trim($this->_mysqli->error), FALSE, FALSE, 41, '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, 41, '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)";
                          if (!($rResult = pg_query($this->_pgsql_database_link, $sQuery))) {
                              $this->WriteLog("Error: Unable to access the database: ".pg_last_error(), FALSE, FALSE, 41, '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)) {
                              $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")) {
                                      $first_line = 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 = 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' == trim($line_array[0])) {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                      $locked = TRUE;
                                                  }
                                              }
                                              if ('desactivated' == trim($line_array[0])) {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                      $desactivated = TRUE;
                                                  }
                                              }
                                          }
                                      }
                                      fclose($file_handler);
                                      $users_count++;
                                      
                                      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: ".trim($this->_mysqli->error), FALSE, FALSE, 41, '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, 41, '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(), FALSE, FALSE, 41, '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")) {
                                  $first_line = 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 = 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' == trim($line_array[0])) {
                                              if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                  $desactivated = TRUE;
                                              }
                                          }
                                          if ('locked' == 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: ".trim($this->_mysqli->error), FALSE, FALSE, 41, '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, 41, '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(), FALSE, FALSE, 41, '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")) {
                                      $first_line = 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 = 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' == trim($line_array[0])) {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                    $desactivated = TRUE;
                                                  }
                                              }
                                              if ('locked' == trim($line_array[0])) {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                    $locked = TRUE;
                                                  }
                                              }
                                              if ('request_ldap_pwd' == trim($line_array[0])) {
                                                  if (1 == (isset($line_array[1])?$line_array[1]:0)) {
                                                    $request_ldap_pwd = TRUE;
                                                  }
                                              }
                                              if ('algorithm' == 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 = 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',
                                '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: ".trim($this->_mysqli->error), FALSE, FALSE, 41, '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, 41, '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(), FALSE, FALSE, 41, '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'],
                                                  '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'],
                                                  '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(),
                                              '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()
  {
      $algorithms_list = '';
      $algorithms_array = explode("*",$this->_valid_algorithms);
      foreach ($algorithms_array as $algorithm_one) {
          if ('' != trim($algorithm_one)) {
              $algorithms_list.= (('' != $algorithms_list)?"\t":'').trim($algorithm_one);
          }
      }
      return $algorithms_list;
  }


  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 array();
      }
      return (explode(",",$this->_user_data['scratch_passwords']));
  }


  function RemoveUserUsedScratchPassword(
      $to_remove
  ) {
      $scratch_passwords = trim($this->_user_data['scratch_passwords']);
      if (FALSE !== ($pos = mb_strpos($scratch_passwords, $to_remove))) {
          $scratch_passwords = 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 = trim($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(
      $value
  ) {
      $this->_user_data['multi_account'] = $value;
      return TRUE;
  }


  function GetUserMultiAccount()
  {
      return $this->_user_data['multi_account'];
  }


  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 $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_strlenmb_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 $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 $this->_user_data['description'];
  }


  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 $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 == ($this->_user_data['request_prefix_pin']));
  }


  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 == ($this->_user_data['request_ldap_pwd']));
  }


  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');
          $result = TRUE;
      } else {
          $this->WriteLog("Error: ".$algorithm." algorithm is unknown", FALSE, FALSE, 23, 'User');
      }
      return $result;
  }


  function GetUserAlgorithm(
      $user = ''
  ) {
      if($user != '') {
          $this->SetUser($user);
      }
      $result = mb_strtolower($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 = bin2hex(base32_decode($seed));
          } else {
              $the_seed = bin2hex($seed);
          }
      }
      $this->_user_data['token_seed'] = $the_seed;
  }


  function GetUserTokenSeed()
  {
      return $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 $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 $this->_user_data['key_id'];
  }


  function SetUserTokenNumberOfDigits(
      $number_of_digits
  ) {
      $this->_user_data['number_of_digits'] = $number_of_digits;
  }


  function GetUserTokenNumberOfDigits()
  {
      return $this->_user_data['number_of_digits'];
  }


  function SetUserTokenTimeInterval(
      $interval
  ) {
      if (intval($interval) > 0) {
          $this->_user_data['time_interval'] = intval($interval);
      }
  }


  function GetUserTokenTimeInterval()
  {
      return $this->_user_data['time_interval'];
  }


  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 intval($this->_user_data['last_event']);
  }


  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($this->_user_data['locked']);
  }


  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($this->_user_data['desactivated'] > 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($this->_user_data['synchronized']);
  }
  

  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 ($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 ($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 ($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($this->_user_data['synchronized_time']);
  }


  function SetUserErrorCounter(
      $counter
  ) {
      $this->_user_data['error_counter'] = $counter;
  }


  function GetUserErrorCounter()
  {
      return $this->_user_data['error_counter'];
  }


  /*********************************************************************
   *
   * 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 = '',
      $number_of_digits = 6,
      $time_interval_or_next_event = -1,
      $manufacturer = 'multiOTP',
      $issuer = '',
      $description = '',
      $token_algo_suite = ''
  ) {
      $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 = 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)?30:$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(6);
    $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(30);
    $this->SetUserTokenLastEvent(0 - 1);
    return ($this->WriteUserData() && $this->WriteTokenData());
  }


  function SetTokenSerialNumberLength(
      $value
  ) {
      $this->_config_data['token_serial_number_length'] = trim($value);
  }
  
  
  function AddTokenSerialNumberLength(
      $length
  ) {
      if (intval($length) > 0) {
          $actual = 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(" ",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'] = trim($value);
  }
  
  
  function AddTokenOtpListOfLength(
      $length
  ) {
      if (intval($length) > 0) {
          $actual = 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(" ",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, 28, '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().": ".trim($this->_mysqli->error), FALSE, FALSE, 28, '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, 28, '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, 29, 'Token');
                                  } else {
                                      $this->WriteLog("Info: Token ".$this->GetToken()." successfully renamed to $the_new_token", FALSE, FALSE, 19, 'Token');
                                      $result = TRUE;
                                  }
                              }
                          }
                          break;
                      case 'pgsql':
                          $esc_actual = pg_escape_string($this->GetToken());
                          $esc_new    = pg_escape_string($the_new_token);
                          if ($this->OpenPGSQLDatabase()) {
                              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(), FALSE, FALSE, 28, '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, 29, 'Token');
                                  } else {
                                      $this->WriteLog("Info: Token ".$this->GetToken()." successfully renamed to $the_new_token", FALSE, FALSE, 19, '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($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 ('' != 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: ".trim($this->_mysqli->error), FALSE, FALSE, 41, '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, 41, '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, 41, 'System', '', 3);
                              }
                              $result = FALSE;
                          } else {
                              $result = TRUE;
                          }
                      }
                      break;
                  case 'pgsql':
                      $check_token_escaped = pg_escape_string($check_token);
                      if ($this->OpenPGSQLDatabase()) {
                          $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(), FALSE, FALSE, 41, '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, 41, '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, 41, 'System', '', 3);
                      }
                      break;
              }
          }
      }
      return $result;
  }


  function ResetLastImportedTokensArray()
  {
      $this->_last_imported_tokens = array();
  }


  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'] = trim($issue_no);
  }


  function GetTokenIssueNo()
  {
      return $this->_token_data['issue_no'];
  }


  function SetTokenKeyId(
      $key_id
  ) {
      $this->_token_data['key_id'] = trim($key_id);
  }


  function GetTokenKeyId()
  {
      return $this->_token_data['key_id'];
  }


  function SetTokenKeyUsage(
      $key_usage
  ) {
      $this->_token_data['key_usage'] = 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 = 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(','.trim($this->GetTokenAttributedUsers()).',', ','.$data.',')) {
              $actual = str_replace(','.$data.',',',',','.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 = 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(','.trim($this->GetUserAttributedTokens()).',', ','.$data.',')) {
              $actual = str_replace(','.$data.',',',',','.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, 23, '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 = bin2hex(base32_decode($seed));
          } else {
              $the_seed = 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, 28, '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: ".trim($this->_mysqli->error), FALSE, FALSE, 41, '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, 41, '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(), FALSE, FALSE, 41, '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, 29, 'Token',  '');
              } else {
                  $this->WriteLog("Error: Unable to delete token ".$this->GetToken(), FALSE, FALSE, 29, 'Token',  '');
              }
          } else {
              $result = unlink($this->GetTokensFolder().$token_filename);
              if ($result) {
                  if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Info: *Token ".$this->GetToken()." successfully deleted", FALSE, FALSE, 19, 'Token', '');
                  }
              } else {
                  $this->WriteLog("Error: Unable to delete token ".$this->GetToken(), FALSE, FALSE, 28, '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().": ".trim($this->_mysqli->error), FALSE, FALSE, 28, '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, 28, '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, 29, 'Token', '');
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *token ".$this->GetToken()." successfully deleted", FALSE, FALSE, 19, 'Token', '');
                              }
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  $token_escaped = pg_escape_string($this->GetToken());
                  if ($this->OpenPGSQLDatabase()) {
                      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(), FALSE, FALSE, 28, '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, 29, 'Token', '');
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *token ".$this->GetToken()." successfully deleted", FALSE, FALSE, 19, '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, 29, 'System', '');
              }
          } else {
              if ($file_handler = @fopen($this->GetTokensFolder().$token_filename, "rt")) {
                  $first_line = trim(fgets($file_handler));
                  
                  while (!feof($file_handler)) {
                      $line = 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 ('' != 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, 33, '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) : ".trim($this->_mysqli->error).' ', TRUE, FALSE, 40, '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, 40, '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, 98, 'System', '');
                                  }
                                  $result = TRUE;
                              }
                              if(0 == count($aRow) && !$create) {
                                  $this->WriteLog("Error: SQL database entry for token ".$this->_token." does not exist", FALSE, FALSE, 29, '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, 33, 'System', '');
                              $result = FALSE;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  $token_escaped = pg_escape_string($this->_token);
                  if ($this->OpenPGSQLDatabase()) {
                      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(), TRUE, FALSE, 40, '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 (('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, 98, 'System', '');
                                  }
                                  $result = TRUE;
                              }
                              if(0 == count($aRow) && !$create) {
                                  $this->WriteLog("Error: SQL database entry for token ".$this->_token." does not exist", FALSE, FALSE, 29, '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, 33, '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 = array()
  ) {
      if ('' == 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 = array();
  }


  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, 39, 'System', '', 3);
          $this->EnableLdapError();
      } elseif (('' != $this->GetLdapDomainControllers()) && ('' != $ldap_username) && ('' != $ldap_password)) {
          $domain_controllers = explode(" ",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(),
                                '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, 30, '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 ('' != trim($this->GetLdapInGroup())) {
        $in_groups_array_raw = explode("\t",trim(str_replace(",","\t",str_replace(";","\t",$this->GetLdapInGroup()))));
      } else {
        $in_groups_array_raw = array();
      }

      if (!function_exists('ldap_connect')) {
          $result = FALSE;
          $this->WriteLog("Error: LDAP library not installed", FALSE, FALSE, 39, 'System', '', 3);
          $this->EnableLdapError();
      } elseif (('' != $this->GetLdapDomainControllers()) && ('' != $this->GetLdapBindDn()) && ('' != $this->GetLdapServerPassword())) {
          
          $this->DisableLdapError();
          $in_groups_array = array();
      
          // TODO: later, we could loop in several base-dn (semicolon separated)
          $domain_controllers = explode(" ",trim(str_replace(","," ",str_replace(";"," ",$this->GetLdapDomainControllers()))));
          
          $users_dn_array_raw = explode("\t",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(),
                                '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, 79, 'LDAP', '');
              $this->EnableLdapError();
          } else {
              // We continue only if there is no error

              $in_groups_filtering = array();
              $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);
                      }
                  }
              }

              $pageSize = 1000;
              $page_cookie = '';

              $users_in_groups = array();

              if ('' != trim($this->GetLdapInGroup())) {
                  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",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 (function_exists('ldap_control_paged_result')) {
                          ldap_control_paged_result($ldap_connection->_conn_paged, $pageSize, false, $page_cookie); // Page size of 1000
                      }

                      $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, 30, 'LDAP', '');
                          return FALSE;
                      }
                      if ('' != $ldap_connection->get_warning_message()) {
                          $this->WriteLog("Warning: LDAP warning: ".$ldap_connection->get_warning_message(), FALSE, FALSE, 98, 'LDAP', '');
                      }
                      if ($this->GetVerboseFlag() && ('' != $ldap_connection->get_debug_message())) {
                          $this->WriteLog("Debug: *LDAP debug: ".$ldap_connection->get_debug_message(), FALSE, FALSE, 98, 'LDAP', '');
                      }

                      do {
                          $accountdisable = FALSE;
                          $groups_lower_array = array();
                          $in_groups_array = array();
                          $in_groups_lower_array = 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 ('' == trim($this->GetLdapInGroup())) {
                                      $in_a_group = TRUE;
                                  } else {
                                      $in_a_group = FALSE;
                                      foreach ($in_groups_array_raw as $one_group) {
                                          $in_groups_array[] = trim($one_group);
                                          $in_groups_lower_array[] = mb_strtolower(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 = array();
                                          }
                                          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, 98, '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, 98, '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 = trim($one_user['description'][0]);
                                      }
                                      if (('' == $description) && (isset($one_user['gecos'][0]))) {
                                          $description = trim($one_user['gecos'][0]);
                                      }
                                      if (('' == $description) && (isset($one_user['displayname'][0]))) {
                                          $description = trim($one_user['displayname'][0]);
                                      }
                                      if ('' == $description) {
                                          $description = $account;
                                      }

                                      $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 (function_exists('ldap_control_paged_result_response')) {
                          if (FALSE !== $ldap_connection->_oui_paged_sr) {
                            ldap_control_paged_result_response($ldap_connection->_conn_paged, $ldap_connection->_oui_paged_sr, $page_cookie);
                          }
                      }
                  } while (($page_cookie !== null) && ($page_cookie != '') && ($ldap_connection->_oui_paged_sr !== FALSE));
                  // ldap pagination loop

                  if (function_exists('ldap_control_paged_result')) {
                      // Reset LDAP paged result
                      ldap_control_paged_result($ldap_connection->_conn_paged, $pageSize, false);
                  }
              }
          }
      } else {
          $this->WriteLog("Error: No LDAP connection information", FALSE, FALSE, 30, 'LDAP', '');
      }
      return $users_list;
  }


  function GetLdapUsersInfoArray(
      $user_filter = "*",
      $include_disabled = TRUE,
      $ignore_in_group = FALSE
  ) {
      $result_array = 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 ('' != trim($this->GetLdapInGroup())) {
        $in_groups_array_raw = explode("\t",trim(str_replace(",","\t",str_replace(";","\t",$this->GetLdapInGroup()))));
      } else {
        $in_groups_array_raw = array();
      }

      if (!function_exists('ldap_connect')) {
          $this->WriteLog("Error: LDAP library not installed", FALSE, FALSE, 39, 'System', '', 3);
          $this->EnableLdapError();
      } elseif (('' != $this->GetLdapDomainControllers()) && ('' != $this->GetLdapBindDn()) && ('' != $this->GetLdapServerPassword())) {

          $this->DisableLdapError();
          $in_groups_array = array();

          // TODO: later, we could loop in several base-dn (semicolon separated)
          $domain_controllers = explode(" ",trim(str_replace(","," ",str_replace(";"," ",$this->GetLdapDomainControllers()))));
          
          $users_dn_array_raw = explode("\t",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(),
                                '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, 79, 'LDAP', '');
              $this->EnableLdapError();
          } else {
              // We continue only if there is no error

              $in_groups_filtering = array();
              $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);
                      }
                  }
              }

              $pageSize = 1000;
              $page_cookie = '';

              $users_in_groups = array();

              if ('' != trim($this->GetLdapInGroup())) {
                  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",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 (function_exists('ldap_control_paged_result')) {
                          ldap_control_paged_result($ldap_connection->_conn_paged, $pageSize, false, $page_cookie); // Page size of 1000
                      }

                      $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, 30, 'LDAP', '');
                          return FALSE;
                      }
                      if ('' != $ldap_connection->get_warning_message()) {
                          $this->WriteLog("Warning: LDAP warning: ".$ldap_connection->get_warning_message(), FALSE, FALSE, 98, 'LDAP', '');
                      }
                      if ($this->GetVerboseFlag() && ('' != $ldap_connection->get_debug_message())) {
                          $this->WriteLog("Debug: *LDAP debug: ".$ldap_connection->get_debug_message(), FALSE, FALSE, 98, 'LDAP', '');
                      }

                      do {
                          $accountdisable = FALSE;
                          $groups_lower_array = array();
                          $in_groups_array = array();
                          $in_groups_lower_array = 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 ('' == trim($this->GetLdapInGroup())) {
                                      $in_a_group = TRUE;
                                  } else {
                                      $in_a_group = FALSE;
                                      foreach ($in_groups_array_raw as $one_group) {
                                          $in_groups_array[] = trim($one_group);
                                          $in_groups_lower_array[] = mb_strtolower(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 = array();
                                          }
                                          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, 98, '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, 98, '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 = trim($one_user['description'][0]);
                                      }
                                      if (('' == $description) && (isset($one_user['gecos'][0]))) {
                                          $description = trim($one_user['gecos'][0]);
                                      }
                                      if (('' == $description) && (isset($one_user['displayname'][0]))) {
                                          $description = trim($one_user['displayname'][0]);
                                      }
                                      if ('' == $description) {
                                          $description = $account;
                                      }

                                      $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]['displayname'] = (isset($one_user['displayname'][0]) ? $this->EncodeForBackend($one_user['displayname'][0]) : "");
                                      $result_array[$user]['description'] = (isset($one_user['description'][0]) ? $this->EncodeForBackend($one_user['description'][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 (function_exists('ldap_control_paged_result_response')) {
                          if (FALSE !== $ldap_connection->_oui_paged_sr) {
                            ldap_control_paged_result_response($ldap_connection->_conn_paged, $ldap_connection->_oui_paged_sr, $page_cookie);
                          }
                      }
                  } while (($page_cookie !== null) && ($page_cookie != '') && ($ldap_connection->_oui_paged_sr !== FALSE));
                  // ldap pagination loop

                  if (function_exists('ldap_control_paged_result')) {
                      // Reset LDAP paged result
                      ldap_control_paged_result($ldap_connection->_conn_paged, $pageSize, false);
                  }
              }
          }
      } else {
          $this->WriteLog("Error: No LDAP connection information", FALSE, FALSE, 30, '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.= "              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.= "                  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 (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 ('' != trim($this->GetLdapInGroup())) {
        $in_groups_array_raw = explode("\t",trim(str_replace(",","\t",str_replace(";","\t",$this->GetLdapInGroup()))));
      } else {
        $in_groups_array_raw = array();
      }

      $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, 39, '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 = trim(@file_get_contents($ldap_sync_file_lock));
                  $this->WriteLog("Info: Previous AD/LDAP synchronization in progress... $additional_info", FALSE, FALSE, 19, '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, "wt")) {
              // $additional_info = "started for ".gmdate("H:i:s", time()-$start_sync_time);
              $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 = array();

          $this->WriteLog("Info: AD/LDAP synchronization started", FALSE, FALSE, 19, 'LDAP', '');

          // TODO: later, we could loop in several base-dn (semicolon separated)
          $domain_controllers = explode(" ",trim(str_replace(","," ",str_replace(";"," ",$this->GetLdapDomainControllers()))));

          $users_dn_array_raw = explode("\t",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(),
                                '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, 79, 'LDAP', '');
              $this->EnableLdapError();
          } else {
              // We continue only if there is no error

              $in_groups_filtering = array();
              $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;

              $pageSize = 1000;
              $page_cookie = '';

              $users_in_groups = array();

              if ('' != trim($this->GetLdapInGroup())) {
                  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",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 (function_exists('ldap_control_paged_result')) {
                          ldap_control_paged_result($ldap_connection->_conn_paged, $pageSize, false, $page_cookie); // Page size of 1000
                      }

                      $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, 30, '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, 98, 'LDAP', '');
                      }
                      if ($this->GetVerboseFlag() && ('' != $ldap_connection->get_debug_message())) {
                          $this->WriteLog("Debug: *DEBUG warning: ".$ldap_connection->get_debug_message(), FALSE, FALSE, 98, '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, "wt")) {
                                  $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 = array();
                          $in_groups_array = array();
                          $in_groups_lower_array = 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 ('' == trim($this->GetLdapInGroup())) {
                                      $in_a_group = TRUE;
                                  } else {
                                      $in_a_group = FALSE;
                                      foreach ($in_groups_array_raw as $one_group) {
                                          $in_groups_array[] = trim($one_group);
                                          $in_groups_lower_array[] = mb_strtolower(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 = array();
                                          }
                                          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, 98, '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, 98, '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 = trim($one_user['description'][0]);
                                      }
                                      if (('' == $description) && (isset($one_user['gecos'][0]))) {
                                          $description = trim($one_user['gecos'][0]);
                                      }
                                      if (('' == $description) && (isset($one_user['displayname'][0]))) {
                                          $description = trim($one_user['displayname'][0]);
                                      }
                                      if ('' == $description) {
                                          $description = $account;
                                      }

                                      // $user;
                                      $ldap_email = trim(isset($one_user['mail'][0])?$this->EncodeForBackend($one_user['mail'][0]):"");
                                      $ldap_group = trim(('' != $group) ? $group : $this->GetDefaultUserGroup());
                                      if ($this->IsMultipleGroupsEnabled()) {
                                          $ldap_group = trim(('' != $user_in_groups) ? $user_in_groups : $this->GetDefaultUserGroup());
                                      }
                                      $ldap_description = $this->EncodeForBackend($description);
                                      $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 = trim(isset($one_user['distinguishedname'][0])?encode_utf8_if_needed($one_user['distinguishedname'][0]):"");
                                      $ldap_synchronized_dn = trim(isset($one_user['distinguishedname'][0])?$this->EncodeForBackend($one_user['distinguishedname'][0]):"");
                                      $ldap_language = mb_strtolower(mb_substr(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]) : "---");

                                      if (!$this->CheckUserExists($account, true, true)) { // $no_server_check = TRUE; $no_error = TRUE
                                      // User doesn't exist yet
                                          if ('' == $ldap_description) {
                                              $ldap_description = $account;
                                          }
                                          $result = $this->FastCreateUser($account,
                                                                          $ldap_email,
                                                                          $ldap_sms,
                                                                          -1, // Prefix pin needed
                                                                          $this->GetLdapDefaultAlgorithm(),
                                                                          $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()) {
                                              $description = $this->GetUserDescription();
                                              $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 = "";

                                              if (('' != $ldap_description) && ($description != $ldap_description)) {
                                                  $this->SetUserDescription($ldap_description);
                                                  $modified = TRUE;
                                                  $detailed_modif = (('' != $detailed_modif) ? ', ' : '') . "Description: $description -> $ldap_description";
                                              }
                                              
                                              if (('' != $ldap_email) && ($email != $ldap_email)) {
                                                  $this->SetUserEmail($ldap_email);
                                                  $modified = TRUE;
                                                  $detailed_modif = (('' != $detailed_modif) ? ', ' : '') . "Email: $email -> $ldap_email";
                                              }

                                              if ($enabled != $ldap_enabled) {
                                                  $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;
                                                }
                                              }

                                              $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 (function_exists('ldap_control_paged_result_response')) {
                          if (FALSE !== $ldap_connection->_oui_paged_sr) {
                            ldap_control_paged_result_response($ldap_connection->_conn_paged, $ldap_connection->_oui_paged_sr, $page_cookie);
                          }
                      }
                  } while ((!$ldap_sync_stop) && ($page_cookie !== null) && ($page_cookie != '') && ($ldap_connection->_oui_paged_sr !== FALSE));
                  // ldap pagination loop

                  if (function_exists('ldap_control_paged_result')) {
                      // Reset LDAP paged result
                      ldap_control_paged_result($ldap_connection->_conn_paged, $pageSize, 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, "wt")) {
                              $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_email = $one_user['email'];
                          $modified_group = 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++;
                              // 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, 19, 'System', '');
                                  $modified_counter++;
                              } elseif ($modified_enabled) {
                                  $this->SetUser($modified_user);
                                  $this->SetUserActivated(0);
                                  $this->WriteUserData(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 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, 19, '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, 19, 'LDAP', '');
                  }
              } else {
                  $this->WriteLog("Info: LDAP sync stopped", FALSE, FALSE, 19, '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, 30, '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   Write information in the log file/database, to the syslog server and on the screen
   *
   * @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, 39, 'System', '', 3);
      } elseif (('' != $this->GetLdapDomainControllers()) && ('' != $this->GetLdapBindDn()) && ('' != $this->GetLdapServerPassword())) {
          $domain_controllers = explode(" ",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(),
                                'network_timeout'    => $this->GetLdapNetworkTimeout(),
                                'port'               => $this->GetLdapPort(),
                                'recursive_groups'   => $this->IsLdapRecursiveGroups(),
                                'time_limit'         => $this->GetLdapTimeLimit(),
                                'use_ssl'            => $this->IsLdapSsl()
                               );

          if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
            define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032);
          }
          
          $domain_controllers = explode(" ",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 = array();
      $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 = array())
  {
    if (isset($options['multiple_groups'])) {
      $multiple_groups = (isset($options['multiple_groups']) ? (TRUE == $options['multiple_groups']) : FALSE);
    } else {
      $multiple_groups = $this->IsMultipleGroupsEnabled();
    }

    $group = trim($this->GetUserGroup());
    if (('' != $group) && ('' != $this->GetGroupAttribute())) {
      $group_array = explode("\t",trim(str_replace(",","\t",str_replace(";","\t",$group))));
      if ($multiple_groups) {
        foreach ($group_array as $one_group) {
          if ("" != 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].'"');
      }
    }

    if (('' != $this->GetLastClearOtpValue()) && ('' != $this->GetClearOtpAttribute())) {
      $this->AddReplyArrayForRadius($this->GetClearOtpAttribute().$this->GetRadiusReplyAttributor().'"'.$this->GetLastClearOtpValue().'"');
    }

    $dialin_ip_address = trim($this->GetUserDialinIpAddress());
    if (is_valid_ipv4($dialin_ip_address)) {
      $dialin_ip_mask = 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 = 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, 28, 'System', '');
          }
      }
  }


  function GetUsersFolder()
  {
      if ('' == $this->_users_folder) {
          $this->SetUsersFolder($this->GetScriptFolder()."users/");
      }
      return $this->ConvertToWindowsPathIfNeeded($this->_users_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, 28, '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, 28, '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, 28, '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 = 62; // ERROR: SMS provider not supported
      
      $sms_provider   = mb_strtolower((('' != $provider)?$provider:$this->GetSmsProvider()),'UTF-8');
      $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 = 18; // 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 = 18; // 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 = 61; // 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 = 99;
      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 = trim($this->GetSmsMessage());
          $sms_now_steps = $now_epoch;
          $sms_digits = $this->GetSmsDigits();
          $sms_seed_bin = hex2bin(md5('sMs'.$this->GetEncryptionKey().$this->GetUserTokenSeed().$user.$now_epoch));
          $sms_token = $this->GenerateOathHotp($sms_seed_bin,$sms_now_steps,$sms_digits);

          $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 = 60; // 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 = 65;
        $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 = 99;
      if ($this->IsEmailCodeAllowed()) {
        $now_epoch = time();
        if ('' != $user) {
          $this->SetUser($user);
        } else {
          $user = $this->GetUser();
        }
        $email = $this->GetUserEmail();
        if ('' != $email) {
          $steps = $now_epoch;
          $digits = $this->GetEmailDigits();
          $seed_bin = hex2bin(md5('EmaiL'.$this->GetEncryptionKey().$this->GetUserTokenSeed().$user.$now_epoch));
          $token = $this->GenerateOathHotp($seed_bin, $steps, $digits);

          $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
                                           )
                                     ) ? 18 : 68);
        } else {
          $result = 67; // 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 = 66;
        $this->WriteLog(array('text'       => "ERROR: Email code request not allowed",
                              'error_code' => $result,
                              'category'   => 'System',
                              'user'       => '')
                       );
      }
      return $result;
  }


  function SendEmail(
      $send_email_array = 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']:'';
      $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']:'';

      $result = FALSE;
      $actual_user = "";

      $specific_language = $this->GetLanguage();

      if ('' == $email) {
        $actual_user = ('' != $user)?$user:$this->GetUser();
        if ('' != $actual_user) {
          $this->ReadUserData($actual_user);
          $email = trim($this->GetUserEmail());
          $specific_language = $this->GetUserLanguage();
        }
      }
          
      if ('' != $language) {
        $specific_language = $language();
      } else {
        $language = $this->GetLanguage();
      }

      if ('' != $email) {
        if ('' == $from_email) {
            $from_email = $this->GetSmtpSender();
        }
        if ('' == $from_email) {
            $from_email = $email;
        }

        if ('' == $from_name) {
            $from_name = 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);
        }

        $text = html2text($content);
        
        $headers = "From: $from_email \r\n" . "X-Mailer: " . $this->_class.' '.$this->_version;

        $result = mail($email, $subject, $text, $headers);
      }
      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 == 14) || ($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 = trim($this->GetState());
      if ('' == $state) {
          $state = "multiOTP".mb_substr(md5($this->GetEncryptionKey().time().mt_rand(100000,999999)),0,24);
      }

      $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 = 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();
          }
      }

      // Force no prefix pin to true if the request come from a multiOTP Credential Provider
      if ($this->GetCredentialProviderMode() || $this->IsCredentialProviderRequest()) {
        $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 trim(str_replace('-','',$input))
      $input_to_check = $input;
      $real_user = $this->GetUser();
      // 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 (1 == intval($this->GetUserMultiAccount())) {
                  $pos = strrpos($input_to_check, " ");
                  if ($pos !== FALSE) {
                      $real_user = mb_substr($input_to_check,0,$pos);
                      $input_to_check = 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 ('' != $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 = 0;
          $this->WriteLog("Info: User ".$real_user." successfully logged in".$supplemental_login_info." using an external server", FALSE, FALSE, $result, 'User');
      } elseif (18 == $server_result) {
          $result = 18; // 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 ((21 == $server_result) && (!$this->IsKeepLocal())) {
          $this->DeleteUser($real_user, TRUE); // $no_error_info = TRUE
          $result = 21; // ERROR: User doesn't exist. (on the server)
          $this->WriteLog("Error: User ".$real_user." doesn't exist", FALSE, FALSE, $result, 'User');
      } elseif ((($server_result >= 0) && (22 <= $server_result) && (70 > $server_result)) || (90 <= $server_result)) {
          // 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 = 21; // 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 = 99; // Unknown error

          // multi account works only if authentication is done with PAP
          if (1 == intval($this->GetUserMultiAccount())) {
              $pos = strrpos($input_to_check, " ");
              if ($pos !== FALSE) {
                  $real_user = mb_substr($input_to_check,0,$pos);
                  $input_to_check = 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 = 34; // 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 = 38; // 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(" ",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(" ",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 = 81; // 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(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() === 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();
                              $this->WriteLog("Error: User $real_user verification failed, unreachable LDAP/AD server(s)", FALSE, FALSE, 99, '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_user, $code_to_check);
                      }
                  }
                  if ($ldap_check_passed || ($input_to_check === $code_to_check)) {
                      $result = 9; // 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 (18 == $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 = 28; // 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(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() === 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();
                              $this->WriteLog("Error: User $real_user verification failed, unreachable LDAP/AD server(s)", FALSE, FALSE, 99, '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_user, $code_confirmed);
                      if ($this->GetVerboseFlag()) {
                        $this->WriteLog("Debug: *CalculateMsChap2Response($real_user, $clear_code_confirmed) for SMS: $code_confirmed", false, false, 19, '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 = 0; // OK: This is the correct SMS token

                  if ($cache_result_enabled) {
                      $this->SetUserLastCachedCredential(trim($input.' '.$input_sync));
                      $this->SetUserLastLoginForCache($now_epoch);
                  }
                  $this->SetUserLastSuccessCredential(trim($input.' '.$input_sync));

                  if (!$this->WriteUserData()) {
                      $result = 28; // 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 (0 == $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(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() === 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();
                              $this->WriteLog("Error: User $real_user verification failed, unreachable LDAP/AD server(s)", FALSE, FALSE, 99, '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_user, $code_confirmed);
                      if ($this->GetVerboseFlag()) {
                        $this->WriteLog("Debug: *CalculateMsChap2Response($real_user, $clear_code_confirmed) for Email: $code_confirmed", false, false, 19, '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 = 0; // OK: This is the correct Email token

                  if ($cache_result_enabled) {
                      $this->SetUserLastCachedCredential(trim($input.' '.$input_sync));
                      $this->SetUserLastLoginForCache($now_epoch);
                  }
                  $this->SetUserLastSuccessCredential(trim($input.' '.$input_sync));

                  if (!$this->WriteUserData()) {
                      $result = 28; // 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 (0 == $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(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() === 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 {
                  // 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_user, $code_confirmed);
                      if ($this->GetVerboseFlag()) {
                        $this->WriteLog("Debug: *CalculateMsChap2Response($real_user, $clear_code_confirmed) for scratch password: $code_confirmed", false, false, 19, '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 = 0; // OK: This is the correct scratch token

                  if ($cache_result_enabled) {
                      $this->SetUserLastCachedCredential(trim($input.' '.$input_sync));
                      $this->SetUserLastLoginForCache($now_epoch);
                  }
                  $this->SetUserLastSuccessCredential(trim($input.' '.$input_sync));

                  if (!$this->WriteUserData()) {
                      $result = 28; // 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 (0 == $result) {
                      $this->AddExtraRadiusInfo();
                  }
                  return $result;
              }
          }

          // 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_user, $code_confirmed),'UTF-8');
              $code_confirmed_upper = mb_strtoupper($this->CalculateMsChap2Response($real_user, $code_confirmed_upper),'UTF-8');
              $code_confirmed_camel = mb_strtoupper($this->CalculateMsChap2Response($real_user, $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_user, $code_confirmed),'UTF-8');
              $code_confirmed_upper = mb_strtoupper($this->CalculateMsChap2Response($real_user, $code_confirmed_upper),'UTF-8');
              $code_confirmed_camel = mb_strtoupper($this->CalculateMsChap2Response($real_user, $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(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 = 0; // 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(trim($input.' '.$input_sync))) {
                          $disable_error_counter = true;
                      }
                  }
              }
          }


          // TODO check multiple tokens (loop)

          $algorithm         = mb_strtolower($this->GetUserAlgorithm(),'UTF-8');
          $pin               = $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     = $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 = 24; // 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 = 25; // 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(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() === 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', '');
                                  }
                              }
                          }
                      }
                  }
                  if (!$ldap_check_passed) {
                      $input_to_check = "LDAP_FAILED_".$input_to_check.'_LDAP_FAILED';
                      $result = 99;
                  }
              }

              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;
                      }
                      $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_user, $code_confirmed_without_pin),'UTF-8');
                                  $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_user, $code_confirmed),'UTF-8');
                                  if ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *CalculateMsChap2Response($real_user, $clear_code_confirmed) for motp: $code_confirmed", false, false, 19, 'Debug', '');
                                  }
                              }
                          }
                          
                          if (('' == $input_sync) && (!$resync_enc_pass)) {
                              // 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 = 0; // OK: This is the correct token
                                      $this->WriteLog("Ok: User ".$real_user." successfully logged in".$supplemental_login_info." with mOTP token", FALSE, FALSE, $result, 'User');
                                  } else {
                                      $result = 26; // ERROR: this token has already been used
                                      if ($this->CompareUserLastSuccessCredential(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 = 14; // INFO: token is now synchronized
                                  $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 = 0; // INFO: authentication is successful, regardless of the PIN code if needed, as the PIN code is already used to generate the token
                              } else {
                                  $result = 27; // 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) && (90 <= $result));
                      if ($display_status) {
                          echo "\r\n";
                      }
                      if (90 <= $result) {
                          if ($this->CompareUserLastFailedCredential(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_user, $code_confirmed_without_pin),'UTF-8');
                                  $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_user, $code_confirmed),'UTF-8');
                                  if ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *CalculateMsChap2Response($real_user, $clear_code_confirmed) for hotp: $code_confirmed", false, false, 19, '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 = 0; // 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 = 26; // ERROR: this token has already been used
                                      if ($this->CompareUserLastSuccessCredential(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 = 14; // 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 = 0; // INFO: authentication is successful, as the prefix has also been typed (if any)
                                  }
                              } else {
                                  $result = 27; // 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) && ((90 <= $result)));
                      if ($display_status) {
                          echo "\r\n";
                      }
                      if (90 <= $result) {
                          if ($this->CompareUserLastFailedCredential(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 = bin2hex(mb_substr($decrypted_part,  0, 6));
                          $result = $yubikey_class->CheckYubicoOtp(mb_substr($input_to_check, -32),
                                                                   $seed,
                                                                   $last_event,
                                                                   $private_id);
                      }

                      if (0 == $result) {
                          $calculated_token = $input_to_check;
                          $this->SetUserLastLogin($now_epoch);
                          $this->SetUserTokenLastLogin($now_epoch);
                          $this->SetUserTokenLastEvent($yubikey_class->GetYubicoOtpLastCount());
                          $this->SetUserErrorCounter(0);
                          $result = 0; // 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 (26 == $result) {
                          $result = 26; // ERROR: this token has already been used
                          if ($this->CompareUserLastSuccessCredential(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(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;
                      }
                      $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_user, $code_confirmed_without_pin),'UTF-8');
                                  $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_user, $code_confirmed),'UTF-8');
                                  if ($this->GetVerboseFlag()) {
                                    $this->WriteLog("Debug: *CalculateMsChap2Response($real_user, $clear_code_confirmed) for totp: $code_confirmed", false, false, 19, '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 = 0; // OK: This is the correct token
                                      $this->WriteLog("OK: User ".$real_user." successfully logged in".$supplemental_login_info." with TOTP token", FALSE, FALSE, $result, 'User');
                                  } else {
                                      $result = 26; // ERROR: this token has already been used
                                      if ($this->CompareUserLastSuccessCredential(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 = 14; // INFO: token is now synchronized
                                  $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 = 0; // INFO: authentication is successful, as the prefix has also been typed (if any)
                                  }
                              } else {
                                  $result = 27; // 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) && (90 <= $result));
                      if ($display_status) {
                          echo "\r\n";
                      }
                      if (90 <= $result) {
                          
                          // 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_user, $code_confirmed_without_pin),'UTF-8');
                                      $code_confirmed = mb_strtolower($this->CalculateMsChap2Response($real_user, $code_confirmed),'UTF-8');
                                      if ($this->GetVerboseFlag()) {
                                        $this->WriteLog("Debug: *CalculateMsChap2Response($real_user, $clear_code_confirmed) for totp: $code_confirmed", false, false, 19, 'Debug', '');
                                      }
                                  }
                              }
                             
                              if (('' == $input_sync) && (!$resync_enc_pass) && ($input_to_check === $code_confirmed)) {
                                  $result = 93; // ERROR: Authentication failed (time based token probably out of sync)
                              } else {
                                  $check_step++;
                              }
                          } while (($check_step < (10 * $max_steps)) && (93 != $result));
                          // END to give an info if time based token is probably out of sync (in a window 10 time bigger)

                          
                          if ($this->CompareUserLastFailedCredential(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';
                      $result = 92; // ERROR: Authentication failed (bad password)
                      if ($input_is_empty) {
                          $input_to_check = '';
                      }

                      $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 != $input_to_check) { // if ($pin != mb_substr($input_to_check, 0, mb_strlen($pin))) {
                                  $this->SetLastClearOtpValue($input_to_check);
                                  $input_to_check.= '_BAD_PREFIX';
                                  $bad_precheck = TRUE;
                              }
                          } 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 = 0; // 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(trim($input.' '.$input_sync))) {
                              $disable_error_counter = true;
                          }
                          if (!$disable_error_counter) {
                              $this->SetUserErrorCounter($error_counter+1);
                          }
                          $this->SetUserTokenLastError($now_epoch);
                      }
                      break;
                  default:
                      $result = 23;
                      $this->WriteLog("Error: ".$algorithm." algorithm is unknown", FALSE, FALSE, $result, 'User');
              }
              if (90 <= $result) {
                  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 (0 == $result) {
              $this->SetUserLocked(0);
          }
          
          if (90 <= $result) {
              $replayed_text = (($disable_error_counter) ? " (same token replayed)" : "");
              if (("" == $replayed_text) && (93 == $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 (92 == $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 = 98;
                      $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 = 98;
                  $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 (0 == $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(trim($input.' '.$input_sync));
                  $this->SetUserLastLoginForCache($now_epoch);
              }
              $this->SetUserLastSuccessCredential(trim($input.' '.$input_sync));
          } else {
              $this->SetUserLastFailedCredential(trim($input.' '.$input_sync));
          }

          if (!$this->WriteUserData()) {
              $result = 28; // 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 (0 == $result) {
          $this->AddExtraRadiusInfo();
      }
      return $result;
  }


  function SelfRegisterHardwareToken(
      $user,
      $serial,
      $input,
      $original_input = ''
  ) {
      // TODO the whole process has to be changed to support multi tokens
      $result = 99; // 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 = 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()) {
        $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 = trim($this->GetTokenAttributedUsers());
              if ('' != trim($attributed_users)) {
                  if (FALSE === mb_strpos(','.$attributed_users.',', ','.$user.',')) {
                      $result = 37; // ERROR: Token already attributed
                      $this->WriteLog("Error: Token ".$this->GetToken()." already attributed", FALSE, FALSE, $result, 'Token', $user);
                  }
                  // else $result = 99; // 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(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() === 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', '');
                                      }
                                  }
                              }
                          }
                      }
                      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 = 99;
                      }
                  }

                  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 = 0; // OK: This is the correct token
                                  } else {
                                      $result = 26; // ERROR: this token has already been used
                                  }
                              } else {
                                  $check_step++;
                              }
                          } while (($check_step < $max_steps) && (90 <= $result));
                          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 = 0; // OK: This is the correct token
                              } else {
                                  $check_step++;
                              }
                          } while (($check_step < $max_steps) && (90 <= $result));
                          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 = bin2hex(mb_substr($decrypted_part,  0, 6));
                              $result = $yubikey_class->CheckYubicoOtp(mb_substr($input_to_check, -32),
                                                                       $seed,
                                                                       $last_event,
                                                                       $private_id);
                          }
                          if (0 == $result) {
                              $calculated_token = $input_to_check;
                              $this->SetTokenLastLogin($now_epoch);
                              $this->SetTokenLastEvent($yubikey_class->GetYubicoOtpLastCount());
                              $this->SetTokenErrorCounter(0);
                              $result = 0; // 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 = 0; // OK: This is the correct token
                                  } else {
                                      $result = 26; // ERROR: this token has already been used
                                  }
                              } else {
                                  $check_step++;
                              }
                          } while (($check_step < $max_steps) && (90 <= $result));
                          break;
                      default:
                          if ($this->GetVerboseFlag()) {
                            $this->WriteLog("Debug: *Invalid algorithm (SelfRegisterHardwareToken): $algorithm", FALSE, FALSE, 8888, 'Debug', '');
                          }
                          $result = 23; // ERROR: Invalid algorithm
                  }

                  if (90 <= $result) {
                      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 = 98;
                              $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 (0 == $result) {
                      $this->AddTokenAttributedUsers($user);
                      if (!$this->WriteTokenData()) {
                          $result = 28; // 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 = 28; // 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, 19, 'User');
                          }
                      }
                  }
              }
          } else {
              $result = 29; // ERROR: Token doesn't exist
              $this->WriteLog("Error: Token ".$this->GetToken()." does not exist", FALSE, FALSE, $result, 'Token');
          }
      } else {
          $result = 29; // 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 = 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, 29, '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 = array();
              $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[0][$KeyPackage_tag] as $keypackage) // this is not working well in PHP4
              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):30);
                  $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(trim(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(bin2hex($Secret));
                          
                          if ($this->CheckTokenExists('', false)) {
                              $this->WriteLog("Info: Token ".$this->GetToken()." already exists", FALSE, FALSE, 32, 'Token', '');
                          } else {
                              $result = $this->WriteTokenData() && $result;
                              $this->AddLastImportedToken($this->GetToken());
                              $this->WriteLog("Info: Token with SerialNo ".$SerialNo." successfully imported", FALSE, FALSE, 15, '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, 32, '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, 29, 'Token', '');
          $result = FALSE;
      } else {
          //Get the document loaded into a variable
          if ($file_handler = @fopen($yubikey_file, "rt")) {

              $yubikey_class = new MultiotpYubikey();
              
              while (!feof($file_handler)) {
                  $line = 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 (trim($line_array[0])) {
                          case 'Yubico OTP':
                              $algorithm = 'yubicootp';
                              break;
                          case 'OATH-HOTP':
                              $algorithm = 'hotp';
                              break;
                          default:
                              $algorithm = "";
                      }
                      $esn = trim($line_array[3]); // modhex
                      if (('hotp' == $algorithm) && (0 == intval($line_array[10]))) {
                          if (1 == intval($line_array[8])) {
                              $esn = mb_substr(trim($line_array[3]),0,2).$yubikey_class->ModHexToHex(mb_substr(trim($line_array[3]),2));
                          } elseif (1 == intval($line_array[9])) {
                              $esn = mb_substr(trim($line_array[3]),0,4).$yubikey_class->ModHexToHex(mb_substr(trim($line_array[3]),4));
                          } else {
                              $esn = $yubikey_class->ModHexToHex(trim($line_array[3]));
                          }
                      }
                      $private_id = "";
                      $seed = 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 = 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(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, 32, 'Token', '');
                          } elseif (!$this->IsValidAlgorithm($algorithm)) {
                              $this->WriteLog("Error: The algorithm ".$algorithm." is not recognized", FALSE, FALSE, 32, 'Token', '');
                          } elseif ($this->CheckTokenExists('', false)) {
                              $this->WriteLog("Info: Token ".$this->GetToken()." already exists", FALSE, FALSE, 32, 'Token', '');
                          } else {
                              $result = $this->WriteTokenData() && $result;
                              $this->AddLastImportedToken($this->GetToken());
                              $this->WriteLog("Info: Token ".$this->GetToken()." successfully imported", FALSE, FALSE, 15, '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, 29, 'Token', '');
          $result = FALSE;
      } else {
          //Get the document loaded into a variable
          if ($file_handler = @fopen($csv_file, "rt")) {
              while (!feof($file_handler)) {
                  $line = 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               = 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(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, 32, 'Token', '');
                      } elseif (!$this->IsValidAlgorithm($algorithm)) {
                          $this->WriteLog("Error: The algorithm ".$algorithm." is not recognized", FALSE, FALSE, 32, 'Token', '');
                      } elseif ($this->CheckTokenExists('', false)) {
                          $this->WriteLog("Info: Token ".$this->GetToken()." already exists", FALSE, FALSE, 32, 'Token', '');
                      } else {
                          $result = $this->WriteTokenData() && $result;
                          $this->AddLastImportedToken($this->GetToken());
                          $this->WriteLog("Info: Token ".$this->GetToken()." successfully imported", FALSE, FALSE, 15, 'Token', '');
                      }
                      $this->ResetTokenArray();
                  }
              }
              fclose($file_handler);
          } else {
              $this->WriteLog("Error: Tokens definition file ".$csv_file." cannot be read", FALSE, FALSE, 29, '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, 29, '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 = array();
          
          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 = bin2hex(base64_decode($device->key[0]->data[0]->secret[0]->plainvalue[0]->tagData));
                      }

                      if ('' == trim($serialno)) {
                          $serialno = trim($keyid);
                      }
                      $this->SetToken($serialno);
                      $this->SetTokenDescription(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, 32, 'Token', '');
                      } else {
                          $result = $this->WriteTokenData() && $result;
                          $this->AddLastImportedToken($this->GetToken());
                          $this->WriteLog("Info: Token with keyid ".$keyid." successfully imported", FALSE, FALSE, 15, '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, 29, '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 = array();
          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(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, 32, 'Token', '');
                      } else {
                          $result = $this->WriteTokenData() && $result;
                          $this->AddLastImportedToken($this->GetToken());
                          
                          $this->WriteLog("Info: Token with serial number ".$serialno." successfully imported", FALSE, FALSE, 15, '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, 29, 'Token', '');
          $result = FALSE;
      } else {
          // SafeWord Authenticator Records
          
          //Get the document loaded into a variable
          if ($file_handler = @fopen($data_file, "rt")) {

              $line = 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 = trim(fgets($file_handler));
                      $line_array = explode(":",$line,2);
                      $line_array[0] = trim($line_array[0]);
                      $line_array[1] = 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(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] = trim($attribute_array[0]);
                                  $attribute_array[1] = 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, 32, '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, 15, '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, 29, 'Token', '');
          $result = FALSE;
      } else {
          // Authenex Authenticator Records
          
          //Get the document loaded into a variable
          if ($file_handler = @fopen($data_file, "rt")) {
          
              $line = 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 = trim(fgets($file_handler));

                      if (FALSE !== mb_strpos(mb_strtoupper($line,'UTF-8'), 'INSERT INTO OTP')) {
                          $token_array = 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(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, 32, 'Token', '');
                          } else {
                              $result = $this->WriteTokenData() && $result;
                              $this->AddLastImportedToken($this->GetToken());
                              $this->WriteLog("Info: Token ".$this->GetToken()." successfully imported", FALSE, FALSE, 15, '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
   *
   * 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
   *
   * @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
  ) {
    $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;
      }
      
      if (is_valid_ipv4($this->_device_data['ip_or_fqdn'])) {
        if ($this->IsDeveloperMode()) {
          $this->WriteLog("Info: 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("Info: 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, 28, 'System', '');
              }
          } else {
              $result = unlink($this->GetDdnsFolder().$ddns_filename);
              if ($result) {
                  if ($this->GetVerboseFlag()) {
                      $this->WriteLog("Info: *Ddns ".$ddns." successfully deleted", FALSE, FALSE, 19, 'Ddns', '');
                  }
              } else {
                  if (!$no_error_info) {
                      $this->WriteLog("Error: Unable to delete ddns ".$ddns, FALSE, FALSE, 28, '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.": ".trim($this->_mysqli->error), FALSE, FALSE, 28, '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, 28, '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, 28, 'System', '');
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *Ddns ".$ddns." successfully deleted", FALSE, FALSE, 19, 'Ddns', '');
                              }
                              $result = TRUE;
                          }
                      }
                  }
                  break;
              case 'pgsql':
                  if ($this->OpenPGSQLDatabase()) {
                      $ddns_escaped = pg_escape_string($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(), FALSE, FALSE, 28, '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, 28, 'System', '');
                              }
                          } else {
                              if ($this->GetVerboseFlag()) {
                                  $this->WriteLog("Info: *Ddns ".$ddns." successfully deleted", FALSE, FALSE, 19, '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 = array();
      $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
          $file_handler = fopen($alternate_file, "rt");
          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)) && ("" != 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;
          $file_handler = fopen($this->GetDdnsFolder().$filename, "rt");
          $first_line = 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)) && ("" != 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 = array()
  ) {
      if ('' == 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'           => 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, 39, 'System', '', 3);
              }
          } else {
              if ($file_handler = @fopen($this->GetDevicesFolder().$device_filename, "rt")) {
                  $first_line = trim(fgets($file_handler));
                  
                  while (!feof($file_handler)) {
                      $line = 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 ('' != 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, 33, '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) : ".trim($this->_mysqli->error).' ', TRUE, FALSE, 40, '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, 40, '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]);
                                  } else