[
  {
    "path": ".gitignore",
    "content": "index.php\n\ncoverage/**/*\nnbproject/**/*\nvendor/**/*\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2014-2015 Jorge Cobis <jcobis@gmail.com>\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# TAD-PHP\n\nA simple PHP class to interacts with ZK Time & Attendance Devices.\n\n##About\n\nTAD: A class that implements an interface to interacts with ZK Time & Attendance devices.\n\nDocumentation found about ZK SOAP api is very limited or poor, however TAD class implements most SOAP functions supported by ZK devices. Specifically TAD class exposes the following 35 methods:\n\n```\nget_date, get_att_log, get_user_info, get_all_user_info, get_user_template, get_combination, get_option, get_free_sizes, get_platform, get_fingerprint_algorithm, get_serial_number, get_oem_vendor, get_mac_address, get_device_name, get_manufacture_time, get_antipassback_mode, get_workcode, get_ext_format_mode, get_encrypted_mode, get_pin2_width, get_ssr_mode, get_firmware_version, set_date, set_user_info, set_user_template, delete_user, delete_template, delete_data, delete_user_password, delete_admin, enable, disable, refresh_db, restart, and poweroff.\n```\nAll methods above are implemented by 2 classes: **Providers\\TADSoap** and **Providers\\TADZKLib**.\n\nThere are some SOAP functions supported by ZK devices that it's suppossed,  according to the official docs (which incidentally it's very limited and so poor!!!) must show an expected behaviour, but when they are invoked don't work like is expected, so they become useless (e.g. Restart SOAP call). For these situations, TAD class implement them by **Providers\\TADZKLib** class ([PHP_ZKLib] - http://dnaextrim.github.io/php_zklib/). This class takes a different approach to \"talk to the device\": it uses UDP protocol at device standard port 4370.\n\nPHP_ZKLib class it's been fully integrated, after a refactoring process taking out all duplicated code (DRY).\n\nFor practical purposes, you don't have to be worried about when to use TAD class or PHP_ZKLib class because you only have to get a TAD instance (as shown below) and call any of its methods available. The class decides about when runs the method invoked using TAD class or PHP_ZKLib class.\n\n##Requirements\n* Any flavour of PHP 5.4+\n* PHPUnit to execute the test suite (optional).\n\n## Supported devices\n\n* All ZK Time & Attendance devices with web server built-in (with ZEM600 or less).\n\n##Getting started\n###Setting up the environment\nAfter download TAD-PHP, you have 2 ways to get your enviroment configured to use the classes:\n\n####Composer\n\n[Composer](https://getcomposer.org) is the PHP's package manager and is the recommended way to get packages for your projects. It's also able to build automatically ***autoloaders*** if you wrote down your code using PSR-0 and/or PSR-4 standards, avoiding you headaches about everything related to loading classes.\n\n**TADPHP** is built follows PSR-4 standard and comes with a specific file named **composer.json** that allows **Composer** to generate a file named **autoload.php** (beside others files of course). This files generated is the only one you need to include in your project to get all classes required by TADPHP loaded in memory:\n\n1. Install Composer:\n\t```\n    curl -s https://getcomposer.org/installer | php\n\t\n\t```\n\n2. Get inside TADPHP root folder and generate the **autoload.php** file:\n\t```\n    php composer.phar dump-autoload\n    ```\n    The command above will generate a folder called **vendor**. Inside of it, you'll see the **autoload.php**\n    \n3. Require/Include **autoload.php** file in the **index.php** of your project or whatever file you need to use **TAD-PHP** classes:\n\t```php\n    <?php\n    require 'vendor/autoload.php';\n    ...\n    \n    ```\n    \n####Loading TAD-PHP classes by hand\nEven if Composer it's the preferred method to generate the files needed to get all classes loaded, maybe you want to do the task by hand:\n\n1. Copy and paste TAD-PHP folder in your project root.\n\n2. Rename TAD-PHP folder to use a shorter name (for example 'tad').\n\n3. Require/Include all classes required by TAD-PHP using the relative TAD-PHP path\n\n\t```php\n    <?php\n\trequire 'tad/lib/TADFactory.php';\n    require 'tad/lib/TAD.php';\n\trequire 'tad/lib/TADResponse.php';\n\trequire 'tad/lib/Providers/TADSoap.php';\n\trequire 'tad/lib/Providers/TADZKLib.php';\n\trequire 'tad/lib/Exceptions/ConnectionError.php';\n\trequire 'tad/lib/Exceptions/FilterArgumentError.php';\n\trequire 'tad/lib/Exceptions/UnrecognizedArgument.php';\n\trequire 'tad/lib/Exceptions/UnrecognizedCommand.php';\n    \n    ```\n    \n####Handling namespaces\nAll TAD-PHP classes are under the namespace named **TADPHP**. So, to use any class you need to use the **Fully qualified class name**. For example, to get a new instance of **TADFactory class** you need to use:\n\n```php\n<?php\n...\n$tad_factory = new TADPHP\\TADFactory();\n...\n```\n\nHowever, as your project grows up using fully qualified class names becomes annoying, so it's better to use PHP **USE** sentence:\n\n```php\n<?php\n...\nuse TADPHP\\TADFactory;\nuse TADPHP\\TAD;\n...\n\n$comands = TAD::commands_available();\n$tad = (new TADFactory(['ip'=>'192.168.100.156', 'com_key'=>0]))->get_instance();\n...\n```\n\n###Class instantiation\nFirst, instantiate a TADFactory object, then use it to create a TAD object.\n```php\n<?php\n...\n$tad_factory = new TADFactory(['ip'=>'192.168.0.1']);\n$tad = $tad_factory->get_instance();\n...\n```\nOr you can get a TAD object in one single step (valid only in PHP 5.4+):\n```php\n<?php\n  $tad = (new TADFactory(['ip'=>'192.168.0.1']))->get_instance();\n```\nYou can customize TAD object traits passing an options array:\n```php\n<?php\n  $options = [\n    'ip' => '192.168.0.1',   // '169.254.0.1' by default (totally useless!!!).\n    'internal_id' => 100,    // 1 by default.\n    'com_key' => 123,        // 0 by default.\n    'description' => 'TAD1', // 'N/A' by default.\n    'soap_port' => 8080,     // 80 by default,\n    'udp_port' => 20000      // 4370 by default.\n    'encoding' => 'utf-8'    // iso8859-1 by default.\n  ];\n  \n  $tad_factory = new TADFactory($options);\n  $tad = $tad_factory->get_instance();  \n```\n##TAD API\nSOAP API is implemented by **TADSoap class**. All methods that use UDP Protocol are implemented by **PHP_ZKLib class**. Even though you have 2 classes, you do not have to be worried about which method is been calling using SOAP api or through PHP_ZKLib. You've got a single interface.\n\nSome methods need that you set up some parameters prior you can call them. TAD class uses associative arrays as way to pass params to the methods. Using associative arrays is a \"more verbose way\" that helps you to remember which params you have to pass.\n\nValid params supported by TAD class are:\n\n```\ncom_key, pin, time, template, name, password, group, privilege, card, pin2, tz1, tz2, tz3, finger_id, option_name, date, size, valid, value\n```\n\nAs you can see, params names are so intuitive and easy to remember.\n\n######Note: All examples shown below asusmes $tad variable holds a TAD object.\n\n###Getting a list of commands available\n```php\n// Get a full list of commands supported by TADPHP\\TAD class.\n$commands_list = TAD::commands_available();\n\n// Get a list of commands implemented via TADPHP\\TADSoap.\n$soap_commands =  = TAD::soap_commands_available();\n\n// Get a list of commands implemented via TAD\\PHP\\TADSoap.\n$zklib_commands = TAD::zklib_commands_available();\n```\n\n###Getting and Setting Date and Time\n\n```php\n// Getting current time and date\n$dt = $tad->get_date();\n\n// Setting device's date to '2014-01-01' (time will be set to now!)\n$response = $tad->set_date(['date'=>'2014-01-01']);\n\n// Setting device's time to '12:30:15' (date will be set to today!)\n$response = $tad->set_date(['time'=>'12:30:15']);\n\n// Setting device's date & time\n$response = $tad->set_date(['date'=>'2014-01-01', 'time'=>'12:30:15']);\n\n// Setting device's date & time to now.\n$response = $tad->set_date();\n```\n###Getting attendance logs\nYou can retrieve attendance logs for all user or just for one:\n```php\n// Getting attendance logs from all users.\n$logs = $tad->get_att_log();\n\n// Getting attendance logs from one user.\n$logs = $tad->get_att_log(['pin'=>123]);\n```\n###Getting information about users.\nYou can get all information about a single user or all users. The information you can get include:\n\n* PIN: Internal user's ID (this is an id generated by the device).\n* Name: User's name.\n* Password: Password used to check in/out.\n* Card: Card number (relevant if you device supports RFID technology).\n* Privilege: User's role (1: regular user, 2: enroller, 6: admin and 14: superadmin)\n* Group: User's group privilege.\n* PIN2: Personal identity number (this is an id you can set according your needs).\n* TZ1: User's time zone 1.\n* TZ2: User's time zone 2.\n* TZ3: User's time zone 3:\n\n```php\n// Getting info from a specific user.\n$user_info = $tad->get_user_info(['pin'=>123]);\n\n// Getting info from all users.\n$all_user_info = $tad->get_all_user_info();\n```\n###Creating / Updating users\nTAD class allows you to register new users in the device or even you can update (change) information about an user already registered. However to achieve this, TAD class needs to delete the user (of course this applies when you are updating user's information) and then creates the user. Maybe this is not the best way to do that, but if TAD just calls the method to create a user, it will be created as many times as you call it.\n\nIf you look into PHP_ZKLib code, you'll see a method to create / update users. However, when you call that method, it generates a PIN code (not PIN2 code) in a way that if that code already exists in the device, it refuses to create the user. This is a method that should be modified to make it working properly but the way how PIN code is created is unknown.\n\nIn the meantime, TAD class uses delete and create SOAP calls. Of course, to make things easy for you, you have to call just 1 method.\n```php\n// We are creating a superadmin user named 'Foo Bar' with a PIN = 123 and password = 4321.\n$r = $tad->set_user_info([\n    'pin' => 123,\n    'name'=> 'Foo Bar',\n    'privilege'=> 14,\n    'password' => 4321\n]);\n```\n######Note: The way TAD class creates / updates users has one big concern you have to be aware. The user's fingerprints stored (templates) are lost!!!, so it is necessary to save them prior you call set_user_info() method.\n\n###Uploading user's fingerprints\nThe device uses an algorithm to encode fingerprints called \"BioBridge\" and it has 2 flavors: VX 9.0 and the new one VX 10.0. According the documentation, VX 10.0 generates shorter encoded fingerprints and it's faster when the device has to make searchings for a fingerprint match process. However, TAD class exposes a method to upload fingerprints but it works only when device is configured to use the old BioBridge VX 9.0 algorithm. When device uses VX 10.0 algorithm, the machine freezes!!!. When asked to ZK Software forum, the answer got was: \"It has to work with any biobridge version. Check your code!\".  Any help about this, would be appreciated.\n\n```php\n/** Setting a user template (fingerprint).\n * \n * You can upload until 10 templates per user. You have to use 'finger_id' param to indicate\n * which fingerprint you are uploading to.\n * \n * Till now, this method only works with VX 9.0 BioBridge algorithm :-(. Any help\n * arround this issue will be appreciated. \n */\n \n// The folowing string represents a fingerprint encoded using BioBridge algorithm VX 9.0\n$template1_vx9 = \"ocosgoulTUEdNKVRwRQ0I27BDTEkdMEONK9KQQunMVSBK6VPLEENk9MwgQ+DP3PBC1FTXEEG4ihpQQQ3vFQBO4K+WwERYilHAQ8ztktBEBbKQ0ELDtJrwQ7dqCiBCz+/IgEGKrBjQQhEO0zBFQNDQYEKFbhrQQdLF1wBDxclfUELMNFXwQRvvmHBCslKUAEZfU1OQRzmIU5BXRW0eoEKPMltgQnQGUyBJQSfRIEUSzIdAQ45l3gBByHUTMEJ5yVhQQmi0UZBFHvYPUEGeKxTAQ6rFGNBCIYURoEOZS9VwR+1M4RoE5m0DRUTF8DHd6HdqxHAxWmj393M28DDX2FkanKi/t7LGsDCWqGarmt1BaL/25nAwVaiipu/cgcQGKG6mcDBU6KYmr5wChQcobmJIsDBUKKJmZ1uExyi+ZaYwMFMgU2CQCSinYdnJsDBR4Ghl3Q4owa3dnfAwUamdlZlR5p2Zi7AwUSndERlfOpWZlfAwUOiQzVkLDhDopRUVTLAwT2iQ0ZjIzVMolNFRcDBN6I0ZlQebVaiEjRVwMEyolVVUxVxXKEBRUTAwS+iZVYyD3JhoQJFTMDBLKJlVUIKcWShBVVTwMIkoWVkFQhyaaEVZ1rAwh6hVlUPAW+iNGd3wMIToWdlBnWiRWZ3aMDDCqRmZjRpZmrAxASjd2Vnh2/gAA==\";\n\n$template1_data = [\n  'pin' => 123,\n  'finger_id' => 0, // First fingerprint has 0 as index.\n  'size' => 514,    // Be careful, this is not string length of $template1_vx9 var.\n  'valid' => 1,\n  'template' => $template1_vx9\n];\n\n$tad->set_user_template( $template1_data );\n```\n\n###Deleting user's fingerprints\nWhen you have to delete user's fingerprints, you delete all of them. You cannot delete fingerprint one by one.\n\n```php\n// Delete all fingerprints (template) for user with PIN = 123.\n$tad->delete_template(['pin'=>123]);\n```\n\n###Deleting user's passwords\n```php\n// Delete password for user with PIN = 123.\n$tad->delete_user_password(['pin'=>123]);\n```\n\n###Deleting users\n```php\n/// Delete user with PIN = 123.\n$tad->delete_user(['pin'=>123]);\n```\n\n###Deleting admin users\nFrom the device point of view, users that have a privilege level not equal to 0, are considered admin users, so this method enables you wipe them out with one single step.\n```php\n$tad->delete_admin();\n```\n\n###Clearing out data from device\nYou can clear information from device according a code you specify.\n\nCode meaning:\n\n1: clear all device data, 2: clear all users templates and 3: clear all attendance logs.\n```php\n// Delete all attendance logs stored.\n$tad->delete_data(['value'=>3]);\n```\n\n###Getting some statistics from the device.\nYou can get some valuable statistics about your device including:\n\n * Space available for templates. \n * Space available for attendance logs.\n * Total storage capacity for attendance logs.\n * Total storage capacity for user templates.\n * Total users stored\n * Total user passwords stored.\n * Total attendance logs stored.\n * Total templates stored. \n\n```php\n// Get some device statistics.\n$fs = $tad->get_free_sizes();\n```\n###Disabling / Enabling the device\nSometimes you need to lock device's screen and keypad to prevent users can use it. You can disable the device and when you want to get it back working, just enable it!\n```php\n// Disabling device.\n$tad->disable();\n\n...\n\n// Enabling device.\n$tad->enable();\n```\n\n###Restarting / Shutting down the device.\n```php\n// Restart the device!!!\n$tad->restart();\n\n...\n\n// Shut down the device!!!\n$tad->poweroff();\n```\n\n##TADResponse API\nEvery command executed via TAD class will return an object that is an instance of **TADResponse** class. This object contains all information about the device's response received. This way you can get full flexibility to manipulate the responses that you got from the device: you can transform it in XML, JSON or even you can get an array. Also you can set some criterias to make a filtering process on the response.\n\n**TADResponse** class offers 14 methods:\n```\nget_response, set_response, get_encoding, set_encoding, get_header, set_header, get_response_body, to_xml, to_json, to_array, count, is_empty_response, filter_xml and filter_by\n\n```\n### Getting and Setting responses\nWhen you call any TAD method, all responses (including those empty ones) are returned as an **TADResponse object**, so you can invoke any of methods mentioned above:\n```php\n$r= $tad->get_date();\n\n// Get response in XML format.\n$xml_date = $r->get_response();\n\n// Or you can specify the format.\n$json_date = $r->get_response(['format'=>'json']);\n\n// Or you want to get just response's body (without XML header).\n$raw_response = $r->get_response_body(); // This always will be in XML format!!!\n```\nSometimes you would like to build a TADResponse object by hand:\n```php\n$response = '<Response><Name>Foo</Name><Address>Bar</Address></Response>';\n$encoding = 'iso8859-1';\n\n$tr = new TADResponse($response, $encoding);\n...\n// Maybe later you want to change the response you set before\n$new_response = '<CrazyResponse><Joke>foo bar taz</Joke></CrazyResponse>';\n$tr->set_response($new_response);\n...\n```\n###Getting and Setting response's encoding\n```php\n$r = $tad->get_date();\n\n// Get current response's encoding.\n$encoding = $r->get_encoding();\n\n// Perhaps you want to change response's encoding.\n$r->set_encoding('utf-8');\n```\n\n###Getting and Setting response's header\nInstead of getting a full XML response, you can get just the header's response and you can change it even:\n```php\n$r = $tad->get_date();\n\n$header = $r->get_header();\n// Method above returns '<?xml version=\"1.0\" encoding=\"iso8859-1\" standalone=\"no\"?>'\n\n$new_header = '<?xml version=\"1.1\" encoding=\"utf-8\" standalone=\"yes\"?>';\n\n// Lets set a new response's header.\n$r->set_header($new_header);\n```\n\n###Transform device's responses in XML, JSON or Array format.\nAs you seen above, you can get device's responses in different formats using **get_response() method** and specifying the format you want. However, you can use the following methods too:\n```php\n// Get attendance logs for all users.\n$att_logs = $tad->get_att_logs(); // $att_logs is an TADResponse object.\n\n// Get response in XML format.\n$xml_att_logs = $att_logs->to_xml();\n\n// Get response in JSON format.\n$json_att_logs = $att_logs->to_json();\n\n// Get an array from response.\n$array_att_logs = $att_logs->to_array().\n\n// Lets get an XML response in one single step.\n$xml = $tad->get_att_logs()->to_xml();\n``` \n\n###Counting how many items has the response\nWhen you are interested just in how many items has the response, just count them:\n```php\n$att_logs = $tad->get_att_logs();\n\n// Get just the number of logs you retrived.\n$logs_number = $att_logs->count();\n```\n\n###Dealing with empty responses\nSometimes some queries to the device returns an empty answer. Because the original response from the device is in XML format, to know if you got any relevant data, you should have to parse the responses. That's not very handy:\n```php\n$r = $tad->get_att_logs(['pin'=>123]); // This employee does not have any logs!!!\n\nif ($r->is_empty_response()) {\n    echo 'The employee does not have logs recorded';\n}\n...\n```\n\n###Filtering response according your needs!!!\nAs you saw above, all device's responses are handled by **TADResponse class**. You get the raw XML but you always get the whole set. What if you you'd like to do some kind of processing on reponses? Now, you can process the whole XML response by applying filters. This way, you can get just XML responses that really needs.\n\n```php\n// Get attendance logs for all users;\n$att_logs = $tad->get_att_logs();\n\n// Now, you want filter the resulset to get att logs between '2014-01-10' and '2014-03-20'.\n$filtered_att_logs = $att_logs->filter_by_date(\n    ['start' => '2014-01-10','end' => '2014-03-20']\n);\n\n// Maybe you need to be more specific: get logs of users with privilege of Enroller\n// that generated a 'check-out' event after '2014-08-01'.\n$filtered_att_logs = $att_logs->filter_by_privilege_and_status_and_date(\n    2, // 2 = Enroller role.\n    1, // 1 = Check-out.\n    ['start'] => '2014-08-01'\n);\n\n// You can do specific string searching too!!!\n$users = $tad->get_all_user_info();\n\n// Searches for all employees with the string 'Foo' as part of its name.\n$foo_employees = $users->filter_by_name(['like'=>'Foo']);\n```\n\nNotes:\n\n* The original response is lost! because it is replaced with the filtered response.\n* If you do a **filter_by** using a non exists tag, you'll always get an **empty response**.\n* When you want to specify specific ranges you have to use an associative array with keys **'start'** (indicates where range begins), and **'end'** (where range ends).\n* **greater than** ranges are indicated by passing only **'start'** key.\n* **less than** ranges are indicated by passing only **'end'** key.\n* To perform searches (filtering) to match just partial strings, you have to use the key **'like'** as you saw in the example above.\n* To perform a full match search (filtering) you have to pass the string directly, without use an array.\n* To filter by 1 exact value you have to pass just the value (not an array!). However, if by any reason you decide to use an array, both keys have to have the same value.\n* If you want to build a very specific filter, you have to use **filter_xml()** method. Using it, you are able to built a customized regex to define how the XML have to be processed.\n\n##Todo\n**TADPHP** is not perfect!!!. As mentioned at the beggining, it's been developed after hours, and hours, and hours of googling and it's been tested using just Fingertec Q2i Time & attendance device (that it's I have in my work), so it's possible that you can find errors when you use it with others devices or even you can find better ways to do the things. For that reason, there are some things to do:\n\n* Make TAD-PHP works perfectly on devices with ZEM greater than 600 (with ZEM800 almost everything works as expected, but there are still some bugs).\n* Make set_user_template() method works with BioBridge VX 10.0 algorithm.\n* Find out how to customize the PIN code generation in the PHP_ZKLib zk_set_user_info() method.\n* Test TAD method get_option(). This method allows you getting detailed information about the device, but it's necessary to set its argument to a valid option name. However, these names are not available, and according to documentation ZK Software can give you all options names but you have to pay for them.\n* Enhance PHP_ZKLib to allows more sophisticated functions like uploading user's photo for example.\n\n##Author\n[Jorge Cobis](<mailto:jcobis@gmail.com>) - <http://twitter.com/cobisja>.\n\nBy the way, I'm from Bolivarian Republic of Venezuela :-D\n\n##Contributing\nFeel free to contribute!!!. Welcome aboard!!!\n\n##Misc\n###Version history\n***0.4.2** (Saturday, 17th January 2015)\n\n* Improved directions to get TAD-PHP running.\n* Some minor documentation changes.\n\n**0.4.1** (Friday, 16th January 2015)\n\n* Enhanced **TADZKLib class** by adding 14 new methods to get operating information about the device.\n* Some minor bug fixes.\n* Some fixes in documentation.\n\n**0.4.0** (Sunday, 11th January 2015)\n\n* Wiped out **TADHelpers class**. All its behavior it's been implemented into **TADResponse class**.\n* All classes has been refactored according the new behavior associated to **TADResponse class**.\n* All test suite has been reviewed and upgraded.\n* Changed global **Provider namespace** to get a consistent namespacing schema.\n* Some minor bugs fixes.\n* Improved **TADZKLib class** documentation.\n* Some fixes in documentation.\n\n**0.3.2** (Saturday, 3rd January 2015)\n\n* Implemented a ***dynamic xml filter*** that allows you to build single or multiple filtering criterias based on XML tags of response.\n* Refactoring of tests.\n* Some bug fixes in documentation.\n\n**0.3.1** (Monday, 29th December 2014)\n\n* Some improvements in README.md.\n* Refactored XML response generated by Providers\\TADZKLib class.\n* Some refactoring in tests to ajust them to the refactored XML response generated by Providers\\TADZKLib class.\n* Tests DRYed.\n* Some bug fixtures.\n\n**0.3.0** (Saturday, 27th December 2014)\n\n* Encoding option added to options set used when you instantiate the TADPHP\\TADFactory class. With this options you can customized encoding for both SOAP requests and responses.\n* Add a new Test class (RealTADTest) the allows you to run tests against a real Time & Attendance device.\n* General refactoring according the new encoding options added.\n* Some bug fixes.\n* General code formatting to ajust it to PSR-1 and PSR-2 (it is not completed yet!).\n* Some improvements in README.md\n\n**0.2.0** (Wednesday, 24th December 2014)\n\n* Some refactoring to make TADPHP\\TADSAP, Providers\\TADSOAP and Providers\\TADZKLib classes simpler.\n* TADPHP\\TADHelpers refactored to contains just methods related with TADPHP\\TAD class responses.\n* Some bug fixes in test suite.\n* Some improvements in README.md\n\n\n**0.1.0** (Monday, 22nd December 2014)\n\n* Initial public release.\n\n\n##License\nCopyright (c) 2014 Jorge Cobis (<jcobis@gmail.com>)\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "composer.json",
    "content": "{\n  \"name\": \"TAD-PHP\",\n  \"type\": \"library\",\n  \"description\": \"PHP class that allows you interact with ZK Time & Attendance devices.\",\n  \"keywords\": [\n  \"attendance\",\n  \"time\",\n  \"ZK\",\n  \"tad\"\n  ],\n  \"license\": \"MIT\",\n  \"authors\": [\n  {\n  \"name\": \"Jorge Cobis\",\n  \"email\": \"jcobis@gmail.com\"\n  }\n  ],\n  \"require\": {\n  \"php\": \">=5.3.0\",\n  \"ext-soap\": \"*\",\n  \"ext-sockets\": \"*\"\n  },\n  \"require-dev\": {\n  \"phpunit/phpunit\": \"~4.0\"\n  },\n  \"autoload\": {\n    \"psr-4\": {\n      \"TADPHP\\\\\": \"lib/\",\n      \"TADPHP\\\\Exceptions\\\\\": \"lib/Exceptions/\",\n      \"TADPHP\\\\Providers\\\\\": \"lib/Providers/\",\n      \"Test\\\\Helpers\\\\\": \"test/helpers/\"\n    }\n  }    \n}"
  },
  {
    "path": "lib/Exceptions/ConnectionError.php",
    "content": "<?php\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2014 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\nnamespace TADPHP\\Exceptions;\n\nclass ConnectionError extends \\Exception\n{\n}\n"
  },
  {
    "path": "lib/Exceptions/FilterArgumentError.php",
    "content": "<?php\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2014 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\nnamespace TADPHP\\Exceptions;\n\nclass FilterArgumentError extends \\Exception\n{\n}\n"
  },
  {
    "path": "lib/Exceptions/UnrecognizedArgument.php",
    "content": "<?php\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2014 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\nnamespace TADPHP\\Exceptions;\n\nclass UnrecognizedArgument extends \\Exception\n{\n}\n"
  },
  {
    "path": "lib/Exceptions/UnrecognizedCommand.php",
    "content": "<?php\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2014 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\nnamespace TADPHP\\Exceptions;\n\nclass UnrecognizedCommand extends \\Exception\n{\n}\n"
  },
  {
    "path": "lib/Providers/TADSoap.php",
    "content": "<?php\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2014 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\nnamespace TADPHP\\Providers;\n\nuse TADPHP\\TADResponse;\n\n/**\n * TADSoap: class that allows to interact with a Time & Attendance device using SOAP.\n */\nclass TADSoap\n{\n    const XML_FAIL_RESPONSE = 'Fail!';\n    const XML_SUCCESS_RESPONSE = 'Succeed!';\n    const SOAP_VERSION = SOAP_1_1;\n\n    /**\n     * @var array SOAP commands array supported by the class.\n     */\n    static private $soap_commands_available = [\n        'get_date'            => '<GetDate><ArgComKey>%com_key%</ArgComKey></GetDate>',\n        'get_att_log'         => '<GetAttLog><ArgComKey>%com_key%</ArgComKey><Arg><PIN>%pin%</PIN></Arg></GetAttLog>',\n        'get_user_info'       => '<GetUserInfo><ArgComKey>%com_key%</ArgComKey><Arg><PIN>%pin%</PIN></Arg></GetUserInfo>',\n        'get_all_user_info'   => '<GetAllUserInfo><ArgComKey>%com_key%</ArgComKey></GetAllUserInfo>',\n        'get_user_template'   => '<GetUserTemplate><ArgComKey>0</ArgComKey><Arg><PIN>%pin%</PIN><FingerID>%finger_id%</FingerID></Arg></GetUserTemplate>',\n        'get_combination'     => '<GetCombination><ArgComKey>%com_key%</ArgComKey></GetCombination>',\n        'get_option'          => '<GetOption><ArgComKey>%com_key%</ArgComKey><Arg><Name>%option_name%</Name></Arg></GetOption>',\n        'set_user_info'       => [ '<DeleteUser><ArgComKey>%com_key%</ArgComKey><Arg><PIN>%pin%</PIN></Arg></DeleteUser>', '<SetUserInfo><ArgComKey>%com_key%</ArgComKey><Arg><Name>%name%</Name><Password>%password%</Password><Group>%group%</Group><Privilege>%privilege%</Privilege><Card>%card%</Card><PIN2>%pin%</PIN2><TZ1>%tz1%</TZ1><TZ2>%tz2%</TZ2><TZ3>%tz3%</TZ3></Arg></SetUserInfo>'],\n        'set_user_template'   => '<SetUserTemplate><ArgComKey>%com_key%</ArgComKey><Arg><PIN>%pin%</PIN><FingerID>%finger_id%</FingerID><Size>%size%</Size><Valid>%valid%</Valid><Template>%template%</Template></Arg></SetUserTemplate>',\n        'delete_user'         => '<DeleteUser><ArgComKey>%com_key%</ArgComKey><Arg><PIN>%pin%</PIN></Arg></DeleteUser>',\n        'delete_template'     => '<DeleteTemplate><ArgComKey>%com_key%</ArgComKey><Arg><PIN>%pin%</PIN></Arg></DeleteTemplate>',\n        'delete_user_password'=> '<ClearUserPassword><ArgComKey>%com_key%</ArgComKey><Arg><PIN>%pin%</PIN></Arg></ClearUserPassword>',\n        'delete_data'         => '<ClearData><ArgComKey>%com_key%</ArgComKey><Arg><Value>%value%</Value></Arg></ClearData>',\n        'refresh_db'          => '<RefreshDB><ArgComKey>%com_key%</ArgComKey></RefreshDB>',\n    ];\n\n    /**\n     * @var SOAPClient Holds a <code>\\SoapClient</code> instance.\n     */\n    private $soap_client;\n\n    /**\n     * @var array Options array required by <code>SoapClient</code> class.\n     */\n    private $soap_client_options;\n\n\n    /**\n     * Returns commands available by the class.\n     *\n     * @param array $options options to define the information level about commands available by the class.\n     * @return array commands available list.\n     */\n    static public function get_commands_available(array $options = [])\n    {\n        return (isset($options['include_command_string']) && $options['include_command_string']) ?\n              self::$soap_commands_available : array_keys(self::$soap_commands_available);\n    }\n\n    /**\n     * Build a <code>TADSoap</code> instance to allow communication with the device via SOAP api.\n     *\n     * @param \\SoapClient $soap_client <code>SoapClient</code> instance\n     * @param array $soap_client_options options required by <code>SoapClient</code> class.\n     */\n    public function __construct(\\SoapClient $soap_client, array $soap_client_options)\n    {\n        $this->soap_client = $soap_client;\n        $this->soap_client_options = $soap_client_options;\n    }\n\n    /**\n     * Get a command, build the SOAP request and send it to device.\n     *\n     * @param mixed $soap_command command to be executed.\n     * @param array $soap_command_args command arguments.\n     * @return string response.\n     */\n    public function execute_soap_command($soap_command, array $soap_command_args, $encoding)\n    {\n        $soap_location = $this->get_soap_provider_options()['location'];\n        $soap_request = $this->build_soap_request($soap_command, $soap_command_args, $encoding);\n\n        $response = !is_array($soap_request) ?\n                $this->execute_single_soap_request($soap_request, $soap_location) :\n                $this->execute_multiple_soap_requests($soap_request, $soap_location);\n\n        return new TADResponse($response, $encoding);\n    }\n\n    /**\n     * Returns params required by <b><code>SoapClient</code></b> class.\n     *\n     * @return array params list.\n     */\n    public function get_soap_provider_options()\n    {\n        return $this->soap_client_options;\n    }\n\n    /**\n     * Returns the SOAP request based on command.\n     *\n     * @param string $command command requested.\n     * @param array $args command params.\n     * @return string SOAP request.\n     */\n    public function build_soap_request($command, array $args, $encoding)\n    {\n        $command_string = $this->get_command_string($command);\n        $soap_request = $this->parse_command_string($command_string, $args);\n\n        if (!is_array($soap_request)) {\n            $soap_request = $this->normalize_xml_string($soap_request, $encoding);\n        } else {\n            $soap_request = array_map(\n                function ($soap_request) use ($encoding) {\n                    return $this->normalize_xml_string($soap_request, $encoding);\n                },\n                $soap_request\n            );\n        }\n\n        return $soap_request;\n    }\n\n    /**\n     * Returns command SOAP definition.\n     *\n     * @param string $key SOAP command requested.\n     * @return string SOAP definition.\n     */\n    private function get_command_string($key)\n    {\n        return self::$soap_commands_available[$key];\n    }\n\n    /**\n     * Sends a SOAP command to device.\n     *\n     * @param mixed $soap_request SOAP command.\n     * @param string $soap_location URI required by SOAP service.\n     * @return string device response.\n     */\n    private function execute_single_soap_request($soap_request, $soap_location)\n    {\n        return $this->soap_client->__doRequest($soap_request, $soap_location, '', self::SOAP_VERSION);\n    }\n\n    /**\n     * Sends multiple SOAP commands to the device.\n     *\n     * @param mixed $soap_requests SOAP commands array.\n     * @param string $soap_location URI required by SOAP service.\n     * @return string device response (Always returns the last command response.)\n     */\n    private function execute_multiple_soap_requests(array $soap_requests, $soap_location)\n    {\n        foreach ($soap_requests as $soap_request) {\n            $result = $this->execute_single_soap_request($soap_request, $soap_location);\n        }\n\n        return $result;\n    }\n\n    /**\n     * Parses SOAP request, replacing formal params with actual params.\n     *\n     * @param string $command_string SOAP request.\n     * @param array $command_args actual args values required by SOAP request.\n     * @return string SOAP request parsed.\n     */\n    private function parse_command_string($command_string, array $command_args)\n    {\n        $parseable_args = array_map(\n            function($item) {\n                return '%' . $item . '%';\n            },\n            array_keys($command_args)\n        );\n\n        $parsed_command = str_replace($parseable_args, array_values($command_args), $command_string);\n\n        return $parsed_command;\n    }\n\n    /**\n     * Build an XML header.\n     *\n     * @param string $xml XML string which header will be added to.\n     * @param string $encoding encoding\n     * @return string full XML string (Header + Body).\n     */\n    public static function normalize_xml_string($xml, $encoding = 'utf-8')\n    {\n        $xml ='<?xml version=\"1.0\" encoding=\"' . $encoding . '\" standalone=\"no\"?>' . $xml;\n\n        return trim(str_replace([ \"\\n\", \"\\r\" ], '', $xml));\n    }\n}\n"
  },
  {
    "path": "lib/Providers/TADZKLib.php",
    "content": "<?php\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2014 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\nnamespace TADPHP\\Providers;\n\nuse TADPHP\\TADResponse;\n\n/**\n * TADZKlib: class that allows to interact with a Time & Attendance device using UDP protocol.\n *\n * This is a modified class of PHP_ZKLib (@link http://dnaextrim.github.io/php_zklib/ )\n *\n * This class has been modified by refactoring most of methods, taking out all duplicated code. The\n * original behavior it's been kept.\n */\nclass TADZKLib\n{\n    const USHRT_MAX = 65535;\n    const CMD_CONNECT = 1000;\n    const CMD_EXIT = 1001;\n    const CMD_ENABLEDEVICE = 1002;\n    const CMD_DISABLEDEVICE = 1003;\n    const CMD_RESTART = 1004;\n    const CMD_POWEROFF = 1005;\n    const CMD_ACK_OK = 2000;\n    const CMD_ACK_ERROR = 2001;\n    const CMD_ACK_DATA = 2002;\n    const CMD_PREPARE_DATA = 1500;\n    const CMD_DATA = 1501;\n    const CMD_USERTEMP_RRQ = 9;\n    const CMD_ATTLOG_RRQ = 13;\n    const CMD_CLEAR_DATA = 14;\n    const CMD_CLEAR_ATTLOG = 15;\n    const CMD_WRITE_LCD = 66;\n    const CMD_GET_TIME = 201;\n    const CMD_SET_TIME = 202;\n    const CMD_VERSION = 1100;\n    const CMD_AUTH = 1102;\n    const CMD_DEVICE = 11;\n    const CMD_CLEAR_ADMIN = 20;\n    const CMD_SET_USER = 8;\n    const CMD_GET_FREE_SIZES = 50;\n\n    const EMPTY_STRING = '';\n    const CUSTOMIZED_COMMAND_STRING = null;\n\n    const DEVICE_GENERAL_INFO_STRING_LENGTH = 184;\n\n    const XML_FAIL_RESPONSE    = 'Fail!';\n    const XML_SUCCESS_RESPONSE = 'Successfully!';\n\n    /**\n     * @var string Device's ip address.\n     */\n    private $ip;\n\n    /**\n     * @var int Device's UDP port.\n     */\n    private $port;\n\n    /**\n     * @var TADZKlib holds a class instance.\n     */\n    private $zkclient;\n\n    /**\n     * @var string device's response (low level format).\n     */\n    private $data_recv = '';\n\n    /**\n     * @var int session id associated to UDP transaction.\n     */\n    private $session_id = 0;\n\n    /**\n     * @var boolean tells if result was successfully (<code>true</code>) or fail (<code>false</code>).\n     */\n    private $result;\n\n    /**\n     * @var array commands set supported by <code>TADZKLib</code> class.\n     */\n    static private $zklib_commands = [\n        'get_platform' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => '~Platform',\n            'should_disconnect' => true,\n            'result_filter_string'=>'~Platform='\n        ],\n        'get_fingerprint_algorithm' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => '~ZKFPVersion',\n            'should_disconnect' => true,\n            'result_filter_string'=>'~ZKFPVersion='\n        ],\n        'get_serial_number' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => '~SerialNumber',\n            'should_disconnect' => true,\n            'result_filter_string'=>'~SerialNumber='\n        ],\n        'get_oem_vendor' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => '~OEMVendor',\n            'should_disconnect' => true,\n            'result_filter_string'=>'~OEMVendor='\n        ],\n        'get_mac_address' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => 'MAC',\n            'should_disconnect' => true,\n            'result_filter_string'=>'MAC='\n        ],\n        'get_device_name' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => '~DeviceName',\n            'should_disconnect' => true,\n            'result_filter_string'=>'~DeviceName='\n        ],\n        'get_manufacture_time' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => '~ProductTime',\n            'should_disconnect' => true,\n            'result_filter_string'=>'~ProductTime='\n        ],\n        'get_antipassback_mode' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => '~APBFO',\n            'should_disconnect' => true,\n            'result_filter_string'=>'~APBFO='\n        ],\n        'get_workcode' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => '~WCFO',\n            'should_disconnect' => true,\n            'result_filter_string'=>'~WCFO='\n        ],\n        'get_ext_format_mode' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => '~ExtendFmt',\n            'should_disconnect' => true,\n            'result_filter_string'=>'~ExtendFmt='\n        ],\n        'get_encrypted_mode' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => 'encrypt_out',\n            'should_disconnect' => true,\n            'result_filter_string'=>'encrypt_out='\n        ],\n        'get_pin2_width' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => '~PIN2Width',\n            'should_disconnect' => true,\n            'result_filter_string'=>'~PIN2Width='\n        ],\n        'get_ssr_mode' => [\n            'command_id' => self::CMD_DEVICE,\n            'command_string' => '~SSR',\n            'should_disconnect' => true,\n            'result_filter_string'=>'~SSR='\n        ],\n        'get_firmware_version' => [\n            'command_id' => self::CMD_VERSION,\n            'command_string' => self::EMPTY_STRING,\n            'should_disconnect' => true,\n            'result_filter_string'=>false\n        ],\n        'get_free_sizes' => [\n            'command_id' => self::CMD_GET_FREE_SIZES,\n            'command_string' => self::EMPTY_STRING,\n            'should_disconnect' => true,\n            'result_filter_string'=>false\n        ],\n        'set_date' => [\n            'command_id' => self::CMD_SET_TIME,\n            'command_string' => self::CUSTOMIZED_COMMAND_STRING,\n            'should_disconnect' => true,\n            'result_filter_string'=>false\n        ],\n        'delete_admin' => [\n            'command_id' => self::CMD_CLEAR_ADMIN,\n            'command_string' => self::EMPTY_STRING,\n            'should_disconnect' => true,\n            'result_filter_string'=>false\n        ],\n        'enable' => [\n            'command_id' => self::CMD_ENABLEDEVICE,\n            'command_string' => self::EMPTY_STRING,\n            'should_disconnect' => true,\n            'result_filter_string'=>false\n        ],\n        'disable' => [\n            'command_id' => self::CMD_DISABLEDEVICE,\n            'command_string' => self::EMPTY_STRING,\n            'should_disconnect' => false,\n            'result_filter_string'=>false\n        ],\n        'restart' => [\n            'command_id' => self::CMD_RESTART,\n            'command_string' => self::EMPTY_STRING,\n            'should_disconnect' => true,\n            'result_filter_string'=>false\n        ],\n        'poweroff' => [\n            'command_id' => self::CMD_POWEROFF,\n            'command_string' => self::EMPTY_STRING,\n            'should_disconnect' => true,\n            'result_filter_string'=>false\n        ]\n    ];\n\n    /**\n     * Returns commands available by the class.\n     *\n     * @return array commands list.\n     */\n    static public function get_commands_available()\n    {\n        return array_keys(self::$zklib_commands);\n    }\n\n    /**\n     * Iniatialize TADZKLib class and sets its attributes.\n     *\n     * @param array $options options (ip, udp port and connection timeout).\n     */\n    public function __construct(array $options)\n    {\n        $this->ip = $options['ip'];\n        $this->port = $options['udp_port'];\n\n        $this->zkclient = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);\n\n        $timeout = ['sec' => $options['connection_timeout'], 'usec' => 500000];\n        socket_set_option($this->zkclient, SOL_SOCKET, SO_RCVTIMEO, $timeout);\n    }\n\n    /**\n     * Magic call to implement dynamic method calling.\n     *\n     * @param string $command method invoked.\n     * @param array $args arguments passed.\n     * @return TADResponse\n     */\n    public function __call($command, array $args)\n    {\n        $should_disconnect = true;\n        $args = count($args) === 0 ? [] : array_shift($args);\n        $encoding = $args['encoding'];\n        unset($args['encoding']);\n\n        $this->connect();\n\n        switch($command){\n            case 'set_date':\n                $response = $this->zk_set_date($args);\n                break;\n\n            case 'get_free_sizes':\n                $response = $this->zk_get_free_sizes();\n                break;\n\n            default:\n                $should_disconnect = self::$zklib_commands[$command]['should_disconnect'];\n\n                $response = $this->send_command_to_device(\n                    self::$zklib_commands[$command]['command_id'],\n                    self::$zklib_commands[$command]['command_string']\n                );\n        }\n\n        $result_filter_string = self::$zklib_commands[$command]['result_filter_string'];\n        $response = $this->build_command_response($command, $this->result, $response, $encoding, $result_filter_string);\n        $should_disconnect && $this->disconnect();\n\n        return new TADResponse($response, $encoding);\n    }\n\n    /**\n     * Establish a connection to the device.\n     *\n     * @return boolean <b><code>true</code></b> on successfully conection, otherwise returns <b><code>false</code></b>.\n     */\n    private function connect()\n    {\n        $command = self::CMD_CONNECT;\n        $command_string = self::EMPTY_STRING;\n        $chksum = 0;\n        $session_id = 0;\n        $reply_id = -1 + self::USHRT_MAX;\n\n        $buf = $this->createHeader($command, $chksum, $session_id, $reply_id, $command_string);\n\n        socket_sendto($this->zkclient, $buf, strlen($buf), 0, $this->ip, $this->port);\n\n        try {\n            socket_recvfrom($this->zkclient, $this->data_recv, 1024, 0, $this->ip, $this->port);\n            if (strlen($this->data_recv) > 0) {\n                $u = unpack('H2h1/H2h2/H2h3/H2h4/H2h5/H2h6', substr($this->data_recv, 0, 8));\n\n                $this->session_id =  hexdec($u['h6'].$u['h5']);\n                return $this->checkValid($this->data_recv);\n            } else {\n                return false;\n            }\n        } catch (ErrorException $e) {\n            return false;\n        } catch (exception $e) {\n            return false;\n        }\n    }\n\n    /**\n     * Disconnects from the device.\n     *\n     * @return boolean <b><code>true</code></b> on successfully, otherwise returns <b><code>false</code></b>.\n     */\n    private function disconnect()\n    {\n        $command = self::CMD_EXIT;\n        $command_string = '';\n        $chksum = 0;\n        $session_id = $this->session_id;\n\n        $u = unpack('H2h1/H2h2/H2h3/H2h4/H2h5/H2h6/H2h7/H2h8', substr($this->data_recv, 0, 8));\n        $reply_id = hexdec($u['h8'].$u['h7']);\n\n        $buf = $this->createHeader($command, $chksum, $session_id, $reply_id, $command_string);\n\n        socket_sendto($this->zkclient, $buf, strlen($buf), 0, $this->ip, $this->port);\n        try {\n            socket_recvfrom($this->zkclient, $this->data_recv, 1024, 0, $this->ip, $this->port);\n\n            return $this->checkValid($this->data_recv);\n        } catch (ErrorException $e) {\n            return false;\n        } catch (Exception $e) {\n            return false;\n        }\n    }\n\n    /**\n     * Sets device's time and date.\n     *\n     * @param array $dt date and time data.\n     * @return boolean <b><code>true</code></b> on successfully, otherwise returns <b><code>false</code></b>.\n     */\n    private function zk_set_date(array $dt = [])\n    {\n        $normalized_datetime = $this->setup_datetime($dt);\n        $encoded_time = $this->encode_time($normalized_datetime);\n        return $this->send_command_to_device(self::CMD_SET_TIME, pack('I', $encoded_time));\n    }\n\n    /**\n     * Gets device's information about current device's storage.\n     *\n     * @return array device's storage information.\n     */\n    private function zk_get_free_sizes()\n    {\n        $fs = [];\n        $free_sizes_info = $this->reverse_hex(bin2hex($this->send_command_to_device(self::CMD_GET_FREE_SIZES)));\n\n        if (!$free_sizes_info) {\n            $fs = false;\n        } else {\n            if (self::DEVICE_GENERAL_INFO_STRING_LENGTH > strlen($free_sizes_info)) {\n                $free_sizes_info = '000000000000000000000000' . $free_sizes_info;\n            }\n\n            $fs['att_logs_available']  = hexdec(substr($free_sizes_info, 27, 5));\n            $fs['templates_available'] = hexdec(substr($free_sizes_info, 44, 4));\n            $fs['att_logs_capacity']   = hexdec(substr($free_sizes_info, 51, 5));\n            $fs['templates_capacity']  = hexdec(substr($free_sizes_info, 60, 4));\n            $fs['passwords_stored']    = hexdec(substr($free_sizes_info, 76, 4));\n            $fs['admins_stored']       = hexdec(substr($free_sizes_info, 84, 4));\n            $fs['att_logs_stored']     = hexdec(substr($free_sizes_info, 116, 4));\n            $fs['templates_stored']    = hexdec(substr($free_sizes_info, 132, 4));\n            $fs['users_stored']        = hexdec(substr($free_sizes_info, 148, 4));\n        }\n\n        return $fs;\n    }\n\n    /**\n     * Helper that allows sending command to device.\n     *\n     * @param integer $command command code.\n     * @param string $command_string subcommand.\n     * @param int $reply_id device's reply.\n     * @return boolean <b><code>true</code></b> on successfully, otherwise returns <b><code>false</code></b>.\n     */\n    private function send_command_to_device($command, $command_string = '', $reply_id =null)\n    {\n        $chksum = 0;\n        $session_id = $this->session_id;\n\n        $u = unpack('H2h1/H2h2/H2h3/H2h4/H2h5/H2h6/H2h7/H2h8', substr($this->data_recv, 0, 8));\n\n        if (is_null($reply_id)) {\n            $reply_id = hexdec($u['h8'].$u['h7']);\n        }\n\n        $buf = $this->createHeader($command, $chksum, $session_id, $reply_id, $command_string);\n\n        socket_sendto($this->zkclient, $buf, strlen($buf), 0, $this->ip, $this->port);\n\n        try {\n            socket_recvfrom($this->zkclient, $this->data_recv, 1024, 0, $this->ip, $this->port);\n            $u = unpack('H2h1/H2h2/H2h3/H2h4/H2h5/H2h6', substr($this->data_recv, 0, 8));\n            $this->session_id =  hexdec($u['h6'].$u['h5']);\n\n            $this->result = $this->checkValid($this->data_recv);\n\n            return substr($this->data_recv, 8);\n        } catch (ErrorException $e) {\n            return false;\n        } catch (exception $e) {\n            return false;\n        }\n    }\n\n    /**\n     * Calculates the chksum of the packet to be sent to the device.\n     *\n     * @param string $p packed sent to the device.\n     * @return string checksum calculated.\n     */\n    private function createChkSum($p)\n    {\n        /*This function\n\n        Copied from zkemsdk.c*/\n\n        $l = count($p);\n        $chksum = 0;\n        $i = $l;\n        $j = 1;\n        while ($i > 1) {\n            $u = unpack('S', pack('C2', $p['c'.$j], $p['c'.($j+1)]));\n\n            $chksum += $u[1];\n\n            if ($chksum > self::USHRT_MAX) {\n                $chksum -= self::USHRT_MAX;\n            }\n\n            $i-=2;\n            $j+=2;\n        }\n\n        if ($i) {\n            $chksum = $chksum + $p['c'.strval(count($p))];\n        }\n\n        while ($chksum > self::USHRT_MAX) {\n            $chksum -= self::USHRT_MAX;\n        }\n\n        if ($chksum > 0) {\n            $chksum = -($chksum);\n        } else {\n            $chksum = abs($chksum);\n        }\n\n        $chksum -= 1;\n        while ($chksum < 0) {\n            $chksum += self::USHRT_MAX;\n        }\n\n        return pack('S', $chksum);\n    }\n\n    /**\n     *  Creates UDP header to be sent to the device.\n     *\n     * @param int $command command id.\n     * @param string $chksum checksum associated.\n     * @param int $session_id session id associated.\n     * @param int $reply_id reply id associated.\n     * @param string $command_string subcomand.\n     * @return string UDP header.\n     */\n    private function createHeader($command, $chksum, $session_id, $reply_id, $command_string)\n    {\n        /*This function puts a the parts that make up a packet together and\n        packs them into a byte string*/\n        $buf = pack('SSSS', $command, $chksum, $session_id, $reply_id).$command_string;\n\n        $buf = unpack('C'.(8+strlen($command_string)).'c', $buf);\n\n        $u = unpack('S', $this->createChkSum($buf));\n\n        if (is_array($u)) {\n            while (list($key) = each($u)) {\n                $u = $u[$key];\n                break;\n            }\n        }\n\n        $chksum = $u;\n        $reply_id += 1;\n\n        if ($reply_id >= self::USHRT_MAX) {\n            $reply_id -= self::USHRT_MAX;\n        }\n\n        $buf = pack('SSSS', $command, $chksum, $session_id, $reply_id);\n\n        return $buf.$command_string;\n    }\n\n    /**\n     * Checks a returned packet to see if it returned CMD_ACK_OK, indicating success.\n     *\n     * @param string $reply packet received from the device.\n     *\n     * @return boolean <b><code>true</code></b> on valid packet, otherwise returns <b><code>false</code></b>.\n     */\n    private function checkValid($reply)\n    {\n        $u = unpack('H2h1/H2h2', substr($reply, 0, 8));\n\n        $command = hexdec($u['h2'].$u['h1']);\n\n        if ($command == self::CMD_ACK_OK) {\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    /**\n     * Builds a command response with a XML format to keep TAD behavior.\n     *\n     * @param string $command command executed.\n     * @param mixed $result command result.\n     * @return string XML response.\n     */\n    private function build_command_response($command, $result_code, $result, $encoding, $result_filter_string=false)\n    {\n        $response_data = [];\n\n        $xml_tag = str_replace('_', ' ', $command);\n        $base_xml_tag = join('', explode(' ', ucwords($xml_tag))) . 'Response';\n\n        if (is_array($result)) {\n            if (0 === count($result)) {\n                $xml_header = '';\n                $response = $xml_header . '<' . $base_xml_tag . '>' . '</' . $base_xml_tag . '>';\n                return $response;\n            }\n            $response_data = ['Row'=>$result];\n        } else {\n            if (!is_bool($result) && true === $result_code) {\n                $result_filter_string = $result_filter_string ? $result_filter_string : null;\n\n                $result_data = str_replace($result_filter_string, '', $result);\n            } else {\n                $result_data = ($result_code ? self::XML_SUCCESS_RESPONSE : self::XML_FAIL_RESPONSE);\n            }\n\n            $result_code = $result_code ? '1' : '0';\n            $response_data = ['Row'=>['Result'=> $result_code, 'Information'=> $result_data]];\n        }\n\n        return $this->array_to_xml(new \\SimpleXMLElement('<' . $base_xml_tag . '/>'), $response_data, $encoding);\n    }\n\n    /**\n     * Take an array in the form of <code>['date'=>date_value, 'time'=>time_value]</code> y returns\n     * another array with the following form:\n     *\n     * <code>\n     * ['year'=>foo_year, 'month'=>bar_month, 'day'=>taz_day,\n     *  'hour'=>foo_hour, 'minute=>bar_minute, 'second'=>taz_minute]\n     * </code>\n     *\n     * Any missing item from input array is replaced by corresponding element generated from\n     * current date and time.\n     *\n     * @param array $dt input 'datetime' array.\n     * @return array array generated.\n     */\n    private function setup_datetime(array $dt=[])\n    {\n        $now = explode(' ', date(\"Y-m-d H:i:s\"));\n        $dt = array_filter($dt, 'strlen');\n\n        !isset($dt['date']) ? $dt['date'] = $now[0] : null;\n        !isset($dt['time']) ? $dt['time'] = $now[1] : null;\n\n        $date = explode('-', $dt['date']);\n        $time = explode(':', $dt['time']);\n\n        return [\n            'year'=>$date[0], 'month'=>$date[1], 'day'=>$date[2],\n            'hour'=>$time[0], 'minute'=>$time[1], 'second'=>$time[2]];\n    }\n\n    /**\n     * Method taken from PHPLib @link http://dnaextrim.github.io/php_zklib/ project.\n     *\n     * @param string $hexstr hex string.\n     * @return string hex string reversed.\n     */\n    private function reverse_hex($hexstr)\n    {\n        $tmp = '';\n\n        for ($i=strlen($hexstr); $i>=0; $i--) {\n            $tmp .= substr($hexstr, $i, 2);\n            $i--;\n        }\n\n        return $tmp;\n    }\n\n    /**\n     * Method taken from PHPZKLib @link http://dnaextrim.github.io/php_zklib/ project.\n     *\n     * It's been modified to accept an associative array as input.\n     *\n     * @param array $t array with a timestamp data.\n     * @return int timestamp encoded.\n     */\n    private function encode_time(array $t)\n    {\n        /*Encode a timestamp send at the timeclock\n\n        copied from zkemsdk.c - EncodeTime*/\n        $d = ( ($t['year'] % 100) * 12 * 31 + (($t['month'] - 1) * 31) + $t['day'] - 1) *\n             (24 * 60 * 60) + ($t['hour'] * 60 + $t['minute']) * 60 + $t['second'];\n\n        return $d;\n    }\n\n    /**\n     * Transforms an array into an XML string.\n     *\n     * @param \\SimpleXMLElement $object <code>SimpleXMLElement</code> instance.\n     * @param array $data input array to be transformed.\n     * @return string XML string generated.\n     */\n    private function array_to_xml(\\SimpleXMLElement $object, array $data)\n    {\n        foreach ($data as $key => $value) {\n            if (is_array($value)) {\n                $new_object = $object->addChild($key);\n                $this->array_to_xml($new_object, $value);\n            } else {\n                $object->addChild($key, $value);\n            }\n        }\n\n        $xml = trim(str_replace(\"<?xml version=\\\"1.0\\\"?>\", '', $object->asXML()));\n\n        return $xml;\n    }\n}\n"
  },
  {
    "path": "lib/TAD.php",
    "content": "<?php\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2014 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\nnamespace TADPHP;\n\nuse TADPHP\\Providers\\TADSoap;\nuse TADPHP\\Providers\\TADZKLib;\nuse TADPHP\\Exceptions\\ConnectionError;\nuse TADPHP\\Exceptions\\UnrecognizedArgument;\nuse TADPHP\\Exceptions\\UnrecognizedCommand;\n\n/**\n * TAD: Time & Attendance Device is a class that implements some function presents in time and attendance device.\n *\n *\n * For developing purposes, it's been used Fingertec Q2i device (that it's been using were I work).\n *\n * Methods exposed by TAD class have been tested using Q2i deviced only , that has a Linux 2.6.21 kernel (ZEM-600).\n * However, it's possible that TAD class works with similar devices, since most of them use the same SOAP API from\n * ZK Software.\n *\n * There are some SOAP functions that it's suppossed,  according to the official docs (which incidentally it's\n * very limited and so poor!!!) must show a expected behaviour, but when they are invoked don't work like\n * it's expected, so they become useless (e.g. Restart SOAP call). For these situations, I found (after googling for\n * hours, and hours, and hours!!!), a PHP class named PHP_ZKLib (@link http://dnaextrim.github.io/php_zklib/ ) that\n * take a different approach to \"talk to device\": it uses UDP protocol at device standard port 4370.\n *\n * PHP_ZKLib class it's been fully integrated, after a refactoring process to take out all duplicated code (DRY)\n *\n * TAD Class has been tested as far as I could. If you check the test code coverage you'll notice that it does not\n * reach 100%. The reason is that it was not possible to write some tests fof PHP_ZKLib class. I have to admit that\n * I not have fully understanding about what it's done by some methods. I could not find technical information\n * about UDP protocol for ZK devices.\n *\n * For practical purposes, you don't have to be worried about when to use TAD class or PHP_ZKLib class because\n * you only have to get a TAD instance (as shown below) and call any of methods available. The class decides\n * about when run the method invoked using TAD class or PHP_ZKLib class.\n *\n * To get a TAD instance, call <code><b>get_instance()</b></code> method from <code><b>TADFactory</b></code> class.\n *\n * Please note that all device's responses are handled by TAD through <b><code>TADResponse</code></b> class. For\n * that reason all responses are returned embedded in <code>TADResponse</code> object.\n *\n * You can get responses in XML, JSON or Array using the respective methods exposed by <code>TADResponse</code> class.\n * (<code>to_xml(), to_json(), to_array(), get_reponse()</code>)\n *\n * Some examples:\n *\n * - Get a TAD instance for device with ip = '192.168.100.156':\n * <code>\n * $b1 = (new TADFactory(['ip'=>'192.168.100.156']))->get_instance();\n * </code>\n *\n * Getting device time and date:\n * <code>\n * $dt = $b1->get_date(); // method executed via TAD class.\n * </code>\n *\n * Setting device date and time:\n * <code>\n * $r = $b1->set_date(['date'=>'2014-12-31', 'time'=>'23:59:59']); // method executed via PHP_ZKLib.\n * </code>\n *\n * All device responses are <code>TADResponse</code> objects. You can transform them as shown below:\n *\n * Get an array with logs of user with pin = 99999999:\n * <code>\n * $logs = $b1->get_att_log(['pin'=>'99999999'])->to_array();\n * </code>\n *\n * If you want to filter logs by date:\n * <code>\n * $logs = $b1->get_att_log(['pin'=>'99999999']);\n * $logs = $logs->filter_by_date(['start_date'=>'2014-11-27', 'end_date'=>'2014-12-02']);\n * </code>\n *\n * For more information see README.md\n *\n * @author Jorge Cobis - email: jcobis@gmail.com / twitter: @cobisja\n */\nclass TAD\n{\n    /**\n     * Valid commands args array.\n     *\n     * @var array\n     */\n    static private $parseable_args = [\n        'com_key', 'pin', 'time', 'template',\n        'name', 'password', 'group', 'privilege',\n        'card', 'pin2', 'tz1', 'tz2', 'tz3',\n        'finger_id', 'option_name', 'date',\n        'size', 'valid', 'value'\n    ];\n\n    /**\n     * @var string Device ip address.\n     */\n    private $ip;\n\n    /**\n     * @var mixed Device internal id.\n     */\n    private $internal_id;\n\n    /**\n     * @var string Device description (just for info purposes).\n     */\n    private $description;\n\n    /**\n     * Security communication code (required for SOAP functions calls).\n     *\n     * @var mixed\n     */\n    private $com_key;\n\n    /**\n     * @var int Connection timeout in seconds.\n     */\n    private $connection_timeout;\n\n    /**\n     * @var string Encoding for XML commands and responses.\n     */\n    private $encoding;\n\n    /**\n     * @var int UDP port number.\n     */\n    private $udp_port;\n\n    /**\n     * Holds a <code>TADSoap</code> instance to talk to device via SOAP.\n     *\n     * @var object\n     */\n    private $tad_soap;\n\n    /**\n     * Holds <code>PHP_ZKLib</code> instance to talk to device via UDP.\n     *\n     * @var object\n     */\n    private $zklib;\n\n\n    /**\n     * Returns an array with a full list of commands available.\n     *\n     * @return array list of commands available.\n     */\n    public static function commands_available()\n    {\n        return array_merge(static::soap_commands_available(), static::zklib_commands_available());\n    }\n\n    /**\n     * Returns an array with SOAP commands list available.\n     *\n     * @return array SOAP commands list.\n     */\n    public static function soap_commands_available(array $options = [])\n    {\n        return TADSoap::get_commands_available($options);\n    }\n\n    /**\n     * Returns an array with PHP_ZKLib commands available.\n     *\n     * @return array PHP_ZHLib commands list.\n     */\n    public static function zklib_commands_available()\n    {\n        return TADZKLib::get_commands_available();\n    }\n\n    /**\n     * Returns valid commands arguments list.\n     *\n     * @return array arguments list.\n     */\n    public static function get_valid_commands_args()\n    {\n        return self::$parseable_args;\n    }\n\n    /**\n     * Tells if device is \"online\" to process commands requests.\n     *\n     * @param string $ip device ip address\n     * @param int $timeout seconds to wait for device.\n     * @return boolean <b>true</b> if device is alive, <b>false</b> otherwise.\n     */\n    public static function is_device_online($ip, $timeout = 1)\n    {\n        $handler = curl_init($ip);\n        curl_setopt_array($handler, [ CURLOPT_TIMEOUT => $timeout, CURLOPT_RETURNTRANSFER => true ]);\n        $response = curl_exec($handler);\n        curl_close($handler);\n\n        return (boolean)$response;\n    }\n\n    /**\n     * Get a new TAD class instance.\n     *\n     * @param TADSoap $soap_provider code><b>TADSoap</b></code> class instance.\n     * @param TADZKLib $zklib_provider <code><b>ZKLib</b></code> class instance.\n     * @param array $options device parameters.\n     */\n    public function __construct(TADSoap $soap_provider, TADZKLib $zklib_provider, array $options = [])\n    {\n        $this->ip = $options['ip'];\n        $this->internal_id = (integer) $options['internal_id'];\n        $this->com_key = (integer) $options['com_key'];\n        $this->description = $options['description'];\n        $this->connection_timeout = (integer) $options['connection_timeout'];\n        $this->encoding = strtolower($options['encoding']);\n        $this->udp_port = (integer) $options['udp_port'];\n\n        $this->tad_soap = $soap_provider;\n        $this->zklib = $zklib_provider;\n    }\n\n    /**\n     * Magic __call method overriding to define in runtime the methods should be called based on method invoked.\n     * (Something like Ruby metaprogramming :-P). In this way, we decrease the number of methods required\n     * (usually should be one method per SOAP or PHP_ZKLib command exposed).\n     *\n     * Note:\n     *\n     * Those methods that add, update o delete device information, call SOAP method <b><code>refresh_db()</code></b>\n     * to properly update device database.\n     *\n     * @param string $command command to be invoked.\n     * @param array $args commands args.\n     * @return string device response in XML format.\n     * @throws ConnectionError.\n     * @throws UnrecognizedCommand.\n     * @throws UnrecognizedArgument.\n     */\n    public function __call($command, array $args)\n    {\n        $command_args = count($args) === 0 ? [] : array_shift($args);\n\n        $this->check_for_connection() &&\n        $this->check_for_valid_command($command) &&\n        $this->check_for_unrecognized_args($command_args);\n\n        if (in_array($command, TADSoap::get_commands_available())) {\n            $response = $this->execute_command_via_tad_soap($command, $command_args);\n        } else {\n            $response = $this->execute_command_via_zklib($command, $command_args);\n        }\n\n        $this->check_for_refresh_tad_db($command);\n\n        return $response;\n    }\n\n    /**\n     * Send a command to device using a <code>TADSoap</code> instance class.\n     *\n     * @param string $command command to be sending.\n     * @param array $args command args.\n     * @return string device response.\n     */\n    public function execute_command_via_tad_soap($command, array $args = [])\n    {\n        $command_args = $this->config_array_items(array_merge(['com_key' => $this->get_com_key()], $args));\n\n        return $this->tad_soap->execute_soap_command($command, $command_args, $this->encoding);\n    }\n\n    /**\n     * Send a command to device using <code>PHP_ZKLib</code> class.\n     *\n     * All responses generate by PHP_ZKLib class are not in XML format, it is used <code>build_command_response</code>\n     * to build an XML response, just to keep the TAD class behavior. For this purpose, the method uses class constans\n     * <code>ZKLib::XML_SUCCESS_RESPONSE</code> and <code>ZKLib::XML_FAIL_RESPONSE</code>.\n     *\n     * @param string $command command to be sending.\n     * @param array $args command args.\n     * @return string string device response.\n     */\n    public function execute_command_via_zklib($command, array $args = [])\n    {\n        $command_args = $this->config_array_items($args);\n        $response = $this->zklib->{$command}(array_merge(['encoding'=>$this->encoding], $command_args));\n\n        return $response;\n    }\n\n    /**\n     * Returns device's IP address.\n     *\n     * @return string IP address.\n     */\n    public function get_ip()\n    {\n        return $this->ip;\n    }\n\n    /**\n     * Returns device's internal code.\n     *\n     * @return int internal code.\n     */\n    public function get_internal_id()\n    {\n        return $this->internal_id;\n    }\n\n    /**\n     * Returns device's comm code.\n     *\n     * @return int code.\n     */\n    public function get_com_key()\n    {\n        return $this->com_key;\n    }\n\n    /**\n     * Returns device's string description.\n     *\n     * @return string device description.\n     */\n    public function get_description()\n    {\n        return $this->description;\n    }\n\n    /**\n     * Returns device's connection timeout.\n     *\n     * @return int connection timeout.\n     */\n    public function get_connection_timeout()\n    {\n        return $this->connection_timeout;\n    }\n\n    /**\n     * Returns device's encoding (used for SOAP requests and responses).\n     *\n     * @return string encoding.\n     */\n    public function get_encoding()\n    {\n        return $this->encoding;\n    }\n\n    /**\n     * Return device's UDP port number.\n     *\n     * @return int port number.\n     */\n    public function get_udp_port()\n    {\n        return $this->udp_port;\n    }\n\n    /**\n     * Tells if device is ready (alive) to process requests.\n     *\n     * @return boolean <b>true</b> if device is alive, <b>false</b> otherwise.\n     */\n    public function is_alive()\n    {\n        return static::is_device_online($this->get_ip(), $this->connection_timeout);\n    }\n\n    /**\n     * Throws an Exception when device is not alive.\n     *\n     * @return boolean <b><code>true</code></b> if there is a connection with the device.\n     * @throws ConnectionError\n     */\n    private function check_for_connection()\n    {\n        if (!$this->is_alive()) {\n            throw new ConnectionError('Imposible iniciar conexión con dispositivo ' . $this->get_ip());\n        }\n\n        return true;\n    }\n\n    /**\n     * Tells if the command requested is in valid commands set.\n     *\n     * @param string $command command requested.\n     * @return boolean <code><b>true</b></code> if the command es known by the class.\n     * @throws UnrecognizedCommand\n     */\n    private function check_for_valid_command($command)\n    {\n        $tad_commands = static::commands_available();\n\n        if (!in_array($command, $tad_commands)) {\n            throw new UnrecognizedCommand(\"Comando $command no reconocido!\");\n        }\n\n        return true;\n    }\n\n    /**\n     * Tells if the arguments supplied are in valid args set.\n     *\n     * @param array $args args array to be verified.\n     * @return <b><code>true</code></b> if all args supplied are valid (known by the class).\n     * @throws TAD\\Exceptions\\UnrecognizedArgument\n     */\n    private function check_for_unrecognized_args(array $args)\n    {\n        if (0 !== count($unrecognized_args = array_diff(array_keys($args), static::get_valid_commands_args()))) {\n            throw new UnrecognizedArgument('Parámetro(s) desconocido(s): ' . join(', ', $unrecognized_args));\n        }\n\n        return true;\n    }\n\n    /**\n     * Tells if it's necessary to do a device database update. To do this, the method verified the command\n     * executed to see if it did any adding, deleting or updating of database device. In that case, a\n     * <code>refesh_db</code> command is executed.\n     *\n     * @param string $command_executed command executed.\n     */\n    private function check_for_refresh_tad_db($command_executed)\n    {\n        preg_match('/^(set_|delete_)/', $command_executed) && $this->execute_command_via_tad_soap('refresh_db', []);\n    }\n\n    /**\n     * Returns an array with all parseable_args, allowed by the class, initialized with specific values\n     * passed through $values array. Those args not passed in method param will be set to null.\n     *\n     * @param array $values array values to be analized.\n     * @return array array generated.\n     */\n    private function config_array_items(array $values)\n    {\n        $normalized_args = [];\n\n        foreach (static::get_valid_commands_args() as $parseable_arg_key) {\n            $normalized_args[$parseable_arg_key] =\n                    isset($values[$parseable_arg_key]) ? $values[$parseable_arg_key] : null;\n        }\n\n        return $normalized_args;\n    }\n}\n"
  },
  {
    "path": "lib/TADFactory.php",
    "content": "<?php\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2014 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\nnamespace TADPHP;\n\nuse TADPHP\\Providers\\TADSoap;\nuse TADPHP\\Providers\\TADZKLib;\nuse TADPHP\\TAD;\n\nclass TADFactory\n{\n    private $options;\n\n    /**\n     * Registers attributes values for <code>TAD\\TAD</code> and <code>Providers\\ZKLib</code> classes.\n     *\n     * @param array $options attributes values.\n     */\n    public function __construct(array $options = [])\n    {\n        $this->options  = $options;\n    }\n\n    /**\n     * Returns an <code><b>TAD\\TAD</b></code> class instance.\n     *\n     * @return TAD class instance.\n     */\n    public function get_instance()\n    {\n        $options = $this->options;\n        $this->set_options($this->get_default_options(), $options);\n\n        $soap_options = [\n            'location' => \"http://{$options['ip']}/iWsService\",\n            'uri' => 'http://www.zksoftware/Service/message/',\n            'connection_timeout' => $options['connection_timeout'],\n            'exceptions' => false,\n            'trace' => true\n        ];\n\n        $soap_client = new \\SoapClient(null, $soap_options);\n\n        return new TAD(\n            new TADSoap($soap_client, $soap_options),\n            new TADZKLib($options),\n            $options\n        );\n    }\n\n    /**\n     * Returns a default values array used by <code>TAD\\TAD</code> y <code>Providers\\ZKLib</code> classes.\n     *\n     * @return array default values.\n     */\n    private function get_default_options()\n    {\n        $default_options['ip'] = '169.254.0.1';\n        $default_options['internal_id'] = 1;\n        $default_options['com_key'] = 0;\n        $default_options['description'] = 'N/A';\n        $default_options['connection_timeout'] = 5;\n        $default_options['soap_port'] = 80;\n        $default_options['udp_port'] = 4370;\n        $default_options['encoding'] = 'iso8859-1';\n\n        return $default_options;\n    }\n\n    /**\n     * Set all array items to a known default values.\n     *\n     * @param array $base_options default values\n     * @param array $options default values to be changed to a known values.\n     */\n    private function set_options(array $base_options, array &$options)\n    {\n        foreach ($base_options as $key => $default) {\n            !isset($options[$key]) ? $options[$key] = $default : null;\n        }\n    }\n}\n"
  },
  {
    "path": "lib/TADResponse.php",
    "content": "<?php\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2015 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\nnamespace TADPHP;\n\nuse TADPHP\\Exceptions\\FilterArgumentError;\n\n/**\n * Class to handle reponses generated by Time & Attendance devices.\n */\nclass TADResponse\n{\n    const XML_NO_DATA_FOUND = '<Row><Result>0</Result><Information>No data!</Information></Row>';\n    const DEFAULT_XML_HEADER = '<?xml version=\"1.0\" encoding=\"iso8859-1\" standalone=\"no\"?>';\n\n    /**\n     * @var string Response's XML header.\n     */\n    private $response_header;\n\n    /**\n     * @var string Response's XML body.\n     */\n    private $response_body;\n\n    /**\n     * @var string Response's encoding.\n     */\n    private $encoding;\n\n    /**\n     * @var boolean Indicates if Response's has no data (empty).\n     */\n    private $is_empty_response;\n\n    /**\n     * Initialize the class.\n     *\n     * @param string $response XML string that represents the TAD response.\n     */\n    public function __construct($response, $encoding)\n    {\n        $header = $this->extract_xml_header($response);\n\n        if ('' === $header) {\n            $this->set_header(self::DEFAULT_XML_HEADER);\n        }\n\n        $this->set_encoding($encoding);\n        $this->set_response($response);\n    }\n\n    /**\n     * Returns response formatted according $options.\n     *\n     * @param array $options format to apply on the response.\n     * @return mixed Response formatted.\n     */\n    public function get_response(array $options = [])\n    {\n        if (!isset($options['format'])) {\n            $options['format'] = 'xml';\n        }\n\n        return $this->{'to_' . $options['format']}();\n    }\n\n    /**\n     * Sets response.\n     *\n     * @param string $response XML string.\n     */\n    public function set_response($response='')\n    {\n        if ($this->is_there_no_data($response)) {\n            !$this->is_no_data_response($response) && $response = $this->build_no_data_response($response);\n            $this->is_empty_response = true;\n        } else {\n            $this->is_empty_response = false;\n        }\n\n        $xml_header = $this->extract_xml_header($response);\n        if ('' !== $xml_header && 0!== strcmp($this->response_header, $xml_header)) {\n            $this->response_header = $xml_header;\n        }\n\n        $this->response_body = $response;\n    }\n\n    /**\n     * Gets response's encoding.\n     */\n    public function get_encoding()\n    {\n        return $this->encoding;\n    }\n\n    /**\n     * Sets response's encoding\n     *\n     * @param string $encoding encoding.\n     */\n    public function set_encoding($encoding)\n    {\n        $this->encoding = $encoding;\n        $this->set_response_header_encoding($encoding);\n    }\n\n    /**\n     * Returns response's header.\n     *\n     * @return string header.\n     */\n    public function get_header()\n    {\n        return $this->response_header;\n    }\n\n    /**\n     * Sets response's header\n     *\n     * @param string $header header to be set.\n     */\n    public function set_header($header)\n    {\n        $this->response_header = $header;\n    }\n\n    /**\n     * Returns a not sanitized response without header.\n     *\n     * @return string XML string.\n     */\n    public function get_response_body()\n    {\n        return $this->response_body;\n    }\n\n    /**\n     * Tells if response stored by the class is empty (with no data).\n     *\n     * @return boolean <b><code>true</code></b> response is empty otherwise returns <b><code>false</code></b>.\n     */\n    public function is_empty_response()\n    {\n        return $this->is_empty_response;\n    }\n\n    /**\n     * Returns response in XML format.\n     *\n     * @return string XML string.\n     */\n    public function to_xml()\n    {\n        return (string) $this;\n    }\n\n    /**\n     * Returns response in JSON format.\n     *\n     * @return string JSON string generated.\n     */\n    public function to_json()\n    {\n        return $this->is_empty_response() ? '{}' : json_encode(simplexml_load_string((string) $this));\n    }\n\n    /**\n     * Returns response in array format.\n     *\n     * @return array array generated.\n     */\n    public function to_array()\n    {\n        return $this->is_empty_response() ? [] : json_decode($this->to_json((string) $this), true);\n    }\n\n    /**\n     * Return the numbers of nodes that the response has.\n     *\n     * @return int number of nodes.\n     */\n    public function count()\n    {\n        return $this->get_items_number($this->to_xml());\n    }\n\n    /**\n     * Magic method to define a dynamic filter of type 'filter_by'. This filter allows you define\n     * multiple filtering criterias.\n     *\n     * @param string $method method invoked.\n     * @param array $args arguments passed.\n     * @return string filtered XML string.\n     * @throws FilterArgumentError\n     * @throws \\Exception\n     */\n    public function __call($method, $args)\n    {\n        $xml = $this->to_xml();\n\n        if (preg_match('/filter_by_([a-zA-Z]+)/', $method)) {\n            $filters = preg_split('/(by_|_and_)/i', $method, -1);\n            unset($filters[0]);\n\n            if (count($filters) !== count($args)) {\n                throw new FilterArgumentError(\n                    'Invalid number of arguments: '\n                    . count($filters) . ' expected, '\n                    . count($args) . ' given.'\n                );\n            }\n\n            foreach ($filters as $filter) {\n                $filter_args = $this->normalize_filter_args(array_shift($args));\n\n                switch ($filter) {\n                    case 'date':\n                        $filter_regex = '/<DateTime>([0-9]{4}-[0-9]{2}-[0-9]{2})/';\n                        break;\n                    case 'time':\n                        $filter_regex = '/([0-9]{2}:[0-9]{2}:[0-9]{2})<\\/DateTime>/';\n                        break;\n                    case 'datetime':\n                        $filter_regex = '/'\n                            . '<DateTime>([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})<\\/DateTime>'\n                            . '/';\n                        break;\n                    default:\n                        $filter_regex = '/<' . ucwords($filter) . '>(.*?)<\\/' . ucwords($filter) . '>/si';\n                        break;\n                }\n\n                $xml = $this->filter_xml($xml, $filter_regex, $filter_args);\n            }\n\n            $this->set_response($xml);\n\n            return $this;\n        } else {\n            throw new \\Exception('Unknown method ' . $method);\n        }\n    }\n\n    /**\n     * Magic method to get a TADResponse object in string format.\n     *\n     * @return type\n     */\n    public function __toString()\n    {\n        return $this->response_header . $this->sanitize_xml_string($this->response_body);\n    }\n\n    /**\n     * Parses an XML string applying an specific filter.\n     *\n     * @param string $xml input XML string.\n     * @param string $filter regex to be applied on input.\n     * @param array $range boundaries searching criteria.\n     * @param string $xml_init_row_tag XML root tag.\n     * @return string XML string filtered.\n     */\n    public function filter_xml($xml, $filter, array $range=[], $xml_init_row_tag='<Row>')\n    {\n        $xml_header = $this->extract_xml_header($xml);\n\n        $matches = [];\n        $filtered_xml = self::XML_NO_DATA_FOUND;\n\n        $rows = explode($xml_init_row_tag, $xml);\n        $main_xml_init_tag = trim(array_shift($rows));\n        $main_xml_end_tag = '' !== $main_xml_init_tag  ? '</' . str_replace('<', '', $main_xml_init_tag) : '';\n\n        if ('' !== $main_xml_end_tag) {\n            $rows[] = str_replace($main_xml_end_tag, '', array_pop($rows));\n        }\n\n        if (preg_match_all($filter, $xml, $matches)) {\n            $indexes = array_keys(\n                array_filter(\n                    $matches[1],\n                    function($data) use ($range) {\n                        switch (count($range)) {\n                            case 1:\n                                if (isset($range['like'])) {\n                                    $result = false === strpos($data, $range['like']) ? false : true;\n                                } elseif (isset($range['start'])) {\n                                    $result = is_numeric($data) ?\n                                            $data >= $range['start'] :\n                                            0 <= strcmp($data, $range['start']);\n                                } else {\n                                    $result = is_numeric($data) ?\n                                            $data <= $range['end'] :\n                                            0 >= strcmp($data, $range['end']);\n                                }\n                                break;\n                            case 2:\n                                $result = is_numeric($data) ?\n                                    $data >= $range['start'] && $data <= $range['end'] :\n                                    !(strcmp($data, $range['start']) < 0 || strcmp($data, $range['end']) > 0);\n                                break;\n                            default:\n                                $result = false;\n                        }\n\n                        return $result;\n                    }\n                )\n            );\n\n                $filtered_xml = (\n                    0 === count($indexes) ?\n                    self::XML_NO_DATA_FOUND :\n                    join(\n                        '',\n                        array_map(\n                            function($index) use ($rows, $xml_init_row_tag) {\n                                return $xml_init_row_tag . $rows[$index];\n                            },\n                            $indexes\n                        )\n                    )\n                );\n        }\n\n        $xml = $xml_header . $main_xml_init_tag . trim($filtered_xml) . $main_xml_end_tag;\n\n        return $this->sanitize_xml_string($xml);\n    }\n\n    /**\n     * Gets response's XML header.\n     *\n     * @param string $xml XML string.\n     * @return string XML header.\n     */\n    private function extract_xml_header(&$xml)\n    {\n        $xml_header = '';\n\n        if (false !== strpos($xml, '?>')) {\n            $xml_items = explode('?>', $xml);\n\n            $xml_header = $xml_items[0] . '?>';\n            $xml = $xml_items[1];\n        }\n\n        $xml = $this->sanitize_xml_string($xml);\n        return trim($xml_header);\n    }\n\n    /**\n     * Adds encoding to XML header.\n     *\n     * @param string $encoding encoding to be set in XML header.\n     * @return string XML header with encoding.\n     */\n    private function set_response_header_encoding($encoding = 'utf-8')\n    {\n        $header = $this->response_header;\n\n        if (is_null($header) || '' === $header) {\n            $header ='<?xml version=\"1.0\" encoding=\"' . $encoding . '\" standalone=\"no\"?>';\n        } else {\n            $header = preg_filter('/encoding=\"([^\"]+)\"/', 'encoding=\"' . $encoding . '\"', $header);\n        }\n\n        $this->response_header = $header;\n    }\n\n    /**\n     * Cleans a XML string from undesired chars (in this case EOL by default).\n     *\n     * @param string $xml string to be cleaned out.\n     * @return string XML string cleaned out.\n     */\n    private function sanitize_xml_string($xml, array $undesired_chars = [ \"\\n\", \"\\r\", \"\\t\" ])\n    {\n        return trim(str_replace($undesired_chars, '', $xml));\n    }\n\n    /**\n     * Sets boundaries to be used as filter criteria.\n     *\n     * @param mixed $args boundaries.\n     * @return array boundaries validated.\n     */\n    private function normalize_filter_args($args)\n    {\n        $normalized_filter_args = [];\n        $valid_range_filter = ['start', 'end', 'like'];\n\n        if (is_array($args)) {\n            $args_keys = array_keys($args);\n            array_walk(\n                $args_keys,\n                function ($item) use ($valid_range_filter) {\n                    if (!in_array($item, $valid_range_filter)) {\n                        throw new FilterArgumentError('Invalid range key ' . $item);\n                    }\n                }\n            );\n\n            isset($args['start']) ? $normalized_filter_args['start'] = $args['start'] : null;\n            isset($args['end']) ? $normalized_filter_args['end'] = $args['end'] : null;\n            isset($args['like']) ? $normalized_filter_args['like'] = $args['like'] : null;\n\n        } else {\n            $normalized_filter_args['start'] = $normalized_filter_args['end'] = $args;\n        }\n\n        return $normalized_filter_args;\n    }\n\n    /**\n     * Tells if device's response returns an empty response, represented by an empty XML string\n     * (a string with just an open and end tags).\n     *\n     * @param string $response device response.\n     * @return boolean <b><code>true</code></b> if it is a empty response.\n     */\n    private function is_there_no_data($response)\n    {\n        return ($this->is_no_data_response($response) || 0 === $this->get_items_number($response));\n    }\n\n    /**\n     * Returns the numbers of nodes that the XML response has.\n     *\n     * @param type $response XML string.\n     * @return int number of nodes.\n     */\n    private function get_items_number($response)\n    {\n        if (is_null($response) || '' === trim($response)) {\n            return 0;\n        }\n\n        $response = $this->sanitize_xml_string($response);\n        $xml = new \\SimpleXMLElement($response);\n        $items_number = $xml->count();\n\n        return $items_number;\n    }\n\n    /**\n     * Generates a modified XML response with a NO DATA text.\n     *\n     * @param string $response device response.\n     * @return string modified XML response.\n     */\n    private function build_no_data_response($response)\n    {\n        (is_null($response) || '' === trim($response)) ? $response = '<Response></Response>' : null;\n\n        $response = $this->get_header() . $response;\n        $pos = strpos($response, '>', strpos($response, '>') + 1);\n        $no_data_response = substr_replace($response, self::XML_NO_DATA_FOUND, $pos + 1, false);\n\n        return $no_data_response;\n    }\n\n    /**\n     * Tells if response passed represents an empty XML response.\n     *\n     * @param type $response XML string to be evaluated.\n     * @return boolean <b><code>true</code></b> if it is a empty response.\n     */\n    private function is_no_data_response($response)\n    {\n        return false !== strpos($response, self::XML_NO_DATA_FOUND);\n    }\n}\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<phpunit bootstrap=\"vendor/autoload.php\"\n         colors=\"true\"\n         convertErrorsToExceptions=\"true\"\n         convertNoticesToExceptions=\"true\"\n         convertWarningsToExceptions=\"true\"\n         processIsotalion=\"false\"\n         stopOnFailure=\"false\"\n         syntaxCheck=\"false\"\n         verbose=\"true\"\n>\n  <testsuites>\n    <testsuite name=\"TAD-PHP test suite\">\n      <directory>test</directory>\n    </testsuite>\n  </testsuites>\n</phpunit>"
  },
  {
    "path": "test/helpers/ClassReflection.php",
    "content": "<?php\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2014 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\nnamespace Test\\Helpers;\n\nabstract class ClassReflection\n{\n    /**\n     * Allows you to invoking protected or privated methods of class given.\n     *\n     * Source code taken from website:\n     * @link https://jtreminio.com/2013/03/unit-testing-tutorial-part-3-testing-protected-private-methods-coverage-reports-and-crap/\n     *\n     * @param object &$object object from where you want to invoke protected or private methods.\n     * @param string $method_name method's name\n     * @param array  $parameters method's params.\n     *\n     * @return mixed method's result.\n     */\n    static public function invoke_method(&$object, $method_name, array $parameters = [])\n    {\n        $reflection = new \\ReflectionClass(get_class($object));\n        $method = $reflection->getMethod($method_name);\n        $method->setAccessible(true);\n\n        return $method->invokeArgs($object, $parameters);\n    }\n}\n"
  },
  {
    "path": "test/lib/Providers/TADSoapTest.php",
    "content": "<?php\nnamespace Test\\Providers;\n\nuse TADPHP\\TAD;\nuse TADPHP\\Providers\\TADSoap;\nuse Test\\Helpers\\ClassReflection;\n\n\nclass TADSoapTest extends \\PHPUnit_Framework_TestCase\n{\n  public function testBuildTADSoap()\n  {\n    $soap_options = $this->get_soap_options();\n\n    $soap_client = new \\SoapClient( null, $soap_options );\n    $tad_soap = new TADSoap($soap_client, $soap_options);\n\n    $this->assertNotNull($tad_soap);\n    $this->assertInstanceOf('TADPHP\\Providers\\TADSoap', $tad_soap);\n\n    return $tad_soap;\n  }\n\n  /**\n   * @depends testBuildTADSoap\n   */\n  public function testGetSoapProviderOptions($tad_soap_instance)\n  {\n    $soap_providers_options = $tad_soap_instance->get_soap_provider_options();\n\n    $this->assertInternalType('array', $soap_providers_options);\n    $this->assertArrayHasKey('location', $soap_providers_options);\n    $this->assertArrayHasKey('uri', $soap_providers_options);\n    $this->assertEquals('http://127.0.0.1/iWsService', $soap_providers_options['location']);\n    $this->assertEquals('http://www.zksoftware/Service/message/', $soap_providers_options['uri']);\n  }\n\n  /**\n   * @depends testBuildTADSoap\n   * @dataProvider soapCommandsFixtures\n   */\n  public function testBuildSoapRequest($command, array $args, $expected_soap_string, $encoding, TADSoap $tad_soap)\n  {\n    $args += array_fill_keys( TAD::get_valid_commands_args(), null );\n    $soap_request = $tad_soap->build_soap_request( $command, $args, $encoding );\n\n    $this->assertEquals( $expected_soap_string, $soap_request );\n  }\n\n  /**\n   * @depends testBuildTADSoap\n   */\n  public function testBuildMultipleSoapRequest(TADSoap $tad_soap)\n  {\n    $args = array_fill_keys( TAD::get_valid_commands_args(), null );\n\n    // We uses 'set_user_info' command defined in TADSoap class.\n    // Maybe there is a better way to test this. :-P\n    $soap_request = $tad_soap->build_soap_request('set_user_info', $args, 'iso8859-1');\n\n    $this->assertInternalType('array', $soap_request);\n  }\n\n  public function testExecuteSoapRequest()\n  {\n    $mock_response = '<GetUserInfoResponse></GetUserInfoResponse>';\n    $encoding = 'iso8859-1';\n\n    $soap_options = $this->get_soap_options();\n\n    $soap_client = $this->getMockBuilder('\\SoapClient')\n      ->setConstructorArgs( [ null, [ 'location'=>$soap_options['location'], 'uri'=>$soap_options['uri'] ] ] )\n      ->setMethods( [ '__doRequest' ] )\n      ->getMock();\n\n    $soap_client->expects( $this->any() )\n      ->method( '__doRequest' )\n      ->with( $this->anything(), $soap_options['location'], '', SOAP_1_1 )\n      ->will( $this->returnValue( $mock_response ) );\n\n    $tad_soap = $this->getMockBuilder('TADPHP\\Providers\\TADSoap')\n      ->setConstructorArgs( [ $soap_client, $soap_options ] )\n      ->setMethods( null )\n      ->getMock();\n\n    $args = array_fill_keys( TAD::get_valid_commands_args(), null );\n    $args['com_key'] = 0;\n    $args['pin'] = 123;\n\n    $response = $tad_soap->execute_soap_command( 'get_user_info', $args, $encoding );\n\n    $this->assertNotEmpty( $response );\n  }\n\n  public function testExecuteMultipleSoapRequests()\n  {\n    $soap_requests = [\n        '<GetDate><ArgComKey>0</ArgComKey></GetDate>',\n        '<Restart><ArgComKey>0</ArgComKey></Restart>'\n    ];\n\n    $mock_response = '<RestartResponse><Row><Result>1</Result><Information>Success!</Information></Row></RestartResponse>';\n\n    $soap_options = $this->get_soap_options();\n\n    $soap_client = $this->getMockBuilder('\\SoapClient')\n      ->setConstructorArgs( [ null, [ 'location'=>$soap_options['location'], 'uri'=>$soap_options['uri'] ] ] )\n      ->setMethods( [ '__doRequest' ] )\n      ->getMock();\n\n    $soap_client->expects( $this->any() )\n      ->method( '__doRequest' )\n      ->with( $this->anything(), $soap_options['location'], '', SOAP_1_1 )\n      ->will( $this->returnValue( $mock_response ) );\n\n    $tad_soap = $this->getMockBuilder('TADPHP\\Providers\\TADSoap')\n      ->setConstructorArgs( [ $soap_client, $soap_options ] )\n      ->setMethods( null )\n      ->getMock();\n\n\n    $result = ClassReflection::invoke_method(\n            $tad_soap,\n            'execute_multiple_soap_requests',\n            [ $soap_requests, $soap_options['location'] ] );\n\n    $this->assertNotEmpty( $result );\n  }\n\n\n  public function soapCommandsFixtures()\n  {\n    $encoding = 'iso8859-1';\n\n    return [\n      [ 'get_date', ['com_key'=>0], '<?xml version=\"1.0\" encoding=\"'. $encoding .'\" standalone=\"no\"?><GetDate><ArgComKey>0</ArgComKey></GetDate>', $encoding ],\n      [ 'get_att_log', ['com_key'=>0], '<?xml version=\"1.0\" encoding=\"' . $encoding. '\" standalone=\"no\"?><GetAttLog><ArgComKey>0</ArgComKey><Arg><PIN></PIN></Arg></GetAttLog>', $encoding ],\n      [ 'get_att_log', ['com_key'=>0, 'pin'=>'99999999'], '<?xml version=\"1.0\" encoding=\"' . $encoding . '\" standalone=\"no\"?><GetAttLog><ArgComKey>0</ArgComKey><Arg><PIN>99999999</PIN></Arg></GetAttLog>', $encoding ],\n      [ 'set_user_template', [\n              'com_key' => 0,\n              'pin' => '999',\n              'finger_id' => '0',\n              'size' => '514',\n              'valid' => '1',\n              'template' => 'foobartaz123'\n            ],\n          '<?xml version=\"1.0\" encoding=\"' . $encoding . '\" standalone=\"no\"?><SetUserTemplate><ArgComKey>0</ArgComKey><Arg><PIN>999</PIN><FingerID>0</FingerID><Size>514</Size><Valid>1</Valid><Template>foobartaz123</Template></Arg></SetUserTemplate>',\n          $encoding\n      ]\n    ];\n  }\n\n  private function get_soap_options()\n  {\n    $soap_options = [\n        'location' => \"http://127.0.0.1/iWsService\",\n        'uri' => 'http://www.zksoftware/Service/message/',\n        'connection_timeout' => 1,\n        'exceptions' => false,\n        'trace' => true\n    ];\n\n    return $soap_options;\n  }\n}"
  },
  {
    "path": "test/lib/Providers/TADZKLibTest.php",
    "content": "<?php\nnamespace Test\\Providers;\n\nuse TADPHP\\Providers\\TADZKLib;\nuse Test\\Helpers\\ClassReflection;\n\n\nclass TADZKLibTest extends \\PHPUnit_Framework_TestCase\n{\n\n  public function testBuildTADZKLibIsOk()\n  {\n    $options = ['ip' => '127.0.0.1', 'udp_port' => 4370, 'connection_timeout'=>1];\n    $zk = new TADZKLib( $options );\n\n    $this->assertNotNull($zk);\n    $this->assertInstanceOf('TADPHP\\Providers\\TADZKLib', $zk);\n\n    return $zk;\n  }\n  /**\n   * @depends testBuildTADZKLibIsOk\n   * @dataProvider build_commands_fixtures\n   */\n  public function testBuildCommandResponse($command, $result_code, $result, $expected_xml, $encoding, TADZKLib $zk)\n  {\n    $result = ClassReflection::invoke_method($zk, 'build_command_response', [ $command, $result_code, $result, $encoding ]);\n\n    $this->assertEquals($expected_xml, $result);\n  }\n\n  /**\n   * @depends testBuildTADZKLibIsOk\n   * @dataProvider datetime_fixtures\n   */\n  public function testSettingUpDateIsOk($datetime, TADZKLib $zk)\n  {\n    $valid_datetime_keys = ['year', 'month', 'day', 'hour', 'minute', 'second'];\n\n    $result = ClassReflection::invoke_method( $zk, 'setup_datetime', [$datetime] );\n    $result_keys = array_keys($result);\n\n    $this->assertEmpty( array_diff( $valid_datetime_keys, $result_keys ), 'invalid keys found!');\n  }\n\n  /**\n   * @depends testBuildTADZKLibIsOk\n   */\n  public function testReverseHex(TADZKLib $zk)\n  {\n    $hex_data = \"000000000000000000000000000000002202000000000000420400000000000043000000000000004a0a00000000000002000000020000001027000010270000400d0300ce220000ee240000fd0c0300000000000000000000000000\";\n\n    $reversed_hex = ClassReflection::invoke_method($zk, 'reverse_hex', [$hex_data]);\n    $reversed_reversed_hex = ClassReflection::invoke_method($zk, 'reverse_hex', [$reversed_hex]);\n\n    $this->assertEquals( strlen($hex_data), strlen($reversed_hex));\n    $this->assertEquals( $hex_data, $reversed_reversed_hex );\n  }\n\n  /**\n   * @depends testBuildTADZKLibIsOk\n   */\n  public function testEncodeTime(TADZKLib $zk)\n  {\n    $expected_encoded_time = 480003771; // This integer represents '2014-12-07 14:22:51' timestamp.\n\n    $dt = ['date'=>'2014-12-07', 'time'=>'14:22:51'];\n    $t = ClassReflection::invoke_method($zk, 'setup_datetime', [$dt]);\n    $encoded_time = ClassReflection::invoke_method($zk, 'encode_time', [$t]);\n\n    $this->assertInternalType('integer', $encoded_time);\n    $this->assertEquals($expected_encoded_time, $encoded_time);\n  }\n\n\n\n  public function build_commands_fixtures()\n  {\n    $encoding = 'iso8859-1';\n    return [\n      [ 'restart', true, true, '<RestartResponse><Row><Result>1</Result><Information>Successfully!</Information></Row></RestartResponse>', $encoding],\n      [ 'poweroff', false, false, '<PoweroffResponse><Row><Result>0</Result><Information>Fail!</Information></Row></PoweroffResponse>', $encoding],\n      [ 'foo', true, ['bar'=>0, 'taz'=>0], '<FooResponse><Row><bar>0</bar><taz>0</taz></Row></FooResponse>', $encoding],\n      [ 'foo', true, [], '<FooResponse></FooResponse>', $encoding]\n    ];\n  }\n\n  public function datetime_fixtures()\n  {\n    return [\n      'empty_args' => [ [] ],\n      'only_date'  => [ ['date'=>'2014-12-06'] ],\n      'only_time'  => [ ['time'=>'08:38:23'] ],\n      'valid_args' => [ ['date'=>'2014-12-06', 'time'=>'08:38:23'] ] ,\n      'crazy_args' => [ ['foo'=>'123', 'bar'=>'abc', 'baz'=>'#$%'] ]\n    ];\n  }\n}"
  },
  {
    "path": "test/lib/RealTADTest.php",
    "content": "<?php\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2014 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\n/**\n * RealTADTest: Tests about real interaction between TAD-PHP classes and\n * ZK Time & Attendance Devices.\n */\nnamespace Test;\n\nuse TADPHP\\TAD;\nuse TADPHP\\TADFactory;\n\nclass RealTADTest extends \\PHPUnit_Framework_TestCase\n{\n    /**\n     * @var string TAD real ip address.\n     */\n    private $tad_ip;\n\n    /**\n     * @var string TAD encoding.\n     */\n    private $tad_encoding;\n\n    public function setUp()\n    {\n        $this->tad_ip = '192.168.100.156';\n        $this->tad_encoding = 'iso8859-1';\n\n        if (!TAD::is_device_online($this->tad_ip)) {\n            $this->markTestSkipped(\"Real TAD tests disabled. Device in {$this->tad_ip} is offline!\");\n        }\n    }\n\n    public function testDeviceIsOnLine()\n    {\n        $tad_options = [ 'ip' => $this->tad_ip, 'encoding' => $this->tad_encoding ];\n        $tad = (new TADFactory($tad_options))->get_instance();\n\n        $this->assertNotNull($tad);\n        $this->assertInstanceOf('TADPHP\\TAD', $tad);\n\n        return $tad;\n    }\n\n    /**\n     * @depends testDeviceIsOnLine\n     */\n    public function testGetDate(TAD $tad)\n    {\n        $date = $tad->get_date();\n\n        $xml_object = new \\SimpleXMLElement($date);\n        $this->assertNotNull($xml_object->Row->Date);\n        $this->assertNotNull($xml_object->Row->Time);\n        $this->assertRegExp('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', (string) $xml_object->Row->Date);\n        $this->assertRegExp('/^[0-9]{2}:[0-9]{2}:[0-9]{2}$/', (string) $xml_object->Row->Time);\n    }\n\n    /**\n     * @depends testDeviceIsOnLine\n     */\n    public function testSetDate(TAD $tad)\n    {\n        $date = '2000-01-01';\n        $time = '12:15:30';\n        $expected_response = $this->build_expected_response('SetDateResponse');\n\n        $response = $tad->set_date(['date'=>$date, 'time'=>$time])->to_xml();\n        $this->assertEquals($expected_response, $response);\n\n        $dt = $tad->get_date();\n        $xml_object = new \\SimpleXMLElement($dt);\n        $tad->set_date();\n\n        $this->assertEquals($date, (string)$xml_object->Row->Date);\n        $this->assertRegExp('/12:15:[0-9]{2}/', (string)$xml_object->Row->Time);\n    }\n\n    /**\n     * @depends testDeviceIsOnLine\n     */\n    public function testGetFreeSizes(TAD $tad)\n    {\n        $expected_free_sizes_keys = [\n            'att_logs_available', 'templates_available', 'att_logs_capacity',\n            'templates_capacity', 'passwords_stored', 'admins_stored',\n            'att_logs_stored', 'templates_stored', 'users_stored'\n        ];\n\n        $expected_free_sizes_items = count($expected_free_sizes_keys);\n\n        $free_sizes = $tad->get_free_sizes();\n        $xml_object = new \\SimpleXMLElement($free_sizes->to_xml());\n\n        $this->assertEquals($expected_free_sizes_items, $xml_object->Row->children()->count());\n\n        $free_sizes_array = $free_sizes->to_array()['Row'];\n        $free_sizes_keys = array_keys($free_sizes_array);\n\n        $this->assertTrue($expected_free_sizes_keys === $free_sizes_keys);\n    }\n\n    /**\n     * @depends testDeviceIsOnLine\n     */\n    public function testSetUserInfoAndDeleteUser(TAD $tad)\n    {\n        $user_info = [\n            'pin' => 123,\n            'name' => 'Foo Bar',\n            'password' => 8888,\n            'privilege' => 0,\n        ];\n\n        $expected_set_user_info_response = $this->build_expected_response('SetUserInfoResponse');\n        $expected_delete_user_info_response = $this->build_expected_response('DeleteUserResponse');\n\n        $fs = $tad->get_free_sizes();\n        $total_users_before = (integer) $fs->to_array()['Row']['users_stored'];\n\n        $set_user_info_response = $tad->set_user_info($user_info)->to_xml();\n\n        $fs = $tad->get_free_sizes();\n        $total_users_after = (integer) $fs->to_array()['Row']['users_stored'];\n\n        $delete_user_response = $tad->delete_user([ 'pin' => $user_info['pin'] ])->to_xml();\n\n        $this->assertEquals($expected_set_user_info_response, $set_user_info_response);\n        $this->assertEquals($expected_delete_user_info_response, $delete_user_response);\n        $this->assertTrue($total_users_after === $total_users_before + 1);\n    }\n\n    /**\n     * @depends testDeviceIsOnLine\n     */\n    public function testGetUserInfo(TAD $tad)\n    {\n        $user_info = [\n            'pin' => 123,\n            'name' => 'Foo Bar',\n            'password' => 8888,\n            'privilege' => 0,\n        ];\n\n        $tad->set_user_info($user_info);\n\n        $response = $tad->get_user_info(['pin'=>123]);\n\n        $user_info_response = $response->to_array()['Row'];\n        $tad->delete_user(['pin'=>123]);\n\n        $this->assertEquals($user_info_response['PIN2'], $user_info['pin']);\n        $this->assertEquals($user_info_response['Name'], $user_info['name']);\n        $this->assertEquals($user_info_response['Password'], $user_info['password']);\n        $this->assertEquals($user_info_response['Privilege'], $user_info['privilege']);\n    }\n\n    /**\n     * @depends testDeviceIsOnLine\n     */\n    public function testGetAllUserInfo(TAD $tad)\n    {\n        $fs = $tad->get_free_sizes();\n        $total_users = (integer) $fs->to_array()['Row']['users_stored'];\n\n        $all_user_info_items = $tad->get_all_user_info()->count();\n\n        $this->assertEquals($total_users, $all_user_info_items);\n    }\n\n    /**\n     * @depends testDeviceIsOnLine\n     */\n    public function testDeleteUserPassword(TAD $tad)\n    {\n        $user_info = [\n            'pin' => 123,\n            'name' => 'Foo Bar',\n            'password' => 8888,\n            'privilege' => 0,\n        ];\n\n        $expected_response = $this->build_expected_response('ClearUserPasswordResponse');\n\n        $tad->set_user_info($user_info);\n\n        $before_password = $tad->get_user_info(['pin'=>$user_info['pin']])->to_array()['Row']['Password'];\n        $response = $tad->delete_user_password(['pin'=>$user_info['pin']])->to_xml();\n\n        $after_password = $tad->get_user_info(['pin'=>$user_info['pin']])->to_array()['Row']['Password'];\n        $tad->delete_user(['pin'=>$user_info['pin']]);\n\n        $this->assertEquals($expected_response, $response);\n        $this->assertNotEquals($after_password, $before_password);\n        $this->assertEmpty($after_password);\n    }\n\n    /**\n     * @depends testDeviceIsOnLine\n     */\n    public function testGetAndSetAndDeleteUserTemplate(TAD $tad)\n    {\n        $user_info = [\n            'pin' => 123,\n            'name' => 'Foo Bar',\n            'password' => 8888,\n            'privilege' => 0,\n        ];\n\n        $template1_vx9 = \"ocosgoulTUEdNKVRwRQ0I27BDTEkdMEONK9KQQunMVSBK6VPLEENk9MwgQ+DP3PBC1FTXEEG4ihpQQQ3vFQBO4K+WwERYilHAQ8ztktBEBbKQ0ELDtJrwQ7dqCiBCz+/IgEGKrBjQQhEO0zBFQNDQYEKFbhrQQdLF1wBDxclfUELMNFXwQRvvmHBCslKUAEZfU1OQRzmIU5BXRW0eoEKPMltgQnQGUyBJQSfRIEUSzIdAQ45l3gBByHUTMEJ5yVhQQmi0UZBFHvYPUEGeKxTAQ6rFGNBCIYURoEOZS9VwR+1M4RoE5m0DRUTF8DHd6HdqxHAxWmj393M28DDX2FkanKi/t7LGsDCWqGarmt1BaL/25nAwVaiipu/cgcQGKG6mcDBU6KYmr5wChQcobmJIsDBUKKJmZ1uExyi+ZaYwMFMgU2CQCSinYdnJsDBR4Ghl3Q4owa3dnfAwUamdlZlR5p2Zi7AwUSndERlfOpWZlfAwUOiQzVkLDhDopRUVTLAwT2iQ0ZjIzVMolNFRcDBN6I0ZlQebVaiEjRVwMEyolVVUxVxXKEBRUTAwS+iZVYyD3JhoQJFTMDBLKJlVUIKcWShBVVTwMIkoWVkFQhyaaEVZ1rAwh6hVlUPAW+iNGd3wMIToWdlBnWiRWZ3aMDDCqRmZjRpZmrAxASjd2Vnh2/gAA==\";\n        $template1_data = [\n          'pin' => $user_info['pin'],\n          'finger_id' => 0,\n          'size' => 514,\n          'valid' => 1,\n          'template' => $template1_vx9\n        ];\n\n        $no_template_response = $this->build_expected_response('GetUserTemplateResponse', false, 'No data!');\n        $expected_set_user_template_response = $this->build_expected_response('SetUserTemplateResponse');\n        $expected_delete_template_response = $this->build_expected_response('DeleteTemplateResponse');\n\n        // Create a test user.\n        $tad->set_user_info($user_info);\n\n        // GetUserTemplate test section.\n        $before_set_template = $tad->get_user_template(['pin'=>$user_info['pin']]);\n        $this->assertEquals($no_template_response, $before_set_template->to_xml());\n\n        // SetUerTemplate test section.\n        $response = $tad->set_user_template($template1_data);\n        $this->assertEquals($expected_set_user_template_response, $response->to_xml());\n\n        $after_set_template = $tad->get_user_template(['pin'=>$user_info['pin']]);\n        $raw_after_template = $after_set_template->to_array($after_set_template)['Row']['Template'];\n\n        $this->assertNotEmpty($after_set_template);\n        $this->assertEquals($template1_vx9, $raw_after_template);\n\n        // DeleteTemplate test section.\n        $response = $tad->delete_template(['pin'=>$user_info['pin']]);\n        $this->assertEquals($expected_delete_template_response, $response->to_xml());\n\n        $after_delete_template = $tad->get_user_template(['pin'=>$user_info['pin']]);\n        $this->assertEquals($no_template_response, $after_delete_template->to_xml());\n\n        // Delete the test user created above.\n        $tad->delete_user(['pin'=>$user_info['pin']]);\n    }\n\n    /**\n     * @depends testDeviceIsOnLine\n     */\n    public function testDeleteAdmin(TAD $tad)\n    {\n        $user_info = [\n            'pin' => 123,\n            'name' => 'Foo Bar',\n            'password' => 8888,\n            'privilege' => 14, // Superadmin.\n        ];\n\n        $expected_response = $this->build_expected_response('DeleteAdminResponse');\n\n        $tad->set_user_info($user_info);\n\n        $response = $tad->delete_admin();\n\n        $fs = $tad->get_free_sizes();\n        $total_admins = (integer) $fs->to_array()['Row']['admins_stored'];\n        $tad->delete_user(['pin'=>$user_info['pin']]);\n\n        $this->assertEquals($expected_response, $response);\n        $this->assertTrue(0 === $total_admins);\n    }\n\n    /**\n     * @depends testDeviceIsOnLine\n     */\n    public function testRestartDevice(TAD $tad)\n    {\n        $expected_response = $this->build_expected_response('RestartResponse');\n        $device_ip = $tad->get_ip();\n\n        $response = $tad->restart();\n\n        sleep(2); // Lets give it a few seconds to test if it's online.\n        $is_device_online = TAD::is_device_online($device_ip);\n\n        $this->assertEquals($expected_response, $response);\n        $this->assertNotTrue($is_device_online);\n    }\n\n    private function build_expected_response($base_tag, $result = true, $information = 'Successfully!')\n    {\n        $result_code = (int) $result;\n        $xml_header = '<?xml version=\"1.0\" encoding=\"' . $this->tad_encoding. '\" standalone=\"no\"?>';\n        $base_response = \"<Row><Result>$result_code</Result><Information>$information</Information></Row>\";\n        $open_tag = \"<$base_tag>\";\n        $end_tag = \"</$base_tag>\";\n\n        $expected_response = $xml_header . $open_tag . $base_response . $end_tag;\n\n        return $expected_response;\n    }\n}\n"
  },
  {
    "path": "test/lib/TADResponseTest.php",
    "content": "<?php\n\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2015 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\nnamespace Test;\n\nuse TADPHP\\TADResponse;\n\n\nclass TADResponseTest  extends \\PHPUnit_Framework_TestCase\n{\n    public function testTADResponseIsInstantiatedCorrectly()\n    {\n        $header = '<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>';\n        $response = '<FooResponse><value_1>Bar</value_1><value_2>Taz</value_2></FooResponse>';\n        $encoding = 'utf-8';\n\n        $tr = new TADResponse($header . $response, $encoding);\n\n        $this->assertInstanceOf('TADPHP\\TADResponse', $tr);\n\n        $this->assertEquals($encoding, $tr->get_encoding());\n        $this->assertEquals($header, $tr->get_header());\n        $this->assertEquals($response, $tr->get_response_body());\n        $this->assertEquals($header . $response, $tr->get_response());\n\n        return $tr;\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     */\n    public function testGetResponseInDifferentFormats(TADResponse $tr)\n    {\n        $xml_response = $tr->get_response(['format'=>'xml']);\n        libxml_use_internal_errors(true);\n        $valid_xml = simplexml_load_string($xml_response);\n        libxml_use_internal_errors(false);\n        $this->assertNotFalse($valid_xml);\n\n        $json_response = $tr->get_response(['format'=>'json']);\n        $this->assertNotNull(json_decode($json_response));\n\n        $array_response = $tr->get_response(['format'=>'array']);\n        $this->assertTrue(is_array($array_response) && 0 !== count($array_response));\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     */\n    public function testSetEmptyResponse(TADResponse $tr)\n    {\n        $tr->set_response('');\n        $expected_empty_response = ''\n                . '<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>'\n                . '<Response>'\n                . '<Row><Result>0</Result><Information>No data!</Information></Row>'\n                . '</Response>';\n        $this->assertEquals(1, $tr->count());\n        $this->assertTrue($tr->is_empty_response());\n        $this->assertEquals($expected_empty_response, $tr->get_response());\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     */\n    public function testChangeAnAlreadySetResponse(TADResponse $tr)\n    {\n        $tr = new TADResponse('<Response><data>Foo</data></Response>', 'iso8859-1');\n        $first_response = $tr->to_xml();\n        $tr->set_response(''\n                . '<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>'\n                . '<Response><data>Foo</data></Response>'\n        );\n        $last_response = $tr->to_xml();\n\n        $this->assertNotEquals($last_response, $first_response);\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     */\n    public function testGetHeader(TADResponse $tr)\n    {\n        $expected_header = '<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>';\n        $this->assertEquals($expected_header, $tr->get_header());\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     */\n    public function testSetHeader(TADResponse $tr)\n    {\n        $expected_header = '<?xml version=\"1.1\" encoding=\"iso8859-1\" standalone=\"yes\"?>';\n        $tr->set_header($expected_header);\n\n        $this->assertEquals($expected_header, $tr->get_header());\n    }\n\n    public function testGetResponseBody()\n    {\n        $tr = new TADResponse('<Response><data>Foo</data></Response>', 'iso8859-1');\n        $response_body = '<Response><data>Foo</data></Response>';\n\n        $this->assertEquals($response_body, $tr->get_response_body());\n    }\n\n    public function testIsEmptyResponse()\n    {\n        $tr = new TADResponse('<Response></Response>', 'iso8859-1');\n        $this->assertTrue($tr->is_empty_response());\n    }\n\n    public function testCount()\n    {\n        $tr = new TADResponse('<Response></Response>', 'iso8859-1');\n        $this->assertTrue(0 === $tr->count()-1);\n    }\n\n    /**\n     * @expectedException \\Exception\n     */\n    public function testExceptionIsThrownWhenUnknownMethodIsInvoked()\n    {\n        $tr = new TADResponse('<Response></Response>', 'iso8859-1');\n        $tr->foo();\n    }\n\n    /**\n     * @expectedException TADPHP\\Exceptions\\FilterArgumentError\n     */\n    public function testFilterArgumentExecptionisThrownWhenWrongArgumentNumber()\n    {\n        $tr = new TADResponse('<Response></Response>', 'iso8859-1');\n        $tr->filter_by_date_and_pin(123);\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     * @dataProvider xmlAttLogFixture\n     * @expectedException TADPHP\\Exceptions\\FilterArgumentError\n     */\n    public function testFilterResponseByDateThrowsFilterArgumentExceptionWithInvalidRangeKey($xml, TADResponse $tr)\n    {\n        $tr->set_response($xml);\n\n        $date_range = ['foo'=>'2014-01-01', 'end'=>'2014-11-29'];\n        $tr->filter_by_date($date_range);\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     * @dataProvider xmlAttLogFixture\n     */\n    public function testFilterResponseByDate($xml, TADResponse $tr)\n    {\n        $tr->set_response($xml);\n        $this->assertEquals(9, $tr->filter_by_date(['start'=>'2014-01-01'])->count());\n        $this->assertEquals(1, $tr->filter_by_date(['start'=>'2014-12-04'])->count());\n        $this->assertTrue($tr->filter_by_date(['start'=>'2014-12-05'])->is_empty_response());\n\n        $tr->set_response($xml);\n        $this->assertEquals(9, $tr->filter_by_date(['end'=>'2014-12-31'])->count());\n//        $this->assertEquals(1, $tr->filter_by_date(['end'=>'2014-11-30'])->count());\n        $this->assertTrue($tr->filter_by_date(['end'=>'2014-11-29'])->is_empty_response());\n\n        $tr->set_response($xml);\n        $this->assertEquals(9, $tr->filter_by_date(['start'=>'2014-11-01', 'end'=>'2014-12-31'])->count());\n        $this->assertTrue($tr->filter_by_date(['start'=>'2015-01-01', 'end'=>'2015-12-31'])->is_empty_response());\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     * @dataProvider xmlAttLogFixture\n     */\n    public function testFilterResponseByTime($xml, TADResponse $tr)\n    {\n        $tr->set_response($xml);\n        $this->assertEquals(5, $tr->filter_by_time(['start'=>'18:00:00'])->count());\n        $this->assertEquals(2, $tr->filter_by_time(['end'=>'19:00:00'])->count());\n        $this->assertEquals(1, $tr->filter_by_time(['start'=>'00:00:00', 'end'=>'02:00:00'])->count());\n        $this->assertTrue($tr->filter_by_date(['start'=>'00:00:00', 'end'=>'01:00:00'])->is_empty_response());\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     * @dataProvider xmlAttLogFixture\n     */\n    public function testFilterResponseByDateTime($xml, TADResponse $tr)\n    {\n        $tr->set_response($xml);\n        $this->assertEquals(9, $tr->filter_by_datetime(['start'=>'2014-11-30 18:00:00'])->count());\n        $this->assertEquals(9, $tr->filter_by_datetime(['end'=>'2014-12-31 19:00:00'])->count());\n        $this->assertEquals(1, $tr->filter_by_datetime('2014-12-02 08:01:32')->count());\n        $this->assertTrue($tr->filter_by_datetime('2015-01-01 00:00:00')->is_empty_response());\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     * @dataProvider xmlAttLogFixture\n     */\n    public function testFilterResponseByStatus($xml, TADResponse $tr)\n    {\n        $tr->set_response($xml);\n        $this->assertEquals(9, $tr->filter_by_status(0)->count());\n        $this->assertTrue($tr->filter_by_status(1)->is_empty_response());\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     * @dataProvider xmlUserInfoFixture\n     */\n    public function testFilterResponseByPin($xml, TADResponse $tr)\n    {\n        $tr->set_response($xml);\n        $this->assertEquals(9, $tr->filter_by_pin(['start'=>5])->count());\n\n        $tr->set_response($xml);\n        $this->assertEquals(9, $tr->filter_by_pin(['end'=>9])->count());\n\n        $tr->set_response($xml);\n        $this->assertEquals(1, $tr->filter_by_pin(1)->count());\n\n        $tr->set_response($xml);\n        $this->assertTrue($tr->filter_by_pin(0)->is_empty_response());\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     * @dataProvider xmlUserInfoFixture\n     */\n    public function testFilterResponseByPrivilege($xml, TADResponse $tr)\n    {\n        $tr->set_response($xml);\n        $this->assertEquals(1, $tr->filter_by_privilege(14)->count());\n\n        $tr->set_response($xml);\n        $this->assertTrue($tr->filter_by_privilege(2)->is_empty_response());\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     * @dataProvider xmlUserInfoFixture\n     */\n    public function testFilterResponseByCard($xml, TADResponse $tr)\n    {\n        $tr->set_response($xml);\n        $this->assertEquals(1, $tr->filter_by_card(55555)->count());\n\n        $tr->set_response($xml);\n        $this->assertTrue($tr->filter_by_card(999999)->is_empty_response());\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     * @dataProvider xmlUserInfoFixture\n     */\n    public function testFilterResponseUsingLikeOperator($xml, TADResponse $tr)\n    {\n        $tr->set_response($xml);\n        $this->assertEquals(2, $tr->filter_by_name(['like'=>'Dolor'])->count());\n\n        $tr->set_response($xml);\n        $this->assertTrue($tr->filter_by_name(['like'=>'ultricies'])->is_empty_response());\n    }\n\n    /**\n     * @depends testTADResponseIsInstantiatedCorrectly\n     * @dataProvider xmlUserInfoFixture\n     */\n    public function testFilterResponseUsingTooManyFilterArguments($xml, TADResponse $tr)\n    {\n        $tr->set_response($xml);\n        $this->assertTrue($tr->filter_by_name(['like'=>'Dolor', 'start'=>'Foo', 'end'=>'Bar' ])->is_empty_response());\n    }\n\n\n    public function xmlAttLogFixture()\n    {\n        return [\n            ['\n                <?xml version=\"1.0\" encoding=\"iso8859-1\" standalone=\"no\"?>\n                <GetAttLogResponse>\n                <Row><PIN>10610805</PIN><DateTime>2014-11-30 18:36:49</DateTime><Verified>0</Verified><Status>0</Status><WorkCode>0</WorkCode></Row>\n                <Row><PIN>2</PIN><DateTime>2014-11-30 18:43:27</DateTime><Verified>0</Verified><Status>0</Status><WorkCode>0</WorkCode></Row>\n                <Row><PIN>10610805</PIN><DateTime>2014-11-30 20:52:44</DateTime><Verified>0</Verified><Status>0</Status><WorkCode>0</WorkCode></Row>\n                <Row><PIN>0</PIN><DateTime>2014-11-30 20:52:54</DateTime><Verified>0</Verified><Status>0</Status><WorkCode>0</WorkCode></Row>\n                <Row><PIN>10610805</PIN><DateTime>2014-11-30 21:24:46</DateTime><Verified>0</Verified><Status>0</Status><WorkCode>0</WorkCode></Row>\n                <Row><PIN>0</PIN><DateTime>2014-12-02 08:01:11</DateTime><Verified>0</Verified><Status>0</Status><WorkCode>0</WorkCode></Row>\n                <Row><PIN>10610805</PIN><DateTime>2014-12-02 08:01:23</DateTime><Verified>0</Verified><Status>0</Status><WorkCode>0</WorkCode></Row>\n                <Row><PIN>0</PIN><DateTime>2014-12-02 08:01:32</DateTime><Verified>0</Verified><Status>0</Status><WorkCode>0</WorkCode></Row>\n                <Row><PIN>10610805</PIN><DateTime>2014-12-04 01:06:35</DateTime><Verified>0</Verified><Status>0</Status><WorkCode>0</WorkCode></Row>\n                </GetAttLogResponse>\n            ']\n        ];\n    }\n\n    public function xmlUserInfoFixture()\n    {\n        return [\n            [\n                ''\n                . '<?xml version=\"1.0\" encoding=\"iso8859-1\" standalone=\"no\"?>'\n                . '<GetAllUserInfoResponse>'\n                . '<Row><PIN>1</PIN><Name>Lorem</Name><Password>1234</Password><Group>1</Group><Privilege>14</Privilege><Card>55555</Card><PIN2>1001</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '<Row><PIN>2</PIN><Name>Ipsum</Name><Password></Password><Group>1</Group><Privilege>0</Privilege><Card>0</Card><PIN2>1002</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '<Row><PIN>3</PIN><Name>Dolor Sed</Name><Password></Password><Group>1</Group><Privilege>0</Privilege><Card>0</Card><PIN2>1003</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '<Row><PIN>4</PIN><Name>Sit</Name><Password></Password><Group>1</Group><Privilege>0</Privilege><Card>0</Card><PIN2>1004</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '<Row><PIN>5</PIN><Name>Amet</Name><Password></Password><Group>1</Group><Privilege>0</Privilege><Card>0</Card><PIN2>1005</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '<Row><PIN>6</PIN><Name>Consectetur</Name><Password></Password><Group>1</Group><Privilege>0</Privilege><Card>0</Card><PIN2>1006</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '<Row><PIN>7</PIN><Name>Adipiscing</Name><Password></Password><Group>1</Group><Privilege>0</Privilege><Card>0</Card><PIN2>1007</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '<Row><PIN>8</PIN><Name>Elit</Name><Password></Password><Group>1</Group><Privilege>0</Privilege><Card>0</Card><PIN2>1008</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '<Row><PIN>9</PIN><Name>Nulla</Name><Password></Password><Group>1</Group><Privilege>0</Privilege><Card>0</Card><PIN2>1009</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '<Row><PIN>10</PIN><Name>Imperdiet</Name><Password></Password><Group>1</Group><Privilege>0</Privilege><Card>0</Card><PIN2>1010</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '<Row><PIN>11</PIN><Name>Molestie</Name><Password></Password><Group>1</Group><Privilege>0</Privilege><Card>0</Card><PIN2>1011</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '<Row><PIN>12</PIN><Name>Ante</Name><Password></Password><Group>1</Group><Privilege>0</Privilege><Card>0</Card><PIN2>1012</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '<Row><PIN>13</PIN><Name>Elit Luctus Dolor</Name><Password></Password><Group>1</Group><Privilege>0</Privilege><Card>0</Card><PIN2>1013</PIN2><TZ1>0</TZ1><TZ2>0</TZ2><TZ3>0</TZ3></Row>'\n                . '</GetAllUserInfoResponse>'\n            ]\n\n        ];\n    }\n}\n"
  },
  {
    "path": "test/lib/TADTest.php",
    "content": "<?php\n/*\n * tad-php\n *\n * (The MIT License)\n *\n * Copyright (c) 2014 Jorge Cobis <jcobis@gmail.com / http://twitter.com/cobisja>.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\n\nnamespace Test;\n\nuse TADPHP\\TAD;\nuse TADPHP\\TADFactory;\nuse TADPHP\\Providers\\TADSoap;\nuse TADPHP\\Providers\\TADZKLib;\n\nclass TADTest extends \\PHPUnit_Framework_TestCase\n{\n    public function testSoapCommandsAvailable()\n    {\n        $commands = TAD::soap_commands_available(['include_command_string'=>true]);\n\n        $this->assertInternalType('array', $commands);\n        $this->assertTrue($commands !== array_values($commands));\n    }\n\n    public function testZKLibCommandsAvailable()\n    {\n        $commands = TAD::zklib_commands_available();\n\n        $this->assertInternalType('array', $commands);\n        $this->assertFalse($commands !== array_values($commands));\n    }\n\n    public function testGetValidCommandsArgs()\n    {\n        $valid_args = TAD::get_valid_commands_args();\n\n        $this->assertInternalType('array', $valid_args);\n        $this->assertFalse($valid_args !== array_values($valid_args));\n    }\n\n    /**\n     * @dataProvider get_options\n     */\n    public function testGetOptions(array $options)\n    {\n        $method = array_keys($options)[0];\n        $expected_value = array_values($options)[0];\n\n        $tad_options = array_merge(['ip'=>'127.0.0.1'], $options);\n        $tad = (new TADFactory($tad_options))->get_instance();\n\n        $option_value = $tad->{\"get_$method\"}();\n\n        $this->assertEquals($expected_value, $option_value);\n    }\n\n    public function testDeviceWithInvalidIPAddressIsNotAlive()\n    {\n        $tad = (new TADFactory(['ip'=>'1.2.3.4', 'connection_timeout'=>1]))->get_instance();\n\n        $this->assertFalse($tad->is_alive());\n    }\n\n    /**\n     * @expectedException TADPHP\\Exceptions\\ConnectionError\n     */\n    public function testTADThrowsConnectionErrorExceptionWithInvalidDeviceIPAddress()\n    {\n        $tad = (new TADFactory(['ip'=>'1.2.3.4', 'connection_timeout'=>1]))->get_instance();\n        $tad->get_date();\n    }\n\n    /**\n     * @expectedException TADPHP\\Exceptions\\UnrecognizedCommand\n     */\n    public function testTADThrowsUnrecognizedCommandExceptionWithInvalidCommand()\n    {\n        $tad = (new TADFactory(['ip'=>'127.0.0.1']))->get_instance();\n        $tad->foo();\n    }\n\n    /**\n     * @expectedException TADPHP\\Exceptions\\UnrecognizedArgument\n     */\n    public function testTADThrowsUnrecognizedArgumentExceptionWithValidCommand()\n    {\n        $tad = (new TADFactory(['ip'=>'127.0.0.1']))->get_instance();\n        $tad->get_user_info(['foo'=>'bar']);\n    }\n\n    public function testTAD()\n    {\n        $options = $this->get_tad_and_soap_options();\n\n        $tad_soap_provider  = new TADSoap(new \\SoapClient(null, $options['soap']), $options['soap']);\n        $zklib_provider = new TADZKLib($options['tad']);\n\n        $tad = $this->getMockBuilder('\\TADPHP\\TAD')\n          ->setConstructorArgs([ $tad_soap_provider, $zklib_provider, $options['tad']])\n          ->setMethods(['is_alive', 'execute_command_via_tad_soap', 'execute_command_via_zklib'])\n          ->getMock();\n\n        $tad->expects($this->any())\n          ->method('is_alive')\n          ->will($this->returnValue(true));\n\n        $tad->expects($this->any())\n          ->method('execute_command_via_tad_soap')\n          ->will($this->returnValue('<CommandResponse>Executed via SOAP</CommandResponse>'));\n\n        $tad->expects($this->once())\n          ->method('execute_command_via_zklib')\n          ->will($this->returnValue('<CommandResponse>Executed via ZKLib</CommandResponse>'));\n\n        $this->assertEquals(\n            '<CommandResponse>Executed via SOAP</CommandResponse>',\n            $tad->get_date()\n        );\n        $this->assertEquals(\n            '<CommandResponse>Executed via ZKLib</CommandResponse>',\n            $tad->set_date()\n        );\n    }\n\n    public function testExecuteCommandViaTADSoap()\n    {\n        $options = $this->get_tad_and_soap_options();\n        $mock_response = '<GetDateResponse><row><date>2014-01-01</date><time>12:00:00</time></row></GetDateResponse>';\n\n        $soap_client = new \\SoapClient(null, $options['soap']);\n\n        $tad_soap = $this->getMockBuilder('TADPHP\\Providers\\TADSoap')\n          ->setConstructorArgs([ $soap_client, $options['soap'] ])\n          ->setMethods(['execute_soap_command'])\n          ->getMock();\n\n        $tad_soap->expects($this->once())\n          ->method('execute_soap_command')\n          ->will($this->returnValue($mock_response));\n\n        $zklib_provider = new TADZKLib($options['tad']);\n\n        $tad = $this->getMockBuilder('\\TADPHP\\TAD')\n          ->setConstructorArgs([ $tad_soap, $zklib_provider, $options['tad'] ])\n          ->setMethods(null)\n          ->getMock();\n\n        $response = $tad->execute_command_via_tad_soap('get_date', []);\n\n        $this->assertNotEmpty($response);\n    }\n\n    public function testExecuteCommandViaZKLib()\n    {\n        $options = $this->get_tad_and_soap_options();\n        $mock_response = ''\n                . '<SetDateResponse>'\n                . '<Result>1</Result>'\n                . '<Information>Successfully!</Information>'\n                . '</SetDateResponse>';\n\n        $soap_client = new \\SoapClient(null, $options['soap']);\n        $tad_soap = new TADSoap($soap_client, $options['soap']);\n\n        $zk = $this->getMockBuilder('TADPHP\\Providers\\TADZKLib')\n          ->setConstructorArgs([ $options['tad'] ])\n          ->setMethods(['__call'])\n          ->getMock();\n\n        $zk->expects($this->once())\n           ->method('__call')\n           ->will($this->returnValue($mock_response));\n\n        $tad = $this->getMockBuilder('\\TADPHP\\TAD')\n          ->setConstructorArgs([ $tad_soap, $zk, $options['tad'] ])\n          ->setMethods(null)\n          ->getMock();\n\n        $response = $tad->set_date();\n\n        $this->assertNotEmpty($response);\n    }\n\n    protected function soap_options()\n    {\n        return [\n            []\n        ];\n    }\n\n    protected function get_tad_and_soap_options()\n    {\n        $options = array_reduce(array_reduce($this->get_options(), 'array_merge', []), 'array_merge', []);\n\n        $soap_options = [\n            'location' => \"http://{$options['ip']}/iWsService\",\n            'uri' => 'http://www.zksoftware/Service/message/'\n        ];\n\n        return ['tad' => $options, 'soap' => $soap_options];\n    }\n\n    public function get_options()\n    {\n        $options = [\n            [['ip' => '127.0.0.1']],\n            [['com_key' => 0]],\n            [['internal_id' => 100]],\n            [['description' => 'Foo']],\n            [['connection_timeout'=> 1]],\n            [['udp_port' => 4370]],\n            [['encoding'=>'iso8859-1']]\n        ];\n\n        return $options;\n    }\n}\n"
  }
]