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