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 @@
+
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 @@
+
diff --git a/flectra/addons/base/module/module.py b/flectra/addons/base/module/module.py
index 1ec0fc53..bbee93ce 100644
--- a/flectra/addons/base/module/module.py
+++ b/flectra/addons/base/module/module.py
@@ -11,6 +11,7 @@ import os
import shutil
import tempfile
import zipfile
+import datetime,json
import requests
@@ -28,6 +29,7 @@ from flectra.exceptions import AccessDenied, UserError
from flectra.tools.parse_version import parse_version
from flectra.tools.misc import topological_sort
from flectra.http import request
+from flectra.addons.web.models.crypt import *
_logger = logging.getLogger(__name__)
@@ -293,6 +295,7 @@ class Module(models.Model):
application = fields.Boolean('Application', readonly=True)
icon = fields.Char('Icon URL')
icon_image = fields.Binary(string='Icon', compute='_get_icon_image')
+ contract_certificate = fields.Char('Required Contract')
_sql_constraints = [
('name_uniq', 'UNIQUE (name)', 'The name of the module must be unique!'),
@@ -433,6 +436,20 @@ class Module(models.Model):
"- %s (%s)" % (module.shortdesc, labels[module.state])
for module in modules
]))
+ ir_config = self.env['ir.config_parameter'].sudo()
+ exp_date = ir_config.get_param('database.expiration_date')
+ reason = ir_config.get_param('database.expiration_reason')
+ set_param = ir_config.set_param
+
+ for mod in self:
+ if mod.contract_certificate and not (reason == 'contract_expire' and exp_date):
+ expire_date = datetime.datetime.now() + datetime.timedelta(days=15)
+ set_param('database.expiration_date', expire_date.replace(microsecond=0))
+ set_param('database.expiration_reason', 'contract_expire')
+ set_param('contract.validity',
+ base64.encodestring(
+ encrypt(json.dumps(str(expire_date.replace(microsecond=0))),
+ str(expire_date.replace(microsecond=0)))))
return dict(ACTION_DICT, name=_('Install'))
@@ -573,6 +590,13 @@ class Module(models.Model):
raise UserError(_("The `base` module cannot be uninstalled"))
deps = self.downstream_dependencies()
(self + deps).write({'state': 'to remove'})
+ modules = self.env['ir.module.module'].search([('contract_certificate', '!=', False), ('state', '=', 'installed')])
+ ir_config = self.env['ir.config_parameter'].sudo()
+ set_param = ir_config.set_param
+ if len(modules) <= 0:
+ set_param('database.expiration_date', False)
+ set_param('database.expiration_reason', False)
+ set_param('contract.validity', False)
return dict(ACTION_DICT, name=_('Uninstall'))
@assert_log_admin_access
@@ -697,6 +721,8 @@ class Module(models.Model):
updated_values['state'] = 'uninstalled'
if parse_version(terp.get('version', default_version)) > parse_version(mod.latest_version or default_version):
res[0] += 1
+ if terp.get('contract_certificate'):
+ mod.write({'contract_certificate': terp.get('contract_certificate') or False})
if updated_values:
mod.write(updated_values)
else:
diff --git a/flectra/addons/base/module/module_view.xml b/flectra/addons/base/module/module_view.xml
index c00c0a08..c0078d65 100644
--- a/flectra/addons/base/module/module_view.xml
+++ b/flectra/addons/base/module/module_view.xml
@@ -94,6 +94,7 @@
+
diff --git a/requirements.txt b/requirements.txt
index 0daa655e..b928badf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -45,3 +45,4 @@ xlrd==1.0.0
unittest2==1.1.0
numpy==1.14.3
pypiwin32 ; sys_platform == 'win32'
+pycrypto==2.6.1
\ No newline at end of file