diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py index 989321bd..4a632fef 100644 --- a/addons/base_setup/models/res_config_settings.py +++ b/addons/base_setup/models/res_config_settings.py @@ -2,8 +2,10 @@ # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. from flectra import api, fields, models, _ -import json - +import json, base64, datetime, logging +from flectra.exceptions import UserError +from flectra.addons.web.models.crypt import * +_logger = logging.getLogger(__name__) class ResConfigSettings(models.TransientModel): @@ -37,6 +39,8 @@ class ResConfigSettings(models.TransientModel): external_report_layout = fields.Selection(related="company_id.external_report_layout") send_statistics = fields.Boolean( "Send Statistics") + activator_key = fields.Binary('Upload Activation Key') + contract_id = fields.Char('Contract ID') @api.model def get_values(self): @@ -72,6 +76,8 @@ class ResConfigSettings(models.TransientModel): self.env['ir.config_parameter'].sudo().set_param( "base_setup.send_statistics", send_statistics) self.env.ref('base.res_partner_rule').write({'active': not self.company_share_partner}) + if self.activator_key: + self._check_authorization() @api.multi def open_company(self): @@ -123,3 +129,22 @@ class ResConfigSettings(models.TransientModel): 'view_id': template.id, 'target': 'new', } + + def _check_authorization(self): + if self.activator_key and self.contract_id: + try: + set_param = self.env['ir.config_parameter'].sudo().set_param + binary = json.loads(base64.decodestring(self.activator_key)).encode('ascii') + binary = base64.decodestring(binary) + enc = json.dumps(decrypt(binary, self.contract_id)) + if enc: + dt = datetime.datetime.strptime(json.loads(enc),'"%Y-%m-%d %H:%M:%S"') + set_param('database.expiration_date', dt) + set_param('contract.validity', + base64.encodestring( + encrypt(json.dumps(str(dt)), + str(dt)))) + except Exception: + _logger.info(_('Please double-check your Contract Key!'), exc_info=True) + raise UserError( + _('Authorization error!') + ' ' + _('Please double-check your Contract Key!')) diff --git a/addons/base_setup/views/res_config_settings_views.xml b/addons/base_setup/views/res_config_settings_views.xml index ba138535..1232fd6f 100644 --- a/addons/base_setup/views/res_config_settings_views.xml +++ b/addons/base_setup/views/res_config_settings_views.xml @@ -203,6 +203,19 @@ +
+
+
+

System Parameter

diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 642bcc82..a374f0bc 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -52,6 +52,7 @@ import requests from flectra.tools import config from flectra import release from flectra.http import root +from ..models.crypt import * _logger = logging.getLogger(__name__) @@ -69,6 +70,8 @@ env.filters["json"] = json.dumps BUNDLE_MAXAGE = 60 * 60 * 24 * 7 DBNAME_PATTERN = '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$' +FILENAME = 'licence' +EXT = 'key' #---------------------------------------------------------- # Flectra Web helpers @@ -1861,3 +1864,16 @@ class ReportController(http.Controller): @http.route(['/report/check_wkhtmltopdf'], type='json', auth="user") def check_wkhtmltopdf(self): return request.env['ir.actions.report'].get_wkhtmltopdf_state() + +class LicensingController(http.Controller): + @http.route('/flectra/licensing', type='http', auth="user") + def download(self, binary='', **kwargs): + filename = '%s.%s' % (FILENAME, EXT) + content = binary + return request.make_response( + content, + headers=[ + ('Content-Type', 'plain/text' or 'application/octet-stream'), + ('Content-Disposition', content_disposition(filename)) + ] + ) diff --git a/addons/web/models/crypt.py b/addons/web/models/crypt.py new file mode 100644 index 00000000..85da455c --- /dev/null +++ b/addons/web/models/crypt.py @@ -0,0 +1,40 @@ +import Crypto.Random +from Crypto.Cipher import AES +import hashlib + +# salt size in bytes +SALT_SIZE = 16 + +# number of iterations in the key generation +NUMBER_OF_ITERATIONS = 20 + +# the size multiple required for AES +AES_MULTIPLE = 16 + + +def generate_key(password, salt, iterations): + assert iterations > 0 + key = str.encode(password) + salt + for i in range(iterations): + key = hashlib.sha256(key).digest() + return key + + +def pad_text(text, multiple): + return (text) + (chr((multiple - (len(text) % multiple))) * ((multiple - (len(text) % multiple)))) + + +def unpad_text(padded_text): + return padded_text.decode('utf-8')[:-ord(padded_text.decode('utf-8')[-1])] + + +def encrypt(plaintext, contract_id): + salt = Crypto.Random.get_random_bytes(SALT_SIZE) + return salt + (AES.new((generate_key(contract_id, salt, NUMBER_OF_ITERATIONS)), AES.MODE_ECB).encrypt( + (pad_text(plaintext, AES_MULTIPLE)))) + + +def decrypt(ciphertext, contract_id): + salt = ciphertext[0:SALT_SIZE] + return unpad_text( + AES.new((generate_key(contract_id, salt, NUMBER_OF_ITERATIONS)), AES.MODE_ECB).decrypt(ciphertext[SALT_SIZE:])) diff --git a/addons/web/models/ir_http.py b/addons/web/models/ir_http.py index 94431f3d..893b24c6 100644 --- a/addons/web/models/ir_http.py +++ b/addons/web/models/ir_http.py @@ -2,9 +2,11 @@ # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. import json +import base64 -from flectra import models +from flectra import models,api from flectra.http import request +from .crypt import * import flectra @@ -22,6 +24,16 @@ class Http(models.AbstractModel): user = request.env.user display_switch_company_menu = user.has_group('base.group_multi_company') and len(user.company_ids) > 1 version_info = flectra.service.common.exp_version() + ir_module_module_ids = self.env['ir.module.module'].sudo().search( + [('contract_certificate', '!=', False), ('state', '=', 'installed')]) + IrConfig = request.env['ir.config_parameter'].sudo() + contracted_module_list, is_valid = None, False + if ir_module_module_ids: + contracted_module_list = str(self.get_contracted_modules(ir_module_module_ids=ir_module_module_ids)) + is_valid = self.check_validate_date(IrConfig) + else: + is_valid = True + return { "session_id": request.session.sid, "uid": request.session.uid, @@ -38,9 +50,35 @@ class Http(models.AbstractModel): "user_companies": {'current_company': (user.company_id.id, user.company_id.name), 'allowed_companies': [(comp.id, comp.name) for comp in user.company_ids]} if display_switch_company_menu else False, "currencies": self.get_currencies() if request.session.uid else {}, "web.base.url": self.env['ir.config_parameter'].sudo().get_param('web.base.url', default=''), + 'expiration_date' : IrConfig.get_param('database.expiration_date'), + 'expiration_reason': IrConfig.get_param('database.expiration_reason'), + 'contracted_module_list': contracted_module_list, + 'contract_validation':is_valid } def get_currencies(self): Currency = request.env['res.currency'] currencies = Currency.search([]).read(['symbol', 'position', 'decimal_places']) return { c['id']: {'symbol': c['symbol'], 'position': c['position'], 'digits': [69,c['decimal_places']]} for c in currencies} + + def get_contracted_modules(self, contract_key='', ir_module_module_ids=None): + if ir_module_module_ids: + contracted_module_list = ir_module_module_ids.mapped('name') + contracts = encrypt(json.dumps(contracted_module_list), contract_key) + return contracts + + @api.model + def contract_validate_file(self, contract_id): + ir_module_module_ids = self.env['ir.module.module'].sudo().search( + [('contract_certificate', '!=', False), ('state', '=', 'installed')]) + contracts = self.get_contracted_modules(contract_id,ir_module_module_ids) + return json.dumps(base64.encodestring(contracts).decode('ascii')) + + def check_validate_date(self, config): + exp_date = config.get_param('database.expiration_date') + validity = config.get_param('contract.validity') + try: + decrypt(base64.decodestring(str.encode(validity)), str(exp_date)) + except Exception: + return False + return True diff --git a/addons/web/static/src/js/backend_theme_customizer/DialogRegisterContract.js b/addons/web/static/src/js/backend_theme_customizer/DialogRegisterContract.js new file mode 100644 index 00000000..dd855bd9 --- /dev/null +++ b/addons/web/static/src/js/backend_theme_customizer/DialogRegisterContract.js @@ -0,0 +1,41 @@ +flectra.define('FlectraLicensing.DialogRegisterContract', function (require) { + "use strict"; + var Dialog = require('web.Dialog'); + var rpc = require('web.rpc'); + + return Dialog.extend({ + template: 'FlectraLicense.dialog_contract_registration', + init: function (parent) { + var options = { + title: 'Register Contract', + size: 'small', + buttons: [ + { + text: "save", + classes: 'btn-success', + click: _.bind(this.save, this) + }, + {text: "Cancel", classes: 'btn-danger', close: true} + ] + }; + this._super(parent, options); + }, + + save: function () { + var contract_id = this.$el.find('#contract_id').val(); + var self = this; + if (!contract_id) { + return; + } + rpc.query({ + model: 'ir.http', + method: 'contract_validate_file', + args: [contract_id] + }).done(function (bin) { + self.trigger('get_key', {'key': contract_id, 'binary': bin}); + self.close(); + }); + } + }) + +}); \ No newline at end of file diff --git a/addons/web/static/src/js/chrome/web_client.js b/addons/web/static/src/js/chrome/web_client.js index 22514ed7..bfa5b91d 100644 --- a/addons/web/static/src/js/chrome/web_client.js +++ b/addons/web/static/src/js/chrome/web_client.js @@ -11,6 +11,9 @@ var SystrayMenu = require('web.SystrayMenu'); var UserMenu = require('web.UserMenu'); var UserProfile = require('web.UserProfile'); var config = require('web.config'); +var rpc = require('web.rpc'); +var qweb = core.qweb; +var Dialog = require('FlectraLicensing.DialogRegisterContract'); return AbstractWebClient.extend({ events: { @@ -55,6 +58,9 @@ return AbstractWebClient.extend({ this.systray_menu.setElement(this.$el.parents().find('.oe_systray')); var systray_menu_loaded = this.systray_menu.start(); + if ((session.expiration_date && session.expiration_reason === 'contract_expire') || !session['contract_validation']) { + this.validate_days_of_contract(); + } // Start the menu once both systray and user menus are rendered // to prevent overflows while loading return $.when(systray_menu_loaded, user_menu_loaded).then(function() { @@ -198,6 +204,95 @@ return AbstractWebClient.extend({ this.menu.reflow(); } }, + validate_days_of_contract: function () { + var today = new moment(); + var dbexpiration_date = new moment(session.expiration_date); + var duration = moment.duration(dbexpiration_date.diff(today)); + var params = { + 'difference': Math.round(duration.asDays()), + 'reason': session.expiration_reason, + }; + this.show_contract_registration(params); + }, + + show_contract_registration: function (params) { + var self = this; + var bg_color = params.difference <= 10 ? '#e55e50' : '#f3be5d'; + var difference = params.difference || 0; + if (difference <= 15 || !session['contract_validation']) { + if (difference > 15){ + difference = 0; + bg_color = '#e55e50'; + } + var message = 'Register your contract, only ' + difference + ' days left'; + var $panel = $(qweb.render('FlectraLicense.contract_expire_panel', { + 'difference': params.difference, + 'message': message, + 'background': bg_color + })); + $('nav').after($panel); + if (difference <= 0) { + return self.contract_expired() + } + $panel.find('#register_contract').bind('click', self.register_contract); + } + }, + register_contract: function () { + var self = this; + var dialog = new Dialog(self).open(); + dialog.on('get_key', self, function (key) { + session.get_file({ + url: '/flectra/licensing', + data: { + 'binary': key['binary'] + } + }); + }); + }, + contract_expired: function () { + var self = this; + var $message = $('#expiration-message').parent(); + var $clone = $message.clone(); + $clone.find('#contract-message').text('Contract Expired !!!').addClass('contract-block'); + $clone.find('button.close').remove(); + $message.hide(); + $clone.find('div#register_contract').after( + $('
').append( + $('').text('Apply Key'))); + $clone.find('span#btn_register_contract').off('click').on('click', function () { + $.unblockUI(); + self.register_contract(); + }); + $clone.find('#register_contract,#apply_contract').addClass('contract-mrg10'); + $clone.find('span#btn_apply_key').off('click').on('click', function () { + $.unblockUI(); + rpc.query({ + model: 'ir.actions.act_window', + method: 'search_read', + domain: [['context', '=', "{'module' : 'general_settings'}"]] + }).done(function (res) { + if (!res) + window.location.reload(); + self.do_action(res[0]['id']).done(function () { + var $el = $('div[name=activator_key]'); + if ($el && $el[0]){ + $el[0].scrollIntoView({behavior: 'smooth', block: 'center'}); + $el.parents('.o_setting_box').animate({backgroundColor: "rgb(239, 234, 208)"}, 2000, function () { + $el.parents('.o_setting_box').animate({backgroundColor: ''}) + }); + } + }); + }); + }); + setTimeout(function () { + $.blockUI({ + message: $clone, + css: {cursor: 'auto'}, + overlayCSS: {cursor: 'auto'} + }); + self.contract_expired(); + }, 15000); + }, }); }); diff --git a/addons/web/static/src/less/webclient.less b/addons/web/static/src/less/webclient.less index ae708f62..d85295f4 100644 --- a/addons/web/static/src/less/webclient.less +++ b/addons/web/static/src/less/webclient.less @@ -134,3 +134,52 @@ div.o_boolean_toggle { .bg-info-full { background-color: @brand-info; } + +#expiration-message { + position: relative; + top: 0; + left: 0; + width: 100%; + text-align: center; + color: white; + font-size: 20px; + font-weight: bold; + + .alert { + padding: 10px !important; + border-radius: 0 !important; + } +} + +#inner-message { + margin: 0 auto; + #register_contract, #apply_contract { + background: #5dae7e; + cursor: pointer; + display: inline-block; + span { + margin: 10px; + } + } +} + +.contract-block { + display: block; +} + +.bg-animate { + background-color: rgb(253, 249, 240); +} + +.contract-mrg10 { + margin: 10px; +} + +.noselect { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; +} diff --git a/addons/web/static/src/xml/backend_theme.xml b/addons/web/static/src/xml/backend_theme.xml index a86f2b3c..81ac3564 100755 --- a/addons/web/static/src/xml/backend_theme.xml +++ b/addons/web/static/src/xml/backend_theme.xml @@ -102,4 +102,31 @@
  • + +
    +
    +
    +
    + + + +
    + Register +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + +
    +
    +
    + diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 5201e42d..ffc1a5c0 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -269,6 +269,7 @@