Bladeren bron

OrdersGuru Signer ready for use

Jan Sušnik 8 jaren geleden
bovenliggende
commit
851cc9bab0

File diff suppressed because it is too large
+ 56 - 2
README.md


+ 17 - 0
gui/AboutDialog.py

@@ -0,0 +1,17 @@
+from PyQt5.QtWidgets import QDialog
+from PyQt5.QtCore import Qt
+from ui.about import Ui_About
+
+
+class AboutDialog(QDialog):
+    def __init__(self, parent=None):
+        super(AboutDialog, self).__init__(
+            parent, Qt.WindowSystemMenuHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint
+        )
+        self.ui = Ui_About()
+        self.ui.setupUi(self)
+
+    @staticmethod
+    def show_(parent=None):
+        about = AboutDialog(parent)
+        about.exec_()

+ 112 - 0
gui/SettingsDialog.py

@@ -0,0 +1,112 @@
+from PyQt5.QtWidgets import QDialog, QMessageBox, QFileDialog
+from PyQt5.QtCore import Qt, QSize
+from ui.settings import Ui_Settings
+import os
+
+
+class SettingsDialog(QDialog):
+    def __init__(self, parent=None, config=None, initial=False, errors=None):
+        super(SettingsDialog, self).__init__(
+            parent, Qt.WindowSystemMenuHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint
+        )
+        self.ui = Ui_Settings()
+        self.ui.setupUi(self)
+        self.set_values(config)
+        self.set_mode(initial, errors)
+        self.set_connections()
+
+    def set_values(self, config):
+        if config:
+            self.ui.leXMLSchema.setText(config['xmlschema'])
+            self.ui.leCertificate.setText(config['certificate'])
+
+    def set_mode(self, initial, errors):
+        if initial:
+            self.setWindowTitle('OrdersGuru Signer Initial Settings')
+        elif errors:
+            self.setWindowTitle('OrdersGuru Signer Settings')
+            self.ui.lblWelcomeErrorsHeader.setText('<center><b>OrdersGuru Signer detected a problem!</b></center>')
+            self.ui.lblWelcomeErrors.setText(
+                'There is a problem with following ...<br /><span style="color:red">' +
+                '<br />'.join('&nbsp;' * 4 + '- ' + err for err in errors) + '</span>'
+            )
+            self.toggle_buttons()
+        else:
+            self.ui.lblWelcomeErrorsHeader.setVisible(False)
+            self.ui.lblWelcomeErrors.setText('Settings used for validating and signing XML files.')
+            self.resize(455, 290)
+            self.setMinimumSize(QSize(455, 290))
+            self.setMaximumSize(QSize(455, 290))
+            self.toggle_buttons()
+
+    def set_connections(self):
+        self.ui.btnXMLSchemaBrowse.clicked.connect(self.browse_schema)
+        self.ui.btnCertificateBrowse.clicked.connect(self.browse_certificate)
+        self.ui.btnCancel.clicked.connect(self.close)
+        self.ui.btnOk.clicked.connect(self.validate_fields)
+        self.ui.btnReset.clicked.connect(self.reset_settings)
+
+    def browse_schema(self):
+        fbrowser = QFileDialog(self)
+        fbrowser.selectFile(self.ui.leXMLSchema.displayText())
+        fbrowser.setFileMode(QFileDialog.ExistingFile)
+        fbrowser.setNameFilter('XML Schema Definition files (*.xsd)')
+        if fbrowser.exec_():
+            self.ui.leXMLSchema.setText(fbrowser.selectedFiles()[0])
+            self.toggle_buttons_fields()
+
+    def browse_certificate(self):
+        fbrowser = QFileDialog(self)
+        fbrowser.selectFile(self.ui.leCertificate.displayText())
+        fbrowser.setFileMode(QFileDialog.ExistingFile)
+        fbrowser.setNameFilter('PKCS #12 files (*.p12)')
+        if fbrowser.exec_():
+            self.ui.leCertificate.setText(fbrowser.selectedFiles()[0])
+            self.toggle_buttons_fields()
+
+    def toggle_buttons(self, toggle=True):
+        self.ui.btnReset.setEnabled(toggle)
+        self.ui.btnOk.setEnabled(toggle)
+
+    def toggle_buttons_fields(self):
+        if self.ui.leXMLSchema.displayText() and self.ui.leCertificate.displayText():
+            self.toggle_buttons()
+        elif self.ui.leXMLSchema.displayText() or self.ui.leCertificate.displayText():
+            self.ui.btnReset.setEnabled(True)
+
+    def reset_settings(self):
+        self.ui.leXMLSchema.setText('')
+        self.ui.leCertificate.setText('')
+        self.toggle_buttons(False)
+
+    def validate_fields(self):
+        errors = []
+        if len(self.ui.leXMLSchema.displayText()) == 0:
+            errors.append('XML Schema file path is not set.')
+        elif not os.path.isfile(self.ui.leXMLSchema.displayText()):
+            errors.append('XML Schema file doesn\'t exist.')
+        if len(self.ui.leCertificate.displayText()) == 0:
+            errors.append('Certificate path is not set.')
+        elif not os.path.isfile(self.ui.leCertificate.displayText()):
+            errors.append('Certificate file doesn\'t exist.')
+
+        if errors:
+            error = QMessageBox(self)
+            error.setIcon(QMessageBox.Critical)
+            error.setWindowTitle('Error')
+            error.setText('Something went wrong with chosen files.')
+            error.setInformativeText('Please correct errors if you want to proceed.')
+            error.setStandardButtons(QMessageBox.Ok)
+            error.setDetailedText('\n'.join(errors))
+            error.exec_()
+        else:
+            self.accept()
+
+    @staticmethod
+    def get_data(parent=None, config=None, initial=False, errors=None):
+        settings = SettingsDialog(parent, config, initial, errors)
+        result = settings.exec_()
+        return ({
+            'xmlschema': settings.ui.leXMLSchema.displayText(),
+            'certificate': settings.ui.leCertificate.displayText()
+        }, result == QDialog.Accepted)

+ 0 - 0
gui/__init__.py


+ 0 - 0
helpers/__init__.py


+ 58 - 0
helpers/config.py

@@ -0,0 +1,58 @@
+from configparser import ConfigParser
+import os
+
+
+class Config:
+    DEFAULT_XML_REFERENCE_TYPE = 'http://www.gzs.si/shemas/eslog/racun/1.6#Racun'
+    DEFAULT_CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.config')
+
+    def __init__(self, config_file):
+        self.config_file = os.path.join(self.DEFAULT_CONFIG_DIR, config_file)
+        self.config = ConfigParser()
+        self.config.read(self.config_file)
+        self.errors = []
+
+    def exists(self):
+        return 'settings' in self.config and 'xmlschema' in self.config['settings'] and\
+                        'certificate' in self.config['settings']
+
+    def get_data(self):
+        return {
+            'xmlreftype': self.config.get('settings', 'xmlreftype'),
+            'xmlschema': self.config.get('settings', 'xmlschema'),
+            'certificate': self.config.get('settings', 'certificate')
+        }
+
+    def verify_data(self):
+        if not self.exists():
+            self.errors.append('Config wasn\'t set up yet.')
+            return False
+
+        if not os.path.isfile(self.config['settings']['xmlschema']):
+            self.errors.append('XML Schema file doesn\'t exist.')
+        if not os.path.isfile(self.config['settings']['certificate']):
+            self.errors.append('Certificate file doesn\'t exist.')
+        return len(self.errors) == 0
+
+    def get_errors(self):
+        errors = self.errors.copy()
+        self.errors.clear()
+        return errors
+
+    def save_data(self, config):
+        if 'settings' not in self.config:
+            self.config.add_section('settings')
+            self.config.set('settings', 'xmlreftype', self.DEFAULT_XML_REFERENCE_TYPE)
+        self.config.set('settings', 'xmlschema', config['xmlschema'])
+        self.config.set('settings', 'certificate', config['certificate'])
+        try:
+            if not os.path.isfile(self.config_file):
+                os.makedirs(self.DEFAULT_CONFIG_DIR, exist_ok=True)
+            with open(self.config_file, 'w') as f:
+                self.config.write(f)
+        except:
+            raise ConfigException('Cannot write config.')
+
+
+class ConfigException(Exception):
+    pass

+ 261 - 0
helpers/xadessigner.py

@@ -0,0 +1,261 @@
+from lxml import etree
+from datetime import datetime
+from OpenSSL import crypto
+from hashlib import sha1
+from base64 import b64encode
+
+
+class XAdESSigner:
+    """
+    XAdES-BES Enveloped signing of XML files
+    """
+
+    XMLNS_DS = 'http://www.w3.org/2000/09/xmldsig#'
+    XMLNS_XDS = 'http://uri.etsi.org/01903/v1.1.1#'
+    SIGNATURE_ALG = 'sha1'
+    DIGEST_ALG_SPEC = XMLNS_DS + SIGNATURE_ALG
+    SIGNATURE_ALG_SPEC = XMLNS_DS + 'rsa-' + SIGNATURE_ALG
+    CANONICAL_ALG_SPEC = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'
+    SIGNEDPROPS_REFERENCE_TYPE = XMLNS_XDS + 'SignedProperties'
+
+    def __init__(self, xml_root, xml_reference_type, certificate_file):
+        """
+        Initialize object with XML root element, XML reference type URL and certificate file path
+        :param xml_root: lxml.etree.Element
+        :param xml_reference_type: str
+        :param certificate_file: str
+        """
+        self.xml_root = xml_root
+        self.xml_reference_type = xml_reference_type
+        self.certificate_file = certificate_file
+
+    def sign(self, certificate_password):
+        """
+        Sign instance's XML and return it
+        :param certificate_password: str
+        :return: lxml.etree.Element
+        """
+        main_id = self.__prepare_xml()
+        try:
+            with open(self.certificate_file, 'rb') as cf:
+                p12 = crypto.load_pkcs12(cf.read(), certificate_password)
+            self.__create_signature(main_id, p12.get_privatekey(), p12.get_certificate())
+            return self.xml_root
+        except IOError:
+            raise XaDESSignerException('Problem with accessing certificate file.')
+        except crypto.Error:
+            raise XaDESSignerException('Certificate password is wrong.')
+
+    def __prepare_xml(self, main_id='data'):
+        """
+        Check if XML already contains signature and add appropriate namespaces to root
+        """
+        elements_num = len(self.xml_root)
+        if (elements_num > 1 and
+                self.xml_root[elements_num - 1].prefix and 'Signature' in self.xml_root[elements_num - 1].tag):
+            raise XaDESSignerException('XML was already signed.')
+        nsmap = self.xml_root.nsmap.copy()
+        nsmap.update(self.__get_namespaces())
+        xml_root = etree.Element(self.xml_root.tag, nsmap=nsmap)
+        xml_root[:] = self.xml_root[:]
+        self.xml_root = xml_root
+        main_element = self.xml_root[0]
+        if 'Id' in main_element.keys() and len(main_element.attrib['Id']) > 0:
+            return main_element.attrib['Id']
+        main_element.set('Id', main_id)
+        return main_id
+
+    def __create_signature(self, main_id, private_key, certificate):
+        """
+        Create signature of all main XML parts along signed properties
+        :param main_id: str
+        :param certificate: OpenSSL.crypto.X509
+        :return: lxml.etree.Element
+        """
+        signature_id = 'SignatureId'
+        ds_object, signedprops_id = self.__create_signature_object(signature_id, certificate)
+        ds_signature = etree.Element(self.__get_namespaced_tag('ds', 'Signature'))
+        ds_signature.set('Id', signature_id)
+        ds_signedinfo = etree.SubElement(ds_signature, self.__get_namespaced_tag('ds', 'SignedInfo'))
+        ds_canonicalmethod = etree.SubElement(ds_signedinfo, self.__get_namespaced_tag('ds', 'CanonicalizationMethod'))
+        ds_canonicalmethod.set('Algorithm', self.CANONICAL_ALG_SPEC)
+        ds_signaturemethod = etree.SubElement(ds_signedinfo, self.__get_namespaced_tag('ds', 'SignatureMethod'))
+        ds_signaturemethod.set('Algorithm', self.SIGNATURE_ALG_SPEC)
+        ds_reference_main = etree.SubElement(ds_signedinfo, self.__get_namespaced_tag('ds', 'Reference'))
+        ds_reference_main.set('Type', self.xml_reference_type)
+        ds_reference_main.set('URI', '#' + main_id.strip('#'))
+        ds_refmain_digestmethod = etree.SubElement(ds_reference_main, self.__get_namespaced_tag('ds', 'DigestMethod'))
+        ds_refmain_digestmethod.set('Algorithm', self.DIGEST_ALG_SPEC)
+        ds_refmain_digestvalue = etree.SubElement(ds_reference_main, self.__get_namespaced_tag('ds', 'DigestValue'))
+        ds_reference_signedprops = etree.SubElement(ds_signedinfo, self.__get_namespaced_tag('ds', 'Reference'))
+        ds_reference_signedprops.set('Type', self.SIGNEDPROPS_REFERENCE_TYPE)
+        ds_reference_signedprops.set('URI', '#' + signedprops_id.strip('#'))
+        ds_refsp_digestmethod = etree.SubElement(
+            ds_reference_signedprops,
+            self.__get_namespaced_tag('ds', 'DigestMethod')
+        )
+        ds_refsp_digestmethod.set('Algorithm', self.DIGEST_ALG_SPEC)
+        ds_refsp_digestvalue = etree.SubElement(ds_reference_signedprops, self.__get_namespaced_tag('ds', 'DigestValue'))
+        ds_signaturevalue = etree.SubElement(ds_signature, self.__get_namespaced_tag('ds', 'SignatureValue'))
+        ds_keyinfo = etree.SubElement(ds_signature, self.__get_namespaced_tag('ds', 'KeyInfo'))
+        ds_X509data = etree.SubElement(ds_keyinfo, self.__get_namespaced_tag('ds', 'X509Data'))
+        etree.SubElement(
+            ds_X509data,
+            self.__get_namespaced_tag('ds', 'X509Certificate')
+        ).text = self.__get_certificate_der(certificate)
+
+        # Append signature object to signature and signature to root to inherit all used namespaces
+        ds_signature.append(ds_object)
+        self.xml_root.append(ds_signature)
+
+        # We generate digest values after XML is fully composed
+        # First we generate digest value for main elements in XML
+        ds_refmain_digestvalue.text = self.__generate_digest_value(self.xml_root[:-1])
+        # Then we generate digest for SignedProperties contained inside signature object
+        ds_refsp_digestvalue.text = self.__generate_digest_value(ds_signature[-1][0][:1])
+
+        # Sign XML with private key after we have completed SignedInfo with digests
+        ds_signaturevalue.text = self.__get_signed_xml(private_key, ds_signature[:1])
+
+        return ds_signature
+
+    def __get_signed_xml(self, private_key, nodes):
+        """
+        Sign canonicalized XML with provided private key, generated from applied nodes
+        :param private_key: OpenSSL.crypto.PKey
+        :param nodes: list
+        :return: str
+        """
+        return b64encode(crypto.sign(private_key, self.__get_canonical_xml(nodes), self.SIGNATURE_ALG)).decode()
+
+    def __get_certificate_der(self, certificate):
+        """
+        Return certificate in DER format encoded in base64
+        :param certificate: OpenSSL.crypto.X509
+        :return: str
+        """
+        return b64encode(crypto.dump_certificate(crypto.FILETYPE_ASN1, certificate)).decode()
+
+    def __get_canonical_xml(self, nodes):
+        """
+        Generate canonical XML from applied nodes and replace all unknown namespaces
+        :param nodes: list
+        :return: bytearray
+        """
+        canonical_xml = b''
+        for node in nodes:
+            canonical_xml += etree.tostring(node, method='c14n', with_comments=False)
+        return canonical_xml.replace(b' xmlns=""', b'')
+
+    def __generate_digest_value(self, nodes):
+        """
+        Generate digest value for generated canonical XML from applied nodes
+        :param nodes: list
+        :return: str
+        """
+        return XAdESSigner.base64_sha1_str(self.__get_canonical_xml(nodes))
+
+    def __create_signature_object(self, signature_id, certificate):
+        """
+        Create signature object which must also be signed as part of XadES signature
+        :param signature_id: str
+        :param certificate: OpenSSL.crypto.X509
+        :return: tuple
+        """
+        signedprops_id = 'SignedPropertiesId'
+        ds_object = etree.Element(self.__get_namespaced_tag('ds', 'Object'))
+        xds_qualifyingprops = etree.SubElement(ds_object, self.__get_namespaced_tag('xds', 'QualifyingProperties'))
+        xds_qualifyingprops.set('Target', '#' + signature_id.strip('#'))
+        xds_signedprops = etree.SubElement(xds_qualifyingprops, self.__get_namespaced_tag('xds', 'SignedProperties'))
+        xds_signedprops.set('Id', signedprops_id)
+        xds_signedsigprops = etree.SubElement(
+            xds_signedprops,
+            self.__get_namespaced_tag('xds', 'SignedSignatureProperties')
+        )
+        etree.SubElement(
+            xds_signedsigprops,
+            self.__get_namespaced_tag('xds', 'SigningTime')
+        ).text = datetime.utcnow().isoformat()[:-3] + 'Z'
+        xds_signingcert = etree.SubElement(xds_signedsigprops, self.__get_namespaced_tag('xds', 'SigningCertificate'))
+        xds_cert = etree.SubElement(xds_signingcert, self.__get_namespaced_tag('xds', 'Cert'))
+        xds_certdigest = etree.SubElement(xds_cert, self.__get_namespaced_tag('xds', 'CertDigest'))
+        xds_digestmethod = etree.SubElement(xds_certdigest, self.__get_namespaced_tag('xds', 'DigestMethod'))
+        xds_digestmethod.set('Algorithm', self.DIGEST_ALG_SPEC)
+        etree.SubElement(
+            xds_certdigest,
+            self.__get_namespaced_tag('xds', 'DigestValue')
+        ).text = self.__get_certificate_digest(certificate)
+        xds_issuerserial = etree.SubElement(xds_cert, self.__get_namespaced_tag('xds', 'IssuerSerial'))
+        etree.SubElement(
+            xds_issuerserial,
+            self.__get_namespaced_tag('ds', 'X509IssuerName')
+        ).text = self.__get_certificate_issuer(certificate)
+        etree.SubElement(
+            xds_issuerserial,
+            self.__get_namespaced_tag('ds', 'X509SerialNumber')
+        ).text = '%d' % certificate.get_serial_number()
+        xds_signaturepolicyid = etree.SubElement(
+            xds_signedsigprops,
+            self.__get_namespaced_tag('xds', 'SignaturePolicyIdentifier')
+        )
+        etree.SubElement(xds_signaturepolicyid, self.__get_namespaced_tag('xds', 'SignaturePolicyImplied'))
+        return ds_object, signedprops_id
+
+    def __get_namespaced_tag(self, namespace, tag):
+        """
+        Generate tag with prefixed namespace for lxml element name
+        :param namespace: str
+        :param tag: str
+        :return: str
+        """
+        namespaces = self.__get_namespaces()
+        if namespace not in namespaces:
+            raise XaDESSignerValueError('Namespace unknown.')
+        return '{%s}%s' % (namespaces[namespace], tag)
+
+    def __get_namespaces(self):
+        """
+        Wrapper method which returns available namespaces in dictionary form
+        :return: dict
+        """
+        return {
+            'ds': self.XMLNS_DS,
+            'xds': self.XMLNS_XDS
+        }
+
+    def __get_certificate_digest(self, certificate):
+        """
+        Get certificate digest
+        :param certificate: OpenSSL.crypto.X509
+        :return: str
+        """
+        return XAdESSigner.base64_sha1_str(crypto.dump_certificate(crypto.FILETYPE_ASN1, certificate))
+
+    def __get_certificate_issuer(self, certificate):
+        """
+        Get certificate issuer - consists of everything known about issuer, separated with commas
+        :param certificate: OpenSSL.crypto.X509
+        :return: str
+        """
+        return ','.join(m.decode() + '=' + v.decode() for m, v in certificate.get_issuer().get_components())
+
+    @staticmethod
+    def base64_sha1_str(string):
+        """
+        Hash string or bytearray to SHA1 and encode it with base64
+        :param string: str or byte
+        :return: str
+        """
+        try:
+            string = string.encode()
+        except AttributeError:
+            pass
+        return b64encode(sha1(string).digest()).decode()
+
+
+class XaDESSignerException(Exception):
+    pass
+
+
+class XaDESSignerValueError(Exception):
+    pass

+ 58 - 0
helpers/xmlhandler.py

@@ -0,0 +1,58 @@
+from lxml import etree
+from helpers.xadessigner import XAdESSigner, XaDESSignerException
+from copy import deepcopy
+import unicodedata
+import string
+
+
+class XMLHandler:
+    def __init__(self, xml_file, xml_reference_type):
+        self.xml_file = xml_file
+        self.xml_tree = self.__parse_xml(xml_file)
+        self.xml_reference_type = xml_reference_type
+
+    def __parse_xml(self, xml_file):
+        try:
+            xml_tree = etree.parse(xml_file)
+            return xml_tree
+        except:
+            raise XMLHandlerException('Input XML is invalid.')
+
+    def valid(self, xml_schema):
+        try:
+            xml_schema_tree = etree.XMLSchema(etree.parse(xml_schema))
+            return xml_schema_tree.validate(self.xml_tree)
+        except:
+            return False
+
+    def sign(self, certificate_file, certificate_password):
+        try:
+            xml_signer = XAdESSigner(deepcopy(self.xml_tree.getroot()), self.xml_reference_type, certificate_file)
+            xml_signed_tree = xml_signer.sign(certificate_password)
+            with open(self.xml_file, 'wb') as signed_xml:
+                etree.ElementTree(xml_signed_tree).write(signed_xml, encoding='utf-8', xml_declaration=True)
+        except IOError:
+            raise XMLHandlerException('Problem with writing signed XML file.')
+        except XaDESSignerException as e:
+            raise XMLHandlerException(str(e))
+        except:
+            raise XMLHandlerException('Something went wrong with writing signed XML file.')
+
+    def get_general_filename(self):
+        default_filename = 'document'
+        xml_root = self.xml_tree.getroot()
+        if xml_root is not None and len(xml_root) > 0 and len(xml_root[0]) > 0 and len(xml_root[0][0]) > 1:
+            return self.__normalize_filename(xml_root[0][0][1].text.strip(), default_filename)
+        return default_filename
+
+    def __normalize_filename(self, filename, default_filename='document'):
+        if len(filename) == 0:
+            return default_filename
+        valid_chars = '-_%s%s' % (string.ascii_letters, string.digits)
+        replacement_char = '_'
+        clean_name = unicodedata.normalize('NFKD', filename).encode('ascii', 'ignore').decode('utf-8')
+        return ''.join(c.lower() if c in valid_chars else replacement_char for c in clean_name)
+
+
+class XMLHandlerException(Exception):
+    pass

+ 141 - 0
main.py

@@ -0,0 +1,141 @@
+import sys
+import os
+
+from PyQt5.QtWidgets import QMainWindow, QApplication, QFileDialog, QProgressBar, QMessageBox, QInputDialog, QLineEdit
+from PyQt5.QtGui import QIcon
+from helpers.config import Config
+from threads.Sign import Sign
+from gui.SettingsDialog import SettingsDialog
+from gui.AboutDialog import AboutDialog
+
+from ui.mainwindow import Ui_MainWindow
+
+# Program dependent variables
+app_root = os.path.dirname(os.path.abspath(__file__))
+config_file = 'ordersguru_signer.ini'
+
+
+# Main window class
+class MainWindow(QMainWindow):
+    def __init__(self, parent=None):
+        QMainWindow.__init__(self, parent)
+        self.ui = Ui_MainWindow()
+        self.ui.setupUi(self)
+        self.setWindowIcon(QIcon(os.path.join(app_root, 'resources', 'icon.ico')))
+        self.progressBar = QProgressBar(self)
+        self.progressBar.setRange(0, 1)
+        self.ui.statusbar.addPermanentWidget(self.progressBar)
+
+        self.config = Config(config_file)
+
+        self.set_connections()
+        self.check_config()
+        self.ui.statusbar.showMessage('Ready for signing')
+
+    def set_connections(self):
+        self.ui.action_Settings.triggered.connect(lambda: self.show_settings(self.config.get_data()))
+        self.ui.action_About.triggered.connect(lambda: AboutDialog.show_(self))
+        self.ui.action_Quit.triggered.connect(self.close)
+        self.ui.btnDirectoryBrowse.clicked.connect(self.browse_directory)
+        self.ui.btnFileBrowse.clicked.connect(self.browse_file)
+        self.ui.btnDirectorySignArrange.clicked.connect(self.sign_directory)
+        self.ui.btnFileSign.clicked.connect(self.sign_file)
+
+    def check_config(self):
+        if not self.config.exists():
+            self.show_settings(initial=True)
+        elif not self.config.verify_data():
+            self.show_settings(self.config.get_data(), False, self.config.get_errors())
+
+    def show_settings(self, config=None, initial=False, errors=None):
+        config, success = SettingsDialog.get_data(self, config, initial, errors)
+        if success:
+            try:
+                self.config.save_data(config)
+            except BaseException as e:
+                self.show_message((str(e), False))
+                sys.exit()
+        elif initial or errors:
+            sys.exit()
+
+    def browse_directory(self):
+        fbrowser = QFileDialog(self)
+        fbrowser.selectFile(self.ui.leDirectory.displayText())
+        fbrowser.setFileMode(QFileDialog.DirectoryOnly)
+        if fbrowser.exec_():
+            self.ui.leDirectory.setText(fbrowser.selectedFiles()[0])
+            self.ui.btnDirectorySignArrange.setEnabled(True)
+
+    def sign_directory(self):
+        password = self.get_password()
+        if password:
+            self.ui.btnDirectorySignArrange.setEnabled(False)
+            self.ui.statusbar.showMessage('Signing & arranging ...')
+            self.progressBar.setRange(0, 0)
+            sign = Sign(self, {
+                'directory': self.ui.leDirectory.displayText(),
+                'config': self.config.get_data(),
+                'password': password
+            })
+            sign.status_bar_signal.connect(self.ui.statusbar.showMessage)
+            sign.sign_button_signal.connect(self.ui.btnDirectorySignArrange.setEnabled)
+            sign.progress_bar_signal.connect(self.set_progress)
+            sign.message_signal.connect(self.show_message)
+            sign.start()
+
+    def browse_file(self):
+        fbrowser = QFileDialog(self)
+        fbrowser.selectFile(self.ui.leFile.displayText())
+        fbrowser.setFileMode(QFileDialog.ExistingFile)
+        fbrowser.setNameFilter('XML files (*.xml)')
+        if fbrowser.exec_():
+            self.ui.leFile.setText(fbrowser.selectedFiles()[0])
+            self.ui.btnFileSign.setEnabled(True)
+
+    def sign_file(self):
+        password = self.get_password()
+        if password:
+            self.ui.btnFileSign.setEnabled(False)
+            self.ui.statusbar.showMessage('Signing ...')
+            self.progressBar.setRange(0, 0)
+            sign = Sign(self, {
+                'file': self.ui.leFile.displayText(),
+                'config': self.config.get_data(),
+                'password': password
+            })
+            sign.status_bar_signal.connect(self.ui.statusbar.showMessage)
+            sign.sign_button_signal.connect(self.ui.btnFileSign.setEnabled)
+            sign.progress_bar_signal.connect(self.set_progress)
+            sign.message_signal.connect(self.show_message)
+            sign.start()
+
+    def set_progress(self, in_progress):
+        self.progressBar.setRange(0, 0 if in_progress else 1)
+
+    def show_message(self, message):
+        msg, success = message
+        error = QMessageBox(self)
+        error.setIcon(QMessageBox.Information if success else QMessageBox.Critical)
+        error.setWindowTitle('Success' if success else 'Error')
+        error.setText(msg)
+        error.setStandardButtons(QMessageBox.Ok)
+        error.exec_()
+        self.ui.statusbar.showMessage('Ready for signing')
+
+    def get_password(self):
+        password, success = QInputDialog.getText(
+            self,
+            'Certificate password',
+            'Please enter certificate password:',
+            echo=QLineEdit.Password
+        )
+        if success:
+            return password
+        return False
+
+# Run program by default
+if __name__ == '__main__':
+    app = QApplication(sys.argv)
+    og_signer = MainWindow()
+    og_signer.show()
+    sys.exit(app.exec_())

BIN
resources/about.png


+ 120 - 0
resources/about.ui

@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>About</class>
+ <widget class="QDialog" name="About">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>330</width>
+    <height>160</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>330</width>
+    <height>160</height>
+   </size>
+  </property>
+  <property name="maximumSize">
+   <size>
+    <width>330</width>
+    <height>160</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>About</string>
+  </property>
+  <property name="windowIcon">
+   <iconset>
+    <normaloff>resources/icon.ico</normaloff>resources/icon.ico</iconset>
+  </property>
+  <property name="accessibleName">
+   <string/>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QGridLayout" name="gridLayout">
+     <item row="0" column="0">
+      <widget class="QLabel" name="lblIcon">
+       <property name="text">
+        <string/>
+       </property>
+       <property name="pixmap">
+        <pixmap>resources/icon.ico</pixmap>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="QLabel" name="lblAbout">
+       <property name="text">
+        <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;OrdersGuru Signer&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Application for signing and arranging orders documents.&lt;/p&gt;&lt;p&gt;Created by &lt;a href=&quot;http://jodlajodla.si/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;Jan Sušnik&lt;/span&gt;&lt;/a&gt;, 2017&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+       </property>
+       <property name="wordWrap">
+        <bool>true</bool>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>20</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Close</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>About</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>About</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>

BIN
resources/icon.ico


BIN
resources/logo.png


+ 309 - 0
resources/mainwindow.ui

@@ -0,0 +1,309 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>450</width>
+    <height>281</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>450</width>
+    <height>281</height>
+   </size>
+  </property>
+  <property name="maximumSize">
+   <size>
+    <width>450</width>
+    <height>281</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>OrdersGuru Signer</string>
+  </property>
+  <property name="windowIcon">
+   <iconset>
+    <normaloff>resources/icon.ico</normaloff>resources/icon.ico</iconset>
+  </property>
+  <property name="dockOptions">
+   <set>QMainWindow::AllowTabbedDocks|QMainWindow::AnimatedDocks</set>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <layout class="QVBoxLayout" name="verticalLayout">
+    <item>
+     <widget class="QLabel" name="lblLogo">
+      <property name="text">
+       <string/>
+      </property>
+      <property name="pixmap">
+       <pixmap>resources/logo.png</pixmap>
+      </property>
+      <property name="scaledContents">
+       <bool>false</bool>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignCenter</set>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QTabWidget" name="tabWidget">
+      <property name="layoutDirection">
+       <enum>Qt::LeftToRight</enum>
+      </property>
+      <property name="styleSheet">
+       <string notr="true"/>
+      </property>
+      <property name="tabShape">
+       <enum>QTabWidget::Rounded</enum>
+      </property>
+      <property name="currentIndex">
+       <number>0</number>
+      </property>
+      <property name="elideMode">
+       <enum>Qt::ElideNone</enum>
+      </property>
+      <property name="movable">
+       <bool>false</bool>
+      </property>
+      <property name="tabBarAutoHide">
+       <bool>false</bool>
+      </property>
+      <widget class="QWidget" name="tabDirectory">
+       <attribute name="title">
+        <string>Directory</string>
+       </attribute>
+       <attribute name="whatsThis">
+        <string>Sign XML file in directory and rename files appropriately</string>
+       </attribute>
+       <layout class="QVBoxLayout" name="verticalLayout_5">
+        <item>
+         <widget class="QGroupBox" name="gbDirectory">
+          <property name="title">
+           <string>Sign XML and arrange files in directory</string>
+          </property>
+          <layout class="QVBoxLayout" name="verticalLayout_4">
+           <item>
+            <layout class="QGridLayout" name="glDirectory">
+             <property name="horizontalSpacing">
+              <number>12</number>
+             </property>
+             <item row="1" column="1">
+              <widget class="QLineEdit" name="leDirectory">
+               <property name="enabled">
+                <bool>true</bool>
+               </property>
+               <property name="echoMode">
+                <enum>QLineEdit::Normal</enum>
+               </property>
+               <property name="dragEnabled">
+                <bool>true</bool>
+               </property>
+               <property name="readOnly">
+                <bool>false</bool>
+               </property>
+               <property name="placeholderText">
+                <string/>
+               </property>
+              </widget>
+             </item>
+             <item row="1" column="0">
+              <widget class="QLabel" name="lblDirectory">
+               <property name="text">
+                <string>Directory:</string>
+               </property>
+              </widget>
+             </item>
+             <item row="1" column="2">
+              <widget class="QPushButton" name="btnDirectoryBrowse">
+               <property name="text">
+                <string>Browse</string>
+               </property>
+               <property name="autoDefault">
+                <bool>false</bool>
+               </property>
+               <property name="default">
+                <bool>true</bool>
+               </property>
+              </widget>
+             </item>
+            </layout>
+           </item>
+          </layout>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="btnDirectorySignArrange">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="text">
+           <string>Sign XML and arrange files</string>
+          </property>
+          <property name="autoDefault">
+           <bool>false</bool>
+          </property>
+          <property name="default">
+           <bool>false</bool>
+          </property>
+          <property name="flat">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QWidget" name="tabFile">
+       <attribute name="title">
+        <string>File</string>
+       </attribute>
+       <attribute name="whatsThis">
+        <string>Sign single XML file</string>
+       </attribute>
+       <layout class="QVBoxLayout" name="verticalLayout_6">
+        <item>
+         <widget class="QGroupBox" name="gbFile">
+          <property name="title">
+           <string>Sign single XML file</string>
+          </property>
+          <layout class="QVBoxLayout" name="verticalLayout_3">
+           <item>
+            <layout class="QGridLayout" name="glFile">
+             <property name="horizontalSpacing">
+              <number>12</number>
+             </property>
+             <item row="1" column="1">
+              <widget class="QLineEdit" name="leFile">
+               <property name="enabled">
+                <bool>true</bool>
+               </property>
+               <property name="echoMode">
+                <enum>QLineEdit::Normal</enum>
+               </property>
+               <property name="dragEnabled">
+                <bool>true</bool>
+               </property>
+               <property name="readOnly">
+                <bool>false</bool>
+               </property>
+               <property name="placeholderText">
+                <string/>
+               </property>
+              </widget>
+             </item>
+             <item row="1" column="0">
+              <widget class="QLabel" name="lblFile">
+               <property name="text">
+                <string>File:</string>
+               </property>
+              </widget>
+             </item>
+             <item row="1" column="2">
+              <widget class="QPushButton" name="btnFileBrowse">
+               <property name="text">
+                <string>Browse</string>
+               </property>
+               <property name="autoDefault">
+                <bool>false</bool>
+               </property>
+               <property name="default">
+                <bool>true</bool>
+               </property>
+              </widget>
+             </item>
+            </layout>
+           </item>
+          </layout>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="btnFileSign">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="text">
+           <string>Sign XML file</string>
+          </property>
+          <property name="autoDefault">
+           <bool>false</bool>
+          </property>
+          <property name="default">
+           <bool>false</bool>
+          </property>
+          <property name="flat">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>450</width>
+     <height>25</height>
+    </rect>
+   </property>
+   <widget class="QMenu" name="menuFile">
+    <property name="title">
+     <string>&amp;File</string>
+    </property>
+    <addaction name="action_Settings"/>
+    <addaction name="separator"/>
+    <addaction name="action_Quit"/>
+   </widget>
+   <widget class="QMenu" name="menuHelp">
+    <property name="title">
+     <string>&amp;Help</string>
+    </property>
+    <addaction name="action_About"/>
+   </widget>
+   <addaction name="menuFile"/>
+   <addaction name="menuHelp"/>
+  </widget>
+  <widget class="QStatusBar" name="statusbar">
+   <property name="sizeGripEnabled">
+    <bool>false</bool>
+   </property>
+  </widget>
+  <action name="action_Quit">
+   <property name="icon">
+    <iconset>
+     <normaloff>resources/quit.png</normaloff>resources/quit.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Quit</string>
+   </property>
+  </action>
+  <action name="action_About">
+   <property name="icon">
+    <iconset>
+     <normaloff>resources/about.png</normaloff>resources/about.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;About</string>
+   </property>
+  </action>
+  <action name="action_Settings">
+   <property name="icon">
+    <iconset>
+     <normaloff>resources/settings.png</normaloff>resources/settings.png</iconset>
+   </property>
+   <property name="text">
+    <string>&amp;Settings</string>
+   </property>
+  </action>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

BIN
resources/quit.png


BIN
resources/screenshot.png


BIN
resources/settings.png


+ 215 - 0
resources/settings.ui

@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Settings</class>
+ <widget class="QDialog" name="Settings">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>455</width>
+    <height>350</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>455</width>
+    <height>350</height>
+   </size>
+  </property>
+  <property name="maximumSize">
+   <size>
+    <width>455</width>
+    <height>350</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Settings</string>
+  </property>
+  <property name="windowIcon">
+   <iconset>
+    <normaloff>resources/icon.ico</normaloff>resources/icon.ico</iconset>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="lblWelcomeErrorsHeader">
+     <property name="text">
+      <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;center&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Welcome to OrdersGuru Signer!&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="lblWelcomeErrors">
+     <property name="enabled">
+      <bool>true</bool>
+     </property>
+     <property name="text">
+      <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In order to use this tool you must provide XML schema for validating files you will sign and certificate with which you'll sign XMLs.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <spacer name="vsHeader">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>40</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="gbXMLValidation">
+     <property name="title">
+      <string>XML Validation</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_2">
+      <item>
+       <widget class="QLabel" name="lblXMLValidation">
+        <property name="text">
+         <string>Provide XML schema used for validating all XML documents.</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <layout class="QGridLayout" name="glXMLValidation">
+        <item row="0" column="0">
+         <widget class="QLabel" name="lblXMLSchema">
+          <property name="text">
+           <string>XML Schema:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="1">
+         <widget class="QLineEdit" name="leXMLSchema">
+          <property name="readOnly">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="2">
+         <widget class="QPushButton" name="btnXMLSchemaBrowse">
+          <property name="text">
+           <string>Browse</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="gbCertificate">
+     <property name="title">
+      <string>Certificate</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_3">
+      <item>
+       <widget class="QLabel" name="lblCertificate">
+        <property name="text">
+         <string>Provide certificate in PKCS 12 format, used for signing documents.</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <layout class="QGridLayout" name="glCertificate">
+        <item row="0" column="1">
+         <widget class="QLineEdit" name="leCertificate">
+          <property name="readOnly">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="0">
+         <widget class="QLabel" name="lblCertificateFile">
+          <property name="text">
+           <string>Certificate:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="2">
+         <widget class="QPushButton" name="btnCertificateBrowse">
+          <property name="text">
+           <string>Browse</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <spacer name="vsFooter">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>40</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="hlButtonBox">
+     <item>
+      <widget class="QPushButton" name="btnReset">
+       <property name="enabled">
+        <bool>false</bool>
+       </property>
+       <property name="text">
+        <string>Reset</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="hsButtonBox">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="btnCancel">
+       <property name="text">
+        <string>&amp;Cancel</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="btnOk">
+       <property name="enabled">
+        <bool>false</bool>
+       </property>
+       <property name="text">
+        <string>&amp;OK</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 69 - 0
threads/Sign.py

@@ -0,0 +1,69 @@
+from PyQt5.QtCore import QThread, pyqtSignal
+from helpers.xmlhandler import XMLHandler
+import os
+
+
+class Sign(QThread):
+    status_bar_signal = pyqtSignal(str)
+    sign_button_signal = pyqtSignal(bool)
+    progress_bar_signal = pyqtSignal(bool)
+    message_signal = pyqtSignal(tuple)
+
+    def __init__(self, parent, data):
+        super(Sign, self).__init__(parent)
+        self.data = data
+
+    def run(self):
+        if 'file' in self.data and self.sign_file(self.data['file']):
+            self.change_status('XML successfully signed.', 'Signing successful')
+        elif 'directory' in self.data and self.sign_arrange_directory(self.data['directory']):
+            self.change_status('XML successfully signed and files arranged.', 'Signing & arranging successful')
+
+    def sign_file(self, path):
+        try:
+            xml_file = XMLHandler(path, self.data['config']['xmlreftype'])
+            if not xml_file.valid(self.data['config']['xmlschema']):
+                self.change_status('XML file does not comply schema.', 'XML file error', False)
+            xml_file.sign(self.data['config']['certificate'], self.data['password'])
+            return xml_file
+        except BaseException as e:
+            self.change_status(str(e), 'Signing error', False)
+            return None
+
+    def sign_arrange_directory(self, path):
+        files = list()
+        extensions = list()
+        xml_path = None
+        for file in os.listdir(path):
+            filename, extension = os.path.splitext(file)
+            extension = extension.lower()
+            extensions.append(extension)
+            if extension == '.xml':
+                if xml_path is None:
+                    xml_path = os.path.join(path, file)
+                else:
+                    break
+            files.append((file, extension))
+        if len(extensions) != len(set(extensions)):
+            self.change_status('There are multiple files with same extension in directory.', 'Arranging error', False)
+            return False
+        elif xml_path is None:
+            self.change_status('There is no XML file in directory.', 'Signing error', False)
+            return False
+        xml_file = self.sign_file(xml_path)
+        if not xml_file:
+            return False
+        general_filename = xml_file.get_general_filename()
+        for file, extension in files:
+            try:
+                os.rename(os.path.join(path, file), os.path.join(path, general_filename + extension))
+            except:
+                self.change_status('Something went wrong with arranging files.', 'Arranging error', False)
+                return False
+        return True
+
+    def change_status(self, message, status_msg, success=True):
+        self.message_signal.emit((message, success))
+        self.status_bar_signal.emit(status_msg)
+        self.progress_bar_signal.emit(False)
+        self.sign_button_signal.emit(True)

+ 0 - 0
threads/__init__.py


+ 0 - 0
ui/__init__.py


+ 52 - 0
ui/about.py

@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'about.ui'
+#
+# Created by: PyQt5 UI code generator 5.5.1
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+class Ui_About(object):
+    def setupUi(self, About):
+        About.setObjectName("About")
+        About.resize(330, 160)
+        About.setMinimumSize(QtCore.QSize(330, 160))
+        About.setMaximumSize(QtCore.QSize(330, 160))
+        icon = QtGui.QIcon()
+        icon.addPixmap(QtGui.QPixmap("resources/icon.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        About.setWindowIcon(icon)
+        About.setAccessibleName("")
+        self.verticalLayout = QtWidgets.QVBoxLayout(About)
+        self.verticalLayout.setObjectName("verticalLayout")
+        self.gridLayout = QtWidgets.QGridLayout()
+        self.gridLayout.setObjectName("gridLayout")
+        self.lblIcon = QtWidgets.QLabel(About)
+        self.lblIcon.setText("")
+        self.lblIcon.setPixmap(QtGui.QPixmap("resources/icon.ico"))
+        self.lblIcon.setObjectName("lblIcon")
+        self.gridLayout.addWidget(self.lblIcon, 0, 0, 1, 1)
+        self.lblAbout = QtWidgets.QLabel(About)
+        self.lblAbout.setWordWrap(True)
+        self.lblAbout.setObjectName("lblAbout")
+        self.gridLayout.addWidget(self.lblAbout, 0, 1, 1, 1)
+        self.verticalLayout.addLayout(self.gridLayout)
+        spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+        self.verticalLayout.addItem(spacerItem)
+        self.buttonBox = QtWidgets.QDialogButtonBox(About)
+        self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
+        self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Close)
+        self.buttonBox.setObjectName("buttonBox")
+        self.verticalLayout.addWidget(self.buttonBox)
+
+        self.retranslateUi(About)
+        self.buttonBox.accepted.connect(About.accept)
+        self.buttonBox.rejected.connect(About.reject)
+        QtCore.QMetaObject.connectSlotsByName(About)
+
+    def retranslateUi(self, About):
+        _translate = QtCore.QCoreApplication.translate
+        About.setWindowTitle(_translate("About", "About"))
+        self.lblAbout.setText(_translate("About", "<html><head/><body><p><span style=\" font-weight:600;\">OrdersGuru Signer</span></p><p>Application for signing and arranging orders documents.</p><p>Created by <a href=\"http://jodlajodla.si/\"><span style=\" text-decoration: underline; color:#0000ff;\">Jan Sušnik</span></a>, 2017</p></body></html>"))
+

+ 172 - 0
ui/mainwindow.py

@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'mainwindow.ui'
+#
+# Created by: PyQt5 UI code generator 5.5.1
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+class Ui_MainWindow(object):
+    def setupUi(self, MainWindow):
+        MainWindow.setObjectName("MainWindow")
+        MainWindow.resize(450, 281)
+        MainWindow.setMinimumSize(QtCore.QSize(450, 281))
+        MainWindow.setMaximumSize(QtCore.QSize(450, 281))
+        icon = QtGui.QIcon()
+        icon.addPixmap(QtGui.QPixmap("resources/icon.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        MainWindow.setWindowIcon(icon)
+        MainWindow.setDockOptions(QtWidgets.QMainWindow.AllowTabbedDocks|QtWidgets.QMainWindow.AnimatedDocks)
+        self.centralwidget = QtWidgets.QWidget(MainWindow)
+        self.centralwidget.setObjectName("centralwidget")
+        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
+        self.verticalLayout.setObjectName("verticalLayout")
+        self.lblLogo = QtWidgets.QLabel(self.centralwidget)
+        self.lblLogo.setText("")
+        self.lblLogo.setPixmap(QtGui.QPixmap("resources/logo.png"))
+        self.lblLogo.setScaledContents(False)
+        self.lblLogo.setAlignment(QtCore.Qt.AlignCenter)
+        self.lblLogo.setObjectName("lblLogo")
+        self.verticalLayout.addWidget(self.lblLogo)
+        self.tabWidget = QtWidgets.QTabWidget(self.centralwidget)
+        self.tabWidget.setLayoutDirection(QtCore.Qt.LeftToRight)
+        self.tabWidget.setStyleSheet("")
+        self.tabWidget.setTabShape(QtWidgets.QTabWidget.Rounded)
+        self.tabWidget.setElideMode(QtCore.Qt.ElideNone)
+        self.tabWidget.setMovable(False)
+        self.tabWidget.setTabBarAutoHide(False)
+        self.tabWidget.setObjectName("tabWidget")
+        self.tabDirectory = QtWidgets.QWidget()
+        self.tabDirectory.setObjectName("tabDirectory")
+        self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.tabDirectory)
+        self.verticalLayout_5.setObjectName("verticalLayout_5")
+        self.gbDirectory = QtWidgets.QGroupBox(self.tabDirectory)
+        self.gbDirectory.setObjectName("gbDirectory")
+        self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.gbDirectory)
+        self.verticalLayout_4.setObjectName("verticalLayout_4")
+        self.glDirectory = QtWidgets.QGridLayout()
+        self.glDirectory.setHorizontalSpacing(12)
+        self.glDirectory.setObjectName("glDirectory")
+        self.leDirectory = QtWidgets.QLineEdit(self.gbDirectory)
+        self.leDirectory.setEnabled(True)
+        self.leDirectory.setEchoMode(QtWidgets.QLineEdit.Normal)
+        self.leDirectory.setDragEnabled(True)
+        self.leDirectory.setReadOnly(False)
+        self.leDirectory.setPlaceholderText("")
+        self.leDirectory.setObjectName("leDirectory")
+        self.glDirectory.addWidget(self.leDirectory, 1, 1, 1, 1)
+        self.lblDirectory = QtWidgets.QLabel(self.gbDirectory)
+        self.lblDirectory.setObjectName("lblDirectory")
+        self.glDirectory.addWidget(self.lblDirectory, 1, 0, 1, 1)
+        self.btnDirectoryBrowse = QtWidgets.QPushButton(self.gbDirectory)
+        self.btnDirectoryBrowse.setAutoDefault(False)
+        self.btnDirectoryBrowse.setDefault(True)
+        self.btnDirectoryBrowse.setObjectName("btnDirectoryBrowse")
+        self.glDirectory.addWidget(self.btnDirectoryBrowse, 1, 2, 1, 1)
+        self.verticalLayout_4.addLayout(self.glDirectory)
+        self.verticalLayout_5.addWidget(self.gbDirectory)
+        self.btnDirectorySignArrange = QtWidgets.QPushButton(self.tabDirectory)
+        self.btnDirectorySignArrange.setEnabled(False)
+        self.btnDirectorySignArrange.setAutoDefault(False)
+        self.btnDirectorySignArrange.setDefault(False)
+        self.btnDirectorySignArrange.setFlat(False)
+        self.btnDirectorySignArrange.setObjectName("btnDirectorySignArrange")
+        self.verticalLayout_5.addWidget(self.btnDirectorySignArrange)
+        self.tabWidget.addTab(self.tabDirectory, "")
+        self.tabFile = QtWidgets.QWidget()
+        self.tabFile.setObjectName("tabFile")
+        self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.tabFile)
+        self.verticalLayout_6.setObjectName("verticalLayout_6")
+        self.gbFile = QtWidgets.QGroupBox(self.tabFile)
+        self.gbFile.setObjectName("gbFile")
+        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gbFile)
+        self.verticalLayout_3.setObjectName("verticalLayout_3")
+        self.glFile = QtWidgets.QGridLayout()
+        self.glFile.setHorizontalSpacing(12)
+        self.glFile.setObjectName("glFile")
+        self.leFile = QtWidgets.QLineEdit(self.gbFile)
+        self.leFile.setEnabled(True)
+        self.leFile.setEchoMode(QtWidgets.QLineEdit.Normal)
+        self.leFile.setDragEnabled(True)
+        self.leFile.setReadOnly(False)
+        self.leFile.setPlaceholderText("")
+        self.leFile.setObjectName("leFile")
+        self.glFile.addWidget(self.leFile, 1, 1, 1, 1)
+        self.lblFile = QtWidgets.QLabel(self.gbFile)
+        self.lblFile.setObjectName("lblFile")
+        self.glFile.addWidget(self.lblFile, 1, 0, 1, 1)
+        self.btnFileBrowse = QtWidgets.QPushButton(self.gbFile)
+        self.btnFileBrowse.setAutoDefault(False)
+        self.btnFileBrowse.setDefault(True)
+        self.btnFileBrowse.setObjectName("btnFileBrowse")
+        self.glFile.addWidget(self.btnFileBrowse, 1, 2, 1, 1)
+        self.verticalLayout_3.addLayout(self.glFile)
+        self.verticalLayout_6.addWidget(self.gbFile)
+        self.btnFileSign = QtWidgets.QPushButton(self.tabFile)
+        self.btnFileSign.setEnabled(False)
+        self.btnFileSign.setAutoDefault(False)
+        self.btnFileSign.setDefault(False)
+        self.btnFileSign.setFlat(False)
+        self.btnFileSign.setObjectName("btnFileSign")
+        self.verticalLayout_6.addWidget(self.btnFileSign)
+        self.tabWidget.addTab(self.tabFile, "")
+        self.verticalLayout.addWidget(self.tabWidget)
+        MainWindow.setCentralWidget(self.centralwidget)
+        self.menubar = QtWidgets.QMenuBar(MainWindow)
+        self.menubar.setGeometry(QtCore.QRect(0, 0, 450, 25))
+        self.menubar.setObjectName("menubar")
+        self.menuFile = QtWidgets.QMenu(self.menubar)
+        self.menuFile.setObjectName("menuFile")
+        self.menuHelp = QtWidgets.QMenu(self.menubar)
+        self.menuHelp.setObjectName("menuHelp")
+        MainWindow.setMenuBar(self.menubar)
+        self.statusbar = QtWidgets.QStatusBar(MainWindow)
+        self.statusbar.setSizeGripEnabled(False)
+        self.statusbar.setObjectName("statusbar")
+        MainWindow.setStatusBar(self.statusbar)
+        self.action_Quit = QtWidgets.QAction(MainWindow)
+        icon1 = QtGui.QIcon()
+        icon1.addPixmap(QtGui.QPixmap("resources/quit.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        self.action_Quit.setIcon(icon1)
+        self.action_Quit.setObjectName("action_Quit")
+        self.action_About = QtWidgets.QAction(MainWindow)
+        icon2 = QtGui.QIcon()
+        icon2.addPixmap(QtGui.QPixmap("resources/about.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        self.action_About.setIcon(icon2)
+        self.action_About.setObjectName("action_About")
+        self.action_Settings = QtWidgets.QAction(MainWindow)
+        icon3 = QtGui.QIcon()
+        icon3.addPixmap(QtGui.QPixmap("resources/settings.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        self.action_Settings.setIcon(icon3)
+        self.action_Settings.setObjectName("action_Settings")
+        self.menuFile.addAction(self.action_Settings)
+        self.menuFile.addSeparator()
+        self.menuFile.addAction(self.action_Quit)
+        self.menuHelp.addAction(self.action_About)
+        self.menubar.addAction(self.menuFile.menuAction())
+        self.menubar.addAction(self.menuHelp.menuAction())
+
+        self.retranslateUi(MainWindow)
+        self.tabWidget.setCurrentIndex(0)
+        QtCore.QMetaObject.connectSlotsByName(MainWindow)
+
+    def retranslateUi(self, MainWindow):
+        _translate = QtCore.QCoreApplication.translate
+        MainWindow.setWindowTitle(_translate("MainWindow", "OrdersGuru Signer"))
+        self.gbDirectory.setTitle(_translate("MainWindow", "Sign XML and arrange files in directory"))
+        self.lblDirectory.setText(_translate("MainWindow", "Directory:"))
+        self.btnDirectoryBrowse.setText(_translate("MainWindow", "Browse"))
+        self.btnDirectorySignArrange.setText(_translate("MainWindow", "Sign XML and arrange files"))
+        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabDirectory), _translate("MainWindow", "Directory"))
+        self.gbFile.setTitle(_translate("MainWindow", "Sign single XML file"))
+        self.lblFile.setText(_translate("MainWindow", "File:"))
+        self.btnFileBrowse.setText(_translate("MainWindow", "Browse"))
+        self.btnFileSign.setText(_translate("MainWindow", "Sign XML file"))
+        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabFile), _translate("MainWindow", "File"))
+        self.menuFile.setTitle(_translate("MainWindow", "&File"))
+        self.menuHelp.setTitle(_translate("MainWindow", "&Help"))
+        self.action_Quit.setText(_translate("MainWindow", "&Quit"))
+        self.action_About.setText(_translate("MainWindow", "&About"))
+        self.action_Settings.setText(_translate("MainWindow", "&Settings"))
+

+ 114 - 0
ui/settings.py

@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'settings.ui'
+#
+# Created by: PyQt5 UI code generator 5.5.1
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+class Ui_Settings(object):
+    def setupUi(self, Settings):
+        Settings.setObjectName("Settings")
+        Settings.resize(455, 350)
+        Settings.setMinimumSize(QtCore.QSize(455, 350))
+        Settings.setMaximumSize(QtCore.QSize(455, 350))
+        icon = QtGui.QIcon()
+        icon.addPixmap(QtGui.QPixmap("resources/icon.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+        Settings.setWindowIcon(icon)
+        self.verticalLayout = QtWidgets.QVBoxLayout(Settings)
+        self.verticalLayout.setObjectName("verticalLayout")
+        self.lblWelcomeErrorsHeader = QtWidgets.QLabel(Settings)
+        self.lblWelcomeErrorsHeader.setObjectName("lblWelcomeErrorsHeader")
+        self.verticalLayout.addWidget(self.lblWelcomeErrorsHeader)
+        self.lblWelcomeErrors = QtWidgets.QLabel(Settings)
+        self.lblWelcomeErrors.setEnabled(True)
+        self.lblWelcomeErrors.setWordWrap(True)
+        self.lblWelcomeErrors.setObjectName("lblWelcomeErrors")
+        self.verticalLayout.addWidget(self.lblWelcomeErrors)
+        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+        self.verticalLayout.addItem(spacerItem)
+        self.gbXMLValidation = QtWidgets.QGroupBox(Settings)
+        self.gbXMLValidation.setObjectName("gbXMLValidation")
+        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.gbXMLValidation)
+        self.verticalLayout_2.setObjectName("verticalLayout_2")
+        self.lblXMLValidation = QtWidgets.QLabel(self.gbXMLValidation)
+        self.lblXMLValidation.setWordWrap(True)
+        self.lblXMLValidation.setObjectName("lblXMLValidation")
+        self.verticalLayout_2.addWidget(self.lblXMLValidation)
+        self.glXMLValidation = QtWidgets.QGridLayout()
+        self.glXMLValidation.setObjectName("glXMLValidation")
+        self.lblXMLSchema = QtWidgets.QLabel(self.gbXMLValidation)
+        self.lblXMLSchema.setObjectName("lblXMLSchema")
+        self.glXMLValidation.addWidget(self.lblXMLSchema, 0, 0, 1, 1)
+        self.leXMLSchema = QtWidgets.QLineEdit(self.gbXMLValidation)
+        self.leXMLSchema.setReadOnly(True)
+        self.leXMLSchema.setObjectName("leXMLSchema")
+        self.glXMLValidation.addWidget(self.leXMLSchema, 0, 1, 1, 1)
+        self.btnXMLSchemaBrowse = QtWidgets.QPushButton(self.gbXMLValidation)
+        self.btnXMLSchemaBrowse.setObjectName("btnXMLSchemaBrowse")
+        self.glXMLValidation.addWidget(self.btnXMLSchemaBrowse, 0, 2, 1, 1)
+        self.verticalLayout_2.addLayout(self.glXMLValidation)
+        self.verticalLayout.addWidget(self.gbXMLValidation)
+        self.gbCertificate = QtWidgets.QGroupBox(Settings)
+        self.gbCertificate.setObjectName("gbCertificate")
+        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gbCertificate)
+        self.verticalLayout_3.setObjectName("verticalLayout_3")
+        self.lblCertificate = QtWidgets.QLabel(self.gbCertificate)
+        self.lblCertificate.setWordWrap(True)
+        self.lblCertificate.setObjectName("lblCertificate")
+        self.verticalLayout_3.addWidget(self.lblCertificate)
+        self.glCertificate = QtWidgets.QGridLayout()
+        self.glCertificate.setObjectName("glCertificate")
+        self.leCertificate = QtWidgets.QLineEdit(self.gbCertificate)
+        self.leCertificate.setReadOnly(True)
+        self.leCertificate.setObjectName("leCertificate")
+        self.glCertificate.addWidget(self.leCertificate, 0, 1, 1, 1)
+        self.lblCertificateFile = QtWidgets.QLabel(self.gbCertificate)
+        self.lblCertificateFile.setObjectName("lblCertificateFile")
+        self.glCertificate.addWidget(self.lblCertificateFile, 0, 0, 1, 1)
+        self.btnCertificateBrowse = QtWidgets.QPushButton(self.gbCertificate)
+        self.btnCertificateBrowse.setObjectName("btnCertificateBrowse")
+        self.glCertificate.addWidget(self.btnCertificateBrowse, 0, 2, 1, 1)
+        self.verticalLayout_3.addLayout(self.glCertificate)
+        self.verticalLayout.addWidget(self.gbCertificate)
+        spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+        self.verticalLayout.addItem(spacerItem1)
+        self.hlButtonBox = QtWidgets.QHBoxLayout()
+        self.hlButtonBox.setObjectName("hlButtonBox")
+        self.btnReset = QtWidgets.QPushButton(Settings)
+        self.btnReset.setEnabled(False)
+        self.btnReset.setObjectName("btnReset")
+        self.hlButtonBox.addWidget(self.btnReset)
+        spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+        self.hlButtonBox.addItem(spacerItem2)
+        self.btnCancel = QtWidgets.QPushButton(Settings)
+        self.btnCancel.setObjectName("btnCancel")
+        self.hlButtonBox.addWidget(self.btnCancel)
+        self.btnOk = QtWidgets.QPushButton(Settings)
+        self.btnOk.setEnabled(False)
+        self.btnOk.setObjectName("btnOk")
+        self.hlButtonBox.addWidget(self.btnOk)
+        self.verticalLayout.addLayout(self.hlButtonBox)
+
+        self.retranslateUi(Settings)
+        QtCore.QMetaObject.connectSlotsByName(Settings)
+
+    def retranslateUi(self, Settings):
+        _translate = QtCore.QCoreApplication.translate
+        Settings.setWindowTitle(_translate("Settings", "Settings"))
+        self.lblWelcomeErrorsHeader.setText(_translate("Settings", "<html><head/><body><p align=\"center\"><span style=\" font-weight:600;\">Welcome to OrdersGuru Signer!</span></p></body></html>"))
+        self.lblWelcomeErrors.setText(_translate("Settings", "<html><head/><body><p>In order to use this tool you must provide XML schema for validating files you will sign and certificate with which you\'ll sign XMLs.</p></body></html>"))
+        self.gbXMLValidation.setTitle(_translate("Settings", "XML Validation"))
+        self.lblXMLValidation.setText(_translate("Settings", "Provide XML schema used for validating all XML documents."))
+        self.lblXMLSchema.setText(_translate("Settings", "XML Schema:"))
+        self.btnXMLSchemaBrowse.setText(_translate("Settings", "Browse"))
+        self.gbCertificate.setTitle(_translate("Settings", "Certificate"))
+        self.lblCertificate.setText(_translate("Settings", "Provide certificate in PKCS 12 format, used for signing documents."))
+        self.lblCertificateFile.setText(_translate("Settings", "Certificate:"))
+        self.btnCertificateBrowse.setText(_translate("Settings", "Browse"))
+        self.btnReset.setText(_translate("Settings", "Reset"))
+        self.btnCancel.setText(_translate("Settings", "&Cancel"))
+        self.btnOk.setText(_translate("Settings", "&OK"))
+