# sabclient module
#
# sabclient module contains a SABClient class
#
# List of SABClient class methods:
#
#  login
#  logout
#
#  add_account
#  add_certificate
#  add_credit_card
#  add_file
#  add_secret_account
#
#  delete_record
#  update_record_simply
#  upload_file
#  search_text
#  search_by_note
#  own_search_by_note
#
#  get_safebox_list
#  get_safebox
#  get_record_list
#  get_record
#  get_station_password
#  get_ticket_password
#
#  find_record_by_name
#  find_safe_box_by_name
#

import urllib
import sys
import certifi
import requests
import ssl
import json
import warnings


#########################################################
# _handle_error parses the JSON REST request response
# If the request returns an error, the function reads
#   details from the 'msg' subobject in the JSON object
# The function returns True if the request is error-free
#
def _handle_error(response):
    s = str(response)
    if s.lower() != '<response [200]>':
        data = response.json()
        result = data["msg"]
        for rep in result:
            msg = rep['severity']
            if msg == 'ERROR':
                print('\n\n ERROR: ', rep['message'])

        return False
    else:
        return True


##############################################################################################
#                                                                                            #
#           SABClient class                                                                  #
#                                                                                            #
# SABClient interacts with SecureAnyBox server using sending REST requests                   #
#                                                                                            #
##############################################################################################

class SABClient:

    # session variable is valid during connection to a server
    session = requests.Session()

    # callback called by SABClient when logged user has requested for the second factor
    # the callback returns string with second factor
    get_second_factor_callback = None

    ##############################################################################################
    # class constructor has parameters:
    #   sab_url - contains base url of the SecureAnyBox server (eg: https://127.0.0.1/secureanybox)
    #   callback - holds a pointer to the second factor function
    #
    def __init__(self, sab_url, callback=None):
        self.base_url = sab_url.strip('/')
        self.get_second_factor_callback = callback
        self._access_code = None

    ##############################################################################################
    # private method of the client, which is used by other methods for adding concrete record 
    #     adds a record to box with id box_id
    # parameter record_fields (type dictionary) contains all record fields
    #
    # method call request post with parameters
    #   url - consists of base_url and URI of request; the parameter {0} is a placeholder  
    #           for the safe box ID in which the record will be inserted
    #   headers - contains the SAB-Access-Code value, which is required when
    #               adding record to SAB
    #   data - the data parameter contains a JSON object with fields of record
    #   verify - if Verify is True, the request can verify SSL certificates for HTTPS connection
    #
    # If the request is successful _add_record method saves id of just saved record to
    #   _last_id object private variable.
    #
    # If request is successful, the return value is True.
    #
    def _add_record(self, box_id, record_fields):
        self._last_id = None
        url = self.base_url + '/safe/boxes/{0}/records'
        if self._access_code is None:
            headers = {"Accept": 'application/json', "charset": 'utf8'}
        else:
            headers = {"Accept": 'application/json', "SAB-Access-Code": self._access_code, "charset": 'utf8'}
                                                                               
        response = self.session.post(url.format(box_id), data=json.dumps(record_fields), headers=headers,
                                     verify=True)

        result = _handle_error(response)
        if result:
            data = response.json()
            self._last_id = data["id"]

        return result

    ##############################################################################################
    # Internal client method used by the login method
    # Returns response object with several status attributes
    # The response object contains the status of the logged-on client, 
    #   including the hasrequiredsecondfactor flag
    #
    # Method calls get request.
    # If parameter Verify is True, the request can verify SSL certificates for HTTPS  connection
    #
    # The function returns object of the Response type
    #
    def _get_login_status(self):
        url = self.base_url + '/public/api/loginStatus'
        headers = {"Accept": 'application/json', "charset": 'utf8'}

        response = self.session.get(url, headers=headers, verify=True)

        return response

    ##############################################################################################
    #                                                                                            #
    #           add_account                                                                      #
    #                                                                                            #
    # Client method to add a record of the account type.                                         #
    #                                                                                            #
    # Parameters:                                                                                #
    #   box_id - ID of safe box where the account will be added                                  #
    #   name - Name of account (string)                                                          #
    #   description - Description of record (string)                                             #
    #   tags - Comma separated tag list (string)                                                 #
    #   note - Additional notes (string); lines are separated by \n                              #
    #   login_site - Hostname or domain name and port identifying server or server group         #
    #                  matching an account (string)                                              #
    #   password - Account password (string)                                                     #
    #   login_name - Account user login name (string)                                            #
    #   server_address - URL of web associated with account (string)                             #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If request is successful, the return value is True.                                      #
    #   In case of failure, the method returns False.                                            #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   add_account calls internal method _add_record with following parameters                  #
    #     box_id - the safe box identifier where the account is stored                           #
    #     account_fields - array of fields (dictionary)                                          #
    #                                                                                            #
    ##############################################################################################

    def add_account(self, box_id, name, description=None, tags=None, note=None,
                    login_site=None, password=None, login_name=None, server_address=None):

        account_fields = {
            "id": 0,
            "template": "account",
            "name": name,
            "serverAddress": server_address,
            "site": login_site,
            "description": description,
            "tags": tags,
            "note": note,
            "loginName": login_name,
            "password": password,
            "passwordRe": password
        }
        return self._add_record(box_id, account_fields)

    ##############################################################################################
    #                                                                                            #
    #           add_certificate                                                                  #
    #                                                                                            #
    # Client method to add a record of the certificate type.                                     #
    #                                                                                            #
    # Parameters:                                                                                #
    #   box_id - ID of safe box where the account will be added                                  #
    #   name - Name of account (string)                                                          #
    #   description - Description of record (string)                                             #
    #   tags - Comma separated tag list (string)                                                 #
    #   note - Additional notes (string); lines are separated by \n                              #
    #   alias - Unique alias of the certificate (string)                                         #
    #   certificate - Certificate file name (string)                                             #
    #   certificate_password - 	Certificate file password  (string)                              #
    #   secret_note - Additional secret notes (string); lines are separated by \n                #
    #   certificate_path - Certificate file location on disk                                     #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If request is successful, the return value is True.                                      #
    #   In case of failure, the method returns False.                                            #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   add_account calls internal method _add_record with following parameters                  #
    #     box_id - the safe box identifier where the account is stored                           #
    #     certificate_fields - array of fields (dictionary)                                      #
    #                                                                                            #
    #   If the request is successful _add_record method saves id of just saved record to         #
    #     _last_id object variable.                                                              #
    #                                                                                            #
    #   Then the method calls upload_file method to save certificate file with parameters        #
    #     record_id - ID of the record, used _last_id value                                      #
    #     file_name - name of certificate file                                                   #
    #     file_path - full path to the certificate file                                          #
    #                                                                                            #
    ##############################################################################################

    def add_certificate(self, box_id, name, description=None, tags=None, note=None,
                        alias=None, certificate=None, certificate_password=None, secret_note=None,
                        certificate_path=None):

        certificate_fields = {
            "id": 0,
            "template": "cert",
            "name": name,
            "description": description,
            "tags": tags,
            "note": note,
            "attributes": {
                "alias": alias,
                "filePassword": certificate_password,
                "sec-note": secret_note
            }
        }
        result = self._add_record(box_id, certificate_fields)

        if result and certificate_path != '':
            result = self.upload_file(self._last_id, certificate, certificate_path)

        return result

    ##############################################################################################
    #                                                                                            #
    #           add_credit_card                                                                  #
    #                                                                                            #
    # Client method to add a record of the credit card type.                                     #
    #                                                                                            #
    # Parameters:                                                                                #
    #   box_id - ID of safe box where the account will be added                                  #
    #   name - Name of account (string)                                                          #
    #   description - Description of record (string)                                             #
    #   tags - Comma separated tag list (string)                                                 #
    #   note - Additional notes (string); lines are separated by \n                              #
    #   number - Credit Card number (string)                                                     #
    #   expiration - Expiration Date (string)                                                    #
    #   cvv - CVV (string)                                                                       #
    #   pin - PIN  (string)                                                                      #
    #   sec_note - Secret notes (string); lines are separated by \n                              #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If request is successful, the return value is True.                                      #
    #   In case of failure, the method returns False.                                            #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   add_account calls internal method _add_record with following parameters                  #
    #     box_id - the safe box identifier where the account is stored                           #
    #     account_fields - array of fields (dictionary)                                          #
    #                                                                                            #
    ##############################################################################################

    def add_credit_card(self, box_id, name, description=None, tags=None, note=None,
                        number=None, expiration=None, cvv=None, pin=None, sec_note=None):

        credit_card_fields = {
            "id": 0,
            "template": "ccard",
            "name": name,
            "description": description,
            "tags": tags,
            "note": note,
            "attributes": {
                "number": number,
                "expiration": expiration,
                "cvv": cvv,
                "pin": pin,
                "sec-note": sec_note
            }
        }
        return self._add_record(box_id, credit_card_fields)

    ##############################################################################################
    #                                                                                            #
    #           add_file                                                                         #
    #                                                                                            #
    # Client method to add a record of the file type including a file                            #
    #                                                                                            #
    # Parameters:                                                                                #
    #   box_id - ID of safe box where the account will be added                                  #
    #   name - Name of account (string)                                                          #
    #   description - Description of record (string)                                             #
    #   tags - Comma separated tag list (string)                                                 #
    #   note - Additional notes (string); lines are separated by \n                              #
    #   sec_note - Secret notes (string); lines are separated by \n                              #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If request is successful, the return value is True.                                      #
    #   In case of failure, the method returns False.                                            #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   add_file calls internal method _add_record with following parameters                     #
    #     box_id - the safe box identifier where the account is stored                           #
    #     file_fields - array of fields (dictionary)                                             #
    #                                                                                            #
    #   If the request is successful _add_record method saves id of just saved record to         #
    #     _last_id object variable.                                                              #
    #                                                                                            #
    #   Then the method calls upload_file method to save certificate file with parameters        #
    #     record_id - ID of the record, used _last_id value                                      #
    #     file_name - name of certificate file                                                   #
    #     file_path - full path to the file                                                      #
    #                                                                                            #
    ##############################################################################################

    def add_file(self, box_id, name, description=None, tags=None, note=None,
                 file_name=None, sec_note=None, file_path=None):

        file_fields = {
            "id": 0,
            "template": "file",
            "name": name,
            "description": description,
            "tags": tags,
            "note": note,
            "attributes": {
                "sec-note": sec_note
            }
        }
        result = self._add_record(box_id, file_fields)

        if result and file_path != '':
            result = self.upload_file(self._last_id, file_name, file_path)

        return result

    ##############################################################################################
    #                                                                                            #
    #           add_secret_account                                                               #
    #                                                                                            #
    # Client method to add a record of the secret account type.                                  #
    #                                                                                            #
    # Parameters:                                                                                #
    #   box_id - ID of safe box where the account will be added                                  #
    #   name - Name of account (string)                                                          #
    #   description - Description of record (string)                                             #
    #   tags - Comma separated tag list (string)                                                 #
    #   note - Additional notes (string); lines are separated by \n                              #
    #   login_site - Hostname or domain name and port identifying server or server group         #
    #                  matching an account (string)                                              #
    #   password - Account password (string)                                                     #
    #   sec_login_name - Secret account user login name (string)                                 #
    #   sec_server_address - Secret URL of web associated with account (string)                  #
    #   sec_note - Secret notes (string); lines are separated by \n                              #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If request is successful, the return value is True.                                      #
    #   In case of failure, the method returns False.                                            #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   add_account calls internal method _add_record with following parameters                  #
    #     box_id - the safe box identifier where the account is stored                           #
    #     account_fields - array of fields (dictionary)                                          #
    #                                                                                            #
    ##############################################################################################

    def add_secret_account(self, box_id, name, description=None, tags=None, note=None,
                           login_site=None, password=None, sec_server_address=None,
                           sec_login_name=None, sec_note=None):

        sec_account_fields = {
            "id": 0,
            "template": "account-sec",
            "name": name,
            "site": login_site,
            "description": description,
            "tags": tags,
            "note": note,
            "password": password,
            "passwordRe": password,
            "attributes": {
                "sec-serverAddress": sec_server_address,
                "sec-loginName": sec_login_name,
                "sec-note": sec_note
            }
        }
        return self._add_record(box_id, sec_account_fields)

    ##############################################################################################
    #                                                                                            #
    #           update_record_simply                                                             #
    #                                                                                            #
    # Client method to update existing record base fields in safe box                            #
    #                                                                                            #
    # Parameters:                                                                                #
    #   record_fields       - updated SAB record                                                 #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If request is successful, the return value is True.                                      #
    #   In case of failure, the method returns False.                                            #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   update_record calls session.put() method with following parameters                       #
    #     url - parameter url consists of base_url and URI of request; the parameter {0} is      #
    #           a placeholder for ID of deleted record                                           #
    #                                                                                            #
    #     data    - json object with fields of the record                                        #
    #     headers - contains the SAB-Access-Code value, which is required when                   #
    #               deleting a record from the safe box                                          #
    #     verify - if Verify is True, the request can verify SSL certificates for HTTPS          #
    #              connection                                                                    #
    #                                                                                            #
    ##############################################################################################

    def update_record_simply(self, record_fields):

        url = self.base_url + '/safe/records/{0}'
        if self._access_code is None:
            headers = {"Accept": 'application/json', "charset": 'utf8'}
        else:
            headers = {"Accept": 'application/json', "SAB-Access-Code": self._access_code, "charset": 'utf8'}

        id = record_fields["id"]
        response = self.session.put(url.format(id), data=json.dumps(record_fields), headers=headers, verify=True)
        result = _handle_error(response)

        return result

    ##############################################################################################
    #                                                                                            #
    #           delete_record                                                                    #
    #                                                                                            #
    # Client method to delete record from safe box                                               #
    #                                                                                            #
    # Parameters:                                                                                #
    #   record_id - ID of existing record (int); when the method is called from add_file or      #
    #               add_certif, it uses last_id for the parameter                                #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If request is successful, the return value is True.                                      #
    #   In case of failure, the method returns False.                                            #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   delete_record calls session.delete() method with following parameters                    #
    #     url - parameter url consists of base_url and URI of request; the parameter {0} is      #
    #           a placeholder for ID of deleted record                                           #
    #                                                                                            #
    #     headers - contains the SAB-Access-Code value, which is required when                   #
    #               deleting a record from the safe box                                          #
    #     verify - if Verify is True, the request can verify SSL certificates for HTTPS          #
    #              connection                                                                    #
    #                                                                                            #
    ##############################################################################################

    def delete_record(self, record_id):

        url = self.base_url + '/safe/records/{0}'
        if self._access_code is None:
            headers = {"Accept": 'application/json', "charset": 'utf8'}
        else:
            headers = {"Accept": 'application/json', "SAB-Access-Code": self._access_code, "charset": 'utf8'}

        response = self.session.delete(url.format(record_id), headers=headers, verify=True)
        result = _handle_error(response)

        return result

    ##############################################################################################
    #                                                                                            #
    #           find_record_by_name                                                              #
    #                                                                                            #
    # Client method to finding a record by field of name                                         #
    #                                                                                            #
    # Parameters:                                                                                #
    #   record_name - exactly the name of the record we're looking for                           #
    #   box_name - the exact name of the safe box in which the entry is being searched           #
    #   parent_box - the safe box parent identifier if the safe box is not in the root           #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If the request is successful, the return value is a record object in JSON.               #
    #   In case of failure, the method returns None.                                             #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   find_record_by_name calls get_box_list to get the safe box list. If any of the boxes     #
    #     matches the safe box name, the method calls get records with the identifier            #
    #     of safe box found. In the list, it searchs for the required record.                    #
    #                                                                                            #
    ##############################################################################################

    # record_name == exact name of the record to find
    # box_name == exact name of the safe box where the record is
    # if parent_box_id == None return safe box found in root safe boxes
    def find_record_by_name(self, record_name, box_name, parent_box_id=None):
        box_list = self.get_safebox_list(parent_box_id)

        for safe_box in box_list:
            if safe_box['name'] == box_name:
                record_list = self.get_record_list(safe_box["id"])

                for record in record_list:
                    if record['name'] == record_name:
                        return record
                        break

        return None

    ##############################################################################################
    #                                                                                            #
    #           find_safe_box_by_name                                                            #
    #                                                                                            #
    # Client method to finding a safe box by field of name                                       #
    #                                                                                            #
    # Parameters:                                                                                #
    #   box_name - the exact name of the safe box which is being looked up                       #
    #   parent_box - the safe box parent identifier if the safe box is not in the root           #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If the request is successful, the return value is a safe box object in JSON.             #
    #   In case of failure, the method returns None.                                             #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   find_record_by_name calls get_box_list to get the safe box list. If any of the boxes     #
    #     matches the safe box name, the method returns safe box object in JSON                  #
    #                                                                                            #
    ##############################################################################################

    # box_name == exact name of the safe box to find
    # if parent_box_id == None return safe box found in root safe boxes
    def find_safe_box_by_name(self, box_name, parent_box_id=None):
        box_list = self.get_safebox_list(parent_box_id)

        for safe_box in box_list:
            if safe_box['name'] == box_name:
                return safe_box
                break

        return None

    ##############################################################################################
    #                                                                                            #
    #           get_record                                                                       #
    #                                                                                            #
    # Client method to reading a record by ID of the record                                      #
    #                                                                                            #
    # Parameters:                                                                                #
    #   record_id - ID of the record                                                             #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If the request is successful, the return value is a record object in JSON.               #
    #   In case of failure, the method returns an empty record                                   #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   method calls request session.get with parameters                                         #
    #     url - consists of base_url and URI of request; the parameter {0} is a placeholder      #
    #           for the record ID                                                                #
    #     headers - contains the SAB-Access-Code value, which is required when                   #
    #               getting alll fields from the record                                          #
    #     verify - if Verify is True, the request can verify SSL certificates for HTTPS          #
    #              connection                                                                    #
    #                                                                                            #
    ##############################################################################################

    def get_record(self, record_id):

        record = {}

        url = self.base_url + '/safe/records/{0}'
        if self._access_code is None:
            headers = {"Accept": 'application/json', "charset": 'utf8'}
        else:
            headers = {"Accept": 'application/json', "SAB-Access-Code": self._access_code, "charset": 'utf8'}

        response = self.session.get(url.format(record_id), headers=headers, verify=True)
        result = _handle_error(response)

        if result:
            data = response.json()
            record = data["record"]

        return record

    ##############################################################################################
    #                                                                                            #
    #           get_record_list                                                                  #
    #                                                                                            #
    # Client method to reading a list of records in a safe box by its ID                         #
    #                                                                                            #
    # Parameters:                                                                                #
    #   parent_box_id - ID of the parent safe box                                                #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If the request is successful, the return value is a record list in JSON.                 #
    #   In case of failure, the method returns an empty list                                     #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   method calls request session.get with parameters                                         #
    #     url - consists of base_url and URI of request; the parameter {0} is a placeholder      #
    #           for the safe box ID                                                              #
    #     headers - contains the SAB-Access-Code value, which is required when                   #
    #               getting alll fields from the record                                          #
    #     verify - if Verify is True, the request can verify SSL certificates for HTTPS          #
    #              connection                                                                    #
    #                                                                                            #
    ##############################################################################################

    def get_record_list(self, safe_box_id):

        record_list = {}

        url = self.base_url + '/safe/boxes/{0}/records'
        if self._access_code is None:
            headers = {"Accept": 'application/json', "charset": 'utf8'}
        else:
            headers = {"Accept": 'application/json', "SAB-Access-Code": self._access_code, "charset": 'utf8'}

        response = self.session.get(url.format(safe_box_id), headers=headers, verify=True)
        result = _handle_error(response)

        if result:
            data = response.json()
            record_list = data["records"]

        return record_list

    ##############################################################################################
    #                                                                                            #
    #           get_safebox_list                                                                 #
    #                                                                                            #
    # Client method to reading a list of safe boxes of the parent safe box or root level         #
    #                                                                                            #
    # Parameters:                                                                                #
    #   parent_box_id - ID of the parent safe box                                                #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If the request is successful, the return value is a safe box list in JSON.               #
    #   In case of failure, the method returns an empty safe box list                            #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   method calls request session.get with parameters                                         #
    #     url - consists of base_url and URI of request; the parameter {0} is a placeholder      #
    #           for the safe box ID. If the placeholder is missing in URI request returns        #
    #           safe box list from the root level                                                #
    #     verify - if Verify is True, the request can verify SSL certificates for HTTPS          #
    #              connection                                                                    #
    #                                                                                            #
    ##############################################################################################

    # if parent_box_id == None return root safe boxes
    def get_safebox_list(self, parent_box_id=None):

        box_list = {}

        if parent_box_id is not None and parent_box_id > 0:
            url = self.base_url + '/safe/boxes/{0}/boxes'
        else:
            url = self.base_url + '/safe/boxes'

        headers = {"Accept": 'application/json', "charset": 'utf8'}

        response = self.session.get(url.format(parent_box_id), verify=True)
        result = _handle_error(response)

        if result:
            data = response.json()
            box_list = data["safeboxes"]

        return box_list

    ##############################################################################################
    #                                                                                            #
    #           get_safebox                                                                      #
    #                                                                                            #
    # Client method to reading a safebox by ID                                                   #
    #                                                                                            #
    # Parameters:                                                                                #
    #   safebox_id - ID of the safebox                                                           #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If the request is successful, the return value is a safebox object in JSON.              #
    #   In case of failure, the method returns an empty record                                   #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   method calls request session.get with parameters                                         #
    #     url - consists of base_url and URI of request; the parameter {0} is a placeholder      #
    #           for the record ID                                                                #
    #     headers - contains the SAB-Access-Code value, which is required when                   #
    #               getting alll fields from the record                                          #
    #     verify - if Verify is True, the request can verify SSL certificates for HTTPS          #
    #              connection                                                                    #
    #                                                                                            #
    ##############################################################################################

    def get_safebox(self, safebox_id):

        safe_box = {}
        group = {}

        url = self.base_url + '/safe/boxes/{0}'
        if self._access_code is None:
            headers = {"Accept": 'application/json', "charset": 'utf8'}
        else:
            headers = {"Accept": 'application/json', "SAB-Access-Code": self._access_code, "charset": 'utf8'}

        response = self.session.get(url.format(safebox_id), headers=headers, verify=True)
        result = _handle_error(response)

        if result:
            data = response.json()
            safe_box = data["safebox"]
            safe_box["group_name"] = ''

            if safe_box["parentId"] is not None and safe_box["parentId"] > 1:
                response = self.session.get(url.format(safe_box["parentId"]), headers=headers, verify=True)
                result = _handle_error(response)

                if result:
                    data = response.json()
                    group = data["safebox"]
                    safe_box["group_name"] = group["name"]

        return safe_box

    ##############################################################################################
    #                                                                                            #
    #           get_station_password                                                             #
    #                                                                                            #
    # Client method for obtaining the station password. The station does not have                #
    #   to be registered in SecureAnyBox.                                                        #
    #                                                                                            #
    # Parameters:                                                                                #
    #   date - local date of the station                                                         #
    #   station_name - station's name. The name has to be in a format specified                  #
    #     in the agent configuration                                                             #
    #   username - the name by which the user logs in to the station                             #
    #   configuration - the agent configuration                                                  #
    #   platform - an operating system of the station                                            #
    #   time_zone - name of timezone for which passwordDate is specified                         #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If the request is successful, the return dictionary with station name and password       #
    #   In case of failure, the method returns an empty dictionary                               #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   Who calls of the method must be logged on to the server.                                 #
    #                                                                                            #
    #   The platform can be one of the following values: WINDOWS, LINUX, MAC or LDAP             #
    #                                                                                            #
    ##############################################################################################

    def get_station_password(self, station_name, date=None, username='Administrator', configuration='DEFAULT', platform='WINDOWS', time_zone=None):

        data = {}

        url = self.base_url + '/agent/getPassword?configName={0}&platform={1}&stationName={2}&userName={3}'
        url_date = '&passwordDate={0}'
        url_zone = '&timezone={0}'

        headers = {"Accept": 'application/json', "charset": 'utf8'}

        url = url.format(configuration, platform, station_name, username)
        if date != None:
            date_str = date(date).strftime('%d/%m/%y')
            url = url + url_date.format(date_str)

        if time_zone != None:
            url = url + url_zone.format(time_zone)

        response = self.session.get(url, headers=headers, verify=True)
        result = _handle_error(response)

        if result:
            data = response.json()

        return data

    ##############################################################################################
    #                                                                                            #
    #           get_ticket_password                                                              #
    #                                                                                            #
    # Client method for obtaining the station password from the ticket.                          #
    #                                                                                            #
    # Parameters:                                                                                #
    #   ticket_id - ticket created in SecureAnyBox                                               #
    #   station_name - station's name. The name has to be in a format specified                  #
    #     in the agent configuration                                                             #
    #   username - optional station user                                                         #
    #   platform - an operating system of the station                                            #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If the request is successful, the return dictionary with station name and password       #
    #   In case of failure, the method returns the dictionary with empty values                  #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   Tickets are intended for sharing access to getting passwords for the stations.           #
    #     Once the ticket created, it can be shared with anybody (even with people without       #
    #     access to SecureAnyBox). Therefore, whoever calls the method may not be logged on to   #
    #     the server.                                                                            #
    #                                                                                            #
    #   The platform can be one of the following values: WINDOWS, LINUX, MAC or LDAP             #
    #                                                                                            #
    #   A successful getPassword request returns a string in the format "OK,username,password"   #
    #                                                                                            #
    ##############################################################################################

    def get_ticket_password(self, ticket_id, station_name, user_name=None, platform='WINDOWS'):

        data = {
            "userName": None,
            "password": None
        }

        url = self.base_url + '/public/api/getPassword?ticket={0}&station={1}&platform={2}'
        url_user = '&user={0}'

        headers = {"Accept": 'application/json', "charset": 'utf8'}

        url =  url.format(ticket_id, station_name, platform)

        if user_name != None:
            url = url + url_user.format(user_name)

        response = self.session.get(url, headers=headers, verify=True)
        result = _handle_error(response)

        if result:
            str = response.content.decode('utf-8')
            result = str.split(',')
            if result[0] == 'OK':
                data['userName'] = result[1]
                data['password'] = result[2]
            else:
                print(str)

        return data

    ##############################################################################################
    #                                                                                            #
    #           login                                                                            #
    #                                                                                            #
    # Client method for connecting and logging on to a SecureAnyBox server                       #
    #                                                                                            #
    # Parameters:                                                                                #
    #   username - required user name (string)                                                   #
    #   password - required user password (string)                                               #
    #   domain - optional parametr, if ommited, default 'system' is used                         #
    #   access_code - value of the access code, it is recommended to enter it                    #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If the request is successful, the return value is a safe box list in JSON.               #
    #   In case of failure, the method returns an empty safe box list                            #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   method calls request session.get with parameters                                         #
    #     url - consists of base_url and URI of request                                          #
    #     verify - if Verify is True, the request can verify SSL certificates for HTTPS          #
    #              connection                                                                    #
    #                                                                                            #
    #   After a successful connection, the method calls get_login_status function                #
    #   to determine whether a second factor is required. If so, the method calls the callback   #
    #   function, which must return a six-digit combination with the second factor.              #
    #   Then it sends a second factor to the server.                                             #
    #                                                                                            #
    ##############################################################################################

    def login(self, username, password, domain='system', access_code=None):

        if access_code is None:
            self._access_code = None
        else:
            self._access_code = urllib.parse.quote(access_code)

        if domain != "":
            login = domain + '\\' + username
            params = {
                "domain": domain,
                "login": login,
                "username": username,
                "password": password,
            }
        else:
            login = username
            params = {
                "login": login,
                "password": password,
            }
        url = self.base_url + '/api/login'

        response = self.session.post(url, data=params, verify=True)
        result = _handle_error(response)

        if result:
            response = self._get_login_status()

            data = response.json()
            second_factor_required = data.get("secondFactorRequired", False)

            if second_factor_required:
                second_factor = self.get_second_factor_callback()

                factor2 = {
                    "securityCode": second_factor
                }
                url = self.base_url + '/api/user/secondfactor'
                response = self.session.post(url, data=factor2, verify=True)
                result = _handle_error(response)

        return result

    ##############################################################################################
    #                                                                                            #
    #           logout                                                                           #
    #                                                                                            #
    # Client method to disconnecting from SecureAnyBox server                                    #
    # Method also clears the access_code                                                         #
    #                                                                                            #
    ##############################################################################################

    def logout(self):
        response = self.session.disconnect
        self._access_code = None

    ##############################################################################################
    #                                                                                            #
    #           own_search_by_note                                                               #
    #                                                                                            #
    # Client method for searching objects of SecureAnyBox by a text pattern in Note field        #
    #                                                                                            #
    # Parameters:                                                                                #
    #   pattern - text pattern                                                                   #
    #   show_browsing_safeboxes - lists safeboxes and groups that pass through                   #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If the request is successful, the return value is the list of ids of SAB objects         #
    #   In case of failure, the method returns an empty list                                     #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   method searches pattern in notes of all records in a case-sensitive manner               #
    #   uses methods of object: get_safebox_list, get_record_list, get_record                    #
    #                                                                                            #
    #   it is recommended not to enter the access code in the login method to speed up           #
    #     the run of the own_search_by_note method                                               #
    #                                                                                            #
    ##############################################################################################

    def own_search_by_note(self, pattern, show_browsing_safeboxes=None):

        result_list = []

        box_list = self.get_safebox_list()
        for safe_box in box_list:
            if safe_box['type'] == "SAFEBOX_GROUP":
                if show_browsing_safeboxes is not None:
                    if show_browsing_safeboxes:
                        print('*', safe_box['name'])
                group_list = self.get_safebox_list(safe_box['id'])

                for box in group_list:
                    if show_browsing_safeboxes is not None:
                        if show_browsing_safeboxes:
                            print(' ', box['name'])
                    record_list = self.get_record_list(box["id"])
                    for rec in record_list:
                        record = self.get_record(rec["id"])
                        if pattern in record['note']:
                            tuple = (record['id'], box["id"])
                            result_list.append(tuple)

            else:
                if show_browsing_safeboxes is not None:
                    if show_browsing_safeboxes:
                        print(safe_box['name'])
                record_list = self.get_record_list(safe_box["id"])
                for rec in record_list:
                    record = self.get_record(rec["id"])
                    if pattern in record['note']:
                        tuple = (record['id'], safe_box["id"])
                        result_list.append(tuple)

        return result_list


    ##############################################################################################
    #                                                                                            #
    #           search_by_note                                                                   #
    #                                                                                            #
    # Client method for searching objects of SecureAnyBox by a text pattern in Note field        #
    #                                                                                            #
    # Parameters:                                                                                #
    #   pattern - text pattern                                                                   #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If the request is successful, the return value is the list of ids of SAB objects         #
     #   In case of failure, the method returns an empty list                                     #
    #                                                                                            #
    ##############################################################################################

    def search_by_note(self, pattern):

        result_list = []

        url = self.base_url + '/safe/searchIdsByNote/' + pattern

        headers = {"Accept": 'application/json'}

        response = self.session.get(url, headers=headers, verify=True)

        result = _handle_error(response)

        if result:
            data = response.json()
            result_list = data["recordIds"]

        return result_list


    ##############################################################################################
    #                                                                                            #
    #           search_text                                                                      #
    #                                                                                            #
    # Client method for searching objects of SecureAnyBox by a text pattern                      #
    #                                                                                            #
    # Parameters:                                                                                #
    #   pattern - text pattern                                                                   #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If the request is successful, the return value is the list of SAB objects                #
    #   In case of failure, the method returns an empty object list                              #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   method calls request session.get with parameters                                         #
    #     url - consists of base_url and request URI followed by a text pattern                  #
    #     verify - if Verify is True, the request can verify SSL certificates for HTTPS          #
    #              connection                                                                    #
    #                                                                                            #
    ##############################################################################################

    def search_text(self, pattern):

        result_list = {}

        url = self.base_url + '/safe/search/' + pattern

        headers = {"Accept": 'application/json'}

        response = self.session.get(url, headers=headers, verify=True)

        result = _handle_error(response)

        if result:
            data = response.json()
            result_list = data["result"]

        return result_list

    ##############################################################################################
    #                                                                                            #
    #           upload_file                                                                      #
    #                                                                                            #
    # Client method to add a file to the existing record                                         #
    #                                                                                            #
    # Parameters:                                                                                #
    #   record_id - ID of existing record (int); when the method is called from add_file or      #
    #               add_certif, it uses last_id for the parameter                                #
    #   file_name - Name of file (string)                                                        #
    #   file_path - File location of disk (string)                                               #
    #                                                                                            #
    # Return Value:                                                                              #
    #   If request is successful, the return value is True.                                      #
    #   In case of failure, the method returns False.                                            #
    #                                                                                            #
    # Remarks:                                                                                   #
    #   upload_file calls session.post() method with following parameters                        #
    #     url - parameter url consists of base_url and URI of request; the parameter {0}         #
    #           is a placeholder for record ID in which the file will be inserted                #
    #                                                                                            #
    #     files - request sends a multi-part form POST body with the file field                  #
    #             set to file_name and the contents of the file_path file.                       #
    #                                                                                            #
    #     headers - contains the SAB-Access-Code value, which is required when                   #
    #               uploading file to SAB                                                        #
    #     verify - if Verify is True, the request can verify SSL certificates for HTTPS          #
    #              connection                                                                    #
    #                                                                                            #
    ##############################################################################################

    def upload_file(self, record_id, file_name, file_path):

        url = self.base_url + '/safe/records/{0}/upload/file'

        files = {
            'file': (file_name, open(file_path, 'rb')),
            'Content-Type': 'Application/octet-string',
            'content-disposition': 'form-data',
        }

        if self._access_mode is None:
            headers = {"Accept": 'application/json'}
        else:
            headers = {"Accept": 'application/json', "SAB-Access-Code": self._access_code}

        response = self.session.post(url.format(record_id), files=files, headers=headers, verify=True)

        return _handle_error(response)
