diff --git a/plugin/add_cas_login_button/README.md b/plugin/add_cas_login_button/README.md
new file mode 100644
index 000000000..52071b311
--- /dev/null
+++ b/plugin/add_cas_login_button/README.md
@@ -0,0 +1,11 @@
+Add CAS login button plugin
+======
+This plugin adds a button to allow users to login to Chamilo through CAS authentication.
+
+In order for the plugin to work, you'll have to:
+
+* enable your CAS connection to display this button
+* configure your CAS connection to have the button work
+* go to Administration > Configuration settings > CAS and follow the instructions
+
+This plugin has been made to be added in the login_top region, but you can put it wherever you want.
\ No newline at end of file
diff --git a/plugin/add_cas_login_button/css.css b/plugin/add_cas_login_button/css.css
new file mode 100644
index 000000000..60689dd4b
--- /dev/null
+++ b/plugin/add_cas_login_button/css.css
@@ -0,0 +1,12 @@
+.cas_plugin_image {
+ float:left;
+ height:50px;
+ margin: 0px 5px 5px 0px;
+}
+.cas_plugin_comm {
+ font-style:italic;
+}
+.cas_plugin_clear {
+ clear:both;
+ height:1px;
+}
diff --git a/plugin/add_cas_login_button/index.php b/plugin/add_cas_login_button/index.php
new file mode 100644
index 000000000..dc10a6432
--- /dev/null
+++ b/plugin/add_cas_login_button/index.php
@@ -0,0 +1,29 @@
+Plugins).
+ *
+ * @package chamilo.plugin
+ *
+ * @author Julio Montoya
+ */
+/**
+ * Plugin details (must be present).
+ */
+
+//the plugin title
+$plugin_info['title'] = 'Add a button to login using CAS';
+
+//the comments that go with the plugin
+$plugin_info['comment'] = "If CAS is activated, this plugin add a text and a button on the login page to login with CAS. Configure plugin to add title, comment and logo.";
+//the plugin version
+$plugin_info['version'] = '1.0';
+//the plugin author
+$plugin_info['author'] = 'Hubert Borderiou';
+
+//the plugin configuration
+$form = new FormValidator('add_cas_button_form');
+$form->addElement('text', 'cas_button_label', 'CAS connexion title', '');
+$form->addElement('text', 'cas_button_comment', 'CAS connexion description', '');
+$form->addElement('text', 'cas_image_url', 'Logo URL if any (image, 50px height)');
+$form->addButtonSave(get_lang('Save'), 'submit_button');
+//get default value for form
+$tab_default_add_cas_login_button_cas_button_label = api_get_setting('add_cas_login_button_cas_button_label');
+$tab_default_add_cas_login_button_cas_button_comment = api_get_setting('add_cas_login_button_cas_button_comment');
+$tab_default_add_cas_login_button_cas_image_url = api_get_setting('add_cas_login_button_cas_image_url');
+$defaults = [];
+if ($tab_default_add_cas_login_button_cas_button_label) {
+ $defaults['cas_button_label'] = $tab_default_add_cas_login_button_cas_button_label['add_cas_login_button'];
+}
+if ($tab_default_add_cas_login_button_cas_button_comment) {
+ $defaults['cas_button_comment'] = $tab_default_add_cas_login_button_cas_button_comment['add_cas_login_button'];
+}
+if ($tab_default_add_cas_login_button_cas_image_url) {
+ $defaults['cas_image_url'] = $tab_default_add_cas_login_button_cas_image_url['add_cas_login_button'];
+}
+$form->setDefaults($defaults);
+//display form
+$plugin_info['settings_form'] = $form;
+
+//set the templates that are going to be used
+$plugin_info['templates'] = ['template.tpl'];
diff --git a/plugin/add_cas_login_button/template.tpl b/plugin/add_cas_login_button/template.tpl
new file mode 100644
index 000000000..45c784b8f
--- /dev/null
+++ b/plugin/add_cas_login_button/template.tpl
@@ -0,0 +1,22 @@
+{% if add_cas_login_button.show_message %}
+
+
+ {% if add_cas_login_button.cas_activated %}
+ {% if add_cas_login_button.cas_configured %}
+ {{ add_cas_login_button.form }}
+ {% else %}
+ CAS isn't configured. Go to Admin > Configuration > CAS.
+ {% endif %}
+ {% else %}
+ CAS isn't activated. Go to Admin > Configuration > CAS.
+ {% endif %}
+
+{% endif %}
diff --git a/plugin/add_cas_logout_button/README.md b/plugin/add_cas_logout_button/README.md
new file mode 100644
index 000000000..c5ca74393
--- /dev/null
+++ b/plugin/add_cas_logout_button/README.md
@@ -0,0 +1,12 @@
+Add CAS logout button plugin
+===
+
+This plugin adds a button to allow users to logout from their CAS session.
+
+In order for the plugin to work, you'll have to:
+
+* enable your CAS connection to display this button.
+* configure your CAS connection to have the button works.
+* go to Administration > Configuration settings > CAS and follow the instructions.
+
+This plugin has been made to be added in the login_top region, but you can put it wherever you want.
\ No newline at end of file
diff --git a/plugin/add_cas_logout_button/css.css b/plugin/add_cas_logout_button/css.css
new file mode 100644
index 000000000..60689dd4b
--- /dev/null
+++ b/plugin/add_cas_logout_button/css.css
@@ -0,0 +1,12 @@
+.cas_plugin_image {
+ float:left;
+ height:50px;
+ margin: 0px 5px 5px 0px;
+}
+.cas_plugin_comm {
+ font-style:italic;
+}
+.cas_plugin_clear {
+ clear:both;
+ height:1px;
+}
diff --git a/plugin/add_cas_logout_button/index.php b/plugin/add_cas_logout_button/index.php
new file mode 100644
index 000000000..668f60907
--- /dev/null
+++ b/plugin/add_cas_logout_button/index.php
@@ -0,0 +1,26 @@
+Plugins).
+ *
+ * @package chamilo.plugin
+ *
+ * @author Julio Montoya
+ */
+/**
+ * Plugin details (must be present).
+ */
+
+//the plugin title
+$plugin_info['title'] = 'Add a button to logout from CAS';
+
+//the comments that go with the plugin
+$plugin_info['comment'] = "If CAS is activated, this plugin add a text and a button on the user page to logout from a CAS session. Configure plugin to add title, comment and logo.";
+//the plugin version
+$plugin_info['version'] = '1.0';
+//the plugin author
+$plugin_info['author'] = 'Hubert Borderiou';
+//the plugin configuration
+$form = new FormValidator('add_cas_button_form');
+$form->addElement('text', 'cas_logout_label', 'CAS logout title', '');
+$form->addElement('text', 'cas_logout_comment', 'CAS logout description', '');
+$form->addElement('text', 'cas_logout_image_url', 'Logo URL if any (image, 50px height)');
+$form->addButtonSave(get_lang('Save'), 'submit_button');
+//get default value for form
+$defaults = [];
+$tab_default_add_cas_logout_button_cas_logout_label = api_get_setting('add_cas_logout_button_cas_logout_label');
+$tab_default_add_cas_logout_button_cas_logout_comment = api_get_setting('add_cas_logout_button_cas_logout_comment');
+$tab_default_add_cas_logout_button_cas_logout_image_url = api_get_setting('add_cas_logout_button_cas_logout_image_url');
+if ($tab_default_add_cas_logout_button_cas_logout_label) {
+ $defaults['cas_logout_label'] = $tab_default_add_cas_logout_button_cas_logout_label['add_cas_logout_button'];
+}
+if ($tab_default_add_cas_logout_button_cas_logout_comment) {
+ $defaults['cas_logout_comment'] = $tab_default_add_cas_logout_button_cas_logout_comment['add_cas_logout_button'];
+}
+
+if ($tab_default_add_cas_logout_button_cas_logout_image_url) {
+ $defaults['cas_logout_image_url'] = $tab_default_add_cas_logout_button_cas_logout_image_url['add_cas_logout_button'];
+}
+
+$form->setDefaults($defaults);
+//display form
+$plugin_info['settings_form'] = $form;
+
+// Set the templates that are going to be used
+$plugin_info['templates'] = ['template.tpl'];
diff --git a/plugin/add_cas_logout_button/template.tpl b/plugin/add_cas_logout_button/template.tpl
new file mode 100644
index 000000000..44ad94991
--- /dev/null
+++ b/plugin/add_cas_logout_button/template.tpl
@@ -0,0 +1,15 @@
+{% if add_cas_logout_button.show_message %}
+
+
+ {% if add_cas_logout_button.logout_image_url %}
+
+ {% endif %}
+
{{add_cas_logout_button.logout_comment}}
+ {{ add_cas_logout_button.form }}
+
+
+{% endif %}
diff --git a/plugin/add_facebook_login_button/README.md b/plugin/add_facebook_login_button/README.md
new file mode 100644
index 000000000..dfd998095
--- /dev/null
+++ b/plugin/add_facebook_login_button/README.md
@@ -0,0 +1,16 @@
+Add Facebook login button plugin
+===
+
+This plugin adds a button to allow users to log into Chamilo through their Facebook account.
+
+To display this button on your portal, you have to:
+
+* enable the Facebook authentication setting and configure it
+* enable and configure Facebook authentication on your Chamilo platform: go to Administration > Configuration settings > Facebook
+* add the App ID and the Secret Key provided by Facebook inside the app/config/auth.conf.php file
+* set the following line in your app/config/configuration.php
+```
+$_configuration['facebook_auth'] = 1;
+```
+
+This plugin has been developed to be added to the login_top or login_bottom region in Chamilo, but you can put it in whichever region you want.
diff --git a/plugin/add_facebook_login_button/css.css b/plugin/add_facebook_login_button/css.css
new file mode 100644
index 000000000..945a00d0c
--- /dev/null
+++ b/plugin/add_facebook_login_button/css.css
@@ -0,0 +1,12 @@
+ .cas_plugin_image {
+ float:left;
+ height:50px;
+ margin: 0px 5px 5px 0px;
+ }
+ .cas_plugin_comm {
+ font-style:italic;
+ }
+ .cas_plugin_clear {
+ clear:both;
+ height:1px;
+ }
diff --git a/plugin/add_facebook_login_button/index.php b/plugin/add_facebook_login_button/index.php
new file mode 100644
index 000000000..ca8d52fce
--- /dev/null
+++ b/plugin/add_facebook_login_button/index.php
@@ -0,0 +1,17 @@
+
+ */
+/**
+ * Plugin details.
+ */
+
+//the plugin title
+$plugin_info['title'] = 'Add a button to login using a FACEBOOK account';
+
+//the comments that go with the plugin
+$plugin_info['comment'] = "If Facebook authentication is enabled, this plugin adds a button Facebook Connexion on the login page. Configure the plugin to add a title, a comment and a logo. Should be placed in login_top region";
+//the plugin version
+$plugin_info['version'] = '1.0';
+//the plugin author
+$plugin_info['author'] = 'Konrad Banasiak, Hubert Borderiou';
+//the plugin configuration
+$form = new FormValidator('add_facebook_button_form');
+$form->addElement(
+ 'text',
+ 'facebook_button_url',
+ 'Facebook connexion image URL',
+ ''
+);
+$form->addButtonSave(get_lang('Save'), 'submit_button');
+//get default value for form
+$tab_default_add_facebook_login_button_facebook_button_url = api_get_setting(
+ 'add_facebook_login_button_facebook_button_url'
+);
+
+$defaults = [];
+
+if ($tab_default_add_facebook_login_button_facebook_button_url) {
+ $defaults['facebook_button_url'] = $tab_default_add_facebook_login_button_facebook_button_url['add_facebook_login_button'];
+}
+
+$form->setDefaults($defaults);
+//display form
+$plugin_info['settings_form'] = $form;
+
+// Set the templates that are going to be used
+$plugin_info['templates'] = ['template.tpl'];
diff --git a/plugin/add_facebook_login_button/template.tpl b/plugin/add_facebook_login_button/template.tpl
new file mode 100644
index 000000000..f9165173d
--- /dev/null
+++ b/plugin/add_facebook_login_button/template.tpl
@@ -0,0 +1,14 @@
+{% if add_facebook_login_button.show_message %}
+
+ {% if add_facebook_login_button.facebook_button_url %}
+
+
+
+ {% else %}
+
+ {{ 'FacebookMainActivateTitle'|get_lang }}
+
+ {% endif %}
+
+{% endif %}
diff --git a/plugin/add_shibboleth_login_button/README.md b/plugin/add_shibboleth_login_button/README.md
new file mode 100644
index 000000000..ef8899b24
--- /dev/null
+++ b/plugin/add_shibboleth_login_button/README.md
@@ -0,0 +1,10 @@
+Add Shibboleth login button plugin
+===
+
+This plugin adds a button to allow users to login to Chamilo through Shibboleth authentication.
+
+You have to configure your Shibboleth connexion before using this plugin.
+
+To activate and configure Shibboleth for your Chamilo platform, go to Administration > Configuration settings > Shibboleth
+
+This plugin has been created to be added in the login_top region, but you can put it wherever you want.
\ No newline at end of file
diff --git a/plugin/add_shibboleth_login_button/css.css b/plugin/add_shibboleth_login_button/css.css
new file mode 100644
index 000000000..a79de810c
--- /dev/null
+++ b/plugin/add_shibboleth_login_button/css.css
@@ -0,0 +1,12 @@
+.shibboleth_plugin_image {
+ float:left;
+ height:50px;
+ margin: 0px 5px 5px 0px;
+}
+.shibboleth_plugin_comm {
+ font-style:italic;
+}
+.shibboleth_plugin_clear {
+ clear:both;
+ height:1px;
+}
diff --git a/plugin/add_shibboleth_login_button/index.php b/plugin/add_shibboleth_login_button/index.php
new file mode 100644
index 000000000..8b2d719dd
--- /dev/null
+++ b/plugin/add_shibboleth_login_button/index.php
@@ -0,0 +1,22 @@
+Plugins).
+ *
+ * @package chamilo.plugin
+ *
+ * @author Julio Montoya
+ */
+/**
+ * Plugin details (must be present).
+ */
+
+//the plugin title
+$plugin_info['title'] = 'Add a button to login using Shibboleth';
+
+//the comments that go with the plugin
+$plugin_info['comment'] = "If Shibboleth is configured, this plugin add a text and a button on the login page to login with Shibboleth. Configure plugin to add title, comment and logo.";
+//the plugin version
+$plugin_info['version'] = '1.0';
+//the plugin author
+$plugin_info['author'] = 'Hubert Borderiou';
+
+//the plugin configuration
+$form = new FormValidator('add_shibboleth_button_form');
+$form->addElement(
+ 'text',
+ 'shibboleth_button_label',
+ 'shibboleth connexion title',
+ ''
+);
+$form->addElement(
+ 'text',
+ 'shibboleth_button_comment',
+ 'shibboleth connexion description',
+ ''
+);
+$form->addElement(
+ 'text',
+ 'shibboleth_image_url',
+ 'Logo URL if any (image, 50px height)'
+);
+$form->addButtonSave(get_lang('Save'), 'submit_button');
+//get default value for form
+$tab_default_add_shibboleth_login_button_shibboleth_button_label = api_get_setting(
+ 'add_shibboleth_login_button_shibboleth_button_label'
+);
+$tab_default_add_shibboleth_login_button_shibboleth_button_comment = api_get_setting(
+ 'add_shibboleth_login_button_shibboleth_button_comment'
+);
+$tab_default_add_shibboleth_login_button_shibboleth_image_url = api_get_setting(
+ 'add_shibboleth_login_button_shibboleth_image_url'
+);
+$defaults = [];
+if ($tab_default_add_shibboleth_login_button_shibboleth_button_label) {
+ $defaults['shibboleth_button_label'] = $tab_default_add_shibboleth_login_button_shibboleth_button_label['add_shibboleth_login_button'];
+}
+
+if ($tab_default_add_shibboleth_login_button_shibboleth_button_comment) {
+ $defaults['shibboleth_button_comment'] = $tab_default_add_shibboleth_login_button_shibboleth_button_comment['add_shibboleth_login_button'];
+}
+
+if ($tab_default_add_shibboleth_login_button_shibboleth_image_url) {
+ $defaults['shibboleth_image_url'] = $tab_default_add_shibboleth_login_button_shibboleth_image_url['add_shibboleth_login_button'];
+}
+
+$form->setDefaults($defaults);
+//display form
+$plugin_info['settings_form'] = $form;
+
+//set the templates that are going to be used
+$plugin_info['templates'] = ['template.tpl'];
diff --git a/plugin/add_shibboleth_login_button/template.tpl b/plugin/add_shibboleth_login_button/template.tpl
new file mode 100644
index 000000000..e42d26d6a
--- /dev/null
+++ b/plugin/add_shibboleth_login_button/template.tpl
@@ -0,0 +1,15 @@
+
+{% if add_shibboleth_login_button.show_message %}
+
+
+ {% if add_shibboleth_login_button.url_label %}
+
+ {% endif %}
+
{{ add_shibboleth_login_button.comm_label }}
+
+
+{% endif %}
diff --git a/plugin/advanced_subscription/README.md b/plugin/advanced_subscription/README.md
new file mode 100644
index 000000000..0036e183d
--- /dev/null
+++ b/plugin/advanced_subscription/README.md
@@ -0,0 +1,59 @@
+Advanced subscription plugin for Chamilo LMS
+=======================================
+Plugin to manage the registration queue and communication to sessions
+from an external website creating a queue to control session subscription
+and sending emails to approve student subscription requests
+
+# Requirements
+Chamilo LMS 1.10.0 or greater
+
+# Settings
+
+These settings have to be configured in the Configuration screen for the plugin
+
+Parameters | Description
+------------- |-------------
+Webservice url | Url to external website to get user profile (SOAP)
+Induction requirement | Checkbox to enable induction as requirement
+Courses count limit | Number of times a student is allowed at most to course by year
+Yearly hours limit | Teaching hours a student is allowed at most to course by year
+Yearly cost unit converter | The cost of a taxation unit value (TUV)
+Yearly cost limit | Number of TUV student courses is allowed at most to cost by year
+Year start date | Date (dd/mm) when the year limit is renewed
+Minimum percentage profile | Minimum percentage required from external website profile
+
+# Hooks
+
+This plugin uses the following hooks (defined since Chamilo LMS 1.10.0):
+
+* HookAdminBlock
+* HookWSRegistration
+* HookNotificationContent
+* HookNotificationTitle
+
+
+# Web services
+
+This plugin also enables new webservices that can be used from registration.soap.php
+
+* HookAdvancedSubscription..WSSessionListInCategory
+* HookAdvancedSubscription..WSSessionGetDetailsByUser
+* HookAdvancedSubscription..WSListSessionsDetailsByCategory
+
+See `/plugin/advanced_subscription/src/HookAdvancedSubscription.php` to check Web services inputs and outputs
+
+# How does this plugin works?
+
+After install, fill the required parameters (described above)
+Use web services to communicate course session inscription from external website
+This allows students to search course sessions and subscribe if they match
+the requirements.
+
+The normal process is:
+* Student searches course session
+* Student reads session info depending student data
+* Student requests to be subscribed
+* A confirmation email is sent to student
+* An authorization email is sent to student's superior (STUDENT BOSS role) or admins (when there is no superior) who will accept or reject the student request
+* When the superior accepts or rejects, an email will be sent to the student and superior (or admin), respectively
+* To complete the subscription, the request must be validated and accepted by an admin
\ No newline at end of file
diff --git a/plugin/advanced_subscription/ajax/advanced_subscription.ajax.php b/plugin/advanced_subscription/ajax/advanced_subscription.ajax.php
new file mode 100644
index 000000000..3af22fee3
--- /dev/null
+++ b/plugin/advanced_subscription/ajax/advanced_subscription.ajax.php
@@ -0,0 +1,350 @@
+
+ *
+ * @package chamilo.plugin.advanced_subscription
+ */
+/**
+ * Init.
+ */
+require_once __DIR__.'/../config.php';
+
+$plugin = AdvancedSubscriptionPlugin::create();
+// Get validation hash
+$hash = Security::remove_XSS($_REQUEST['v']);
+// Get data from request (GET or POST)
+$data['action'] = Security::remove_XSS($_REQUEST['a']);
+$data['sessionId'] = intval($_REQUEST['s']);
+$data['currentUserId'] = intval($_REQUEST['current_user_id']);
+$data['studentUserId'] = intval($_REQUEST['u']);
+$data['queueId'] = intval($_REQUEST['q']);
+$data['newStatus'] = intval($_REQUEST['e']);
+// Student always is connected
+// $data['is_connected'] = isset($_REQUEST['is_connected']) ? boolval($_REQUEST['is_connected']) : false;
+$data['is_connected'] = true;
+$data['profile_completed'] = isset($_REQUEST['profile_completed']) ? floatval($_REQUEST['profile_completed']) : 0;
+$data['accept_terms'] = isset($_REQUEST['accept_terms']) ? intval($_REQUEST['accept_terms']) : 0;
+$data['courseId'] = isset($_REQUEST['c']) ? intval($_REQUEST['c']) : 0;
+// Init result array
+$result = ['error' => true, 'errorMessage' => get_lang('ThereWasAnError')];
+$showJSON = true;
+// Check if data is valid or is for start subscription
+$verified = $plugin->checkHash($data, $hash) || $data['action'] == 'subscribe';
+if ($verified) {
+ switch ($data['action']) {
+ case 'check': // Check minimum requirements
+ try {
+ $res = AdvancedSubscriptionPlugin::create()->isAllowedToDoRequest($data['studentUserId'], $data);
+ if ($res) {
+ $result['error'] = false;
+ $result['errorMessage'] = 'No error';
+ $result['pass'] = true;
+ } else {
+ $result['errorMessage'] = 'User can not be subscribed';
+ $result['pass'] = false;
+ }
+ } catch (\Exception $e) {
+ $result['errorMessage'] = $e->getMessage();
+ }
+ break;
+ case 'subscribe': // Subscription
+ // Start subscription to queue
+ $res = AdvancedSubscriptionPlugin::create()->startSubscription(
+ $data['studentUserId'],
+ $data['sessionId'],
+ $data
+ );
+ // Check if queue subscription was successful
+ if ($res === true) {
+ $legalEnabled = api_get_plugin_setting('courselegal', 'tool_enable');
+ if ($legalEnabled) {
+ // Save terms confirmation
+ CourseLegalPlugin::create()->saveUserLegal(
+ $data['studentUserId'],
+ $data['courseId'],
+ $data['sessionId'],
+ false
+ );
+ }
+ // Prepare data
+ // Get session data
+ // Assign variables
+ $fieldsArray = [
+ 'description',
+ 'target',
+ 'mode',
+ 'publication_end_date',
+ 'recommended_number_of_participants',
+ ];
+ $sessionArray = api_get_session_info($data['sessionId']);
+ $extraSession = new ExtraFieldValue('session');
+ $extraField = new ExtraField('session');
+ // Get session fields
+ $fieldList = $extraField->get_all([
+ 'variable IN ( ?, ?, ?, ?, ?)' => $fieldsArray,
+ ]);
+ // Index session fields
+ foreach ($fieldList as $field) {
+ $fields[$field['id']] = $field['variable'];
+ }
+
+ $mergedArray = array_merge([$data['sessionId']], array_keys($fields));
+ $sessionFieldValueList = $extraSession->get_all(
+ [
+ 'item_id = ? field_id IN ( ?, ?, ?, ?, ?, ?, ? )' => $mergedArray,
+ ]
+ );
+ foreach ($sessionFieldValueList as $sessionFieldValue) {
+ // Check if session field value is set in session field list
+ if (isset($fields[$sessionFieldValue['field_id']])) {
+ $var = $fields[$sessionFieldValue['field_id']];
+ $val = $sessionFieldValue['value'];
+ // Assign session field value to session
+ $sessionArray[$var] = $val;
+ }
+ }
+ // Get student data
+ $studentArray = api_get_user_info($data['studentUserId']);
+ $studentArray['picture'] = $studentArray['avatar'];
+
+ // Get superior data if exist
+ $superiorId = UserManager::getFirstStudentBoss($data['studentUserId']);
+ if (!empty($superiorId)) {
+ $superiorArray = api_get_user_info($superiorId);
+ } else {
+ $superiorArray = null;
+ }
+ // Get admin data
+ $adminsArray = UserManager::get_all_administrators();
+ $isWesternNameOrder = api_is_western_name_order();
+ foreach ($adminsArray as &$admin) {
+ $admin['complete_name'] = $isWesternNameOrder ?
+ $admin['firstname'].', '.$admin['lastname'] : $admin['lastname'].', '.$admin['firstname']
+ ;
+ }
+ unset($admin);
+ // Set data
+ $data['action'] = 'confirm';
+ $data['student'] = $studentArray;
+ $data['superior'] = $superiorArray;
+ $data['admins'] = $adminsArray;
+ $data['session'] = $sessionArray;
+ $data['signature'] = api_get_setting('Institution');
+
+ // Check if student boss exists
+ if (empty($superiorId)) {
+ // Student boss does not exist
+ // Update status to accepted by boss
+ $res = $plugin->updateQueueStatus($data, ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_APPROVED);
+ if (!empty($res)) {
+ // Prepare admin url
+ $data['admin_view_url'] = api_get_path(WEB_PLUGIN_PATH).
+ 'advanced_subscription/src/admin_view.php?s='.$data['sessionId'];
+ // Send mails
+ $result['mailIds'] = $plugin->sendMail(
+ $data,
+ ADVANCED_SUBSCRIPTION_ACTION_STUDENT_REQUEST_NO_BOSS
+ );
+ // Check if mails were sent
+ if (!empty($result['mailIds'])) {
+ $result['error'] = false;
+ $result['errorMessage'] = 'No error';
+ $result['pass'] = true;
+ // Check if exist an email to render
+ if (isset($result['mailIds']['render'])) {
+ // Render mail
+ $url = $plugin->getRenderMailUrl(['queueId' => $result['mailIds']['render']]);
+ header('Location: '.$url);
+ exit;
+ }
+ }
+ }
+ } else {
+ // Student boss does exist
+ // Get url to be accepted by boss
+ $data['newStatus'] = ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_APPROVED;
+ $data['student']['acceptUrl'] = $plugin->getQueueUrl($data);
+ // Get url to be rejected by boss
+ $data['newStatus'] = ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_DISAPPROVED;
+ $data['student']['rejectUrl'] = $plugin->getQueueUrl($data);
+ // Send mails
+ $result['mailIds'] = $plugin->sendMail($data, ADVANCED_SUBSCRIPTION_ACTION_STUDENT_REQUEST);
+ // Check if mails were sent
+ if (!empty($result['mailIds'])) {
+ $result['error'] = false;
+ $result['errorMessage'] = 'No error';
+ $result['pass'] = true;
+ // Check if exist an email to render
+ if (isset($result['mailIds']['render'])) {
+ // Render mail
+ $url = $plugin->getRenderMailUrl(['queueId' => $result['mailIds']['render']]);
+ header('Location: '.$url);
+ exit;
+ }
+ }
+ }
+ } else {
+ $lastMessageId = $plugin->getLastMessageId($data['studentUserId'], $data['sessionId']);
+ if ($lastMessageId !== false) {
+ // Render mail
+ $url = $plugin->getRenderMailUrl(['queueId' => $lastMessageId]);
+ header('Location: '.$url);
+ exit;
+ } else {
+ if (is_string($res)) {
+ $result['errorMessage'] = $res;
+ } else {
+ $result['errorMessage'] = 'User can not be subscribed';
+ }
+ $result['pass'] = false;
+ $url = $plugin->getTermsUrl($data, ADVANCED_SUBSCRIPTION_TERMS_MODE_FINAL);
+ header('Location: '.$url);
+ exit;
+ }
+ }
+
+ break;
+ case 'confirm':
+ // Check if new status is set
+ if (isset($data['newStatus'])) {
+ if ($data['newStatus'] === ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED) {
+ try {
+ $isAllowToDoRequest = $plugin->isAllowedToDoRequest($data['studentUserId'], $data);
+ } catch (Exception $ex) {
+ $messageTemplate = new Template(null, false, false);
+ $messageTemplate->assign(
+ 'content',
+ Display::return_message($ex->getMessage(), 'error', false)
+ );
+ $messageTemplate->display_no_layout_template();
+ $showJSON = false;
+ break;
+ }
+ }
+
+ // Update queue status
+ $res = $plugin->updateQueueStatus($data, $data['newStatus']);
+ if ($res === true) {
+ // Prepare data
+ // Prepare session data
+ $fieldsArray = [
+ 'description',
+ 'target',
+ 'mode',
+ 'publication_end_date',
+ 'recommended_number_of_participants',
+ ];
+ $sessionArray = api_get_session_info($data['sessionId']);
+ $extraSession = new ExtraFieldValue('session');
+ $extraField = new ExtraField('session');
+ // Get session fields
+ $fieldList = $extraField->get_all([
+ 'variable IN ( ?, ?, ?, ?, ?)' => $fieldsArray,
+ ]);
+ // Index session fields
+ foreach ($fieldList as $field) {
+ $fields[$field['id']] = $field['variable'];
+ }
+
+ $mergedArray = array_merge([$data['sessionId']], array_keys($fields));
+ $sessionFieldValueList = $extraSession->get_all(
+ ['session_id = ? field_id IN ( ?, ?, ?, ?, ?, ?, ? )' => $mergedArray]
+ );
+ foreach ($sessionFieldValueList as $sessionFieldValue) {
+ // Check if session field value is set in session field list
+ if (isset($fields[$sessionFieldValue['field_id']])) {
+ $var = $fields[$sessionFieldValue['field_id']];
+ $val = $sessionFieldValue['value'];
+ // Assign session field value to session
+ $sessionArray[$var] = $val;
+ }
+ }
+ // Prepare student data
+ $studentArray = api_get_user_info($data['studentUserId']);
+ $studentArray['picture'] = $studentArray['avatar'];
+ // Prepare superior data
+ $superiorId = UserManager::getFirstStudentBoss($data['studentUserId']);
+ if (!empty($superiorId)) {
+ $superiorArray = api_get_user_info($superiorId);
+ } else {
+ $superiorArray = null;
+ }
+ // Prepare admin data
+ $adminsArray = UserManager::get_all_administrators();
+ $isWesternNameOrder = api_is_western_name_order();
+ foreach ($adminsArray as &$admin) {
+ $admin['complete_name'] = $isWesternNameOrder ?
+ $admin['firstname'].', '.$admin['lastname'] : $admin['lastname'].', '.$admin['firstname']
+ ;
+ }
+ unset($admin);
+ // Set data
+ $data['student'] = $studentArray;
+ $data['superior'] = $superiorArray;
+ $data['admins'] = $adminsArray;
+ $data['session'] = $sessionArray;
+ $data['signature'] = api_get_setting('Institution');
+ $data['admin_view_url'] = api_get_path(WEB_PLUGIN_PATH)
+ .'advanced_subscription/src/admin_view.php?s='.$data['sessionId'];
+ // Check if exist and action in data
+ if (empty($data['mailAction'])) {
+ // set action in data by new status
+ switch ($data['newStatus']) {
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_APPROVED:
+ $data['mailAction'] = ADVANCED_SUBSCRIPTION_ACTION_SUPERIOR_APPROVE;
+ break;
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_DISAPPROVED:
+ $data['mailAction'] = ADVANCED_SUBSCRIPTION_ACTION_SUPERIOR_DISAPPROVE;
+ break;
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED:
+ $data['mailAction'] = ADVANCED_SUBSCRIPTION_ACTION_ADMIN_APPROVE;
+ break;
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_DISAPPROVED:
+ $data['mailAction'] = ADVANCED_SUBSCRIPTION_ACTION_ADMIN_DISAPPROVE;
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Student Session inscription
+ if ($data['newStatus'] == ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED) {
+ SessionManager::subscribeUsersToSession(
+ $data['sessionId'],
+ [$data['studentUserId']],
+ null,
+ false
+ );
+ }
+
+ // Send mails
+ $result['mailIds'] = $plugin->sendMail($data, $data['mailAction']);
+ // Check if mails were sent
+ if (!empty($result['mailIds'])) {
+ $result['error'] = false;
+ $result['errorMessage'] = 'User has been processed';
+ // Check if exist mail to render
+ if (isset($result['mailIds']['render'])) {
+ // Render mail
+ $url = $plugin->getRenderMailUrl(['queueId' => $result['mailIds']['render']]);
+ header('Location: '.$url);
+ exit;
+ }
+ }
+ } else {
+ $result['errorMessage'] = 'User queue can not be updated';
+ }
+ }
+ break;
+ default:
+ $result['errorMessage'] = 'This action does not exist!';
+ }
+}
+
+if ($showJSON) {
+ // Echo result as json
+ echo json_encode($result);
+}
diff --git a/plugin/advanced_subscription/config.php b/plugin/advanced_subscription/config.php
new file mode 100644
index 000000000..805d5b4d5
--- /dev/null
+++ b/plugin/advanced_subscription/config.php
@@ -0,0 +1,35 @@
+
+ *
+ * @package chamilo.plugin.advanced_subscription
+ */
+define('TABLE_ADVANCED_SUBSCRIPTION_QUEUE', 'plugin_advanced_subscription_queue');
+
+define('ADVANCED_SUBSCRIPTION_ACTION_STUDENT_REQUEST', 0);
+define('ADVANCED_SUBSCRIPTION_ACTION_SUPERIOR_APPROVE', 1);
+define('ADVANCED_SUBSCRIPTION_ACTION_SUPERIOR_DISAPPROVE', 2);
+define('ADVANCED_SUBSCRIPTION_ACTION_SUPERIOR_SELECT', 3);
+define('ADVANCED_SUBSCRIPTION_ACTION_ADMIN_APPROVE', 4);
+define('ADVANCED_SUBSCRIPTION_ACTION_ADMIN_DISAPPROVE', 5);
+define('ADVANCED_SUBSCRIPTION_ACTION_STUDENT_REQUEST_NO_BOSS', 6);
+define('ADVANCED_SUBSCRIPTION_ACTION_REMINDER_STUDENT', 7);
+define('ADVANCED_SUBSCRIPTION_ACTION_REMINDER_SUPERIOR', 8);
+define('ADVANCED_SUBSCRIPTION_ACTION_REMINDER_SUPERIOR_MAX', 9);
+define('ADVANCED_SUBSCRIPTION_ACTION_REMINDER_ADMIN', 10);
+
+define('ADVANCED_SUBSCRIPTION_QUEUE_STATUS_NO_QUEUE', -1);
+define('ADVANCED_SUBSCRIPTION_QUEUE_STATUS_START', 0);
+define('ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_DISAPPROVED', 1);
+define('ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_APPROVED', 2);
+define('ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_DISAPPROVED', 3);
+define('ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED', 10);
+
+define('ADVANCED_SUBSCRIPTION_TERMS_MODE_POPUP', 0);
+define('ADVANCED_SUBSCRIPTION_TERMS_MODE_REJECT', 1);
+define('ADVANCED_SUBSCRIPTION_TERMS_MODE_FINAL', 2);
+
+require_once __DIR__.'/../../main/inc/global.inc.php';
diff --git a/plugin/advanced_subscription/cron/notify_by_mail.php b/plugin/advanced_subscription/cron/notify_by_mail.php
new file mode 100644
index 000000000..3446aaa46
--- /dev/null
+++ b/plugin/advanced_subscription/cron/notify_by_mail.php
@@ -0,0 +1,153 @@
+ [
+ 's.access_start_date >= ? AND uu.relation_type = ? AND asq.updated_at <= ?' => [
+ $now,
+ USER_RELATION_TYPE_BOSS,
+ $weekAgo,
+ ],
+ ],
+ 'order' => 's.id',
+];
+
+$queueList = Database::select($columns, $joinTables, $conditions);
+
+/**
+ * Remind students.
+ */
+$sessionInfoList = [];
+foreach ($queueList as $queueItem) {
+ if (!isset($sessionInfoList[$queueItem['session_id']])) {
+ $sessionInfoList[$queueItem['session_id']] = api_get_session_info($queueItem['session_id']);
+ }
+}
+
+foreach ($queueList as $queueItem) {
+ switch ($queueItem['status']) {
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_START:
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_APPROVED:
+ $data = ['sessionId' => $queueItem['session_id']];
+ $data['session'] = api_get_session_info($queueItem['session_id']);
+ $data['student'] = api_get_user_info($queueItem['student_id']);
+ $plugin->sendMail($data, ADVANCED_SUBSCRIPTION_ACTION_REMINDER_STUDENT);
+ break;
+ default:
+ break;
+ }
+}
+
+/**
+ * Remind superiors.
+ */
+// Get recommended number of participants
+$sessionRecommendedNumber = [];
+foreach ($queueList as $queueItem) {
+ $row =
+ $sessionExtraFieldValue->get_values_by_handler_and_field_variable(
+ $queueItem['session_id'],
+ 'recommended_number_of_participants'
+ );
+ $sessionRecommendedNumber[$queueItem['session_id']] = $row['value'];
+}
+// Group student by superior and session
+$queueBySuperior = [];
+foreach ($queueList as $queueItem) {
+ $queueBySuperior[$queueItem['session_id']][$queueItem['superior_id']][$queueItem['student_id']]['status'] = $queueItem['status'];
+}
+
+foreach ($queueBySuperior as $sessionId => $superiorStudents) {
+ $data = [
+ 'sessionId' => $sessionId,
+ 'session' => $sessionInfoList[$sessionId],
+ 'students' => [],
+ ];
+ $dataUrl = [
+ 'action' => 'confirm',
+ 'sessionId' => $sessionId,
+ 'currentUserId' => 0,
+ 'newStatus' => ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_APPROVED,
+ 'studentUserId' => 0,
+ 'is_connected' => true,
+ 'profile_completed' => 0,
+ ];
+ foreach ($superiorStudents as $superiorId => $students) {
+ $data['superior'] = api_get_user_info($superiorId);
+ // Check if superior has at least one student
+ if (count($students) > 0) {
+ foreach ($students as $studentId => $studentInfo) {
+ if ($studentInfo['status'] == ADVANCED_SUBSCRIPTION_QUEUE_STATUS_START) {
+ $data['students'][$studentId] = api_get_user_info($studentId);
+ $dataUrl['studentUserId'] = $studentId;
+ $dataUrl['newStatus'] = ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_APPROVED;
+ $data['students'][$studentId]['acceptUrl'] = $plugin->getQueueUrl($dataUrl);
+ $dataUrl['newStatus'] = ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_DISAPPROVED;
+ $data['students'][$studentId]['rejectUrl'] = $plugin->getQueueUrl($dataUrl);
+ }
+ }
+
+ if (is_array($data['students']) && count($data['students']) > 0) {
+ // Check if superior have more than recommended
+ if ($sessionRecommendedNumber[$sessionId] >= count($students)) {
+ // Is greater or equal than recommended
+ $plugin->sendMail($data, ADVANCED_SUBSCRIPTION_ACTION_REMINDER_SUPERIOR);
+ } else {
+ // Is less than recommended
+ $plugin->sendMail($data, ADVANCED_SUBSCRIPTION_ACTION_REMINDER_SUPERIOR_MAX);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Remind admins.
+ */
+$admins = UserManager::get_all_administrators();
+$isWesternNameOrder = api_is_western_name_order();
+foreach ($admins as &$admin) {
+ $admin['complete_name'] = $isWesternNameOrder ?
+ $admin['firstname'].', '.$admin['lastname'] : $admin['lastname'].', '.$admin['firstname']
+ ;
+}
+unset($admin);
+$queueByAdmin = [];
+foreach ($queueList as $queueItem) {
+ if ($queueItem['status'] == ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_APPROVED) {
+ $queueByAdmin[$queueItem['session_id']]['students'][$queueItem['student_id']]['user_id'] = $queueItem['student_id'];
+ }
+}
+$data = [
+ 'admins' => $admins,
+];
+foreach ($queueByAdmin as $sessionId => $studentInfo) {
+ $data['sessionId'] = $sessionId;
+ $data['admin_view_url'] = api_get_path(WEB_PLUGIN_PATH).
+ 'advanced_subscription/src/admin_view.php?s='.$data['sessionId'];
+ $data['session'] = $sessionInfoList[$sessionId];
+ $data['students'] = $studentInfo['students'];
+ $plugin->sendMail($data, ADVANCED_SUBSCRIPTION_ACTION_REMINDER_ADMIN);
+}
diff --git a/plugin/advanced_subscription/index.html b/plugin/advanced_subscription/index.html
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/plugin/advanced_subscription/index.html
@@ -0,0 +1 @@
+
diff --git a/plugin/advanced_subscription/install.php b/plugin/advanced_subscription/install.php
new file mode 100644
index 000000000..846f6de68
--- /dev/null
+++ b/plugin/advanced_subscription/install.php
@@ -0,0 +1,17 @@
+install();
diff --git a/plugin/advanced_subscription/lang/english.php b/plugin/advanced_subscription/lang/english.php
new file mode 100644
index 000000000..4ee55705f
--- /dev/null
+++ b/plugin/advanced_subscription/lang/english.php
@@ -0,0 +1,149 @@
+not include induction courses";
+$strings['course_session_credit_year_start_date'] = 'Year start date';
+$strings['course_session_credit_year_start_date_help'] = "a date (dd/mm)";
+$strings['min_profile_percentage'] = "Minimum required of completed percentage profile";
+$strings['min_profile_percentage_help'] = "Percentage number( > 0.00 y < 100.00)";
+$strings['secret_key'] = 'Secret key';
+$strings['terms_and_conditions'] = 'Terms and conditions';
+
+/* String for error message about requirements */
+$strings['AdvancedSubscriptionNotConnected'] = "You are not connected to platform. Please login first";
+$strings['AdvancedSubscriptionProfileIncomplete'] = "You must complete at least %d percent of your profile. You have only completed %d percent at this point";
+$strings['AdvancedSubscriptionIncompleteInduction'] = "You have not yet completed induction course. Please complete it first";
+$strings['AdvancedSubscriptionCostXLimitReached'] = "We are sorry, you have already reached yearly limit %s TUV cost for courses ";
+$strings['AdvancedSubscriptionTimeXLimitReached'] = "We are sorry, you have already reached yearly limit %s hours for courses";
+$strings['AdvancedSubscriptionCourseXLimitReached'] = "We are sorry, you have already reached yearly limit %s times for courses";
+$strings['AdvancedSubscriptionNotMoreAble'] = "We are sorry, you no longer fulfills the initial conditions to subscribe this course";
+$strings['AdvancedSubscriptionIncompleteParams'] = "The parameters are wrong or incomplete.";
+$strings['AdvancedSubscriptionIsNotEnabled'] = "Advanced subscription is not enabled";
+
+$strings['AdvancedSubscriptionNoQueue'] = "You are not subscribed for this course.";
+$strings['AdvancedSubscriptionNoQueueIsAble'] = "You are not subscribed, but you are qualified for this course.";
+$strings['AdvancedSubscriptionQueueStart'] = "Your subscription request is pending for approval by your boss, please wait attentive.";
+$strings['AdvancedSubscriptionQueueBossDisapproved'] = "We are sorry, your subscription was rejected by your boss.";
+$strings['AdvancedSubscriptionQueueBossApproved'] = "Your subscription request has been accepted by your boss, now is pending for vacancies.";
+$strings['AdvancedSubscriptionQueueAdminDisapproved'] = "We are sorry, your subscription was rejected by the administrator.";
+$strings['AdvancedSubscriptionQueueAdminApproved'] = "Congratulations!, your subscription request has been accepted by administrator.";
+$strings['AdvancedSubscriptionQueueDefaultX'] = "There was an error, queue status %d is not defined by system.";
+
+// Mail translations
+$strings['MailStudentRequest'] = 'Student registration request';
+$strings['MailBossAccept'] = 'Registration request accepted by boss';
+$strings['MailBossReject'] = 'Registration request rejected by boss';
+$strings['MailStudentRequestSelect'] = 'Student registration requests selection';
+$strings['MailAdminAccept'] = 'Registration request accepted by administrator';
+$strings['MailAdminReject'] = 'Registration request rejected by administrator';
+$strings['MailStudentRequestNoBoss'] = 'Student registration request without boss';
+$strings['MailRemindStudent'] = 'Subscription request reminder';
+$strings['MailRemindSuperior'] = 'Subscription request are pending your approval';
+$strings['MailRemindAdmin'] = 'Course subscription are pending your approval';
+
+// TPL langs
+$strings['SessionXWithoutVacancies'] = "The course \"%s\" has no vacancies.";
+$strings['SuccessSubscriptionToSessionX'] = "
¡Congratulations!
Your subscription to \"%s\" course has been completed successfully.";
+$strings['SubscriptionToOpenSession'] = "Subscription to open course";
+$strings['GoToSessionX'] = "Go to \"%s\" course";
+$strings['YouAreAlreadySubscribedToSessionX'] = "You are already subscribed to \"%s\" course.";
+
+// Admin view
+$strings['SelectASession'] = 'Select a training session';
+$strings['SessionName'] = 'Session name';
+$strings['Target'] = 'Target audience';
+$strings['Vacancies'] = 'Vacancies';
+$strings['RecommendedNumberOfParticipants'] = 'Recommended number of participants by area';
+$strings['PublicationEndDate'] = 'Publication end date';
+$strings['Mode'] = 'Mode';
+$strings['Postulant'] = 'Postulant';
+$strings['Area'] = 'Area';
+$strings['Institution'] = 'Institution';
+$strings['InscriptionDate'] = 'Inscription date';
+$strings['BossValidation'] = 'Boss validation';
+$strings['Decision'] = 'Decision';
+$strings['AdvancedSubscriptionAdminViewTitle'] = 'Subscription request confirmation result';
+
+$strings['AcceptInfinitive'] = 'Accept';
+$strings['RejectInfinitive'] = 'Reject';
+$strings['AreYouSureYouWantToAcceptSubscriptionOfX'] = 'Are you sure you want to accept the subscription of %s?';
+$strings['AreYouSureYouWantToRejectSubscriptionOfX'] = 'Are you sure you want to reject the subscription of %s?';
+
+$strings['MailTitle'] = 'Received request for course %s';
+$strings['MailDear'] = 'Dear:';
+$strings['MailThankYou'] = 'Thank you.';
+$strings['MailThankYouCollaboration'] = 'Thank you for your help.';
+
+// Admin Accept
+$strings['MailTitleAdminAcceptToAdmin'] = 'Notification: subscription validation received';
+$strings['MailContentAdminAcceptToAdmin'] = 'We have received and registered your subscription validation for student %s to course %s';
+$strings['MailTitleAdminAcceptToStudent'] = 'Accepted: Your subscription to course %s has been accepted!';
+$strings['MailContentAdminAcceptToStudent'] = 'We are pleased to inform you that your registration to course %s starting on %s was validated by an administrator. We wish you good luck and hope you will consider participating to another course soon.';
+$strings['MailTitleAdminAcceptToSuperior'] = 'Notification: Subscription validation of %s to course %s';
+$strings['MailContentAdminAcceptToSuperior'] = 'Subscription of student %s to course %s starting on %s was pending but has been validated a few minutes ago. We kindly hope we can count on you to ensure the necessary availability of your collaborator during the course period.';
+
+// Admin Reject
+$strings['MailTitleAdminRejectToAdmin'] = 'Notification: Rejection received';
+$strings['MailContentAdminRejectToAdmin'] = 'We have received and registered your rejection for the subscription of student %s to course %s';
+$strings['MailTitleAdminRejectToStudent'] = 'Your subscription to course %s was rejected';
+$strings['MailContentAdminRejectToStudent'] = 'We regret to inform you that your subscription to course %s starting on %s was rejected because of a lack of vacancies. We hope you will consider participating to another course soon.';
+$strings['MailTitleAdminRejectToSuperior'] = 'Notification: Subscription refusal for student %s to course %s';
+$strings['MailContentAdminRejectToSuperior'] = 'The subscription of %s to course %s that you previously validated was rejected because of a lack of vacancies. Our sincere apologies.';
+
+// Superior Accept
+$strings['MailTitleSuperiorAcceptToAdmin'] = 'Approval for subscription of %s to course %s ';
+$strings['MailContentSuperiorAcceptToAdmin'] = 'The subscription of student %s to course %s has been accepted by his superior. You can manage subscriptions here';
+$strings['MailTitleSuperiorAcceptToSuperior'] = 'Confirmation: Approval received for %s';
+$strings['MailContentSuperiorAcceptToSuperior'] = 'We have received and registered you validation of subscription to course %s of your collaborator %s';
+$strings['MailContentSuperiorAcceptToSuperiorSecond'] = 'The subscription is now pending for a vacancies confirmation. We will keep you informed about changes of status for this subscription';
+$strings['MailTitleSuperiorAcceptToStudent'] = 'Accepted: Your subscription to course %s has been approved by your superior';
+$strings['MailContentSuperiorAcceptToStudent'] = 'We are pleased to inform you that your subscription to course %s has been accepted by your superior. Your inscription is now pending for a vacancies confirmation. We will notify you as soon as it is confirmed.';
+
+// Superior Reject
+$strings['MailTitleSuperiorRejectToStudent'] = 'Notification: Your subscription to course %s has been refused';
+$strings['MailContentSuperiorRejectToStudent'] = 'We regret to inform your subscription to course %s was NOT accepted. We hope this will not reduce your motivation and encourage you to register to another course or, on another occasion, this same course soon.';
+$strings['MailTitleSuperiorRejectToSuperior'] = 'Confirmation: Rejection of subscription received for %s';
+$strings['MailContentSuperiorRejectToSuperior'] = 'We have received and registered your rejection of subscription to course %s for your collaborator %s';
+
+// Student Request
+$strings['MailTitleStudentRequestToStudent'] = 'Notification: Subscription approval received';
+$strings['MailContentStudentRequestToStudent'] = 'We have received and registered your subscription request to course %s starting on %s';
+$strings['MailContentStudentRequestToStudentSecond'] = 'Your subscription is pending approval, first from your superior, then for the availability of vacancies. An email has been sent to your superior for review and approval. We will inform you when this situation changes.';
+$strings['MailTitleStudentRequestToSuperior'] = 'Course subscription request from your collaborator';
+$strings['MailContentStudentRequestToSuperior'] = 'We have received an subscription request of %s to course %s, starting on %s. Course details: %s.';
+$strings['MailContentStudentRequestToSuperiorSecond'] = 'Your are welcome to accept or reject this subscription, clicking the corresponding button.';
+
+// Student Request No Boss
+$strings['MailTitleStudentRequestNoSuperiorToStudent'] = 'Your subscription request for %s has been received';
+$strings['MailContentStudentRequestNoSuperiorToStudent'] = 'We have received and registered your subscription to course %s starting on %s.';
+$strings['MailContentStudentRequestNoSuperiorToStudentSecond'] = 'Your subscription is pending availability of vacancies. You will get the results of your request approval (or rejection) soon.';
+$strings['MailTitleStudentRequestNoSuperiorToAdmin'] = 'Subscription request of %s to course %s';
+$strings['MailContentStudentRequestNoSuperiorToAdmin'] = 'The subscription of %s to course %s has been approved by default (no direct superior defined). You can manage subscriptions here';
+
+// Reminders
+$strings['MailTitleReminderAdmin'] = 'Subscriptions to %s are pending confirmation';
+$strings['MailContentReminderAdmin'] = 'The subscription requests for course %s are pending validation to be accepted. Please, go to Administration page to validate them.';
+$strings['MailTitleReminderStudent'] = 'Information: Your subscription request is pending approval for course %s';
+$strings['MailContentReminderStudent'] = 'This email is just to confirm we have received and registered your subscription request to course %s, starting on %s.';
+$strings['MailContentReminderStudentSecond'] = 'Your subscription has not been approved by your superior yet, so we sent him a e-mail reminder.';
+$strings['MailTitleReminderSuperior'] = 'Course subscription request for your collaborators';
+$strings['MailContentReminderSuperior'] = 'We kindly remind you that we have received the subscription requests below to course %s from your collaborators. This course is starting on %s. Course details: %s.';
+$strings['MailContentReminderSuperiorSecond'] = 'We invite you to accept or reject this subscription request by clicking the corresponding button for each collaborator.';
+$strings['MailTitleReminderMaxSuperior'] = 'Reminder: Course subscription request for your collaborators';
+$strings['MailContentReminderMaxSuperior'] = 'We kindly remind you that we have received the subscription requests below to course %s from your collaborators. This course is starting on %s. Course details: %s.';
+$strings['MailContentReminderMaxSuperiorSecond'] = 'This course have limited vacancies and has received a high subscription request rate, So we recommend all areas to accept at most %s candidates. We invite you to accept or reject the inscription request by clicking the corresponding button for each collaborator.';
+
+$strings['YouMustAcceptTermsAndConditions'] = 'To subscribe to course %s, you must accept these terms and conditions.';
diff --git a/plugin/advanced_subscription/lang/french.php b/plugin/advanced_subscription/lang/french.php
new file mode 100644
index 000000000..a6d1af5c0
--- /dev/null
+++ b/plugin/advanced_subscription/lang/french.php
@@ -0,0 +1,148 @@
+ne sont pas le cours d'induction";
+$strings['course_session_credit_year_start_date'] = 'Date de début';
+$strings['course_session_credit_year_start_date_help'] = "Date de début de l'année (jour/mois)";
+$strings['min_profile_percentage'] = 'Pourcentage du profil complété mínimum requis';
+$strings['min_profile_percentage_help'] = 'Numéro pourcentage ( > 0.00 et < 100.00)';
+$strings['secret_key'] = 'Clef secrète';
+$strings['terms_and_conditions'] = 'Conditions d\'utilisation';
+
+/* String for error message about requirements */
+$strings['AdvancedSubscriptionNotConnected'] = "Vous n'êtes pas connecté à la plateforme. Merci d'introduire votre nom d'utilisateur / mot de passe afin de vous inscrire";
+$strings['AdvancedSubscriptionProfileIncomplete'] = "Vous devez d'abord compléter votre profil à %d pourcents ou plus. Pour l'instant vous n'avez complété que %d pourcents";
+$strings['AdvancedSubscriptionIncompleteInduction'] = "Vous n'avez pas encore passé le cours d'induction. Merci de commencer par cette étape.";
+$strings['AdvancedSubscriptionCostXLimitReached'] = "Désolé, vous avez déjà atteint la limite de %s unités de taxe pour les cours que vous avez suivi cette année";
+$strings['AdvancedSubscriptionTimeXLimitReached'] = "Désolé, vous avez déjà atteint la limite annuelle du nombre de %s heures pour les cours que vous avez suivi cette année";
+$strings['AdvancedSubscriptionCourseXLimitReached'] = "Désolé, vous avez déjà atteint la limite annuelle du nombre de cours (%s) à suivre cette année";
+$strings['AdvancedSubscriptionNotMoreAble'] = "Désolé, vous ne répondez plus aux conditions d'utilisation minimum pour l'inscription à un cours";
+$strings['AdvancedSubscriptionIncompleteParams'] = "Les paramètres envoyés ne sont pas complets ou sont incorrects.";
+$strings['AdvancedSubscriptionIsNotEnabled'] = "L'inscription avancée n'est pas activée";
+$strings['AdvancedSubscriptionNoQueue'] = "Vous n'êtes pas inscrit dans ce cours";
+$strings['AdvancedSubscriptionNoQueueIsAble'] = "Vous n'êtes pas inscrit mais vous qualifiez pour ce cours";
+$strings['AdvancedSubscriptionQueueStart'] = "Votre demande d'inscription est en attente de l'approbation de votre supérieur(e). Merci de patienter.";
+$strings['AdvancedSubscriptionQueueBossDisapproved'] = "Désolé, votre inscription a été déclinée par votre supérieur(e).";
+$strings['AdvancedSubscriptionQueueBossApproved'] = "Votre demande d'inscription a été acceptée par votre supérieur(e), mais est en attente de places libres.";
+$strings['AdvancedSubscriptionQueueAdminDisapproved'] = "Désolé, votre inscription a été déclinée par l'administrateur.";
+$strings['AdvancedSubscriptionQueueAdminApproved'] = "Félicitations! Votre inscription a été acceptée par l'administrateur.";
+$strings['AdvancedSubscriptionQueueDefaultX'] = "Une erreur est survenue: l'état de la file d'attente %s n'est pas défini dans le système.";
+
+// Mail translations
+$strings['MailStudentRequest'] = 'Demange d\'inscription d\'un(e) apprenant(e)';
+$strings['MailBossAccept'] = 'Demande d\'inscription acceptée par votre supérieur(e)';
+$strings['MailBossReject'] = 'Demande d\'inscription déclinée par votre supérieur(e)';
+$strings['MailStudentRequestSelect'] = 'Sélection des demandes d\'inscriptions d\'apprenants';
+$strings['MailAdminAccept'] = 'Demande d\'inscription acceptée par l\'administrateur';
+$strings['MailAdminReject'] = 'Demande d\'inscription déclinée par l\'administrateur';
+$strings['MailStudentRequestNoBoss'] = 'Demande d\'inscription d\'apprenant sans supérieur(e)';
+$strings['MailRemindStudent'] = 'Rappel de demande d\'inscription';
+$strings['MailRemindSuperior'] = 'Demandes d\'inscription en attente de votre approbation';
+$strings['MailRemindAdmin'] = 'Inscriptions en attente de votre approbation';
+
+// TPL translations
+$strings['SessionXWithoutVacancies'] = "Le cours \"%s\" ne dispose plus de places libres.";
+$strings['SuccessSubscriptionToSessionX'] = "
Félicitations!
Votre inscription au cours \"%s\" est en ordre.";
+$strings['SubscriptionToOpenSession'] = "Inscription à cours ouvert";
+$strings['GoToSessionX'] = "Aller dans le cours \"%s\"";
+$strings['YouAreAlreadySubscribedToSessionX'] = "Vous êtes déjà inscrit(e) au cours \"%s\".";
+
+// Admin view
+$strings['SelectASession'] = 'Sélectionnez une session de formation';
+$strings['SessionName'] = 'Nom de la session';
+$strings['Target'] = 'Public cible';
+$strings['Vacancies'] = 'Places libres';
+$strings['RecommendedNumberOfParticipants'] = 'Nombre recommandé de participants par département';
+$strings['PublicationEndDate'] = 'Date de fin de publication';
+$strings['Mode'] = 'Modalité';
+$strings['Postulant'] = 'Candidats';
+$strings['Area'] = 'Département';
+$strings['Institution'] = 'Institution';
+$strings['InscriptionDate'] = 'Date d\'inscription';
+$strings['BossValidation'] = 'Validation du supérieur';
+$strings['Decision'] = 'Décision';
+$strings['AdvancedSubscriptionAdminViewTitle'] = 'Résultat de confirmation de demande d\'inscription';
+
+$strings['AcceptInfinitive'] = 'Accepter';
+$strings['RejectInfinitive'] = 'Refuser';
+$strings['AreYouSureYouWantToAcceptSubscriptionOfX'] = 'Êtes-vous certain de vouloir accepter l\'inscription de %s?';
+$strings['AreYouSureYouWantToRejectSubscriptionOfX'] = 'Êtes-vous certain de vouloir refuser l\'inscription de %s?';
+
+$strings['MailTitle'] = 'Demande reçue pour le cours %s';
+$strings['MailDear'] = 'Cher/Chère';
+$strings['MailThankYou'] = 'Merci.';
+$strings['MailThankYouCollaboration'] = 'Merci de votre collaboration.';
+
+// Admin Accept
+$strings['MailTitleAdminAcceptToAdmin'] = 'Information: Validation d\'inscription reçue';
+$strings['MailContentAdminAcceptToAdmin'] = 'Nous avons bien reçu et enregistré votre validation de l\'inscription de %s au cours %s';
+$strings['MailTitleAdminAcceptToStudent'] = 'Approuvé(e): Votre inscription au cours %s a été confirmée!';
+$strings['MailContentAdminAcceptToStudent'] = 'C\'est avec plaisir que nous vous informons que votre inscription au cours %s démarrant le %s a été validée par les administrateurs. Nous espérons que votre motivation s\'est maintenue à 100% et que vous participerez à d\'autres cours ou répétiez ce cours à l\'avenir.';
+$strings['MailTitleAdminAcceptToSuperior'] = 'Information: Validation de l\'inscription de %s au cours %s';
+$strings['MailContentAdminAcceptToSuperior'] = 'L\'inscription de %s au cours %s qui démarre le %s, qui était en attente de validation par les organisateurs du cours, vient d\'être validée. Nous espérons que vous nous donnerez un coup de main pour assurer la disponibilité complète de votre collaborateur pour toute la durée du cours';
+
+// Admin Reject
+$strings['MailTitleAdminRejectToAdmin'] = 'Information: refus d\'inscription reçu';
+$strings['MailContentAdminRejectToAdmin'] = 'Nous avons bien reçu et enregistré votre refus pour l\'inscription de %s au cours %s';
+$strings['MailTitleAdminRejectToStudent'] = 'Votre demande d\'inscription au cours %s a été refusée';
+$strings['MailContentAdminRejectToStudent'] = 'Nous déplorons le besoin de vous informer que vote demande d\'inscription au cours %s démarrant le %s a été refusée pour manque de place. Nous espérons que vous maintiendrez votre motivation et que vous pourrez participer au même ou à un autre cours lors d\'une prochaine occasion.';
+$strings['MailTitleAdminRejectToSuperior'] = 'Information: Refus d\'inscription de %s au cours %s';
+$strings['MailContentAdminRejectToSuperior'] = 'L\'inscription de %s au cours %s, qui avait été approuvée antérieurement, a été refusée par manque de place. Nous vous présentons nos excuses sincères.';
+
+// Superior Accept
+$strings['MailTitleSuperiorAcceptToAdmin'] = 'Aprobación de %s al curso %s ';
+$strings['MailContentSuperiorAcceptToAdmin'] = 'La inscripción del alumno %s al curso %s ha sido aprobada por su superior. Puede gestionar las inscripciones al curso aquí';
+$strings['MailTitleSuperiorAcceptToSuperior'] = 'Confirmación: Aprobación recibida para %s';
+$strings['MailContentSuperiorAcceptToSuperior'] = 'Hemos recibido y registrado su decisión de aprobar el curso %s para su colaborador %s';
+$strings['MailContentSuperiorAcceptToSuperiorSecond'] = 'Ahora la inscripción al curso está pendiente de la disponibilidad de cupos. Le mantendremos informado sobre el resultado de esta etapa';
+$strings['MailTitleSuperiorAcceptToStudent'] = 'Aprobado: Su inscripción al curso %s ha sido aprobada por su superior ';
+$strings['MailContentSuperiorAcceptToStudent'] = 'Nos complace informarle que su inscripción al curso %s ha sido aprobada por su superior. Su inscripción ahora solo se encuentra pendiente de disponibilidad de cupos. Le avisaremos tan pronto como se confirme este último paso.';
+
+// Superior Reject
+$strings['MailTitleSuperiorRejectToStudent'] = 'Información: Su inscripción al curso %s ha sido rechazada ';
+$strings['MailContentSuperiorRejectToStudent'] = 'Lamentamos informarle que, en esta oportunidad, su inscripción al curso %s NO ha sido aprobada. Esperamos mantenga todo su ánimo y participe en otro curso o, en otra oportunidad, a este mismo curso.';
+$strings['MailTitleSuperiorRejectToSuperior'] = 'Confirmación: Desaprobación recibida para %s';
+$strings['MailContentSuperiorRejectToSuperior'] = 'Hemos recibido y registrado su decisión de desaprobar el curso %s para su colaborador %s';
+
+// Student Request
+$strings['MailTitleStudentRequestToStudent'] = 'Información: Validación de inscripción recibida';
+$strings['MailContentStudentRequestToStudent'] = 'Hemos recibido y registrado su solicitud de inscripción al curso %s para iniciarse el %s.';
+$strings['MailContentStudentRequestToStudentSecond'] = 'Su inscripción es pendiente primero de la aprobación de su superior, y luego de la disponibilidad de cupos. Un correo ha sido enviado a su superior para revisión y aprobación de su solicitud.';
+$strings['MailTitleStudentRequestToSuperior'] = 'Solicitud de consideración de curso para un colaborador';
+$strings['MailContentStudentRequestToSuperior'] = 'Hemos recibido una solicitud de inscripción de %s al curso %s, por iniciarse el %s. Detalles del curso: %s.';
+$strings['MailContentStudentRequestToSuperiorSecond'] = 'Le invitamos a aprobar o desaprobar esta inscripción, dando clic en el botón correspondiente a continuación.';
+
+// Student Request No Boss
+$strings['MailTitleStudentRequestNoSuperiorToStudent'] = 'Solicitud recibida para el curso %s';
+$strings['MailContentStudentRequestNoSuperiorToStudent'] = 'Hemos recibido y registrado su solicitud de inscripción al curso %s para iniciarse el %s.';
+$strings['MailContentStudentRequestNoSuperiorToStudentSecond'] = 'Su inscripción es pendiente de la disponibilidad de cupos. Pronto recibirá los resultados de su aprobación de su solicitud.';
+$strings['MailTitleStudentRequestNoSuperiorToAdmin'] = 'Solicitud de inscripción de %s para el curso %s';
+$strings['MailContentStudentRequestNoSuperiorToAdmin'] = 'La inscripción del alumno %s al curso %s ha sido aprobada por defecto, a falta de superior. Puede gestionar las inscripciones al curso aquí';
+
+// Reminders
+$strings['MailTitleReminderAdmin'] = 'Inscripciones a %s pendiente de confirmación';
+$strings['MailContentReminderAdmin'] = 'Las inscripciones siguientes al curso %s están pendientes de validación para ser efectivas. Por favor, dirigese a la página de administración para validarlos.';
+$strings['MailTitleReminderStudent'] = 'Información: Solicitud pendiente de aprobación para el curso %s';
+$strings['MailContentReminderStudent'] = 'Este correo es para confirmar que hemos recibido y registrado su solicitud de inscripción al curso %s, por iniciarse el %s.';
+$strings['MailContentReminderStudentSecond'] = 'Su inscripción todavía no ha sido aprobada por su superior, por lo que hemos vuelto a enviarle un correo electrónico de recordatorio.';
+$strings['MailTitleReminderSuperior'] = 'Solicitud de consideración de curso para un colaborador';
+$strings['MailContentReminderSuperior'] = 'Le recordamos que hemos recibido las siguientes solicitudes de suscripción para el curso %s de parte de sus colaboradores. El curso se iniciará el %s. Detalles del curso: %s.';
+$strings['MailContentReminderSuperiorSecond'] = 'Le invitamos a aprobar o desaprobar las suscripciones, dando clic en el botón correspondiente a continuación para cada colaborador.';
+$strings['MailTitleReminderMaxSuperior'] = 'Recordatorio: Solicitud de consideración de curso para colaborador(es)';
+$strings['MailContentReminderMaxSuperior'] = 'Le recordamos que hemos recibido las siguientes solicitudes de suscripción al curso %s de parte de sus colaboradores. El curso se iniciará el %s. Detalles del curso: %s.';
+$strings['MailContentReminderMaxSuperiorSecond'] = 'Este curso tiene una cantidad de cupos limitados y ha recibido una alta tasa de solicitudes de inscripción, por lo que recomendamos que cada área apruebe un máximo de %s candidatos. Le invitamos a aprobar o desaprobar las suscripciones, dando clic en el botón correspondiente a continuación para cada colaborador.';
+
+$strings['YouMustAcceptTermsAndConditions'] = 'Para inscribirse al curso %s, debe aceptar estos términos y condiciones.';
diff --git a/plugin/advanced_subscription/lang/spanish.php b/plugin/advanced_subscription/lang/spanish.php
new file mode 100644
index 000000000..9c5633c57
--- /dev/null
+++ b/plugin/advanced_subscription/lang/spanish.php
@@ -0,0 +1,149 @@
+no sean el curso de inducción";
+$strings['course_session_credit_year_start_date'] = 'Fecha de inicio';
+$strings['course_session_credit_year_start_date_help'] = "Fecha de inicio del año (día/mes)";
+$strings['min_profile_percentage'] = 'Porcentage de perfil completado mínimo requerido';
+$strings['min_profile_percentage_help'] = 'Número porcentage ( > 0.00 y < 100.00)';
+$strings['secret_key'] = 'LLave secreta';
+$strings['terms_and_conditions'] = 'Términos y condiciones';
+
+/* String for error message about requirements */
+$strings['AdvancedSubscriptionNotConnected'] = "Usted no está conectado en la plataforma. Por favor ingrese su usuario / constraseña para poder inscribirse";
+$strings['AdvancedSubscriptionProfileIncomplete'] = "Debe llenar el %d porciento de tu perfil como mínimo. Por ahora has llenado el %d porciento";
+$strings['AdvancedSubscriptionIncompleteInduction'] = "Usted aún no ha completado el curso de inducción. Por favor complete el curso inducción";
+$strings['AdvancedSubscriptionCostXLimitReached'] = "Lo sentimos, usted ya ha alcanzado el límite anual de %s UIT para los cursos que ha seguido este año";
+$strings['AdvancedSubscriptionTimeXLimitReached'] = "Lo sentimos, usted ya ha alcanzado el límite anual de %s horas para los cursos que ha seguido este año";
+$strings['AdvancedSubscriptionCourseXLimitReached'] = "Lo sentimos, usted ya ha alcanzado el límite anual de %s cursos que ha seguido este año";
+$strings['AdvancedSubscriptionNotMoreAble'] = "Lo sentimos, usted ya no cumple con las condiciones iniciales para poder inscribirse al curso";
+$strings['AdvancedSubscriptionIncompleteParams'] = "Los parámetros enviados no están completos o no son los correctos.";
+$strings['AdvancedSubscriptionIsNotEnabled'] = "La inscripción avanzada no está activada";
+
+$strings['AdvancedSubscriptionNoQueue'] = "Usted no está inscrito para este curso.";
+$strings['AdvancedSubscriptionNoQueueIsAble'] = "Usted no está inscrito, pero está calificado para este curso.";
+$strings['AdvancedSubscriptionQueueStart'] = "Su solicitud de inscripción está pendiente de la aprobación de su jefe, por favor espere atentamente.";
+$strings['AdvancedSubscriptionQueueBossDisapproved'] = "Lo sentimos, tu inscripción fue rechazada por tu jefe.";
+$strings['AdvancedSubscriptionQueueBossApproved'] = "Tu solicitud de inscripción ha sido aceptado por tu jefe, ahora está en espera de vacantes.";
+$strings['AdvancedSubscriptionQueueAdminDisapproved'] = "Lo sentimos, Tu inscripción ha sido rechazada por el administrador.";
+$strings['AdvancedSubscriptionQueueAdminApproved'] = "¡Felicitaciones! Tu inscripción ha sido aceptada por el administrador.";
+$strings['AdvancedSubscriptionQueueDefaultX'] = "Hubo un error el estado en cola %d no está definido en el sistema.";
+
+// Mail translations
+$strings['MailStudentRequest'] = 'Solicitud de registro de estudiante';
+$strings['MailBossAccept'] = 'Solicitud de registro aceptada por superior';
+$strings['MailBossReject'] = 'Solicitud de registro rechazada por superior';
+$strings['MailStudentRequestSelect'] = 'Selección de solicitudes de registro de estudiante';
+$strings['MailAdminAccept'] = 'Solicitud de registro aceptada por administrador';
+$strings['MailAdminReject'] = 'Solicitud de registro rechazada por administrador';
+$strings['MailStudentRequestNoBoss'] = 'Solicitud de registro de estudiante sin superior';
+$strings['MailRemindStudent'] = 'Recordatorio de la solicitud de inscripción';
+$strings['MailRemindSuperior'] = 'Solicitudes de inscripción estan pendientes de tu aprobación';
+$strings['MailRemindAdmin'] = 'Inscripciones de cursos estan pendientes de tu aprobación';
+
+// TPL translations
+$strings['SessionXWithoutVacancies'] = "El curso \"%s\" no tiene cupos disponibles.";
+$strings['SuccessSubscriptionToSessionX'] = "
¡Felicitaciones!
Tu inscripción al curso \"%s\" se realizó correctamente.";
+$strings['SubscriptionToOpenSession'] = "Inscripcion a curso abierto";
+$strings['GoToSessionX'] = "Ir al curso \"%s\"";
+$strings['YouAreAlreadySubscribedToSessionX'] = "Usted ya está inscrito al curso \"%s\".";
+
+// Admin view
+$strings['SelectASession'] = 'Elija una sesión de formación';
+$strings['SessionName'] = 'Nombre de la sesión';
+$strings['Target'] = 'Publico objetivo';
+$strings['Vacancies'] = 'Vacantes';
+$strings['RecommendedNumberOfParticipants'] = 'Número recomendado de participantes por área';
+$strings['PublicationEndDate'] = 'Fecha fin de publicación';
+$strings['Mode'] = 'Modalidad';
+$strings['Postulant'] = 'Postulante';
+$strings['Area'] = 'Área';
+$strings['Institution'] = 'Institución';
+$strings['InscriptionDate'] = 'Fecha de inscripción';
+$strings['BossValidation'] = 'Validación del superior';
+$strings['Decision'] = 'Decisión';
+$strings['AdvancedSubscriptionAdminViewTitle'] = 'Resultado de confirmación de solicitud de inscripción';
+
+$strings['AcceptInfinitive'] = 'Aceptar';
+$strings['RejectInfinitive'] = 'Rechazar';
+$strings['AreYouSureYouWantToAcceptSubscriptionOfX'] = '¿Está seguro que quiere aceptar la inscripción de %s?';
+$strings['AreYouSureYouWantToRejectSubscriptionOfX'] = '¿Está seguro que quiere rechazar la inscripción de %s?';
+
+$strings['MailTitle'] = 'Solicitud recibida para el curso %s';
+$strings['MailDear'] = 'Estimado(a):';
+$strings['MailThankYou'] = 'Gracias.';
+$strings['MailThankYouCollaboration'] = 'Gracias por su colaboración.';
+
+// Admin Accept
+$strings['MailTitleAdminAcceptToAdmin'] = 'Información: Validación de inscripción recibida';
+$strings['MailContentAdminAcceptToAdmin'] = 'Hemos recibido y registrado su validación de la inscripción de %s al curso %s';
+$strings['MailTitleAdminAcceptToStudent'] = 'Aprobada: ¡Su inscripción al curso %s fue confirmada!';
+$strings['MailContentAdminAcceptToStudent'] = 'Nos complace informarle que su inscripción al curso %s iniciando el %s fue validada por los administradores. Esperamos mantenga todo su ánimo y participe en otro curso o, en otra oportunidad, a este mismo curso.';
+$strings['MailTitleAdminAcceptToSuperior'] = 'Información: Validación de inscripción de %s al curso %s';
+$strings['MailContentAdminAcceptToSuperior'] = 'La inscripción de %s al curso %s iniciando el %s, que estaba pendiente de validación por los organizadores del curso, fue validada hacen unos minutos. Esperamos nos ayude en asegurar la completa disponibilidad de su colaborador(a) para la duración completa del curso.';
+
+// Admin Reject
+$strings['MailTitleAdminRejectToAdmin'] = 'Información: rechazo de inscripción recibido';
+$strings['MailContentAdminRejectToAdmin'] = 'Hemos recibido y registrado su rechazo de la inscripción de %s al curso %s';
+$strings['MailTitleAdminRejectToStudent'] = 'Rechazamos su inscripción al curso %s';
+$strings['MailContentAdminRejectToStudent'] = 'Lamentamos informarle que su inscripción al curso %s iniciando el %s fue rechazada por falta de cupos. Esperamos mantenga todo su ánimo y participe en otro curso o, en otra oportunidad, a este mismo curso.';
+$strings['MailTitleAdminRejectToSuperior'] = 'Información: Rechazo de inscripción de %s al curso %s';
+$strings['MailContentAdminRejectToSuperior'] = 'La inscripción de %s al curso %s, que había aprobado anteriormente, fue rechazada por falta de cupos. Les presentamos nuestras disculpas sinceras.';
+
+// Superior Accept
+$strings['MailTitleSuperiorAcceptToAdmin'] = 'Aprobación de %s al curso %s ';
+$strings['MailContentSuperiorAcceptToAdmin'] = 'La inscripción del alumno %s al curso %s ha sido aprobada por su superior. Puede gestionar las inscripciones al curso aquí';
+$strings['MailTitleSuperiorAcceptToSuperior'] = 'Confirmación: Aprobación recibida para %s';
+$strings['MailContentSuperiorAcceptToSuperior'] = 'Hemos recibido y registrado su decisión de aprobar el curso %s para su colaborador %s';
+$strings['MailContentSuperiorAcceptToSuperiorSecond'] = 'Ahora la inscripción al curso está pendiente de la disponibilidad de cupos. Le mantendremos informado sobre el resultado de esta etapa';
+$strings['MailTitleSuperiorAcceptToStudent'] = 'Aprobado: Su inscripción al curso %s ha sido aprobada por su superior ';
+$strings['MailContentSuperiorAcceptToStudent'] = 'Nos complace informarle que su inscripción al curso %s ha sido aprobada por su superior. Su inscripción ahora solo se encuentra pendiente de disponibilidad de cupos. Le avisaremos tan pronto como se confirme este último paso.';
+
+// Superior Reject
+$strings['MailTitleSuperiorRejectToStudent'] = 'Información: Su inscripción al curso %s ha sido rechazada ';
+$strings['MailContentSuperiorRejectToStudent'] = 'Lamentamos informarle que, en esta oportunidad, su inscripción al curso %s NO ha sido aprobada. Esperamos mantenga todo su ánimo y participe en otro curso o, en otra oportunidad, a este mismo curso.';
+$strings['MailTitleSuperiorRejectToSuperior'] = 'Confirmación: Desaprobación recibida para %s';
+$strings['MailContentSuperiorRejectToSuperior'] = 'Hemos recibido y registrado su decisión de desaprobar el curso %s para su colaborador %s';
+
+// Student Request
+$strings['MailTitleStudentRequestToStudent'] = 'Información: Validación de inscripción recibida';
+$strings['MailContentStudentRequestToStudent'] = 'Hemos recibido y registrado su solicitud de inscripción al curso %s para iniciarse el %s.';
+$strings['MailContentStudentRequestToStudentSecond'] = 'Su inscripción es pendiente primero de la aprobación de su superior, y luego de la disponibilidad de cupos. Un correo ha sido enviado a su superior para revisión y aprobación de su solicitud.';
+$strings['MailTitleStudentRequestToSuperior'] = 'Solicitud de consideración de curso para un colaborador';
+$strings['MailContentStudentRequestToSuperior'] = 'Hemos recibido una solicitud de inscripción de %s al curso %s, por iniciarse el %s. Detalles del curso: %s.';
+$strings['MailContentStudentRequestToSuperiorSecond'] = 'Le invitamos a aprobar o desaprobar esta inscripción, dando clic en el botón correspondiente a continuación.';
+
+// Student Request No Boss
+$strings['MailTitleStudentRequestNoSuperiorToStudent'] = 'Solicitud recibida para el curso %s';
+$strings['MailContentStudentRequestNoSuperiorToStudent'] = 'Hemos recibido y registrado su solicitud de inscripción al curso %s para iniciarse el %s.';
+$strings['MailContentStudentRequestNoSuperiorToStudentSecond'] = 'Su inscripción es pendiente de la disponibilidad de cupos. Pronto recibirá los resultados de su aprobación de su solicitud.';
+$strings['MailTitleStudentRequestNoSuperiorToAdmin'] = 'Solicitud de inscripción de %s para el curso %s';
+$strings['MailContentStudentRequestNoSuperiorToAdmin'] = 'La inscripción del alumno %s al curso %s ha sido aprobada por defecto, a falta de superior. Puede gestionar las inscripciones al curso aquí';
+
+// Reminders
+$strings['MailTitleReminderAdmin'] = 'Inscripciones a %s pendiente de confirmación';
+$strings['MailContentReminderAdmin'] = 'Las inscripciones siguientes al curso %s están pendientes de validación para ser efectivas. Por favor, dirigese a la página de administración para validarlos.';
+$strings['MailTitleReminderStudent'] = 'Información: Solicitud pendiente de aprobación para el curso %s';
+$strings['MailContentReminderStudent'] = 'Este correo es para confirmar que hemos recibido y registrado su solicitud de inscripción al curso %s, por iniciarse el %s.';
+$strings['MailContentReminderStudentSecond'] = 'Su inscripción todavía no ha sido aprobada por su superior, por lo que hemos vuelto a enviarle un correo electrónico de recordatorio.';
+$strings['MailTitleReminderSuperior'] = 'Solicitud de consideración de curso para un colaborador';
+$strings['MailContentReminderSuperior'] = 'Le recordamos que hemos recibido las siguientes solicitudes de suscripción para el curso %s de parte de sus colaboradores. El curso se iniciará el %s. Detalles del curso: %s.';
+$strings['MailContentReminderSuperiorSecond'] = 'Le invitamos a aprobar o desaprobar las suscripciones, dando clic en el botón correspondiente a continuación para cada colaborador.';
+$strings['MailTitleReminderMaxSuperior'] = 'Recordatorio: Solicitud de consideración de curso para colaborador(es)';
+$strings['MailContentReminderMaxSuperior'] = 'Le recordamos que hemos recibido las siguientes solicitudes de suscripción al curso %s de parte de sus colaboradores. El curso se iniciará el %s. Detalles del curso: %s.';
+$strings['MailContentReminderMaxSuperiorSecond'] = 'Este curso tiene una cantidad de cupos limitados y ha recibido una alta tasa de solicitudes de inscripción, por lo que recomendamos que cada área apruebe un máximo de %s candidatos. Le invitamos a aprobar o desaprobar las suscripciones, dando clic en el botón correspondiente a continuación para cada colaborador.';
+
+$strings['YouMustAcceptTermsAndConditions'] = 'Para inscribirse al curso %s, debe aceptar estos términos y condiciones.';
diff --git a/plugin/advanced_subscription/license.txt b/plugin/advanced_subscription/license.txt
new file mode 100644
index 000000000..a7619191f
--- /dev/null
+++ b/plugin/advanced_subscription/license.txt
@@ -0,0 +1 @@
+This plugin, as the rest of Chamilo, is released under the GNU/GPLv3 license.
diff --git a/plugin/advanced_subscription/plugin.php b/plugin/advanced_subscription/plugin.php
new file mode 100644
index 000000000..89cc82a6a
--- /dev/null
+++ b/plugin/advanced_subscription/plugin.php
@@ -0,0 +1,13 @@
+Plugins).
+ *
+ * @package chamilo.plugin.advanced_subscription
+ */
+/**
+ * Plugin details (must be present).
+ */
+require_once __DIR__.'/config.php';
+$plugin_info = AdvancedSubscriptionPlugin::create()->get_info();
diff --git a/plugin/advanced_subscription/src/AdvancedSubscriptionPlugin.php b/plugin/advanced_subscription/src/AdvancedSubscriptionPlugin.php
new file mode 100644
index 000000000..35f26d4e1
--- /dev/null
+++ b/plugin/advanced_subscription/src/AdvancedSubscriptionPlugin.php
@@ -0,0 +1,1534 @@
+ 'text',
+ 'yearly_hours_limit' => 'text',
+ 'yearly_cost_unit_converter' => 'text',
+ 'courses_count_limit' => 'text',
+ 'course_session_credit_year_start_date' => 'text',
+ 'ws_url' => 'text',
+ 'min_profile_percentage' => 'text',
+ 'check_induction' => 'boolean',
+ 'secret_key' => 'text',
+ 'terms_and_conditions' => 'wysiwyg',
+ ];
+
+ parent::__construct('1.0', 'Imanol Losada, Daniel Barreto', $parameters);
+ $this->errorMessages = [];
+ }
+
+ /**
+ * Instance the plugin.
+ *
+ * @staticvar null $result
+ *
+ * @return AdvancedSubscriptionPlugin
+ */
+ public static function create()
+ {
+ static $result = null;
+
+ return $result ? $result : $result = new self();
+ }
+
+ /**
+ * Install the plugin.
+ */
+ public function install()
+ {
+ $this->installDatabase();
+ $this->addAreaField();
+ $this->installHook();
+ }
+
+ /**
+ * Uninstall the plugin.
+ */
+ public function uninstall()
+ {
+ $setting = api_get_setting('advanced_subscription');
+ if (!empty($setting)) {
+ $this->uninstallHook();
+ // Note: Keeping area field data is intended so it will not be removed
+ $this->uninstallDatabase();
+ }
+ }
+
+ /**
+ * Get the error messages list.
+ *
+ * @return array The message list
+ */
+ public function getErrorMessages()
+ {
+ return $this->errorMessages;
+ }
+
+ /**
+ * Check if is allowed subscribe to open session.
+ *
+ * @param array $params WS params
+ *
+ * @return bool
+ */
+ public function isAllowedSubscribeToOpenSession($params)
+ {
+ $self = self::create();
+ $wsUrl = $self->get('ws_url');
+ $profileCompleted = 0;
+ if (!empty($wsUrl)) {
+ $client = new SoapClient(
+ null,
+ ['location' => $wsUrl, 'uri' => $wsUrl]
+ );
+ $userInfo = api_get_user_info(
+ $params['user_id'],
+ false,
+ false,
+ true
+ );
+
+ try {
+ $profileCompleted = $client->__soapCall(
+ 'getProfileCompletionPercentage',
+ $userInfo['extra']['drupal_user_id']
+ );
+ } catch (\Exception $e) {
+ $profileCompleted = 0;
+ }
+ } elseif (isset($params['profile_completed'])) {
+ $profileCompleted = (float) $params['profile_completed'];
+ }
+ $profileCompletedMin = (float) $self->get('min_profile_percentage');
+
+ if ($profileCompleted < $profileCompletedMin) {
+ $this->errorMessages[] = sprintf(
+ $this->get_lang('AdvancedSubscriptionProfileIncomplete'),
+ $profileCompletedMin,
+ $profileCompleted
+ );
+ }
+
+ $vacancy = $self->getVacancy($params['session_id']);
+ $sessionInfo = api_get_session_info($params['session_id']);
+
+ if ($sessionInfo['nbr_users'] >= $vacancy) {
+ $this->errorMessages[] = sprintf(
+ $this->get_lang('SessionXWithoutVacancies'),
+ $sessionInfo['name']
+ );
+ }
+
+ return empty($this->errorMessages);
+ }
+
+ /**
+ * Return true if user is allowed to be added to queue for session subscription.
+ *
+ * @param int $userId
+ * @param array $params MUST have keys:
+ * "is_connected" Indicate if the user is online on external web
+ * "profile_completed" Percentage of completed profile, given by WS
+ * @param bool $collectErrors Optional. Default is false. Whether collect all errors or throw exeptions
+ *
+ * @throws Exception
+ *
+ * @return bool
+ */
+ public function isAllowedToDoRequest($userId, $params = [], $collectErrors = false)
+ {
+ $plugin = self::create();
+ $wsUrl = $plugin->get('ws_url');
+ // Student always is connected
+ $isConnected = true;
+
+ if (!$isConnected) {
+ $this->errorMessages[] = $this->get_lang('AdvancedSubscriptionNotConnected');
+
+ if (!$collectErrors) {
+ throw new \Exception($this->get_lang('AdvancedSubscriptionNotConnected'));
+ }
+ }
+
+ $profileCompletedMin = (float) $plugin->get('min_profile_percentage');
+ $profileCompleted = 0;
+
+ if (is_string($wsUrl) && !empty($wsUrl)) {
+ $options = [
+ 'location' => $wsUrl,
+ 'uri' => $wsUrl,
+ ];
+ $client = new SoapClient(null, $options);
+ $userInfo = api_get_user_info($userId);
+ try {
+ $profileCompleted = $client->__soapCall('getProfileCompletionPercentage', $userInfo['extra']['drupal_user_id']);
+ } catch (\Exception $e) {
+ $profileCompleted = 0;
+ }
+ } elseif (isset($params['profile_completed'])) {
+ $profileCompleted = (float) $params['profile_completed'];
+ }
+
+ if ($profileCompleted < $profileCompletedMin) {
+ $errorMessage = sprintf(
+ $this->get_lang('AdvancedSubscriptionProfileIncomplete'),
+ $profileCompletedMin,
+ $profileCompleted
+ );
+
+ $this->errorMessages[] = $errorMessage;
+
+ if (!$collectErrors) {
+ throw new \Exception($errorMessage);
+ }
+ }
+
+ $yearlyCostLimit = $plugin->get('yearly_cost_limit');
+ $maxCost = $plugin->get('yearly_cost_unit_converter');
+ $maxCost *= $yearlyCostLimit;
+ $userCost = 0;
+ $now = new DateTime(api_get_utc_datetime());
+ $newYearDate = $plugin->get('course_session_credit_year_start_date');
+ $newYearDate = !empty($newYearDate) ?
+ new \DateTime($newYearDate.$now->format('/Y')) : $now;
+ $extra = new ExtraFieldValue('session');
+ $joinSessionTable = Database::get_main_table(TABLE_MAIN_SESSION_USER).' su INNER JOIN '.
+ Database::get_main_table(TABLE_MAIN_SESSION).' s ON s.id = su.session_id';
+ $whereSessionParams = 'su.relation_type = ? AND s.access_start_date >= ? AND su.user_id = ?';
+ $whereSessionParamsValues = [
+ 0,
+ $newYearDate->format('Y-m-d'),
+ $userId,
+ ];
+ $whereSession = [
+ 'where' => [
+ $whereSessionParams => $whereSessionParamsValues,
+ ],
+ ];
+ $selectSession = 's.id AS id';
+ $sessions = Database::select(
+ $selectSession,
+ $joinSessionTable,
+ $whereSession
+ );
+
+ $expendedTimeMax = $plugin->get('yearly_hours_limit');
+ $expendedTime = 0;
+
+ if (is_array($sessions) && count($sessions) > 0) {
+ foreach ($sessions as $session) {
+ $costField = $extra->get_values_by_handler_and_field_variable($session['id'], 'cost');
+ $userCost += $costField['value'];
+ $teachingHoursField = $extra->get_values_by_handler_and_field_variable($session['id'], 'teaching_hours');
+ $expendedTime += $teachingHoursField['value'];
+ }
+ }
+
+ if (isset($params['sessionId'])) {
+ $costField = $extra->get_values_by_handler_and_field_variable($params['sessionId'], 'cost');
+ $userCost += $costField['value'];
+
+ $teachingHoursField = $extra->get_values_by_handler_and_field_variable($params['sessionId'], 'teaching_hours');
+ $expendedTime += $teachingHoursField['value'];
+ }
+
+ if ($maxCost <= $userCost) {
+ $errorMessage = sprintf(
+ $this->get_lang('AdvancedSubscriptionCostXLimitReached'),
+ $yearlyCostLimit
+ );
+
+ $this->errorMessages[] = $errorMessage;
+
+ if (!$collectErrors) {
+ throw new \Exception($errorMessage);
+ }
+ }
+
+ if ($expendedTimeMax <= $expendedTime) {
+ $errorMessage = sprintf(
+ $this->get_lang('AdvancedSubscriptionTimeXLimitReached'),
+ $expendedTimeMax
+ );
+
+ $this->errorMessages[] = $errorMessage;
+
+ if (!$collectErrors) {
+ throw new \Exception($errorMessage);
+ }
+ }
+
+ $expendedNumMax = $plugin->get('courses_count_limit');
+ $expendedNum = count($sessions);
+
+ if ($expendedNumMax <= $expendedNum) {
+ $errorMessage = sprintf(
+ $this->get_lang('AdvancedSubscriptionCourseXLimitReached'),
+ $expendedNumMax
+ );
+
+ $this->errorMessages[] = $errorMessage;
+
+ if (!$collectErrors) {
+ throw new \Exception($errorMessage);
+ }
+ }
+
+ $checkInduction = $plugin->get('check_induction');
+ $numberOfApprovedInductionSessions = $this->getApprovedInductionSessions($userId);
+ $completedInduction = $numberOfApprovedInductionSessions > 0;
+
+ if ($checkInduction == 'true' && !$completedInduction) {
+ $this->errorMessages[] = $this->get_lang('AdvancedSubscriptionIncompleteInduction');
+
+ if (!$collectErrors) {
+ throw new \Exception($this->get_lang('AdvancedSubscriptionIncompleteInduction'));
+ }
+ }
+
+ return empty($this->errorMessages);
+ }
+
+ /**
+ * Register a user into a queue for a session.
+ *
+ * @param $userId
+ * @param $sessionId
+ *
+ * @return bool|int
+ */
+ public function addToQueue($userId, $sessionId)
+ {
+ // Filter input variables
+ $userId = intval($userId);
+ $sessionId = intval($sessionId);
+ $now = api_get_utc_datetime();
+ $advancedSubscriptionQueueTable = Database::get_main_table(TABLE_ADVANCED_SUBSCRIPTION_QUEUE);
+ $attributes = [
+ 'session_id' => $sessionId,
+ 'user_id' => $userId,
+ 'status' => 0,
+ 'created_at' => $now,
+ 'updated_at' => null,
+ ];
+
+ $id = Database::insert($advancedSubscriptionQueueTable, $attributes);
+
+ return $id;
+ }
+
+ /**
+ * Register message with type and status.
+ *
+ * @param $mailId
+ * @param $userId
+ * @param $sessionId
+ *
+ * @return bool|int
+ */
+ public function saveLastMessage($mailId, $userId, $sessionId)
+ {
+ // Filter variables
+ $mailId = intval($mailId);
+ $userId = intval($userId);
+ $sessionId = intval($sessionId);
+ $queueTable = Database::get_main_table(TABLE_ADVANCED_SUBSCRIPTION_QUEUE);
+ $attributes = [
+ 'last_message_id' => $mailId,
+ 'updated_at' => api_get_utc_datetime(),
+ ];
+
+ $num = Database::update(
+ $queueTable,
+ $attributes,
+ ['user_id = ? AND session_id = ?' => [$userId, $sessionId]]
+ );
+
+ return $num;
+ }
+
+ /**
+ * Check for requirements and register user into queue.
+ *
+ * @param $userId
+ * @param $sessionId
+ * @param $params
+ *
+ * @return bool|string
+ */
+ public function startSubscription($userId, $sessionId, $params)
+ {
+ $result = 'Params not found';
+ if (!empty($sessionId) && !empty($userId)) {
+ $plugin = self::create();
+ try {
+ if ($plugin->isAllowedToDoRequest($userId, $params)) {
+ $result = (bool) $plugin->addToQueue($userId, $sessionId);
+ } else {
+ throw new \Exception($this->get_lang('AdvancedSubscriptionNotMoreAble'));
+ }
+ } catch (Exception $e) {
+ $result = $e->getMessage();
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Send message for the student subscription approval to a specific session.
+ *
+ * @param int|array $studentId
+ * @param int $receiverId
+ * @param string $subject
+ * @param string $content
+ * @param int $sessionId
+ * @param bool $save
+ * @param array $fileAttachments
+ *
+ * @return bool|int
+ */
+ public function sendMailMessage(
+ $studentId,
+ $receiverId,
+ $subject,
+ $content,
+ $sessionId,
+ $save = false,
+ $fileAttachments = []
+ ) {
+ if (!empty($fileAttachments) &&
+ is_array($fileAttachments) &&
+ isset($fileAttachments['files']) &&
+ isset($fileAttachments['comments'])
+ ) {
+ $mailId = MessageManager::send_message(
+ $receiverId,
+ $subject,
+ $content,
+ $fileAttachments['files'],
+ $fileAttachments['comments']
+ );
+ } else {
+ $mailId = MessageManager::send_message(
+ $receiverId,
+ $subject,
+ $content
+ );
+ }
+
+ if ($save && !empty($mailId)) {
+ // Save as sent message
+ if (is_array($studentId) && !empty($studentId)) {
+ foreach ($studentId as $student) {
+ $this->saveLastMessage($mailId, $student['user_id'], $sessionId);
+ }
+ } else {
+ $studentId = intval($studentId);
+ $this->saveLastMessage($mailId, $studentId, $sessionId);
+ }
+ } elseif (!empty($mailId)) {
+ // Update queue row, updated_at
+ Database::update(
+ Database::get_main_table(TABLE_ADVANCED_SUBSCRIPTION_QUEUE),
+ [
+ 'updated_at' => api_get_utc_datetime(),
+ ],
+ [
+ 'user_id = ? AND session_id = ?' => [$studentId, $sessionId],
+ ]
+ );
+ }
+
+ return $mailId;
+ }
+
+ /**
+ * Check if session is open for subscription.
+ *
+ * @param $sessionId
+ * @param string $fieldVariable
+ *
+ * @return bool
+ */
+ public function isSessionOpen($sessionId, $fieldVariable = 'is_open_session')
+ {
+ $extraFieldValue = new ExtraFieldValue('session');
+ $result = $extraFieldValue->get_values_by_handler_and_field_variable(
+ $sessionId,
+ $fieldVariable
+ );
+
+ $isOpen = false;
+ if (!empty($result)) {
+ $isOpen = (bool) $result['value'];
+ }
+
+ return $isOpen;
+ }
+
+ /**
+ * Check if user is in the session's target group based on its area.
+ *
+ * @param $userId
+ * @param $sessionId
+ * @param string $userFieldVariable
+ * @param string $sessionFieldVariable
+ *
+ * @return bool
+ */
+ public function isUserInTargetGroup(
+ $userId,
+ $sessionId,
+ $userFieldVariable = 'area',
+ $sessionFieldVariable = 'target'
+ ) {
+ $extraSessionFieldValue = new ExtraFieldValue('session');
+ $sessionTarget = $extraSessionFieldValue->get_values_by_handler_and_field_variable(
+ $sessionId,
+ $sessionFieldVariable
+ );
+ $extraUserFieldValue = new ExtraFieldValue('user');
+ $userArea = $extraUserFieldValue->get_values_by_handler_and_field_variable(
+ $userId,
+ $userFieldVariable
+ );
+ $isInTargetGroup = false;
+ if (isset($sessionTarget) && (!empty($sessionTarget)) && $sessionTarget['value'] == 'minedu') {
+ if (substr($userArea['value'], 0, 6) == 'MINEDU') {
+ $isInTargetGroup = true;
+ }
+ }
+ if (isset($sessionTarget) && (!empty($sessionTarget)) && $sessionTarget['value'] == 'regiones') {
+ if ((substr($userArea['value'], 0, 4) == 'UGEL') || (substr($userArea['value'], 0, 3) == 'DRE')) {
+ $isInTargetGroup = true;
+ }
+ }
+
+ return $isInTargetGroup;
+ }
+
+ /**
+ * Update the queue status for subscription approval rejected or accepted.
+ *
+ * @param $params
+ * @param $newStatus
+ *
+ * @return bool
+ */
+ public function updateQueueStatus($params, $newStatus)
+ {
+ $newStatus = intval($newStatus);
+ $res = false;
+
+ if (isset($params['queue']['id'])) {
+ $where = [
+ 'id = ?' => intval($params['queue']['id']),
+ ];
+ } elseif (isset($params['studentUserId']) && isset($params['sessionId'])) {
+ $where = [
+ 'user_id = ? AND session_id = ? AND status <> ? AND status <> ?' => [
+ intval($params['studentUserId']),
+ intval($params['sessionId']),
+ $newStatus,
+ ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED,
+ ],
+ ];
+ }
+ if (isset($where)) {
+ $res = (bool) Database::update(
+ Database::get_main_table(TABLE_ADVANCED_SUBSCRIPTION_QUEUE),
+ [
+ 'status' => $newStatus,
+ 'updated_at' => api_get_utc_datetime(),
+ ],
+ $where
+ );
+ }
+
+ return $res;
+ }
+
+ /**
+ * Render and send mail by defined advanced subscription action.
+ *
+ * @param $data
+ * @param $actionType
+ *
+ * @return array
+ */
+ public function sendMail($data, $actionType)
+ {
+ $template = new Template($this->get_lang('plugin_title'));
+ $template->assign('data', $data);
+ $templateParams = [
+ 'user',
+ 'student',
+ 'students',
+ 'superior',
+ 'admins',
+ 'session',
+ 'signature',
+ 'admin_view_url',
+ 'acceptUrl',
+ 'rejectUrl',
+ ];
+ foreach ($templateParams as $templateParam) {
+ $template->assign($templateParam, $data[$templateParam]);
+ }
+ $mailIds = [];
+ switch ($actionType) {
+ case ADVANCED_SUBSCRIPTION_ACTION_STUDENT_REQUEST:
+ // Mail to student
+ $mailIds['render'] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['student']['user_id'],
+ $this->get_lang('MailStudentRequest'),
+ $template->fetch('/advanced_subscription/views/student_notice_student.tpl'),
+ $data['sessionId'],
+ true
+ );
+ // Mail to superior
+ $mailIds[] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['superior']['user_id'],
+ $this->get_lang('MailStudentRequest'),
+ $template->fetch('/advanced_subscription/views/student_notice_superior.tpl'),
+ $data['sessionId']
+ );
+ break;
+ case ADVANCED_SUBSCRIPTION_ACTION_SUPERIOR_APPROVE:
+ // Mail to student
+ $mailIds[] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['student']['user_id'],
+ $this->get_lang('MailBossAccept'),
+ $template->fetch('/advanced_subscription/views/superior_accepted_notice_student.tpl'),
+ $data['sessionId'],
+ true
+ );
+ // Mail to superior
+ $mailIds['render'] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['superior']['user_id'],
+ $this->get_lang('MailBossAccept'),
+ $template->fetch('/advanced_subscription/views/superior_accepted_notice_superior.tpl'),
+ $data['sessionId']
+ );
+ // Mail to admin
+ foreach ($data['admins'] as $adminId => $admin) {
+ $template->assign('admin', $admin);
+ $mailIds[] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $adminId,
+ $this->get_lang('MailBossAccept'),
+ $template->fetch('/advanced_subscription/views/superior_accepted_notice_admin.tpl'),
+ $data['sessionId']
+ );
+ }
+ break;
+ case ADVANCED_SUBSCRIPTION_ACTION_SUPERIOR_DISAPPROVE:
+ // Mail to student
+ $mailIds[] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['student']['user_id'],
+ $this->get_lang('MailBossReject'),
+ $template->fetch('/advanced_subscription/views/superior_rejected_notice_student.tpl'),
+ $data['sessionId'],
+ true
+ );
+ // Mail to superior
+ $mailIds['render'] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['superior']['user_id'],
+ $this->get_lang('MailBossReject'),
+ $template->fetch('/advanced_subscription/views/superior_rejected_notice_superior.tpl'),
+ $data['sessionId']
+ );
+ break;
+ case ADVANCED_SUBSCRIPTION_ACTION_SUPERIOR_SELECT:
+ // Mail to student
+ $mailIds[] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['student']['user_id'],
+ $this->get_lang('MailStudentRequestSelect'),
+ $template->fetch('/advanced_subscription/views/student_notice_student.tpl'),
+ $data['sessionId'],
+ true
+ );
+ // Mail to superior
+ $mailIds['render'] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['superior']['user_id'],
+ $this->get_lang('MailStudentRequestSelect'),
+ $template->fetch('/advanced_subscription/views/student_notice_superior.tpl'),
+ $data['sessionId']
+ );
+ break;
+ case ADVANCED_SUBSCRIPTION_ACTION_ADMIN_APPROVE:
+ $fileAttachments = [];
+ if (api_get_plugin_setting('courselegal', 'tool_enable')) {
+ $courseLegal = CourseLegalPlugin::create();
+ $courses = SessionManager::get_course_list_by_session_id($data['sessionId']);
+ $course = current($courses);
+ $data['courseId'] = $course['id'];
+ $data['course'] = api_get_course_info_by_id($data['courseId']);
+ $termsAndConditions = $courseLegal->getData($data['courseId'], $data['sessionId']);
+ $termsAndConditions = $termsAndConditions['content'];
+ $termsAndConditions = $this->renderTemplateString($termsAndConditions, $data);
+ $tpl = new Template(get_lang('TermsAndConditions'));
+ $tpl->assign('session', $data['session']);
+ $tpl->assign('student', $data['student']);
+ $tpl->assign('sessionId', $data['sessionId']);
+ $tpl->assign('termsContent', $termsAndConditions);
+ $termsAndConditions = $tpl->fetch('/advanced_subscription/views/terms_and_conditions_to_pdf.tpl');
+ $pdf = new PDF();
+ $filename = 'terms'.sha1(rand(0, 99999));
+ $pdf->content_to_pdf($termsAndConditions, null, $filename, null, 'F');
+ $fileAttachments['file'][] = [
+ 'name' => $filename.'.pdf',
+ 'application/pdf' => $filename.'.pdf',
+ 'tmp_name' => api_get_path(SYS_ARCHIVE_PATH).$filename.'.pdf',
+ 'error' => UPLOAD_ERR_OK,
+ 'size' => filesize(api_get_path(SYS_ARCHIVE_PATH).$filename.'.pdf'),
+ ];
+ $fileAttachments['comments'][] = get_lang('TermsAndConditions');
+ }
+ // Mail to student
+ $mailIds[] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['student']['user_id'],
+ $this->get_lang('MailAdminAccept'),
+ $template->fetch('/advanced_subscription/views/admin_accepted_notice_student.tpl'),
+ $data['sessionId'],
+ true,
+ $fileAttachments
+ );
+ // Mail to superior
+ $mailIds[] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['superior']['user_id'],
+ $this->get_lang('MailAdminAccept'),
+ $template->fetch('/advanced_subscription/views/admin_accepted_notice_superior.tpl'),
+ $data['sessionId']
+ );
+ // Mail to admin
+ $adminId = $data['currentUserId'];
+ $template->assign('admin', $data['admins'][$adminId]);
+ $mailIds['render'] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $adminId,
+ $this->get_lang('MailAdminAccept'),
+ $template->fetch('/advanced_subscription/views/admin_accepted_notice_admin.tpl'),
+ $data['sessionId']
+ );
+ break;
+ case ADVANCED_SUBSCRIPTION_ACTION_ADMIN_DISAPPROVE:
+ // Mail to student
+ $mailIds[] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['student']['user_id'],
+ $this->get_lang('MailAdminReject'),
+ $template->fetch('/advanced_subscription/views/admin_rejected_notice_student.tpl'),
+ $data['sessionId'],
+ true
+ );
+ // Mail to superior
+ $mailIds[] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['superior']['user_id'],
+ $this->get_lang('MailAdminReject'),
+ $template->fetch('/advanced_subscription/views/admin_rejected_notice_superior.tpl'),
+ $data['sessionId']
+ );
+ // Mail to admin
+ $adminId = $data['currentUserId'];
+ $template->assign('admin', $data['admins'][$adminId]);
+ $mailIds['render'] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $adminId,
+ $this->get_lang('MailAdminReject'),
+ $template->fetch('/advanced_subscription/views/admin_rejected_notice_admin.tpl'),
+ $data['sessionId']
+ );
+ break;
+ case ADVANCED_SUBSCRIPTION_ACTION_STUDENT_REQUEST_NO_BOSS:
+ // Mail to student
+ $mailIds['render'] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $data['student']['user_id'],
+ $this->get_lang('MailStudentRequestNoBoss'),
+ $template->fetch('/advanced_subscription/views/student_no_superior_notice_student.tpl'),
+ $data['sessionId'],
+ true
+ );
+ // Mail to admin
+ foreach ($data['admins'] as $adminId => $admin) {
+ $template->assign('admin', $admin);
+ $mailIds[] = $this->sendMailMessage(
+ $data['studentUserId'],
+ $adminId,
+ $this->get_lang('MailStudentRequestNoBoss'),
+ $template->fetch('/advanced_subscription/views/student_no_superior_notice_admin.tpl'),
+ $data['sessionId']
+ );
+ }
+ break;
+ case ADVANCED_SUBSCRIPTION_ACTION_REMINDER_STUDENT:
+ $mailIds['render'] = $this->sendMailMessage(
+ $data['student']['user_id'],
+ $data['student']['user_id'],
+ $this->get_lang('MailRemindStudent'),
+ $template->fetch('/advanced_subscription/views/reminder_notice_student.tpl'),
+ $data['sessionId'],
+ true
+ );
+ break;
+ case ADVANCED_SUBSCRIPTION_ACTION_REMINDER_SUPERIOR:
+ $mailIds['render'] = $this->sendMailMessage(
+ $data['students'],
+ $data['superior']['user_id'],
+ $this->get_lang('MailRemindSuperior'),
+ $template->fetch('/advanced_subscription/views/reminder_notice_superior.tpl'),
+ $data['sessionId']
+ );
+ break;
+ case ADVANCED_SUBSCRIPTION_ACTION_REMINDER_SUPERIOR_MAX:
+ $mailIds['render'] = $this->sendMailMessage(
+ $data['students'],
+ $data['superior']['user_id'],
+ $this->get_lang('MailRemindSuperior'),
+ $template->fetch('/advanced_subscription/views/reminder_notice_superior_max.tpl'),
+ $data['sessionId']
+ );
+ break;
+ case ADVANCED_SUBSCRIPTION_ACTION_REMINDER_ADMIN:
+ // Mail to admin
+ foreach ($data['admins'] as $adminId => $admin) {
+ $template->assign('admin', $admin);
+ $mailIds[] = $this->sendMailMessage(
+ $data['students'],
+ $adminId,
+ $this->get_lang('MailRemindAdmin'),
+ $template->fetch('/advanced_subscription/views/reminder_notice_admin.tpl'),
+ $data['sessionId']
+ );
+ }
+ break;
+ default:
+ break;
+ }
+
+ return $mailIds;
+ }
+
+ /**
+ * Count the users in queue filtered by params (sessions, status).
+ *
+ * @param array $params Input array containing the set of
+ * session and status to count from queue
+ * e.g:
+ * array('sessions' => array(215, 218, 345, 502),
+ * 'status' => array(0, 1, 2))
+ *
+ * @return int
+ */
+ public function countQueueByParams($params)
+ {
+ $count = 0;
+ if (!empty($params) && is_array($params)) {
+ $advancedSubscriptionQueueTable = Database::get_main_table(TABLE_ADVANCED_SUBSCRIPTION_QUEUE);
+ $where['1 = ? '] = 1;
+ if (isset($params['sessions']) && is_array($params['sessions'])) {
+ foreach ($params['sessions'] as &$sessionId) {
+ $sessionId = intval($sessionId);
+ }
+ $where['AND session_id IN ( ? ) '] = implode($params['sessions']);
+ }
+ if (isset($params['status']) && is_array($params['status'])) {
+ foreach ($params['status'] as &$status) {
+ $status = intval($status);
+ }
+ $where['AND status IN ( ? ) '] = implode($params['status']);
+ }
+ $where['where'] = $where;
+ $count = Database::select('COUNT(*)', $advancedSubscriptionQueueTable, $where);
+ $count = $count[0]['COUNT(*)'];
+ }
+
+ return $count;
+ }
+
+ /**
+ * This method will call the Hook management insertHook to add Hook observer from this plugin.
+ */
+ public function installHook()
+ {
+ $hookObserver = HookAdvancedSubscription::create();
+ HookAdminBlock::create()->attach($hookObserver);
+ HookWSRegistration::create()->attach($hookObserver);
+ HookNotificationContent::create()->attach($hookObserver);
+ HookNotificationTitle::create()->attach($hookObserver);
+ }
+
+ /**
+ * This method will call the Hook management deleteHook to disable Hook observer from this plugin.
+ */
+ public function uninstallHook()
+ {
+ $hookObserver = HookAdvancedSubscription::create();
+ HookAdminBlock::create()->detach($hookObserver);
+ HookWSRegistration::create()->detach($hookObserver);
+ HookNotificationContent::create()->detach($hookObserver);
+ HookNotificationTitle::create()->detach($hookObserver);
+ }
+
+ /**
+ * Return the status from user in queue to session subscription.
+ *
+ * @param int $userId
+ * @param int $sessionId
+ *
+ * @return bool|int
+ */
+ public function getQueueStatus($userId, $sessionId)
+ {
+ $userId = intval($userId);
+ $sessionId = intval($sessionId);
+ if (!empty($userId) && !empty($sessionId)) {
+ $queueTable = Database::get_main_table(TABLE_ADVANCED_SUBSCRIPTION_QUEUE);
+ $row = Database::select(
+ 'status',
+ $queueTable,
+ [
+ 'where' => [
+ 'user_id = ? AND session_id = ?' => [$userId, $sessionId],
+ ],
+ ]
+ );
+
+ if (count($row) == 1) {
+ return $row[0]['status'];
+ } else {
+ return ADVANCED_SUBSCRIPTION_QUEUE_STATUS_NO_QUEUE;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return the remaining vacancy.
+ *
+ * @param $sessionId
+ *
+ * @return bool|int
+ */
+ public function getVacancy($sessionId)
+ {
+ if (!empty($sessionId)) {
+ $extra = new ExtraFieldValue('session');
+ $var = $extra->get_values_by_handler_and_field_variable(
+ $sessionId,
+ 'vacancies'
+ );
+ $vacancy = intval($var['value']);
+ if (!empty($vacancy)) {
+ $vacancy -= $this->countQueueByParams(
+ [
+ 'sessions' => [$sessionId],
+ 'status' => [ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED],
+ ]
+ );
+ if ($vacancy >= 0) {
+ return $vacancy;
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return the session details data from a session ID (including the extra
+ * fields used for the advanced subscription mechanism).
+ *
+ * @param $sessionId
+ *
+ * @return bool|mixed
+ */
+ public function getSessionDetails($sessionId)
+ {
+ if (!empty($sessionId)) {
+ // Assign variables
+ $fieldsArray = [
+ 'code',
+ 'cost',
+ 'place',
+ 'allow_visitors',
+ 'teaching_hours',
+ 'brochure',
+ 'banner',
+ ];
+ $extraField = new ExtraField('session');
+ // Get session fields
+ $fieldList = $extraField->get_all([
+ 'variable IN ( ?, ?, ?, ?, ?, ?, ? )' => $fieldsArray,
+ ]);
+ // Index session fields
+ $fields = [];
+ foreach ($fieldList as $field) {
+ $fields[$field['id']] = $field['variable'];
+ }
+
+ $mergedArray = array_merge([$sessionId], array_keys($fields));
+
+ $sql = "SELECT * FROM ".Database::get_main_table(TABLE_EXTRA_FIELD_VALUES)."
+ WHERE item_id = %d AND field_id IN (%d, %d, %d, %d, %d, %d, %d)";
+ $sql = vsprintf($sql, $mergedArray);
+ $sessionFieldValueList = Database::query($sql);
+ while ($sessionFieldValue = Database::fetch_assoc($sessionFieldValueList)) {
+ // Check if session field value is set in session field list
+ if (isset($fields[$sessionFieldValue['field_id']])) {
+ $var = $fields[$sessionFieldValue['field_id']];
+ $val = $sessionFieldValue['value'];
+ // Assign session field value to session
+ $sessionArray[$var] = $val;
+ }
+ }
+ $sessionArray['description'] = SessionManager::getDescriptionFromSessionId($sessionId);
+
+ if (isset($sessionArray['brochure'])) {
+ $sessionArray['brochure'] = api_get_path(WEB_UPLOAD_PATH).$sessionArray['brochure'];
+ }
+ if (isset($sessionArray['banner'])) {
+ $sessionArray['banner'] = api_get_path(WEB_UPLOAD_PATH).$sessionArray['banner'];
+ }
+
+ return $sessionArray;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get status message.
+ *
+ * @param int $status
+ * @param bool $isAble
+ *
+ * @return string
+ */
+ public function getStatusMessage($status, $isAble = true)
+ {
+ switch ($status) {
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_NO_QUEUE:
+ $message = $this->get_lang('AdvancedSubscriptionNoQueue');
+ if ($isAble) {
+ $message = $this->get_lang('AdvancedSubscriptionNoQueueIsAble');
+ }
+ break;
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_START:
+ $message = $this->get_lang('AdvancedSubscriptionQueueStart');
+ break;
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_DISAPPROVED:
+ $message = $this->get_lang('AdvancedSubscriptionQueueBossDisapproved');
+ break;
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_APPROVED:
+ $message = $this->get_lang('AdvancedSubscriptionQueueBossApproved');
+ break;
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_DISAPPROVED:
+ $message = $this->get_lang('AdvancedSubscriptionQueueAdminDisapproved');
+ break;
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED:
+ $message = $this->get_lang('AdvancedSubscriptionQueueAdminApproved');
+ break;
+ default:
+ $message = sprintf($this->get_lang('AdvancedSubscriptionQueueDefault'), $status);
+ }
+
+ return $message;
+ }
+
+ /**
+ * Return the url to go to session.
+ *
+ * @param $sessionId
+ *
+ * @return string
+ */
+ public function getSessionUrl($sessionId)
+ {
+ $url = api_get_path(WEB_CODE_PATH).'session/?session_id='.intval($sessionId);
+
+ return $url;
+ }
+
+ /**
+ * Get a url for subscribe a user in session.
+ *
+ * @param int $userId The user ID
+ * @param array $params Params from WS
+ *
+ * @return string
+ */
+ public function getOpenSessionUrl($userId, $params)
+ {
+ $userIsSubscribed = SessionManager::isUserSubscribedAsStudent(
+ $params['session_id'],
+ $userId
+ );
+
+ if ($userIsSubscribed) {
+ return api_get_path(WEB_CODE_PATH)
+ .'session/index.php?session_id='
+ .intval($params['session_id']);
+ }
+
+ $params['secret_key'] = null;
+ $params['user_id'] = null;
+ $params['user_field'] = null;
+ $params['is_connected'] = null;
+
+ $urlParams = array_merge($params, ['user_id' => $userId]);
+
+ $url = api_get_path(WEB_PLUGIN_PATH);
+ $url .= 'advanced_subscription/src/open_session.php?';
+ $url .= http_build_query($urlParams);
+
+ return 'javascript:void(window.open(\''
+ .$url
+ .'\',\'AdvancedSubscriptionTerms\', \'toolbar=no,location=no,'
+ .'status=no,menubar=no,scrollbars=yes,resizable=yes,width=700px,'
+ .'height=600px\', \'100\' ))';
+ }
+
+ /**
+ * Return the url to enter to subscription queue to session.
+ *
+ * @param $params
+ *
+ * @return string
+ */
+ public function getQueueUrl($params)
+ {
+ $url = api_get_path(WEB_PLUGIN_PATH).'advanced_subscription/ajax/advanced_subscription.ajax.php?'.
+ 'a='.Security::remove_XSS($params['action']).'&'.
+ 's='.intval($params['sessionId']).'&'.
+ 'current_user_id='.intval($params['currentUserId']).'&'.
+ 'e='.intval($params['newStatus']).'&'.
+ 'u='.intval($params['studentUserId']).'&'.
+ 'q='.intval($params['queueId']).'&'.
+ 'is_connected=1&'.
+ 'profile_completed='.intval($params['profile_completed']).'&'.
+ 'v='.$this->generateHash($params);
+
+ return $url;
+ }
+
+ /**
+ * Return the list of student, in queue used by admin view.
+ *
+ * @param int $sessionId
+ *
+ * @return array
+ */
+ public function listAllStudentsInQueueBySession($sessionId)
+ {
+ // Filter input variable
+ $sessionId = intval($sessionId);
+ // Assign variables
+ $fieldsArray = [
+ 'target',
+ 'publication_end_date',
+ 'mode',
+ 'recommended_number_of_participants',
+ 'vacancies',
+ ];
+ $sessionArray = api_get_session_info($sessionId);
+ $extraSession = new ExtraFieldValue('session');
+ $extraField = new ExtraField('session');
+ // Get session fields
+ $fieldList = $extraField->get_all([
+ 'variable IN ( ?, ?, ?, ?, ?)' => $fieldsArray,
+ ]);
+ // Index session fields
+ $fields = [];
+ foreach ($fieldList as $field) {
+ $fields[$field['id']] = $field['variable'];
+ }
+
+ $mergedArray = array_merge([$sessionId], array_keys($fields));
+ $sessionFieldValueList = $extraSession->get_all(
+ [
+ 'item_id = ? field_id IN ( ?, ?, ?, ?, ?, ?, ? )' => $mergedArray,
+ ]
+ );
+ foreach ($sessionFieldValueList as $sessionFieldValue) {
+ // Check if session field value is set in session field list
+ if (isset($fields[$sessionFieldValue['field_id']])) {
+ $var = $fields[$sessionFieldValue['field_id']];
+ $val = $sessionFieldValue['value'];
+ // Assign session field value to session
+ $sessionArray[$var] = $val;
+ }
+ }
+ $queueTable = Database::get_main_table(TABLE_ADVANCED_SUBSCRIPTION_QUEUE);
+ $userTable = Database::get_main_table(TABLE_MAIN_USER);
+ $userJoinTable = $queueTable.' q INNER JOIN '.$userTable.' u ON q.user_id = u.user_id';
+ $where = [
+ 'where' => [
+ 'q.session_id = ?' => [
+ $sessionId,
+ ],
+ ],
+ 'order' => 'qstatus DESC, lastname ASC',
+ ];
+ $select = 'u.user_id, u.firstname, u.lastname, q.created_at, q.updated_at, q.status as qstatus, q.id as queue_id';
+ $students = Database::select($select, $userJoinTable, $where);
+ foreach ($students as &$student) {
+ $status = intval($student['qstatus']);
+ switch ($status) {
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_NO_QUEUE:
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_START:
+ $student['validation'] = '';
+ break;
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_DISAPPROVED:
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_DISAPPROVED:
+ $student['validation'] = 'No';
+ break;
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_APPROVED:
+ case ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED:
+ $student['validation'] = 'Yes';
+ break;
+ default:
+ error_log(__FILE__.' '.__FUNCTION__.' Student status no detected');
+ }
+ }
+ $return = [
+ 'session' => $sessionArray,
+ 'students' => $students,
+ ];
+
+ return $return;
+ }
+
+ /**
+ * List all session (id, name) for select input.
+ *
+ * @param int $limit
+ *
+ * @return array
+ */
+ public function listAllSessions($limit = 100)
+ {
+ $limit = intval($limit);
+ $sessionTable = Database::get_main_table(TABLE_MAIN_SESSION);
+ $columns = 'id, name';
+ $conditions = [];
+ if ($limit > 0) {
+ $conditions = [
+ 'order' => 'name',
+ 'limit' => $limit,
+ ];
+ }
+
+ return Database::select($columns, $sessionTable, $conditions);
+ }
+
+ /**
+ * Generate security hash to check data send by url params.
+ *
+ * @param string $data
+ *
+ * @return string
+ */
+ public function generateHash($data)
+ {
+ $key = sha1($this->get('secret_key'));
+ // Prepare array to have specific type variables
+ $dataPrepared['action'] = strval($data['action']);
+ $dataPrepared['sessionId'] = intval($data['sessionId']);
+ $dataPrepared['currentUserId'] = intval($data['currentUserId']);
+ $dataPrepared['studentUserId'] = intval($data['studentUserId']);
+ $dataPrepared['queueId'] = intval($data['queueId']);
+ $dataPrepared['newStatus'] = intval($data['newStatus']);
+ $dataPrepared = serialize($dataPrepared);
+
+ return sha1($dataPrepared.$key);
+ }
+
+ /**
+ * Verify hash from data.
+ *
+ * @param string $data
+ * @param string $hash
+ *
+ * @return bool
+ */
+ public function checkHash($data, $hash)
+ {
+ return $this->generateHash($data) == $hash;
+ }
+
+ /**
+ * Copied and fixed from plugin.class.php
+ * Returns the "system" name of the plugin in lowercase letters.
+ *
+ * @return string
+ */
+ public function get_name()
+ {
+ return 'advanced_subscription';
+ }
+
+ /**
+ * Return the url to show subscription terms.
+ *
+ * @param array $params
+ * @param int $mode
+ *
+ * @return string
+ */
+ public function getTermsUrl($params, $mode = ADVANCED_SUBSCRIPTION_TERMS_MODE_POPUP)
+ {
+ $urlParams = [
+ 'a' => Security::remove_XSS($params['action']),
+ 's' => intval($params['sessionId']),
+ 'current_user_id' => intval($params['currentUserId']),
+ 'e' => intval($params['newStatus']),
+ 'u' => intval($params['studentUserId']),
+ 'q' => intval($params['queueId']),
+ 'is_connected' => 1,
+ 'profile_completed' => intval($params['profile_completed']),
+ 'v' => $this->generateHash($params),
+ ];
+
+ switch ($mode) {
+ case ADVANCED_SUBSCRIPTION_TERMS_MODE_POPUP:
+ case ADVANCED_SUBSCRIPTION_TERMS_MODE_FINAL:
+ $urlParams['r'] = 0;
+ break;
+ case ADVANCED_SUBSCRIPTION_TERMS_MODE_REJECT:
+ $urlParams['r'] = 1;
+ break;
+ }
+
+ $url = api_get_path(WEB_PLUGIN_PATH)."advanced_subscription/src/terms_and_conditions.php?";
+ $url .= http_build_query($urlParams);
+
+ // Launch popup
+ if ($mode == ADVANCED_SUBSCRIPTION_TERMS_MODE_POPUP) {
+ $url = 'javascript:void(window.open(\''.$url.'\',\'AdvancedSubscriptionTerms\', \'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=700px,height=600px\', \'100\' ))';
+ }
+
+ return $url;
+ }
+
+ /**
+ * Return the url to get mail rendered.
+ *
+ * @param array $params
+ *
+ * @return string
+ */
+ public function getRenderMailUrl($params)
+ {
+ $url = api_get_path(WEB_PLUGIN_PATH).'advanced_subscription/src/render_mail.php?'.
+ 'q='.$params['queueId'].'&'.
+ 'v='.$this->generateHash($params);
+
+ return $url;
+ }
+
+ /**
+ * Return the last message id from queue row.
+ *
+ * @param int $studentUserId
+ * @param int $sessionId
+ *
+ * @return int|bool
+ */
+ public function getLastMessageId($studentUserId, $sessionId)
+ {
+ $studentUserId = intval($studentUserId);
+ $sessionId = intval($sessionId);
+ if (!empty($sessionId) && !empty($studentUserId)) {
+ $row = Database::select(
+ 'last_message_id',
+ Database::get_main_table(TABLE_ADVANCED_SUBSCRIPTION_QUEUE),
+ [
+ 'where' => [
+ 'user_id = ? AND session_id = ?' => [$studentUserId, $sessionId],
+ ],
+ ]
+ );
+
+ if (count($row) > 0) {
+ return $row[0]['last_message_id'];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return string replacing tags "{{}}"with variables assigned in $data.
+ *
+ * @param string $templateContent
+ * @param array $data
+ *
+ * @return string
+ */
+ public function renderTemplateString($templateContent, $data = [])
+ {
+ $twigString = new \Twig_Environment(new \Twig_Loader_String());
+ $templateContent = $twigString->render(
+ $templateContent,
+ $data
+ );
+
+ return $templateContent;
+ }
+
+ /**
+ * addAreaField() (adds an area field if it is not already created).
+ */
+ private function addAreaField()
+ {
+ $extraField = new ExtraField('user');
+ $extraFieldHandler = $extraField->get_handler_field_info_by_field_variable('area');
+ $areaExists = $extraFieldHandler !== false;
+
+ if (!$areaExists) {
+ $extraField = new ExtraField('user');
+ $extraField->save([
+ 'field_type' => 1,
+ 'variable' => 'area',
+ 'display_text' => get_plugin_lang('Area', 'AdvancedSubscriptionPlugin'),
+ 'default_value' => null,
+ 'field_order' => null,
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+ 'filter' => null,
+ ]);
+ }
+ }
+
+ /**
+ * Create the database tables for the plugin.
+ */
+ private function installDatabase()
+ {
+ $advancedSubscriptionQueueTable = Database::get_main_table(TABLE_ADVANCED_SUBSCRIPTION_QUEUE);
+
+ $sql = "CREATE TABLE IF NOT EXISTS $advancedSubscriptionQueueTable (".
+ "id int UNSIGNED NOT NULL AUTO_INCREMENT, ".
+ "session_id int UNSIGNED NOT NULL, ".
+ "user_id int UNSIGNED NOT NULL, ".
+ "status int UNSIGNED NOT NULL, ".
+ "last_message_id int UNSIGNED NOT NULL, ".
+ "created_at datetime NOT NULL, ".
+ "updated_at datetime NULL, ".
+ "PRIMARY KEY PK_advanced_subscription_queue (id), ".
+ "UNIQUE KEY UK_advanced_subscription_queue (user_id, session_id)); ";
+ Database::query($sql);
+ }
+
+ /**
+ * Drop the database tables for the plugin.
+ */
+ private function uninstallDatabase()
+ {
+ /* Drop plugin tables */
+ $advancedSubscriptionQueueTable = Database::get_main_table(TABLE_ADVANCED_SUBSCRIPTION_QUEUE);
+ $sql = "DROP TABLE IF EXISTS $advancedSubscriptionQueueTable; ";
+ Database::query($sql);
+
+ /* Delete settings */
+ $settingsTable = Database::get_main_table(TABLE_MAIN_SETTINGS_CURRENT);
+ Database::query("DELETE FROM $settingsTable WHERE subkey = 'advanced_subscription'");
+ }
+
+ /**
+ * Get the count of approved induction sessions by a user.
+ *
+ * @param int $userId The user id
+ *
+ * @return int The count of approved sessions
+ */
+ private function getApprovedInductionSessions($userId)
+ {
+ $tSession = Database::get_main_table(TABLE_MAIN_SESSION);
+ $tSessionField = Database::get_main_table(TABLE_EXTRA_FIELD);
+ $tSessionFieldValues = Database::get_main_table(TABLE_EXTRA_FIELD_VALUES);
+ $tSessionUser = Database::get_main_table(TABLE_MAIN_SESSION_USER);
+ $extraFieldType = \Chamilo\CoreBundle\Entity\ExtraField::SESSION_FIELD_TYPE;
+ $sql = "SELECT s.id FROM $tSession AS s
+ INNER JOIN $tSessionFieldValues AS sfv ON s.id = sfv.item_id
+ INNER JOIN $tSessionField AS sf ON sfv.field_id = sf.id
+ INNER JOIN $tSessionUser AS su ON s.id = su.session_id
+ WHERE
+ sf.extra_field_type = $extraFieldType AND
+ sf.variable = 'is_induction_session' AND
+ su.relation_type = 0 AND
+ su.user_id = ".intval($userId);
+
+ $result = Database::query($sql);
+
+ if ($result === false) {
+ return 0;
+ }
+
+ $numberOfApproved = 0;
+
+ while ($session = Database::fetch_assoc($result)) {
+ $numberOfApprovedCourses = 0;
+ $courses = SessionManager::get_course_list_by_session_id($session['id']);
+
+ foreach ($courses as $course) {
+ $courseCategories = Category::load(
+ null,
+ null,
+ $course['code'],
+ null,
+ null,
+ $session['id'],
+ false
+ );
+
+ if (count($courseCategories) > 0 &&
+ Category::userFinishedCourse($userId, $courseCategories[0])
+ ) {
+ $numberOfApprovedCourses++;
+ }
+ }
+
+ if ($numberOfApprovedCourses === count($courses)) {
+ $numberOfApproved++;
+ }
+ }
+
+ return $numberOfApproved;
+ }
+}
diff --git a/plugin/advanced_subscription/src/HookAdvancedSubscription.php b/plugin/advanced_subscription/src/HookAdvancedSubscription.php
new file mode 100644
index 000000000..db89bcf64
--- /dev/null
+++ b/plugin/advanced_subscription/src/HookAdvancedSubscription.php
@@ -0,0 +1,681 @@
+
+ *
+ * @package chamilo.plugin.advanced_subscription
+ */
+require_once __DIR__.'/../config.php';
+
+/**
+ * Class HookAdvancedSubscription extends the HookObserver to implements
+ * specific behaviour when the AdvancedSubscription plugin is enabled.
+ */
+class HookAdvancedSubscription extends HookObserver implements HookAdminBlockObserverInterface, HookWSRegistrationObserverInterface, HookNotificationContentObserverInterface
+{
+ public static $plugin;
+
+ /**
+ * Constructor. Calls parent, mainly.
+ */
+ protected function __construct()
+ {
+ self::$plugin = AdvancedSubscriptionPlugin::create();
+ parent::__construct(
+ 'plugin/advanced_subscription/src/HookAdvancedSubscription.class.php',
+ 'advanced_subscription'
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function hookAdminBlock(HookAdminBlockEventInterface $hook)
+ {
+ $data = $hook->getEventData();
+ // if ($data['type'] === HOOK_EVENT_TYPE_PRE) // Nothing to do
+ if ($data['type'] === HOOK_EVENT_TYPE_POST) {
+ if (isset($data['blocks'])) {
+ $data['blocks']['sessions']['items'][] = [
+ 'url' => '../../plugin/advanced_subscription/src/admin_view.php',
+ 'label' => get_plugin_lang('plugin_title', 'AdvancedSubscriptionPlugin'),
+ ];
+ }
+ } // Else: Hook type is not valid, nothing to do
+
+ return $data;
+ }
+
+ /**
+ * Add Webservices to registration.soap.php.
+ *
+ * @return mixed (int or false)
+ */
+ public function hookWSRegistration(HookWSRegistrationEventInterface $hook)
+ {
+ $data = $hook->getEventData();
+ //if ($data['type'] === HOOK_EVENT_TYPE_PRE) // nothing to do
+ if ($data['type'] === HOOK_EVENT_TYPE_POST) {
+ /** @var \nusoap_server $server */
+ $server = &$data['server'];
+
+ /** WSSessionListInCategory */
+
+ // Output params for sessionBriefList WSSessionListInCategory
+ $server->wsdl->addComplexType(
+ 'sessionBrief',
+ 'complexType',
+ 'struct',
+ 'all',
+ '',
+ [
+ // session.id
+ 'id' => ['name' => 'id', 'type' => 'xsd:int'],
+ // session.name
+ 'name' => ['name' => 'name', 'type' => 'xsd:string'],
+ // session.short_description
+ 'short_description' => ['name' => 'short_description', 'type' => 'xsd:string'],
+ // session.mode
+ 'mode' => ['name' => 'mode', 'type' => 'xsd:string'],
+ // session.date_start
+ 'date_start' => ['name' => 'date_start', 'type' => 'xsd:string'],
+ // session.date_end
+ 'date_end' => ['name' => 'date_end', 'type' => 'xsd:string'],
+ // session.human_text_duration
+ 'human_text_duration' => ['name' => 'human_text_duration', 'type' => 'xsd:string'],
+ // session.vacancies
+ 'vacancies' => ['name' => 'vacancies', 'type' => 'xsd:string'],
+ // session.schedule
+ 'schedule' => ['name' => 'schedule', 'type' => 'xsd:string'],
+ ]
+ );
+
+ //Output params for WSSessionListInCategory
+ $server->wsdl->addComplexType(
+ 'sessionBriefList',
+ 'complexType',
+ 'array',
+ '',
+ 'SOAP-ENC:Array',
+ [],
+ [
+ ['ref' => 'SOAP-ENC:arrayType',
+ 'wsdl:arrayType' => 'tns:sessionBrief[]', ],
+ ],
+ 'tns:sessionBrief'
+ );
+
+ // Input params for WSSessionListInCategory
+ $server->wsdl->addComplexType(
+ 'sessionCategoryInput',
+ 'complexType',
+ 'struct',
+ 'all',
+ '',
+ [
+ 'id' => ['name' => 'id', 'type' => 'xsd:string'], // session_category.id
+ 'name' => ['name' => 'name', 'type' => 'xsd:string'], // session_category.name
+ 'target' => ['name' => 'target', 'type' => 'xsd:string'], // session.target
+ 'secret_key' => ['name' => 'secret_key', 'type' => 'xsd:string'],
+ ]
+ );
+
+ // Input params for WSSessionGetDetailsByUser
+ $server->wsdl->addComplexType(
+ 'advsubSessionDetailInput',
+ 'complexType',
+ 'struct',
+ 'all',
+ '',
+ [
+ // user_field_values.value
+ 'user_id' => ['name' => 'user_id', 'type' => 'xsd:int'],
+ // user_field.user_id
+ 'user_field' => ['name' => 'user_field', 'type' => 'xsd:string'],
+ // session.id
+ 'session_id' => ['name' => 'session_id', 'type' => 'xsd:int'],
+ // user.profile_completes
+ 'profile_completed' => ['name' => 'profile_completed', 'type' => 'xsd:float'],
+ // user.is_connected
+ 'is_connected' => ['name' => 'is_connected', 'type' => 'xsd:boolean'],
+ 'secret_key' => ['name' => 'secret_key', 'type' => 'xsd:string'],
+ ]
+ );
+
+ // Output params for WSSessionGetDetailsByUser
+ $server->wsdl->addComplexType(
+ 'advsubSessionDetail',
+ 'complexType',
+ 'struct',
+ 'all',
+ '',
+ [
+ // session.id
+ 'id' => ['name' => 'id', 'type' => 'xsd:string'],
+ // session.code
+ 'code' => ['name' => 'code', 'type' => 'xsd:string'],
+ // session.place
+ 'cost' => ['name' => 'cost', 'type' => 'xsd:float'],
+ // session.place
+ 'place' => ['name' => 'place', 'type' => 'xsd:string'],
+ // session.allow_visitors
+ 'allow_visitors' => ['name' => 'allow_visitors', 'type' => 'xsd:string'],
+ // session.teaching_hours
+ 'teaching_hours' => ['name' => 'teaching_hours', 'type' => 'xsd:int'],
+ // session.brochure
+ 'brochure' => ['name' => 'brochure', 'type' => 'xsd:string'],
+ // session.banner
+ 'banner' => ['name' => 'banner', 'type' => 'xsd:string'],
+ // session.description
+ 'description' => ['name' => 'description', 'type' => 'xsd:string'],
+ // status
+ 'status' => ['name' => 'status', 'type' => 'xsd:string'],
+ // action_url
+ 'action_url' => ['name' => 'action_url', 'type' => 'xsd:string'],
+ // message
+ 'message' => ['name' => 'error_message', 'type' => 'xsd:string'],
+ ]
+ );
+
+ /** WSListSessionsDetailsByCategory */
+
+ // Input params for WSListSessionsDetailsByCategory
+ $server->wsdl->addComplexType(
+ 'listSessionsDetailsByCategory',
+ 'complexType',
+ 'struct',
+ 'all',
+ '',
+ [
+ // session_category.id
+ 'id' => ['name' => 'id', 'type' => 'xsd:string'],
+ // session_category.access_url_id
+ 'access_url_id' => ['name' => 'access_url_id', 'type' => 'xsd:int'],
+ // session_category.name
+ 'category_name' => ['name' => 'category_name', 'type' => 'xsd:string'],
+ // secret key
+ 'secret_key' => ['name' => 'secret_key', 'type' => 'xsd:string'],
+ ],
+ [],
+ 'tns:listSessionsDetailsByCategory'
+ );
+
+ // Output params for sessionDetailsCourseList WSListSessionsDetailsByCategory
+ $server->wsdl->addComplexType(
+ 'sessionDetailsCourse',
+ 'complexType',
+ 'struct',
+ 'all',
+ '',
+ [
+ 'course_id' => ['name' => 'course_id', 'type' => 'xsd:int'], // course.id
+ 'course_code' => ['name' => 'course_code', 'type' => 'xsd:string'], // course.code
+ 'course_title' => ['name' => 'course_title', 'type' => 'xsd:string'], // course.title
+ 'coach_username' => ['name' => 'coach_username', 'type' => 'xsd:string'], // user.username
+ 'coach_firstname' => ['name' => 'coach_firstname', 'type' => 'xsd:string'], // user.firstname
+ 'coach_lastname' => ['name' => 'coach_lastname', 'type' => 'xsd:string'], // user.lastname
+ ]
+ );
+
+ // Output array for sessionDetails WSListSessionsDetailsByCategory
+ $server->wsdl->addComplexType(
+ 'sessionDetailsCourseList',
+ 'complexType',
+ 'array',
+ '',
+ 'SOAP-ENC:Array',
+ [],
+ [
+ [
+ 'ref' => 'SOAP-ENC:arrayType',
+ 'wsdl:arrayType' => 'tns:sessionDetailsCourse[]',
+ ],
+ ],
+ 'tns:sessionDetailsCourse'
+ );
+
+ // Output params for sessionDetailsList WSListSessionsDetailsByCategory
+ $server->wsdl->addComplexType(
+ 'sessionDetails',
+ 'complexType',
+ 'struct',
+ 'all',
+ '',
+ [
+ // session.id
+ 'id' => [
+ 'name' => 'id',
+ 'type' => 'xsd:int',
+ ],
+ // session.id_coach
+ 'coach_id' => [
+ 'name' => 'coach_id',
+ 'type' => 'xsd:int',
+ ],
+ // session.name
+ 'name' => [
+ 'name' => 'name',
+ 'type' => 'xsd:string',
+ ],
+ // session.nbr_courses
+ 'courses_num' => [
+ 'name' => 'courses_num',
+ 'type' => 'xsd:int',
+ ],
+ // session.nbr_users
+ 'users_num' => [
+ 'name' => 'users_num',
+ 'type' => 'xsd:int',
+ ],
+ // session.nbr_classes
+ 'classes_num' => [
+ 'name' => 'classes_num',
+ 'type' => 'xsd:int',
+ ],
+ // session.date_start
+ 'date_start' => [
+ 'name' => 'date_start',
+ 'type' => 'xsd:string',
+ ],
+ // session.date_end
+ 'date_end' => [
+ 'name' => 'date_end',
+ 'type' => 'xsd:string',
+ ],
+ // session.nb_days_access_before_beginning
+ 'access_days_before_num' => [
+ 'name' => 'access_days_before_num',
+ 'type' => 'xsd:int',
+ ],
+ // session.nb_days_access_after_end
+ 'access_days_after_num' => [
+ 'name' => 'access_days_after_num',
+ 'type' => 'xsd:int',
+ ],
+ // session.session_admin_id
+ 'session_admin_id' => [
+ 'name' => 'session_admin_id',
+ 'type' => 'xsd:int',
+ ],
+ // session.visibility
+ 'visibility' => [
+ 'name' => 'visibility',
+ 'type' => 'xsd:int',
+ ],
+ // session.session_category_id
+ 'session_category_id' => [
+ 'name' => 'session_category_id',
+ 'type' => 'xsd:int',
+ ],
+ // session.promotion_id
+ 'promotion_id' => [
+ 'name' => 'promotion_id',
+ 'type' => 'xsd:int',
+ ],
+ // session.number of registered users validated
+ 'validated_user_num' => [
+ 'name' => 'validated_user_num',
+ 'type' => 'xsd:int',
+ ],
+ // session.number of registered users from waiting queue
+ 'waiting_user_num' => [
+ 'name' => 'waiting_user_num',
+ 'type' => 'xsd:int',
+ ],
+ // extra fields
+ // Array(field_name, field_value)
+ 'extra' => [
+ 'name' => 'extra',
+ 'type' => 'tns:extrasList',
+ ],
+ // course and coaches data
+ // Array(course_id, course_code, course_title, coach_username, coach_firstname, coach_lastname)
+ 'course' => [
+ 'name' => 'courses',
+ 'type' => 'tns:sessionDetailsCourseList',
+ ],
+ ]
+ );
+
+ // Output params for WSListSessionsDetailsByCategory
+ $server->wsdl->addComplexType(
+ 'sessionDetailsList',
+ 'complexType',
+ 'array',
+ '',
+ 'SOAP-ENC:Array',
+ [],
+ [
+ [
+ 'ref' => 'SOAP-ENC:arrayType',
+ 'wsdl:arrayType' => 'tns:sessionDetails[]',
+ ],
+ ],
+ 'tns:sessionDetails'
+ );
+
+ // Register the method for WSSessionListInCategory
+ $server->register(
+ 'HookAdvancedSubscription..WSSessionListInCategory', // method name
+ ['sessionCategoryInput' => 'tns:sessionCategoryInput'], // input parameters
+ ['return' => 'tns:sessionBriefList'], // output parameters
+ 'urn:WSRegistration', // namespace
+ 'urn:WSRegistration#WSSessionListInCategory', // soapaction
+ 'rpc', // style
+ 'encoded', // use
+ 'This service checks if user assigned to course' // documentation
+ );
+
+ // Register the method for WSSessionGetDetailsByUser
+ $server->register(
+ 'HookAdvancedSubscription..WSSessionGetDetailsByUser', // method name
+ ['advsubSessionDetailInput' => 'tns:advsubSessionDetailInput'], // input parameters
+ ['return' => 'tns:advsubSessionDetail'], // output parameters
+ 'urn:WSRegistration', // namespace
+ 'urn:WSRegistration#WSSessionGetDetailsByUser', // soapaction
+ 'rpc', // style
+ 'encoded', // use
+ 'This service return session details to specific user' // documentation
+ );
+
+ // Register the method for WSListSessionsDetailsByCategory
+ $server->register(
+ 'HookAdvancedSubscription..WSListSessionsDetailsByCategory', // method name
+ ['name' => 'tns:listSessionsDetailsByCategory'], // input parameters
+ ['return' => 'tns:sessionDetailsList'], // output parameters
+ 'urn:WSRegistration', // namespace
+ 'urn:WSRegistration#WSListSessionsDetailsByCategory', // soapaction
+ 'rpc', // style
+ 'encoded', // use
+ 'This service returns a list of detailed sessions by a category' // documentation
+ );
+
+ return $data;
+ } // Else: Nothing to do
+
+ return false;
+ }
+
+ /**
+ * @param $params
+ *
+ * @return soap_fault|null
+ */
+ public static function WSSessionListInCategory($params)
+ {
+ global $debug;
+
+ if ($debug) {
+ error_log(__FUNCTION__);
+ error_log('Params '.print_r($params, 1));
+ if (!WSHelperVerifyKey($params)) {
+ error_log(return_error(WS_ERROR_SECRET_KEY));
+ }
+ }
+ // Check if category ID is set
+ if (!empty($params['id']) && empty($params['name'])) {
+ $sessionCategoryId = $params['id'];
+ } elseif (!empty($params['name'])) {
+ // Check if category name is set
+ $sessionCategoryId = SessionManager::getSessionCategoryIdByName($params['name']);
+ if (is_array($sessionCategoryId)) {
+ $sessionCategoryId = current($sessionCategoryId);
+ }
+ } else {
+ // Return soap fault Not valid input params
+
+ return return_error(WS_ERROR_INVALID_INPUT);
+ }
+
+ // Get the session brief List by category
+ $fields = [
+ 'id',
+ 'short_description',
+ 'mode',
+ 'human_text_duration',
+ 'vacancies',
+ 'schedule',
+ ];
+ $datePub = new DateTime();
+ $sessionList = SessionManager::getShortSessionListAndExtraByCategory(
+ $sessionCategoryId,
+ $params['target'],
+ $fields,
+ $datePub
+ );
+
+ return $sessionList;
+ }
+
+ /**
+ * @param $params
+ *
+ * @return soap_fault|null
+ */
+ public static function WSSessionGetDetailsByUser($params)
+ {
+ global $debug;
+
+ if ($debug) {
+ error_log('WSUserSubscribedInCourse');
+ error_log('Params '.print_r($params, 1));
+ }
+ if (!WSHelperVerifyKey($params)) {
+ return return_error(WS_ERROR_SECRET_KEY);
+ }
+ // Check params
+ if (is_array($params) && !empty($params['session_id']) && !empty($params['user_id'])) {
+ $userId = UserManager::get_user_id_from_original_id($params['user_id'], $params['user_field']);
+ $sessionId = (int) $params['session_id'];
+ // Check if user exists
+ if (UserManager::is_user_id_valid($userId) &&
+ SessionManager::isValidId($sessionId)
+ ) {
+ // Check if student is already subscribed
+ $plugin = AdvancedSubscriptionPlugin::create();
+ $isOpen = $plugin->isSessionOpen($sessionId);
+ $status = $plugin->getQueueStatus($userId, $sessionId);
+ $vacancy = $plugin->getVacancy($sessionId);
+ $data = $plugin->getSessionDetails($sessionId);
+ $isUserInTargetGroup = $plugin->isUserInTargetGroup($userId, $sessionId);
+ if (!empty($data) && is_array($data)) {
+ $data['status'] = $status;
+ // Vacancy and queue status cases:
+ if ($isOpen) {
+ // Go to Course session
+ $data['action_url'] = self::$plugin->getOpenSessionUrl($userId, $params);
+ if (SessionManager::isUserSubscribedAsStudent($sessionId, $userId)) {
+ $data['status'] = 10;
+ }
+ } else {
+ if (!$isUserInTargetGroup) {
+ $data['status'] = -2;
+ } else {
+ try {
+ $isAllowed = self::$plugin->isAllowedToDoRequest($userId, $params);
+ $data['message'] = self::$plugin->getStatusMessage($status, $isAllowed);
+ } catch (\Exception $e) {
+ $data['message'] = $e->getMessage();
+ }
+ $params['action'] = 'subscribe';
+ $params['sessionId'] = intval($sessionId);
+ $params['currentUserId'] = 0; // No needed
+ $params['studentUserId'] = intval($userId);
+ $params['queueId'] = 0; // No needed
+ $params['newStatus'] = ADVANCED_SUBSCRIPTION_QUEUE_STATUS_START;
+ if ($vacancy > 0) {
+ // Check conditions
+ if ($status == ADVANCED_SUBSCRIPTION_QUEUE_STATUS_NO_QUEUE) {
+ // No in Queue, require queue subscription url action
+ $data['action_url'] = self::$plugin->getTermsUrl($params);
+ } elseif ($status == ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED) {
+ // send url action
+ $data['action_url'] = self::$plugin->getSessionUrl($sessionId);
+ } // Else: In queue, output status message, no more info.
+ } else {
+ if ($status == ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED) {
+ $data['action_url'] = self::$plugin->getSessionUrl($sessionId);
+ } elseif ($status == ADVANCED_SUBSCRIPTION_QUEUE_STATUS_NO_QUEUE) {
+ // in Queue or not, cannot be subscribed to session
+ $data['action_url'] = self::$plugin->getTermsUrl($params);
+ } // Else: In queue, output status message, no more info.
+ }
+ }
+ }
+ $result = $data;
+ } else {
+ // Return soap fault No result was found
+ $result = return_error(WS_ERROR_NOT_FOUND_RESULT);
+ }
+ } else {
+ // Return soap fault No result was found
+ $result = return_error(WS_ERROR_NOT_FOUND_RESULT);
+ }
+ } else {
+ // Return soap fault Not valid input params
+ $result = return_error(WS_ERROR_INVALID_INPUT);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get a list of sessions (id, coach_id, name, courses_num, users_num, classes_num,
+ * access_start_date, access_end_date, access_days_before_num, session_admin_id, visibility,
+ * session_category_id, promotion_id,
+ * validated_user_num, waiting_user_num,
+ * extra, course) the validated_usernum and waiting_user_num are
+ * used when have the plugin for advance incsription enables.
+ * The extra data (field_name, field_value)
+ * The course data (course_id, course_code, course_title,
+ * coach_username, coach_firstname, coach_lastname).
+ *
+ * @param array $params List of parameters (id, category_name, access_url_id, secret_key)
+ *
+ * @return array|soap_fault Sessions list (id=>[title=>'title',url='http://...',date_start=>'...',date_end=>''])
+ */
+ public static function WSListSessionsDetailsByCategory($params)
+ {
+ global $debug;
+
+ if ($debug) {
+ error_log('WSListSessionsDetailsByCategory');
+ error_log('Params '.print_r($params, 1));
+ }
+ $secretKey = $params['secret_key'];
+
+ // Check if secret key is valid
+ if (!WSHelperVerifyKey($secretKey)) {
+ return return_error(WS_ERROR_SECRET_KEY);
+ }
+
+ // Check if category ID is set
+ if (!empty($params['id']) && empty($params['category_name'])) {
+ $sessionCategoryId = $params['id'];
+ } elseif (!empty($params['category_name'])) {
+ // Check if category name is set
+ $sessionCategoryId = SessionManager::getSessionCategoryIdByName($params['category_name']);
+ if (is_array($sessionCategoryId)) {
+ $sessionCategoryId = current($sessionCategoryId);
+ }
+ } else {
+ // Return soap fault Not valid input params
+
+ return return_error(WS_ERROR_INVALID_INPUT);
+ }
+
+ // Get the session List by category
+ $sessionList = SessionManager::getSessionListAndExtraByCategoryId($sessionCategoryId);
+
+ if (empty($sessionList)) {
+ // If not found any session, return error
+
+ return return_error(WS_ERROR_NOT_FOUND_RESULT);
+ }
+
+ // Get validated and waiting queue users count for each session
+ AdvancedSubscriptionPlugin::create();
+ foreach ($sessionList as &$session) {
+ // Add validated and queue users count
+ $session['validated_user_num'] = self::$plugin->countQueueByParams(
+ [
+ 'sessions' => [$session['id']],
+ 'status' => [ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED],
+ ]
+ );
+ $session['waiting_user_num'] = self::$plugin->countQueueByParams(
+ [
+ 'sessions' => [$session['id']],
+ 'status' => [
+ ADVANCED_SUBSCRIPTION_QUEUE_STATUS_START,
+ ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_APPROVED,
+ ],
+ ]
+ );
+ }
+
+ return $sessionList;
+ }
+
+ /**
+ * Return notification content when the hook has been triggered.
+ *
+ * @return mixed (int or false)
+ */
+ public function hookNotificationContent(HookNotificationContentEventInterface $hook)
+ {
+ $data = $hook->getEventData();
+ if ($data['type'] === HOOK_EVENT_TYPE_PRE) {
+ $data['advanced_subscription_pre_content'] = $data['content'];
+
+ return $data;
+ } elseif ($data['type'] === HOOK_EVENT_TYPE_POST) {
+ if (isset($data['content']) &&
+ !empty($data['content']) &&
+ isset($data['advanced_subscription_pre_content']) &&
+ !empty($data['advanced_subscription_pre_content'])
+ ) {
+ $data['content'] = str_replace(
+ [
+ ' ',
+ ' ',
+ ' ',
+ ],
+ '',
+ $data['advanced_subscription_pre_content']
+ );
+ }
+
+ return $data;
+ } //Else hook type is not valid, nothing to do
+
+ return false;
+ }
+
+ /**
+ * Return the notification data title if the hook was triggered.
+ *
+ * @return array|bool
+ */
+ public function hookNotificationTitle(HookNotificationTitleEventInterface $hook)
+ {
+ $data = $hook->getEventData();
+ if ($data['type'] === HOOK_EVENT_TYPE_PRE) {
+ $data['advanced_subscription_pre_title'] = $data['title'];
+
+ return $data;
+ } elseif ($data['type'] === HOOK_EVENT_TYPE_POST) {
+ if (isset($data['advanced_subscription_pre_title']) &&
+ !empty($data['advanced_subscription_pre_title'])
+ ) {
+ $data['title'] = $data['advanced_subscription_pre_title'];
+ }
+
+ return $data;
+ } // Else: hook type is not valid, nothing to do
+
+ return false;
+ }
+}
diff --git a/plugin/advanced_subscription/src/admin_view.php b/plugin/advanced_subscription/src/admin_view.php
new file mode 100644
index 000000000..62f823ed9
--- /dev/null
+++ b/plugin/advanced_subscription/src/admin_view.php
@@ -0,0 +1,104 @@
+get_lang('plugin_title'));
+// Get all sessions
+$sessionList = $plugin->listAllSessions();
+
+if (!empty($sessionId)) {
+ // Get student list in queue
+ $studentList = $plugin->listAllStudentsInQueueBySession($sessionId);
+ // Set selected to current session
+ $sessionList[$sessionId]['selected'] = 'selected="selected"';
+ $studentList['session']['id'] = $sessionId;
+ // Assign variables
+ $fieldsArray = [
+ 'description',
+ 'target',
+ 'mode',
+ 'publication_end_date',
+ 'recommended_number_of_participants',
+ 'vacancies',
+ ];
+ $sessionArray = api_get_session_info($sessionId);
+ $extraSession = new ExtraFieldValue('session');
+ $extraField = new ExtraField('session');
+ // Get session fields
+ $fieldList = $extraField->get_all([
+ 'variable IN ( ?, ?, ?, ?, ?, ?)' => $fieldsArray,
+ ]);
+ // Index session fields
+ foreach ($fieldList as $field) {
+ $fields[$field['id']] = $field['variable'];
+ }
+ $params = [' item_id = ? ' => $sessionId];
+ $sessionFieldValueList = $extraSession->get_all(['where' => $params]);
+ foreach ($sessionFieldValueList as $sessionFieldValue) {
+ // Check if session field value is set in session field list
+ if (isset($fields[$sessionFieldValue['field_id']])) {
+ $var = $fields[$sessionFieldValue['field_id']];
+ $val = $sessionFieldValue['value'];
+ // Assign session field value to session
+ $sessionArray[$var] = $val;
+ }
+ }
+ $adminsArray = UserManager::get_all_administrators();
+
+ $data['action'] = 'confirm';
+ $data['sessionId'] = $sessionId;
+ $data['currentUserId'] = api_get_user_id();
+ $isWesternNameOrder = api_is_western_name_order();
+
+ foreach ($studentList['students'] as &$student) {
+ $studentId = intval($student['user_id']);
+ $data['studentUserId'] = $studentId;
+
+ $fieldValue = new ExtraFieldValue('user');
+ $areaField = $fieldValue->get_values_by_handler_and_field_variable($studentId, 'area', true);
+
+ $student['area'] = $areaField['value'];
+ if (substr($student['area'], 0, 6) == 'MINEDU') {
+ $student['institution'] = 'Minedu';
+ } else {
+ $student['institution'] = 'Regiones';
+ }
+ $student['userLink'] = api_get_path(WEB_CODE_PATH).'social/profile.php?u='.$studentId;
+ $data['queueId'] = intval($student['queue_id']);
+ $data['newStatus'] = ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED;
+ $data['profile_completed'] = 100;
+ $student['acceptUrl'] = $plugin->getQueueUrl($data);
+ $data['newStatus'] = ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_DISAPPROVED;
+ $student['rejectUrl'] = $plugin->getQueueUrl($data);
+ $student['complete_name'] = $isWesternNameOrder ?
+ $student['firstname'].', '.$student['lastname'] : $student['lastname'].', '.$student['firstname'];
+ }
+ $tpl->assign('session', $sessionArray);
+ $tpl->assign('students', $studentList['students']);
+}
+
+// Assign variables
+$tpl->assign('sessionItems', $sessionList);
+$tpl->assign('approveAdmin', ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_APPROVED);
+$tpl->assign('disapproveAdmin', ADVANCED_SUBSCRIPTION_QUEUE_STATUS_ADMIN_DISAPPROVED);
+// Get rendered template
+$content = $tpl->fetch('/advanced_subscription/views/admin_view.tpl');
+// Assign into content
+$tpl->assign('content', $content);
+// Display
+$tpl->display_one_col_template();
diff --git a/plugin/advanced_subscription/src/open_session.php b/plugin/advanced_subscription/src/open_session.php
new file mode 100644
index 000000000..70a4ceea2
--- /dev/null
+++ b/plugin/advanced_subscription/src/open_session.php
@@ -0,0 +1,61 @@
+
+ *
+ * @package chamilo.plugin.advanced_subscription
+ */
+require_once __DIR__.'/../config.php';
+
+$plugin = AdvancedSubscriptionPlugin::create();
+
+if (!isset($_GET['session_id'], $_GET['user_id'], $_GET['profile_completed'])) {
+ exit;
+}
+
+$sessionInfo = api_get_session_info($_GET['session_id']);
+
+$tpl = new Template(
+ $plugin->get_lang('plugin_title'),
+ false,
+ false,
+ false,
+ false,
+ false
+);
+$tpl->assign('session', $sessionInfo);
+
+if (SessionManager::isUserSubscribedAsStudent(
+ $_GET['session_id'],
+ $_GET['user_id']
+)) {
+ $tpl->assign('is_subscribed', false);
+ $tpl->assign(
+ 'errorMessages',
+ [sprintf(
+ $plugin->get_lang('YouAreAlreadySubscribedToSessionX'),
+ $sessionInfo['name']
+ )]
+ );
+} else {
+ if (!$plugin->isAllowedSubscribeToOpenSession($_GET)) {
+ $tpl->assign('is_subscribed', false);
+ $tpl->assign('errorMessages', $plugin->getErrorMessages());
+ } else {
+ SessionManager::subscribeUsersToSession(
+ $_GET['session_id'],
+ [$_GET['user_id']],
+ SESSION_VISIBLE_READ_ONLY,
+ false
+ );
+
+ $tpl->assign('is_subscribed', true);
+ }
+}
+
+$content = $tpl->fetch('/advanced_subscription/views/open_session.tpl');
+
+$tpl->assign('content', $content);
+$tpl->display_one_col_template();
diff --git a/plugin/advanced_subscription/src/render_mail.php b/plugin/advanced_subscription/src/render_mail.php
new file mode 100644
index 000000000..da23d9230
--- /dev/null
+++ b/plugin/advanced_subscription/src/render_mail.php
@@ -0,0 +1,26 @@
+checkHash($data, $hash);
+if ($verified) {
+ // Render mail
+ $message = MessageManager::get_message_by_id($data['queueId']);
+ $message = str_replace([' ', ' ', ' '], '', $message['content']);
+ echo $message;
+}
diff --git a/plugin/advanced_subscription/src/scripts/insert_session_fields.php b/plugin/advanced_subscription/src/scripts/insert_session_fields.php
new file mode 100644
index 000000000..913ad09ef
--- /dev/null
+++ b/plugin/advanced_subscription/src/scripts/insert_session_fields.php
@@ -0,0 +1,204 @@
+save([
+ 'field_type' => ExtraField::FIELD_TYPE_INTEGER,
+ 'variable' => 'teaching_hours',
+ 'display_text' => get_lang('TeachingHours'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$cost = new ExtraField('session');
+$cost->save([
+ 'field_type' => ExtraField::FIELD_TYPE_FLOAT,
+ 'variable' => 'cost',
+ 'display_text' => get_lang('Cost'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$vacancies = new ExtraField('session');
+$vacancies->save([
+ 'field_type' => ExtraField::FIELD_TYPE_INTEGER,
+ 'variable' => 'vacancies',
+ 'display_text' => get_lang('Vacancies'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$recommendedNumberOfParticipants = new ExtraField('session');
+$recommendedNumberOfParticipants->save([
+ 'field_type' => ExtraField::FIELD_TYPE_INTEGER,
+ 'variable' => 'recommended_number_of_participants',
+ 'display_text' => get_lang('RecommendedNumberOfParticipants'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$place = new ExtraField('session');
+$place->save([
+ 'field_type' => ExtraField::FIELD_TYPE_TEXT,
+ 'variable' => 'place',
+ 'display_text' => get_lang('Place'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$schedule = new ExtraField('session');
+$schedule->save([
+ 'field_type' => ExtraField::FIELD_TYPE_TEXT,
+ 'variable' => 'schedule',
+ 'display_text' => get_lang('Schedule'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$allowVisitors = new ExtraField('session');
+$allowVisitors->save([
+ 'field_type' => ExtraField::FIELD_TYPE_CHECKBOX,
+ 'variable' => 'allow_visitors',
+ 'display_text' => get_lang('AllowVisitors'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$modeOptions = [
+ get_lang('Online'),
+ get_lang('Presencial'),
+ get_lang('B-Learning'),
+];
+
+$mode = new ExtraField('session');
+$mode->save([
+ 'field_type' => ExtraField::FIELD_TYPE_SELECT,
+ 'variable' => 'mode',
+ 'display_text' => get_lang('Mode'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+ 'field_options' => implode('; ', $modeOptions),
+]);
+
+$isInductionSession = new ExtraField('session');
+$isInductionSession->save([
+ 'field_type' => ExtraField::FIELD_TYPE_CHECKBOX,
+ 'variable' => 'is_induction_session',
+ 'display_text' => get_lang('IsInductionSession'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$isOpenSession = new ExtraField('session');
+$isOpenSession->save([
+ 'field_type' => ExtraField::FIELD_TYPE_CHECKBOX,
+ 'variable' => 'is_open_session',
+ 'display_text' => get_lang('IsOpenSession'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$duration = new ExtraField('session');
+$duration->save([
+ 'field_type' => ExtraField::FIELD_TYPE_TEXT,
+ 'variable' => 'human_text_duration',
+ 'display_text' => get_lang('DurationInWords'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$showStatusOptions = [
+ get_lang('Open'),
+ get_lang('InProcess'),
+ get_lang('Closed'),
+];
+
+$showStatus = new ExtraField('session');
+$showStatus->save([
+ 'field_type' => ExtraField::FIELD_TYPE_SELECT,
+ 'variable' => 'show_status',
+ 'display_text' => get_lang('ShowStatus'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+ 'field_options' => implode('; ', $showStatusOptions),
+]);
+
+$publicationStartDate = new ExtraField('session');
+$publicationStartDate->save([
+ 'field_type' => ExtraField::FIELD_TYPE_DATE,
+ 'variable' => 'publication_start_date',
+ 'display_text' => get_lang('PublicationStartDate'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$publicationEndDate = new ExtraField('session');
+$publicationEndDate->save([
+ 'field_type' => ExtraField::FIELD_TYPE_DATE,
+ 'variable' => 'publication_end_date',
+ 'display_text' => get_lang('PublicationEndDate'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$banner = new ExtraField('session');
+$banner->save([
+ 'field_type' => ExtraField::FIELD_TYPE_FILE_IMAGE,
+ 'variable' => 'banner',
+ 'display_text' => get_lang('SessionBanner'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$brochure = new ExtraField('session');
+$brochure->save([
+ 'field_type' => ExtraField::FIELD_TYPE_FILE,
+ 'variable' => 'brochure',
+ 'display_text' => get_lang('Brochure'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$targetOptions = [
+ get_lang('Minedu'),
+ get_lang('Regiones'),
+];
+
+$target = new ExtraField('session');
+$target->save([
+ 'field_type' => ExtraField::FIELD_TYPE_SELECT,
+ 'variable' => 'target',
+ 'display_text' => get_lang('TargetAudience'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+ 'field_options' => implode('; ', $targetOptions),
+]);
+
+$shortDescription = new ExtraField('session');
+$shortDescription->save([
+ 'field_type' => ExtraField::FIELD_TYPE_TEXT,
+ 'variable' => 'short_description',
+ 'display_text' => get_lang('ShortDescription'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
+
+$id = new ExtraField('session');
+$id->save([
+ 'field_type' => ExtraField::FIELD_TYPE_TEXT,
+ 'variable' => 'code',
+ 'display_text' => get_lang('Code'),
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+]);
diff --git a/plugin/advanced_subscription/src/scripts/insert_sessions_categories.php b/plugin/advanced_subscription/src/scripts/insert_sessions_categories.php
new file mode 100644
index 000000000..6395be8ae
--- /dev/null
+++ b/plugin/advanced_subscription/src/scripts/insert_sessions_categories.php
@@ -0,0 +1,21 @@
+get_lang('plugin_title'));
+
+$isAllowToDoRequest = $plugin->isAllowedToDoRequest($data['studentUserId'], $data, true);
+
+if (!$isAllowToDoRequest) {
+ $tpl->assign('errorMessages', $plugin->getErrorMessages());
+}
+
+if (
+ !empty($data['sessionId']) &&
+ !empty($data['studentUserId']) &&
+ api_get_plugin_setting('courselegal', 'tool_enable')
+) {
+ $lastMessageId = $plugin->getLastMessageId($data['studentUserId'], $data['sessionId']);
+ if ($lastMessageId !== false) {
+ // Render mail
+ $url = $plugin->getRenderMailUrl(['queueId' => $lastMessageId]);
+ header('Location: '.$url);
+ exit;
+ }
+ $courses = SessionManager::get_course_list_by_session_id($data['sessionId']);
+ $course = current($courses);
+ $data['courseId'] = $course['id'];
+ $legalEnabled = api_get_plugin_setting('courselegal', 'tool_enable');
+ if ($legalEnabled) {
+ $courseLegal = CourseLegalPlugin::create();
+ $termsAndConditions = $courseLegal->getData($data['courseId'], $data['sessionId']);
+ $termsAndConditions = $termsAndConditions['content'];
+ $termFiles = $courseLegal->getCurrentFile($data['courseId'], $data['sessionId']);
+ } else {
+ $termsAndConditions = $plugin->get('terms_and_conditions');
+ $termFiles = '';
+ }
+
+ $data['session'] = api_get_session_info($data['sessionId']);
+ $data['student'] = api_get_user_info($data['studentUserId']);
+ $data['course'] = api_get_course_info_by_id($data['courseId']);
+ $data['acceptTermsUrl'] = $plugin->getQueueUrl($data);
+ $data['rejectTermsUrl'] = $plugin->getTermsUrl($data, ADVANCED_SUBSCRIPTION_TERMS_MODE_REJECT);
+ // Use Twig with String loader
+ $termsContent = $plugin->renderTemplateString($termsAndConditions, $data);
+} else {
+ $termsContent = '';
+ $termFiles = '';
+ $data['acceptTermsUrl'] = '#';
+ $data['rejectTermsUrl'] = '#';
+}
+
+// Assign into content
+$tpl->assign('termsRejected', $data['termsRejected']);
+$tpl->assign('acceptTermsUrl', $data['acceptTermsUrl']);
+$tpl->assign('rejectTermsUrl', $data['rejectTermsUrl']);
+$tpl->assign('session', $data['session']);
+$tpl->assign('student', $data['student']);
+$tpl->assign('sessionId', $data['sessionId']);
+$tpl->assign('termsContent', $termsContent);
+$tpl->assign('termsFiles', $termFiles);
+
+$content = $tpl->fetch('/advanced_subscription/views/terms_and_conditions.tpl');
+echo $content;
diff --git a/plugin/advanced_subscription/test/mails.php b/plugin/advanced_subscription/test/mails.php
new file mode 100644
index 000000000..824c061eb
--- /dev/null
+++ b/plugin/advanced_subscription/test/mails.php
@@ -0,0 +1,134 @@
+get_all([
+ 'variable IN ( ?, ?, ?, ?, ?)' => $fieldsArray,
+]);
+$fields = [];
+// Index session fields
+foreach ($fieldList as $field) {
+ $fields[$field['id']] = $field['variable'];
+}
+
+$mergedArray = array_merge([$data['sessionId']], array_keys($fields));
+$sessionFieldValueList = $extraSession->get_all(
+ ['item_id = ? field_id IN ( ?, ?, ?, ?, ?, ?, ? )' => $mergedArray]
+);
+foreach ($sessionFieldValueList as $sessionFieldValue) {
+ // Check if session field value is set in session field list
+ if (isset($fields[$sessionFieldValue['field_id']])) {
+ $var = $fields[$sessionFieldValue['field_id']];
+ $val = $sessionFieldValue['value'];
+ // Assign session field value to session
+ $sessionArray[$var] = $val;
+ }
+}
+// Get student data
+$studentArray = api_get_user_info($data['studentUserId']);
+$studentArray['picture'] = $studentArray['avatar'];
+
+// Get superior data if exist
+$superiorId = UserManager::getFirstStudentBoss($data['studentUserId']);
+if (!empty($superiorId)) {
+ $superiorArray = api_get_user_info($superiorId);
+} else {
+ $superiorArray = api_get_user_info(3);
+}
+// Get admin data
+$adminsArray = UserManager::get_all_administrators();
+$isWesternNameOrder = api_is_western_name_order();
+foreach ($adminsArray as &$admin) {
+ $admin['complete_name'] = $isWesternNameOrder ?
+ $admin['firstname'].', '.$admin['lastname'] : $admin['lastname'].', '.$admin['firstname']
+ ;
+}
+unset($admin);
+// Set data
+$data['action'] = 'confirm';
+$data['student'] = $studentArray;
+$data['superior'] = $superiorArray;
+$data['admins'] = $adminsArray;
+$data['admin'] = current($adminsArray);
+$data['session'] = $sessionArray;
+$data['signature'] = api_get_setting('Institution');
+$data['admin_view_url'] = api_get_path(WEB_PLUGIN_PATH).
+ 'advanced_subscription/src/admin_view.php?s='.$data['sessionId'];
+$data['newStatus'] = ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_APPROVED;
+$data['student']['acceptUrl'] = $plugin->getQueueUrl($data);
+$data['newStatus'] = ADVANCED_SUBSCRIPTION_QUEUE_STATUS_BOSS_DISAPPROVED;
+$data['student']['rejectUrl'] = $plugin->getQueueUrl($data);
+$tpl = new Template($plugin->get_lang('plugin_title'));
+$tpl->assign('data', $data);
+$tplParams = [
+ 'user',
+ 'student',
+ 'students',
+ 'superior',
+ 'admins',
+ 'admin',
+ 'session',
+ 'signature',
+ 'admin_view_url',
+ 'acceptUrl',
+ 'rejectUrl',
+];
+foreach ($tplParams as $tplParam) {
+ $tpl->assign($tplParam, $data[$tplParam]);
+}
+
+$dir = __DIR__.'/../views/';
+$files = scandir($dir);
+
+echo ' ', '
+
+
\ No newline at end of file
diff --git a/plugin/advanced_subscription/views/terms_and_conditions_to_pdf.tpl b/plugin/advanced_subscription/views/terms_and_conditions_to_pdf.tpl
new file mode 100644
index 000000000..dcbbc810e
--- /dev/null
+++ b/plugin/advanced_subscription/views/terms_and_conditions_to_pdf.tpl
@@ -0,0 +1,23 @@
+{# start copy from head.tpl #}
+
+
+
+
+{{ prefetch }}
+{{ favico }}
+{{ browser_specific_head }}
+
+
+
+
+{# Use the latest engine in ie8/ie9 or use google chrome engine if available #}
+{# Improve usability in portal devices #}
+
+{{ title_string }}
+{{ css_file_to_string }}
+{{ css_style_print }}
+{{ js_file_to_string }}
+{# end copy from head.tpl #}
+
+ {{ termsContent }}
+
\ No newline at end of file
diff --git a/plugin/ai_helper/AiHelperPlugin.php b/plugin/ai_helper/AiHelperPlugin.php
new file mode 100644
index 000000000..868d56269
--- /dev/null
+++ b/plugin/ai_helper/AiHelperPlugin.php
@@ -0,0 +1,421 @@
+ 'html',
+ 'tool_enable' => 'boolean',
+ 'api_name' => [
+ 'type' => 'select',
+ 'options' => $this->getApiList(),
+ ],
+ 'api_key' => 'text',
+ 'organization_id' => 'text',
+ 'tool_lp_enable' => 'boolean',
+ 'tool_quiz_enable' => 'boolean',
+ 'tokens_limit' => 'text',
+ ];
+
+ parent::__construct($version, $author, $settings);
+ }
+
+ /**
+ * Get the list of APIs available.
+ *
+ * @return array
+ */
+ public function getApiList()
+ {
+ $list = [
+ self::OPENAI_API => 'OpenAI',
+ self::DEEPSEEK_API => 'DeepSeek',
+ ];
+
+ return $list;
+ }
+
+ /**
+ * Get the completion text from the selected API.
+ *
+ * @return string|array
+ */
+ public function getCompletionText(string $prompt, string $toolName)
+ {
+ if (!$this->validateUserTokensLimit(api_get_user_id())) {
+ return [
+ 'error' => true,
+ 'message' => $this->get_lang('ErrorTokensLimit'),
+ ];
+ }
+
+ $apiName = $this->get('api_name');
+
+ switch ($apiName) {
+ case self::OPENAI_API:
+ return $this->openAiGetCompletionText($prompt, $toolName);
+ case self::DEEPSEEK_API:
+ return $this->deepSeekGetCompletionText($prompt, $toolName);
+ default:
+ return [
+ 'error' => true,
+ 'message' => 'API not supported.',
+ ];
+ }
+ }
+
+ /**
+ * Get completion text from OpenAI.
+ */
+ public function openAiGetCompletionText(string $prompt, string $toolName)
+ {
+ try {
+ require_once __DIR__.'/src/openai/OpenAi.php';
+
+ $apiKey = $this->get('api_key');
+ $organizationId = $this->get('organization_id');
+
+ $ai = new OpenAi($apiKey, $organizationId);
+
+ $params = [
+ 'model' => 'gpt-3.5-turbo-instruct',
+ 'prompt' => $prompt,
+ 'temperature' => 0.2,
+ 'max_tokens' => 2000,
+ 'frequency_penalty' => 0,
+ 'presence_penalty' => 0.6,
+ 'top_p' => 1.0,
+ ];
+
+ $complete = $ai->completion($params);
+ $result = json_decode($complete, true);
+
+ if (isset($result['error'])) {
+ $errorMessage = $result['error']['message'] ?? 'Unknown error';
+ error_log("OpenAI Error: $errorMessage");
+
+ return [
+ 'error' => true,
+ 'message' => $errorMessage,
+ ];
+ }
+
+ $resultText = $result['choices'][0]['text'] ?? '';
+
+ if (!empty($resultText)) {
+ $this->saveRequest([
+ 'user_id' => api_get_user_id(),
+ 'tool_name' => $toolName,
+ 'prompt' => $prompt,
+ 'prompt_tokens' => (int) ($result['usage']['prompt_tokens'] ?? 0),
+ 'completion_tokens' => (int) ($result['usage']['completion_tokens'] ?? 0),
+ 'total_tokens' => (int) ($result['usage']['total_tokens'] ?? 0),
+ ]);
+ }
+
+ return $resultText ?: 'No response generated.';
+ } catch (Exception $e) {
+ return [
+ 'error' => true,
+ 'message' => 'An error occurred while connecting to OpenAI: '.$e->getMessage(),
+ ];
+ }
+ }
+
+ /**
+ * Get completion text from DeepSeek.
+ */
+ public function deepSeekGetCompletionText(string $prompt, string $toolName)
+ {
+ $apiKey = $this->get('api_key');
+
+ $url = 'https://api.deepseek.com/chat/completions';
+
+ $payload = [
+ 'model' => 'deepseek-chat',
+ 'messages' => [
+ [
+ 'role' => 'system',
+ 'content' => ($toolName === 'quiz')
+ ? 'You are a helpful assistant that generates Aiken format questions.'
+ : 'You are a helpful assistant that generates learning path contents.',
+ ],
+ [
+ 'role' => 'user',
+ 'content' => $prompt,
+ ],
+ ],
+ 'stream' => false,
+ ];
+
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Content-Type: application/json',
+ "Authorization: Bearer $apiKey",
+ ]);
+
+ $response = curl_exec($ch);
+
+ if ($response === false) {
+ error_log('cURL error: '.curl_error($ch));
+ curl_close($ch);
+
+ return ['error' => true, 'message' => 'Request to AI provider failed.'];
+ }
+
+ curl_close($ch);
+
+ $result = json_decode($response, true);
+
+ if (isset($result['error'])) {
+ return [
+ 'error' => true,
+ 'message' => $result['error']['message'] ?? 'Unknown error',
+ ];
+ }
+
+ $resultText = $result['choices'][0]['message']['content'] ?? '';
+ $this->saveRequest([
+ 'user_id' => api_get_user_id(),
+ 'tool_name' => $toolName,
+ 'prompt' => $prompt,
+ 'prompt_tokens' => 0,
+ 'completion_tokens' => 0,
+ 'total_tokens' => 0,
+ ]);
+
+ return $resultText;
+ }
+
+ /**
+ * Generate questions based on the selected AI provider.
+ *
+ * @param int $nQ Number of questions
+ * @param string $lang Language for the questions
+ * @param string $topic Topic of the questions
+ * @param string $questionType Type of questions (e.g., 'multiple_choice')
+ *
+ * @throws Exception If an error occurs
+ *
+ * @return string Questions generated in Aiken format
+ */
+ public function generateQuestions(int $nQ, string $lang, string $topic, string $questionType = 'multiple_choice'): string
+ {
+ $apiName = $this->get('api_name');
+
+ switch ($apiName) {
+ case self::OPENAI_API:
+ return $this->generateOpenAiQuestions($nQ, $lang, $topic, $questionType);
+ case self::DEEPSEEK_API:
+ return $this->generateDeepSeekQuestions($nQ, $lang, $topic, $questionType);
+ default:
+ throw new Exception("Unsupported API provider: $apiName");
+ }
+ }
+
+ /**
+ * Validates tokens limit of a user per current month.
+ */
+ public function validateUserTokensLimit(int $userId): bool
+ {
+ $em = Database::getManager();
+ $repo = $em->getRepository('ChamiloPluginBundle:AiHelper\Requests');
+
+ $startDate = api_get_utc_datetime(
+ null,
+ false,
+ true)
+ ->modify('first day of this month')->setTime(00, 00, 00)
+ ;
+ $endDate = api_get_utc_datetime(
+ null,
+ false,
+ true)
+ ->modify('last day of this month')->setTime(23, 59, 59)
+ ;
+
+ $qb = $repo->createQueryBuilder('e')
+ ->select('sum(e.totalTokens) as total')
+ ->andWhere('e.requestedAt BETWEEN :dateMin AND :dateMax')
+ ->andWhere('e.userId = :user')
+ ->setMaxResults(1)
+ ->setParameters(
+ [
+ 'dateMin' => $startDate->format('Y-m-d h:i:s'),
+ 'dateMax' => $endDate->format('Y-m-d h:i:s'),
+ 'user' => $userId,
+ ]
+ );
+ $result = $qb->getQuery()->getOneOrNullResult();
+ $totalTokens = !empty($result) ? (int) $result['total'] : 0;
+
+ $valid = true;
+ $tokensLimit = $this->get('tokens_limit');
+ if (!empty($tokensLimit)) {
+ $valid = ($totalTokens <= (int) $tokensLimit);
+ }
+
+ return $valid;
+ }
+
+ /**
+ * Get the plugin directory name.
+ */
+ public function get_name(): string
+ {
+ return 'ai_helper';
+ }
+
+ /**
+ * Get the class instance.
+ *
+ * @staticvar AiHelperPlugin $result
+ */
+ public static function create(): AiHelperPlugin
+ {
+ static $result = null;
+
+ return $result ?: $result = new self();
+ }
+
+ /**
+ * Save user information of openai request.
+ *
+ * @return int
+ */
+ public function saveRequest(array $values)
+ {
+ $em = Database::getManager();
+
+ $objRequest = new Requests();
+ $objRequest
+ ->setUserId($values['user_id'])
+ ->setToolName($values['tool_name'])
+ ->setRequestedAt(new DateTime())
+ ->setRequestText($values['prompt'])
+ ->setPromptTokens($values['prompt_tokens'])
+ ->setCompletionTokens($values['completion_tokens'])
+ ->setTotalTokens($values['total_tokens'])
+ ;
+ $em->persist($objRequest);
+ $em->flush();
+
+ return $objRequest->getId();
+ }
+
+ /**
+ * Install the plugin. Set the database up.
+ */
+ public function install()
+ {
+ $em = Database::getManager();
+
+ if ($em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_REQUESTS])) {
+ return;
+ }
+
+ $schemaTool = new SchemaTool($em);
+ $schemaTool->createSchema(
+ [
+ $em->getClassMetadata(Requests::class),
+ ]
+ );
+ }
+
+ /**
+ * Unistall plugin. Clear the database.
+ */
+ public function uninstall()
+ {
+ $em = Database::getManager();
+
+ if (!$em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_REQUESTS])) {
+ return;
+ }
+
+ $schemaTool = new SchemaTool($em);
+ $schemaTool->dropSchema(
+ [
+ $em->getClassMetadata(Requests::class),
+ ]
+ );
+ }
+
+ /**
+ * Generate questions using OpenAI.
+ */
+ private function generateOpenAiQuestions(int $nQ, string $lang, string $topic, string $questionType): string
+ {
+ $prompt = sprintf(
+ 'Generate %d "%s" questions in Aiken format in the %s language about "%s", making sure there is a \'ANSWER\' line for each question. \'ANSWER\' lines must only mention the letter of the correct answer, not the full answer text and not a parenthesis. The line starting with \'ANSWER\' must not be separated from the last possible answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text without quotes. Include an \'ANSWER_EXPLANATION\' line after the \'ANSWER\' line for each question. The terms between single quotes above must not be translated. There must be a blank line between each question.',
+ $nQ,
+ $questionType,
+ $lang,
+ $topic
+ );
+
+ $result = $this->openAiGetCompletionText($prompt, 'quiz');
+ if (isset($result['error']) && true === $result['error']) {
+ throw new Exception($result['message']);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Generate questions using DeepSeek.
+ */
+ private function generateDeepSeekQuestions(int $nQ, string $lang, string $topic, string $questionType): string
+ {
+ $apiKey = $this->get('api_key');
+ $prompt = sprintf(
+ 'Generate %d "%s" questions in Aiken format in the %s language about "%s", making sure there is a \'ANSWER\' line for each question. \'ANSWER\' lines must only mention the letter of the correct answer, not the full answer text and not a parenthesis. The line starting with \'ANSWER\' must not be separated from the last possible answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text without quotes. Include an \'ANSWER_EXPLANATION\' line after the \'ANSWER\' line for each question. The terms between single quotes above must not be translated. There must be a blank line between each question.',
+ $nQ,
+ $questionType,
+ $lang,
+ $topic
+ );
+ $payload = [
+ 'model' => 'deepseek-chat',
+ 'messages' => [
+ [
+ 'role' => 'system',
+ 'content' => 'You are a helpful assistant that generates Aiken format questions.',
+ ],
+ [
+ 'role' => 'user',
+ 'content' => $prompt,
+ ],
+ ],
+ 'stream' => false,
+ ];
+
+ $deepSeek = new DeepSeek($apiKey);
+ $response = $deepSeek->generateQuestions($payload);
+
+ return $response;
+ }
+}
diff --git a/plugin/ai_helper/Entity/Requests.php b/plugin/ai_helper/Entity/Requests.php
new file mode 100644
index 000000000..85931cda6
--- /dev/null
+++ b/plugin/ai_helper/Entity/Requests.php
@@ -0,0 +1,171 @@
+userId;
+ }
+
+ public function setUserId(int $userId): Requests
+ {
+ $this->userId = $userId;
+
+ return $this;
+ }
+
+ public function getId(): int
+ {
+ return $this->id;
+ }
+
+ public function setId(int $id): Requests
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ public function getRequestedAt(): \DateTime
+ {
+ return $this->requestedAt;
+ }
+
+ public function setRequestedAt(\DateTime $requestedAt): Requests
+ {
+ $this->requestedAt = $requestedAt;
+
+ return $this;
+ }
+
+ public function getRequestText(): string
+ {
+ return $this->requestText;
+ }
+
+ public function setRequestText(string $requestText): Requests
+ {
+ $this->requestText = $requestText;
+
+ return $this;
+ }
+
+ public function getPromptTokens(): int
+ {
+ return $this->promptTokens;
+ }
+
+ public function setPromptTokens(int $promptTokens): Requests
+ {
+ $this->promptTokens = $promptTokens;
+
+ return $this;
+ }
+
+ public function getCompletionTokens(): int
+ {
+ return $this->completionTokens;
+ }
+
+ public function setCompletionTokens(int $completionTokens): Requests
+ {
+ $this->completionTokens = $completionTokens;
+
+ return $this;
+ }
+
+ public function getTotalTokens(): int
+ {
+ return $this->totalTokens;
+ }
+
+ public function setTotalTokens(int $totalTokens): Requests
+ {
+ $this->totalTokens = $totalTokens;
+
+ return $this;
+ }
+
+ public function getToolName(): string
+ {
+ return $this->toolName;
+ }
+
+ public function setToolName(string $toolName): Requests
+ {
+ $this->toolName = $toolName;
+
+ return $this;
+ }
+}
diff --git a/plugin/ai_helper/README.md b/plugin/ai_helper/README.md
new file mode 100644
index 000000000..fb245d8b8
--- /dev/null
+++ b/plugin/ai_helper/README.md
@@ -0,0 +1,68 @@
+AI Helper Plugin
+======
+
+Version 1.2
+
+> This plugin is designed to integrate AI functionality into Chamilo, providing tools for generating educational content, such as quizzes or learning paths, using AI providers like OpenAI or DeepSeek.
+
+---
+
+### Overview
+
+The AI Helper plugin integrates into parts of the Chamilo platform that are most useful to teachers/trainers or learners. It allows pre-generating content, letting teachers/trainers review it before publishing.
+
+Currently, this plugin is integrated into:
+
+- **Exercises:** In the Aiken import form, with options to generate questions using OpenAI or DeepSeek.
+- **Learnpaths:** Option to create structured learning paths with OpenAI or DeepSeek.
+
+---
+
+### Supported AI Providers
+
+#### OpenAI/ChatGPT
+The plugin, created in early 2023, supports OpenAI's ChatGPT API.
+- **Setup:**
+1. Create an account at [OpenAI](https://platform.openai.com/signup) (or login if you already have one).
+2. Generate a secret key at [API Keys](https://platform.openai.com/account/api-keys).
+3. Click "Create new secret key", copy the key, and paste it into the "API key" field in the plugin configuration.
+
+#### DeepSeek
+DeepSeek is an alternative Open Source AI provider.
+- **Setup:**
+1. Create an account at [DeepSeek](https://www.deepseek.com/) (or login if you already have one).
+2. Generate an API key at [API Keys](https://platform.deepseek.com/api_keys).
+3. Click "Create new API key", copy the key, and paste it into the "API key" field in the plugin configuration.
+
+---
+
+### Features
+
+- Generate quizzes in the Aiken format using AI.
+- Create structured learning paths with AI assistance.
+- Support for multiple AI providers, enabling easy switching between OpenAI and DeepSeek.
+- Tracks API requests for monitoring usage and limits.
+
+---
+
+### Database Requirements
+
+No additional database changes are required for v1.2.
+The existing table `plugin_ai_helper_requests` is sufficient for tracking requests from both OpenAI and DeepSeek.
+
+If you're updating from **v1.0**, ensure the following table exists:
+
+```sql
+CREATE TABLE plugin_ai_helper_requests (
+ id int(11) NOT NULL AUTO_INCREMENT,
+ user_id int(11) NOT NULL,
+ tool_name varchar(255) COLLATE utf8_unicode_ci NOT NULL,
+ requested_at datetime DEFAULT NULL,
+ request_text varchar(255) COLLATE utf8_unicode_ci NOT NULL,
+ prompt_tokens int(11) NOT NULL,
+ completion_tokens int(11) NOT NULL,
+ total_tokens int(11) NOT NULL,
+ PRIMARY KEY (id)
+) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
+```
+If you got this update through Git, you will also need to run `composer install` to update the autoload mechanism.
diff --git a/plugin/ai_helper/install.php b/plugin/ai_helper/install.php
new file mode 100644
index 000000000..ec2627011
--- /dev/null
+++ b/plugin/ai_helper/install.php
@@ -0,0 +1,16 @@
+install();
diff --git a/plugin/ai_helper/lang/english.php b/plugin/ai_helper/lang/english.php
new file mode 100644
index 000000000..36ede273f
--- /dev/null
+++ b/plugin/ai_helper/lang/english.php
@@ -0,0 +1,20 @@
+get_info();
diff --git a/plugin/ai_helper/src/deepseek/DeepSeek.php b/plugin/ai_helper/src/deepseek/DeepSeek.php
new file mode 100644
index 000000000..e50494f48
--- /dev/null
+++ b/plugin/ai_helper/src/deepseek/DeepSeek.php
@@ -0,0 +1,91 @@
+apiKey = $apiKey;
+ $this->headers = [
+ 'Content-Type: application/json',
+ "Authorization: Bearer {$this->apiKey}",
+ ];
+ }
+
+ /**
+ * Generate questions using the DeepSeek API.
+ *
+ * @param array $payload Data to send to the API
+ *
+ * @throws Exception If an error occurs during the request
+ *
+ * @return string Decoded response from the API
+ */
+ public function generateQuestions(array $payload): string
+ {
+ $url = DeepSeekUrl::completionsUrl();
+ $response = $this->sendRequest($url, 'POST', $payload);
+
+ if (empty($response)) {
+ throw new Exception('The DeepSeek API returned no response.');
+ }
+
+ $result = json_decode($response, true);
+
+ // Validate errors returned by the API
+ if (isset($result['error'])) {
+ throw new Exception("DeepSeek API Error: {$result['error']['message']}");
+ }
+
+ // Ensure the response contains the expected "choices" field
+ if (!isset($result['choices'][0]['message']['content'])) {
+ throw new Exception('Unexpected response format from the DeepSeek API.');
+ }
+
+ return $result['choices'][0]['message']['content'];
+ }
+
+ /**
+ * Send a request to the DeepSeek API.
+ *
+ * @param string $url Endpoint to send the request to
+ * @param string $method HTTP method (e.g., GET, POST)
+ * @param array $data Data to send as JSON
+ *
+ * @throws Exception If a cURL error occurs
+ *
+ * @return string Raw response from the API
+ */
+ private function sendRequest(string $url, string $method, array $data = []): string
+ {
+ $ch = curl_init($url);
+
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+ if (curl_errno($ch)) {
+ $errorMessage = curl_error($ch);
+ curl_close($ch);
+ throw new Exception("cURL Error: {$errorMessage}");
+ }
+
+ curl_close($ch);
+
+ // Validate HTTP status codes
+ if ($httpCode < 200 || $httpCode >= 300) {
+ throw new Exception("Request to DeepSeek failed with HTTP status code: {$httpCode}");
+ }
+
+ return $response;
+ }
+}
diff --git a/plugin/ai_helper/src/deepseek/DeepSeekUrl.php b/plugin/ai_helper/src/deepseek/DeepSeekUrl.php
new file mode 100644
index 000000000..5d9b1453d
--- /dev/null
+++ b/plugin/ai_helper/src/deepseek/DeepSeekUrl.php
@@ -0,0 +1,17 @@
+contentTypes = [
+ "application/json" => "Content-Type: application/json",
+ "multipart/form-data" => "Content-Type: multipart/form-data",
+ ];
+
+ $this->headers = [
+ $this->contentTypes["application/json"],
+ "Authorization: Bearer $apiKey",
+ ];
+
+ if (!empty($organizationId)) {
+ $this->headers[] = "OpenAI-Organization: $organizationId";
+ }
+ }
+
+ /**
+ * @return bool|string
+ */
+ public function listModels()
+ {
+ $url = OpenAiUrl::fineTuneModel();
+
+ return $this->sendRequest($url, 'GET');
+ }
+
+ /**
+ * @param $model
+ *
+ * @return bool|string
+ */
+ public function retrieveModel($model)
+ {
+ $model = "/$model";
+ $url = OpenAiUrl::fineTuneModel().$model;
+
+ return $this->sendRequest($url, 'GET');
+ }
+
+ /**
+ * @param $opts
+ * @param null $stream
+ *
+ * @return bool|string
+ */
+ public function completion(array $opts, callable $stream = null)
+ {
+ if ($stream !== null && isset($opts['stream']) && $opts['stream']) {
+ $this->streamMethod = $stream;
+ }
+
+ $opts['model'] = $opts['model'] ?? $this->model;
+ $url = OpenAiUrl::completionsURL();
+
+ return $this->sendRequest($url, 'POST', $opts);
+ }
+
+ /**
+ * @param $opts
+ *
+ * @return bool|string
+ */
+ public function createEdit($opts)
+ {
+ $url = OpenAiUrl::editsUrl();
+
+ return $this->sendRequest($url, 'POST', $opts);
+ }
+
+ /**
+ * @param $opts
+ *
+ * @return bool|string
+ */
+ public function image($opts)
+ {
+ $url = OpenAiUrl::imageUrl()."/generations";
+
+ return $this->sendRequest($url, 'POST', $opts);
+ }
+
+ /**
+ * @param $opts
+ *
+ * @return bool|string
+ */
+ public function imageEdit($opts)
+ {
+ $url = OpenAiUrl::imageUrl()."/edits";
+
+ return $this->sendRequest($url, 'POST', $opts);
+ }
+
+ /**
+ * @param $opts
+ *
+ * @return bool|string
+ */
+ public function createImageVariation($opts)
+ {
+ $url = OpenAiUrl::imageUrl()."/variations";
+
+ return $this->sendRequest($url, 'POST', $opts);
+ }
+
+ /**
+ * @param $opts
+ *
+ * @return bool|string
+ */
+ public function moderation($opts)
+ {
+ $url = OpenAiUrl::moderationUrl();
+
+ return $this->sendRequest($url, 'POST', $opts);
+ }
+
+ /**
+ * @param $opts
+ *
+ * @return bool|string
+ */
+ public function uploadFile($opts)
+ {
+ $url = OpenAiUrl::filesUrl();
+
+ return $this->sendRequest($url, 'POST', $opts);
+ }
+
+ /**
+ * @return bool|string
+ */
+ public function listFiles()
+ {
+ $url = OpenAiUrl::filesUrl();
+
+ return $this->sendRequest($url, 'GET');
+ }
+
+ /**
+ * @param $fileId
+ *
+ * @return bool|string
+ */
+ public function retrieveFile($fileId)
+ {
+ $fileId = "/$fileId";
+ $url = OpenAiUrl::filesUrl().$fileId;
+
+ return $this->sendRequest($url, 'GET');
+ }
+
+ /**
+ * @param $fileId
+ *
+ * @return bool|string
+ */
+ public function retrieveFileContent($fileId)
+ {
+ $fileId = "/$fileId/content";
+ $url = OpenAiUrl::filesUrl().$fileId;
+
+ return $this->sendRequest($url, 'GET');
+ }
+
+ /**
+ * @param $fileId
+ *
+ * @return bool|string
+ */
+ public function deleteFile($fileId)
+ {
+ $fileId = "/$fileId";
+ $url = OpenAiUrl::filesUrl().$fileId;
+
+ return $this->sendRequest($url, 'DELETE');
+ }
+
+ /**
+ * @param $opts
+ *
+ * @return bool|string
+ */
+ public function createFineTune($opts)
+ {
+ $url = OpenAiUrl::fineTuneUrl();
+
+ return $this->sendRequest($url, 'POST', $opts);
+ }
+
+ /**
+ * @return bool|string
+ */
+ public function listFineTunes()
+ {
+ $url = OpenAiUrl::fineTuneUrl();
+
+ return $this->sendRequest($url, 'GET');
+ }
+
+ /**
+ * @param $fineTuneId
+ *
+ * @return bool|string
+ */
+ public function retrieveFineTune($fineTuneId)
+ {
+ $fineTuneId = "/$fineTuneId";
+ $url = OpenAiUrl::fineTuneUrl().$fineTuneId;
+
+ return $this->sendRequest($url, 'GET');
+ }
+
+ /**
+ * @param $fineTuneId
+ *
+ * @return bool|string
+ */
+ public function cancelFineTune($fineTuneId)
+ {
+ $fineTuneId = "/$fineTuneId/cancel";
+ $url = OpenAiUrl::fineTuneUrl().$fineTuneId;
+
+ return $this->sendRequest($url, 'POST');
+ }
+
+ /**
+ * @param $fineTuneId
+ *
+ * @return bool|string
+ */
+ public function listFineTuneEvents($fineTuneId)
+ {
+ $fineTuneId = "/$fineTuneId/events";
+ $url = OpenAiUrl::fineTuneUrl().$fineTuneId;
+
+ return $this->sendRequest($url, 'GET');
+ }
+
+ /**
+ * @param $fineTuneId
+ *
+ * @return bool|string
+ */
+ public function deleteFineTune($fineTuneId)
+ {
+ $fineTuneId = "/$fineTuneId";
+ $url = OpenAiUrl::fineTuneModel().$fineTuneId;
+
+ return $this->sendRequest($url, 'DELETE');
+ }
+
+ /**
+ * @param $opts
+ *
+ * @return bool|string
+ */
+ public function embeddings($opts)
+ {
+ $url = OpenAiUrl::embeddings();
+
+ return $this->sendRequest($url, 'POST', $opts);
+ }
+
+ public function setTimeout(int $timeout)
+ {
+ $this->timeout = $timeout;
+ }
+
+ private function sendRequest(string $url, string $method, array $opts = []): string
+ {
+ $post_fields = json_encode($opts);
+
+ if (isset($opts['file']) || isset($opts['image'])) {
+ $this->headers[0] = $this->contentTypes["multipart/form-data"];
+ $post_fields = $opts;
+ } else {
+ $this->headers[0] = $this->contentTypes["application/json"];
+ }
+ $curl_info = [
+ CURLOPT_URL => $url,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_ENCODING => '',
+ CURLOPT_MAXREDIRS => 10,
+ CURLOPT_TIMEOUT => $this->timeout,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
+ CURLOPT_CUSTOMREQUEST => $method,
+ CURLOPT_POSTFIELDS => $post_fields,
+ CURLOPT_HTTPHEADER => $this->headers,
+ ];
+
+ if (empty($opts)) {
+ unset($curl_info[CURLOPT_POSTFIELDS]);
+ }
+
+ if (isset($opts['stream']) && $opts['stream']) {
+ $curl_info[CURLOPT_WRITEFUNCTION] = $this->streamMethod;
+ }
+
+ $curl = curl_init();
+
+ curl_setopt_array($curl, $curl_info);
+ $response = curl_exec($curl);
+ $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+
+ if (curl_errno($curl)) {
+ $errorMessage = curl_error($curl);
+ curl_close($curl);
+ throw new Exception("cURL Error: {$errorMessage}");
+ }
+
+ curl_close($curl);
+
+ if ($httpCode === 429) {
+ throw new Exception("Insufficient quota. Please check your OpenAI account plan and billing details.");
+ }
+
+ if ($httpCode < 200 || $httpCode >= 300) {
+ throw new Exception("HTTP Error: {$httpCode}, Response: {$response}");
+ }
+
+ return $response;
+ }
+}
diff --git a/plugin/ai_helper/src/openai/OpenAiUrl.php b/plugin/ai_helper/src/openai/OpenAiUrl.php
new file mode 100644
index 000000000..1b05ea2a0
--- /dev/null
+++ b/plugin/ai_helper/src/openai/OpenAiUrl.php
@@ -0,0 +1,74 @@
+generateQuestions($nQ, $lang, $topic, $questionType);
+
+ echo json_encode([
+ 'success' => true,
+ 'text' => trim($resultText),
+ ]);
+} catch (Exception $e) {
+ error_log("Error: ".$e->getMessage());
+ echo json_encode([
+ 'success' => false,
+ 'text' => $e->getMessage(),
+ ]);
+}
diff --git a/plugin/ai_helper/tool/learnpath.php b/plugin/ai_helper/tool/learnpath.php
new file mode 100644
index 000000000..05f20c8dd
--- /dev/null
+++ b/plugin/ai_helper/tool/learnpath.php
@@ -0,0 +1,218 @@
+getApiList();
+$apiName = $plugin->get('api_name');
+
+if (!in_array($apiName, array_keys($apiList))) {
+ echo json_encode(['success' => false, 'text' => 'AI Provider not available.']);
+ exit;
+}
+
+$courseLanguage = (string) $_REQUEST['language'];
+$chaptersCount = (int) $_REQUEST['nro_items'];
+$topic = (string) $_REQUEST['lp_name'];
+$wordsCount = (int) $_REQUEST['words_count'];
+$courseCode = (string) $_REQUEST['course_code'];
+$sessionId = (int) $_REQUEST['session_id'];
+$addTests = ('true' === $_REQUEST['add_tests']);
+$nQ = ($addTests ? (int) $_REQUEST['nro_questions'] : 0);
+
+$messageGetItems = 'Generate the table of contents of a course in "%s" in %d or fewer chapters on the topic "%s". Return it as a list of items separated by new lines. Do not include a conclusion chapter.';
+$prompt = sprintf($messageGetItems, $courseLanguage, $chaptersCount, $topic);
+
+$resultText = $plugin->getCompletionText($prompt, 'learnpath');
+
+if (isset($resultText['error']) && $resultText['error']) {
+ echo json_encode(['success' => false, 'text' => $resultText['message']]);
+ exit;
+}
+
+if (empty($resultText)) {
+ echo json_encode(['success' => false, 'text' => 'AI returned no results.']);
+ exit;
+}
+
+$lpItems = [];
+if (!empty($resultText)) {
+ $style = api_get_css_asset('bootstrap/dist/css/bootstrap.min.css');
+ $style .= api_get_css_asset('fontawesome/css/font-awesome.min.css');
+ $style .= api_get_css(ChamiloApi::getEditorDocStylePath());
+ $style .= api_get_css_asset('ckeditor/plugins/codesnippet/lib/highlight/styles/default.css');
+ $style .= api_get_asset('ckeditor/plugins/codesnippet/lib/highlight/highlight.pack.js');
+ $style .= '';
+
+ $items = explode("\n", $resultText);
+ $position = 1;
+ foreach ($items as $item) {
+ if (substr($item, 0, 2) === '- ') {
+ $item = substr($item, 2);
+ }
+ $explodedItem = preg_split('/\d\./', $item);
+ $title = count($explodedItem) > 1 ? $explodedItem[1] : $explodedItem[0];
+ if (!empty($title)) {
+ $lpItems[$position]['title'] = trim($title);
+ $messageGetItemContent = 'In the context of "%s", generate a document with HTML tags in "%s" with %d words of content or less, about "%s", as to be included as one chapter in a larger document on "%s". Consider the context is established for the reader and you do not need to repeat it.';
+ $promptItem = sprintf($messageGetItemContent, $topic, $courseLanguage, $wordsCount, $title, $topic);
+ $resultContentText = $plugin->getCompletionText($promptItem, 'learnpath');
+ if (isset($resultContentText['error']) && $resultContentText['error']) {
+ continue;
+ }
+ $lpItemContent = (!empty($resultContentText) ? trim($resultContentText) : '');
+ if (false !== stripos($lpItemContent, '')) {
+ $lpItemContent = preg_replace("||i", "\r\n$style\r\n\\0", $lpItemContent);
+ } else {
+ $lpItemContent = ''.trim($title).''.$style.''.$lpItemContent.'';
+ }
+ $lpItems[$position]['content'] = $lpItemContent;
+ $position++;
+ }
+ }
+}
+
+// Create the learnpath and return the id generated.
+$return = ['success' => false, 'lp_id' => 0];
+if (!empty($lpItems)) {
+ $lpId = learnpath::add_lp(
+ $courseCode,
+ $topic,
+ '',
+ 'chamilo',
+ 'manual'
+ );
+
+ if (!empty($lpId)) {
+ learnpath::toggle_visibility($lpId, 0);
+ $courseInfo = api_get_course_info($courseCode);
+ $lp = new \learnpath(
+ $courseCode,
+ $lpId,
+ api_get_user_id()
+ );
+ $lp->generate_lp_folder($courseInfo, $topic);
+ $order = 1;
+ $lpItemsIds = [];
+ foreach ($lpItems as $dspOrder => $item) {
+ $documentId = $lp->create_document(
+ $courseInfo,
+ $item['content'],
+ $item['title'],
+ 'html'
+ );
+
+ if (!empty($documentId)) {
+ $prevDocItem = (isset($lpItemsIds[$order - 1]) ? (int) $lpItemsIds[$order - 1]['item_id'] : 0);
+ $lpItemId = $lp->add_item(
+ 0,
+ $prevDocItem,
+ 'document',
+ $documentId,
+ $item['title'],
+ '',
+ 0,
+ 0,
+ api_get_user_id(),
+ $order
+ );
+ $lpItemsIds[$order]['item_id'] = $lpItemId;
+ $lpItemsIds[$order]['item_type'] = 'document';
+ if ($addTests && !empty($lpItemId)) {
+ $promptQuiz = 'Generate %d "%s" questions in Aiken format in the %s language about "%s", making sure there is a \'ANSWER\' line for each question. \'ANSWER\' lines must only mention the letter of the correct answer, not the full answer text and not a parenthesis. The line starting with \'ANSWER\' must not be separated from the last possible answer by a blank line. Each answer starts with an uppercase letter, a dot, one space and the answer text without quotes. Include an \'ANSWER_EXPLANATION\' line after the \'ANSWER\' line for each question. The terms between single quotes above must not be translated. There must be a blank line between each question. Show the question directly without any prefix.';
+ $promptQuiz = sprintf($promptQuiz, $nQ, $courseLanguage, $item['title'], $topic);
+ $resultQuizText = $plugin->getCompletionText($promptQuiz, 'quiz');
+ if (!empty($resultQuizText)) {
+ $request = [];
+ $request['quiz_name'] = get_lang('Exercise').': '.$item['title'];
+ $request['nro_questions'] = $nQ;
+ $request['course_id'] = api_get_course_int_id($courseCode);
+ $request['aiken_format'] = trim($resultQuizText);
+ $exerciseId = aikenImportExercise(null, $request);
+ if (!empty($exerciseId)) {
+ $order++;
+ $prevQuizItem = (isset($lpItemsIds[$order - 1]) ? (int) $lpItemsIds[$order - 1]['item_id'] : 0);
+ $lpQuizItemId = $lp->add_item(
+ 0,
+ $prevQuizItem,
+ 'quiz',
+ $exerciseId,
+ $request['quiz_name'],
+ '',
+ 0,
+ 0,
+ api_get_user_id(),
+ $order
+ );
+ if (!empty($lpQuizItemId)) {
+ $maxScore = (float) $nQ;
+ $minScore = round($nQ / 2, 2);
+ $lpItemsIds[$order]['item_id'] = $lpQuizItemId;
+ $lpItemsIds[$order]['item_type'] = 'quiz';
+ $lpItemsIds[$order]['min_score'] = $minScore;
+ $lpItemsIds[$order]['max_score'] = $maxScore;
+ }
+ }
+ }
+ }
+ }
+ $order++;
+ }
+
+ // Add the final item
+ if ($addTests) {
+ $finalTitle = get_lang('EndOfLearningPath');
+ $finalContent = file_get_contents(api_get_path(SYS_CODE_PATH).'lp/final_item_template/template.html');
+ $finalDocId = $lp->create_document(
+ $courseInfo,
+ $finalContent,
+ $finalTitle
+ );
+ $prevFinalItem = (isset($lpItemsIds[$order - 1]) ? (int) $lpItemsIds[$order - 1]['item_id'] : 0);
+ $lpFinalItemId = $lp->add_item(
+ 0,
+ $prevFinalItem,
+ TOOL_LP_FINAL_ITEM,
+ $finalDocId,
+ $finalTitle,
+ '',
+ 0,
+ 0,
+ api_get_user_id(),
+ $order
+ );
+ $lpItemsIds[$order]['item_id'] = $lpFinalItemId;
+ $lpItemsIds[$order]['item_type'] = TOOL_LP_FINAL_ITEM;
+
+ // Set lp items prerequisites
+ if (count($lpItemsIds) > 0) {
+ for ($i = 1; $i <= count($lpItemsIds); $i++) {
+ $prevIndex = ($i - 1);
+ if (isset($lpItemsIds[$prevIndex])) {
+ $itemId = $lpItemsIds[$i]['item_id'];
+ $prerequisite = $lpItemsIds[$prevIndex]['item_id'];
+ $minScore = ('quiz' === $lpItemsIds[$prevIndex]['item_type'] ? $lpItemsIds[$prevIndex]['min_score'] : 0);
+ $maxScore = ('quiz' === $lpItemsIds[$prevIndex]['item_type'] ? $lpItemsIds[$prevIndex]['max_score'] : 100);
+ $lp->edit_item_prereq($itemId, $prerequisite, $minScore, $maxScore);
+ }
+ }
+ }
+ }
+ }
+ $return = [
+ 'success' => true,
+ 'lp_id' => $lpId,
+ ];
+}
+echo json_encode($return);
diff --git a/plugin/ai_helper/uninstall.php b/plugin/ai_helper/uninstall.php
new file mode 100644
index 000000000..e9729b1e3
--- /dev/null
+++ b/plugin/ai_helper/uninstall.php
@@ -0,0 +1,16 @@
+uninstall();
diff --git a/plugin/azure_active_directory/CHANGELOG.md b/plugin/azure_active_directory/CHANGELOG.md
new file mode 100644
index 000000000..f2c618ee3
--- /dev/null
+++ b/plugin/azure_active_directory/CHANGELOG.md
@@ -0,0 +1,48 @@
+# Azure Active Directory Changelog
+
+## 2.5 - 2024-11-18
+
+* Added new options to get the user and groups with delta query (or change tracking) when syncing with scripts.
+this requires manually doing the following changes to your database if you are upgrading from v2.4
+```sql
+CREATE TABLE azure_ad_sync_state (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) NOT NULL, value LONGTEXT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
+```
+
+## 2.4 - 2024-08-28
+
+* Added a new user extra field to save the unique Azure ID (internal UID).
+This requires manually doing the following changes to your database if you are upgrading from v2.3
+```sql
+INSERT INTO extra_field (extra_field_type, field_type, variable, display_text, default_value, field_order, visible_to_self, visible_to_others, changeable, filter, created_at) VALUES (1, 1, 'azure_uid', 'Azure UID (internal ID)', '', 1, null, null, null, null, '2024-08-28 00:00:00');
+```
+* Added a new option to set the order to verify the existing user in Chamilo
+```sql
+INSERT INTO settings_current (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) VALUES ('azure_active_directory_existing_user_verification_order', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', '', '', 1, 1, 0);
+```
+* Added a new option to update user info during the login proccess.
+```sql
+INSERT INTO settings_current (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) VALUES ('azure_active_directory_update_users', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', '', '', 1, 1, 0);
+```
+* Added new scripts to syncronize users and groups with users and usergroups (classes). And an option to deactivate accounts in Chamilo that do not exist in Azure.
+```sql
+INSERT INTO settings_current (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) VALUES ('azure_active_directory_tenant_id', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', '', '', 1, 1, 0);
+INSERT INTO settings_current (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) VALUES ('azure_active_directory_deactivate_nonexisting_users', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', '', '', 1, 1, 0);
+```
+
+## 2.3 - 2021-03-30
+
+* Added admin, session admin and teacher groups. This requires adding the following fields to your database if
+ upgrading from a previous version of the plugin manually:
+```
+INSERT INTO settings_current (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) VALUES ('azure_active_directory_group_id_admin', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', null, null, 1, 1, 0);
+INSERT INTO settings_current (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) VALUES ('azure_active_directory_group_id_session_admin', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', null, null, 1, 1, 0);
+INSERT INTO settings_current (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) VALUES ('azure_active_directory_group_id_teacher', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', null, null, 1, 1, 0);
+```
+
+## 2.2 - 2021-03-02
+
+* Added provisioning setting
+
+## 2.1 - 2020
+
+* Initial tested implementation of Azure Active Directory single sign on
\ No newline at end of file
diff --git a/plugin/azure_active_directory/README.md b/plugin/azure_active_directory/README.md
new file mode 100644
index 000000000..460199dc3
--- /dev/null
+++ b/plugin/azure_active_directory/README.md
@@ -0,0 +1,46 @@
+# The Azure Active Directory Plugin
+
+This plugin allows users to authenticate (with OAuth2) through Microsoft's Azure Active Directory.
+This will modify the login form to either substitute the default login form or add another option to connect through
+Azure.
+An option allows you to automatically provision/create users in Chamilo from their account on Azure if they don't exist
+in Chamilo yet.
+
+This plugin adds two extra fields for users:
+
+* `organisationemail`, the email registered in Azure Active Directory for each user (under _Email_ in the _Contact info_ section).
+* `azure_id`, to save the internal ID for each user in Azure (which is also the prefix before the _@_ sign in the _User Principal Name_).
+
+### Prerequisites
+This plugin will *not* work if you do not use HTTPS.
+Make sure your portal is in HTTPS before you configure this plugin.
+
+### To configure Azure Active Directory
+* Create and configure an application in your Azure panel (Azure Active Directory -> Applications registration -> New registration)).
+* In the _Authentication_ section, set an _Reply URL_ (or _Redirect URIs_) of `https://{CHAMILO_URL}/plugin/azure_active_directory/src/callback.php`.
+* In the _Front-channel logout URL_, use `https://{CHAMILO_URL}/index.php?logout=logout`.
+* Leave the rest of the _Authentication_ section unchanged.
+* In _Certificates & secrets_, create a secret string (or application password). Keep the _Value_ field at hand. If you don't copy it somewhere at this point, it will later be hidden, so take a copy, seriously!
+* Make sure you actually have users.
+
+### To configure this plugin
+* _Enable_: You can enable the plugin once everything is configured correctly. Disabling it will return to the normal Chamilo login procedure.
+* _Application ID_: Enter the _Application (client) ID_ assigned to your app when you created it in your Azure Active Directory interface, under _App registrations_.
+* _Application secret_: Enter the client secret _value_ created in _Certificate & secrets_ above.
+* _Block name_: (Optional) The name to show above the login button.
+* _Force logout button_: (Optional) Add a button to force logout from Azure.
+* _Management login_: (Optional) Disable the chamilo login and enable an alternative login page for users.
+ You will need copy the `/plugin/azure_active_directory/layout/login_form.tpl` file to `/main/template/overrides/layout/` directory.
+* _Name for the management login_: A name for the manager login. By default, it is set to "Management Login".
+* _Automated provisioning_: Enable if you want users to be created automatically in Chamilo (as students) when they don't exist yet.
+* Assign a region in which the login option will appear. Preferably `login_bottom`.
+
+### Enable through the normal login form
+You can configure the external login procedure to work with the classic Chamilo form login.
+To do it, make sure users have _azure_ in their auth_source field, then add this line in `configuration.php` file
+```php
+$extAuthSource["azure"]["login"] = $_configuration['root_sys']."main/auth/external_login/login.azure.php";
+```
+
+### Dependencies
+> This plugin uses the [`thenetworg/oauth2-azure`](https://github.com/TheNetworg/oauth2-azure) package.
diff --git a/plugin/azure_active_directory/index.php b/plugin/azure_active_directory/index.php
new file mode 100644
index 000000000..d966e32eb
--- /dev/null
+++ b/plugin/azure_active_directory/index.php
@@ -0,0 +1,38 @@
+
+ *
+ * @package chamilo.plugin.azure_active_directory
+ */
+
+// Check if AzureActiveDirectory exists, since this is not loaded as a page.
+// index.php is shown as a block when showing the region to which the plugin is assigned
+if (class_exists(AzureActiveDirectory::class)) {
+ /** @var AzureActiveDirectory $activeDirectoryPlugin */
+ $activeDirectoryPlugin = AzureActiveDirectory::create();
+
+ if ($activeDirectoryPlugin->get(AzureActiveDirectory::SETTING_ENABLE) === 'true') {
+ $_template['block_title'] = $activeDirectoryPlugin->get(AzureActiveDirectory::SETTING_BLOCK_NAME);
+
+ $_template['signin_url'] = $activeDirectoryPlugin->getUrl(AzureActiveDirectory::URL_TYPE_AUTHORIZE);
+
+ if ('true' === $activeDirectoryPlugin->get(AzureActiveDirectory::SETTING_FORCE_LOGOUT_BUTTON)) {
+ $_template['signout_url'] = $activeDirectoryPlugin->getUrl(AzureActiveDirectory::URL_TYPE_LOGOUT);
+ }
+
+ $managementLoginEnabled = 'true' === $activeDirectoryPlugin->get(AzureActiveDirectory::SETTING_MANAGEMENT_LOGIN_ENABLE);
+
+ $_template['management_login_enabled'] = $managementLoginEnabled;
+
+ if ($managementLoginEnabled) {
+ $managementLoginName = $activeDirectoryPlugin->get(AzureActiveDirectory::SETTING_MANAGEMENT_LOGIN_NAME);
+
+ if (empty($managementLoginName)) {
+ $managementLoginName = $activeDirectoryPlugin->get_lang('ManagementLogin');
+ }
+
+ $_template['management_login_name'] = $managementLoginName;
+ }
+ }
+}
diff --git a/plugin/azure_active_directory/install.php b/plugin/azure_active_directory/install.php
new file mode 100644
index 000000000..b1a74509a
--- /dev/null
+++ b/plugin/azure_active_directory/install.php
@@ -0,0 +1,8 @@
+install();
diff --git a/plugin/azure_active_directory/lang/dutch.php b/plugin/azure_active_directory/lang/dutch.php
new file mode 100644
index 000000000..22d229cd8
--- /dev/null
+++ b/plugin/azure_active_directory/lang/dutch.php
@@ -0,0 +1,54 @@
+
+ *
+ * @package chamilo.plugin.azure_active_directory
+ */
+$strings['plugin_title'] = 'Azure Active Directory';
+$strings['plugin_comment'] = 'Sta authenticatie met Microsoft\'s Azure Active Directory toe';
+
+$strings['enable'] = 'Inschakelen';
+$strings['app_id'] = 'Applicatie ID';
+$strings['app_id_help'] = 'Voeg de Applicatie Id toegewezen aan uw app bij de Azure portal, b.v. 580e250c-8f26-49d0-bee8-1c078add1609';
+$strings['app_secret'] = 'Applicatie gehem';
+$strings['force_logout'] = 'Forceer uitlogknop';
+$strings['force_logout_help'] = 'Toon een knop om afmeldingssessie van Azure af te dwingen.';
+$strings['block_name'] = 'Blok naam';
+$strings['management_login_enable'] = 'Beheer login';
+$strings['management_login_enable_help'] = 'Schakel de chamilo-login uit en schakel een alternatieve inlogpagina in voor gebruikers. '
+ .'U zult moeten kopiëren de /plugin/azure_active_directory/layout/login_form.tpl bestand in het /main/template/overrides/layout/ dossier.';
+$strings['management_login_name'] = 'Naam voor de beheeraanmelding';
+$strings['management_login_name_help'] = 'De standaardinstelling is "Beheer login".';
+$strings['existing_user_verification_order'] = 'Existing user verification order';
+$strings['existing_user_verification_order_help'] = 'This value indicates the order in which the user will be searched in Chamilo to verify its existence. '
+ .'By default is 1, 2, 3.'
+ .'
EXTRA_FIELD_ORGANISATION_EMAIL (mail)
EXTRA_FIELD_AZURE_ID (mailNickname)
EXTRA_FIELD_AZURE_UID (id of objectId)
';
+$strings['OrganisationEmail'] = 'Organisatie e-mail';
+$strings['AzureId'] = 'Azure ID (mailNickname)';
+$strings['AzureUid'] = 'Azure UID (internal ID)';
+$strings['ManagementLogin'] = 'Beheer Login';
+$strings['InvalidId'] = 'Deze identificatie is niet geldig (verkeerde log-in of wachtwoord). Errocode: AZMNF';
+$strings['provisioning'] = 'Geautomatiseerde inrichting';
+$strings['update_users'] = 'Update users';
+$strings['update_users_help'] = 'Allow user data to be updated at the start of the session.';
+$strings['provisioning_help'] = 'Maak automatisch nieuwe gebruikers (als studenten) vanuit Azure wanneer ze niet in Chamilo zijn.';
+$strings['group_id_admin'] = 'Groeps-ID voor platformbeheerders';
+$strings['group_id_admin_help'] = 'De groeps-ID is te vinden in de details van de gebruikersgroep en ziet er ongeveer zo uit: ae134eef-cbd4-4a32-ba99-49898a1314b6. Indien leeg, wordt er automatisch geen gebruiker aangemaakt als admin.';
+$strings['group_id_session_admin'] = 'Groeps-ID voor sessiebeheerders';
+$strings['group_id_session_admin_help'] = 'De groeps-ID voor sessiebeheerders. Indien leeg, wordt er automatisch geen gebruiker aangemaakt als sessiebeheerder.';
+$strings['group_id_teacher'] = 'Groeps-ID voor docenten';
+$strings['group_id_teacher_help'] = 'De groeps-ID voor docenten. Indien leeg, wordt er automatisch geen gebruiker aangemaakt als docent.';
+$strings['additional_interaction_required'] = 'Er is aanvullende interactie vereist om u te authenticeren. Log rechtstreeks in via uw authenticatiesysteem en kom dan terug naar deze pagina om in te loggen.';
+$strings['tenant_id'] = 'Mandanten-ID';
+$strings['tenant_id_help'] = 'Required to run scripts.';
+$strings['deactivate_nonexisting_users'] = 'Deactivate non-existing users';
+$strings['deactivate_nonexisting_users_help'] = 'Compare registered users in Chamilo with those in Azure and deactivate accounts in Chamilo that do not exist in Azure.';
+$strings['script_users_delta'] = 'Delta query for users';
+$strings['script_users_delta_help'] = 'Get newly created, updated, or deleted users without having to perform a full read of the entire user collection. By default, is No.';
+$strings['script_usergroups_delta'] = 'Delta query for usergroups';
+$strings['script_usergroups_delta_help'] = 'Get newly created, updated, or deleted groups, including group membership changes, without having to perform a full read of the entire group collection. By default, is No.';
+$strings['group_filter_regex'] = 'Group filter RegEx';
+$strings['group_filter_regex_help'] = 'Regular expression to filter groups (only matches will be synchronized), e.g. .*-FIL-.*.*-PAR-.*.*(FIL|PAR).*^(FIL|PAR).*';
diff --git a/plugin/azure_active_directory/lang/english.php b/plugin/azure_active_directory/lang/english.php
new file mode 100644
index 000000000..8cfd60969
--- /dev/null
+++ b/plugin/azure_active_directory/lang/english.php
@@ -0,0 +1,54 @@
+
+ *
+ * @package chamilo.plugin.azure_active_directory
+ */
+$strings['plugin_title'] = 'Azure Active Directory';
+$strings['plugin_comment'] = 'Allow authentication with Microsoft\'s Azure Active Directory';
+
+$strings['enable'] = 'Enable';
+$strings['app_id'] = 'Application ID';
+$strings['app_id_help'] = 'Enter the Application Id assigned to your app by the Azure portal, e.g. 580e250c-8f26-49d0-bee8-1c078add1609';
+$strings['app_secret'] = 'Application secret';
+$strings['force_logout'] = 'Force logout button';
+$strings['force_logout_help'] = 'Show a button to force logout session from Azure.';
+$strings['block_name'] = 'Block name';
+$strings['management_login_enable'] = 'Management login';
+$strings['management_login_enable_help'] = 'Disable the chamilo login and enable an alternative login page for admin users. '
+ .'You will need to copy the /plugin/azure_active_directory/layout/login_form.tpl file to /main/template/overrides/layout/ directory.';
+$strings['management_login_name'] = 'Name for the management login';
+$strings['management_login_name_help'] = 'The default is "Management Login".';
+$strings['existing_user_verification_order'] = 'Existing user verification order';
+$strings['existing_user_verification_order_help'] = 'This value indicates the order in which the user will be searched in Chamilo to verify its existence. '
+ .'By default is 1, 2, 3.'
+ .'
EXTRA_FIELD_ORGANISATION_EMAIL (mail)
EXTRA_FIELD_AZURE_ID (mailNickname)
EXTRA_FIELD_AZURE_UID (id or objectId)
';
+$strings['OrganisationEmail'] = 'Organisation e-mail';
+$strings['AzureId'] = 'Azure ID (mailNickname)';
+$strings['AzureUid'] = 'Azure UID (internal ID)';
+$strings['ManagementLogin'] = 'Management Login';
+$strings['InvalidId'] = 'Login failed - incorrect login or password. Errocode: AZMNF';
+$strings['provisioning'] = 'Automated provisioning';
+$strings['provisioning_help'] = 'Automatically create new users (as students) from Azure when they are not in Chamilo.';
+$strings['update_users'] = 'Update users';
+$strings['update_users_help'] = 'Allow user data to be updated at the start of the session.';
+$strings['group_id_admin'] = 'Group ID for platform admins';
+$strings['group_id_admin_help'] = 'The group ID can be found in the user group details, looking similar to this: ae134eef-cbd4-4a32-ba99-49898a1314b6. If empty, no user will be automatically created as admin.';
+$strings['group_id_session_admin'] = 'Group ID for session admins';
+$strings['group_id_session_admin_help'] = 'The group ID for session admins. If empty, no user will be automatically created as session admin.';
+$strings['group_id_teacher'] = 'Group ID for teachers';
+$strings['group_id_teacher_help'] = 'The group ID for teachers. If empty, no user will be automatically created as teacher.';
+$strings['additional_interaction_required'] = 'Some additional interaction is required to authenticate you. Please login directly through your authentication system, then come back to this page to login.';
+$strings['tenant_id'] = 'Tenant ID';
+$strings['tenant_id_help'] = 'Required to run scripts.';
+$strings['deactivate_nonexisting_users'] = 'Deactivate non-existing users';
+$strings['deactivate_nonexisting_users_help'] = 'Compare registered users in Chamilo with those in Azure and deactivate accounts in Chamilo that do not exist in Azure.';
+$strings['script_users_delta'] = 'Delta query for users';
+$strings['script_users_delta_help'] = 'Get newly created, updated, or deleted users without having to perform a full read of the entire user collection. By default, is No.';
+$strings['script_usergroups_delta'] = 'Delta query for usergroups';
+$strings['script_usergroups_delta_help'] = 'Get newly created, updated, or deleted groups, including group membership changes, without having to perform a full read of the entire group collection. By default, is No.';
+$strings['group_filter_regex'] = 'Group filter RegEx';
+$strings['group_filter_regex_help'] = 'Regular expression to filter groups (only matches will be synchronized), e.g. .*-FIL-.*.*-PAR-.*.*(FIL|PAR).*^(FIL|PAR).*';
diff --git a/plugin/azure_active_directory/lang/french.php b/plugin/azure_active_directory/lang/french.php
new file mode 100644
index 000000000..d49ce8072
--- /dev/null
+++ b/plugin/azure_active_directory/lang/french.php
@@ -0,0 +1,54 @@
+
+ *
+ * @package chamilo.plugin.azure_active_directory
+ */
+$strings['plugin_title'] = 'Azure Active Directory';
+$strings['plugin_comment'] = 'Permet l\'authentification des utilisateurs via Azure Active Directory de Microsoft';
+
+$strings['enable'] = 'Activer';
+$strings['app_id'] = 'ID de l\'application';
+$strings['app_id_help'] = 'Introduisez l\'ID de l\'application assigné à votre app par le portail d\'Azure, p.ex. 580e250c-8f26-49d0-bee8-1c078add1609';
+$strings['app_secret'] = 'Clef secrète de l\'application';
+$strings['force_logout'] = 'Bouton de logout';
+$strings['force_logout_help'] = 'Affiche un bouton pour se délogger d\'Azure.';
+$strings['block_name'] = 'Nom du bloc';
+$strings['management_login_enable'] = 'Login de gestion';
+$strings['management_login_enable_help'] = 'Désactiver le login de Chamilo et permettre une page de login alternative pour les utilisateurs administrateurs. '
+ .'Vous devez, pour cela, copier le fichier /plugin/azure_active_directory/layout/login_form.tpl dans le répertoire /main/template/overrides/layout/.';
+$strings['management_login_name'] = 'Nom du login de gestion';
+$strings['management_login_name_help'] = 'Le nom par défaut est "Login de gestion".';
+$strings['existing_user_verification_order'] = 'Existing user verification order';
+$strings['existing_user_verification_order_help'] = 'This value indicates the order in which the user will be searched in Chamilo to verify its existence. '
+ .'By default is 1, 2, 3.'
+ .'
EXTRA_FIELD_ORGANISATION_EMAIL (mail)
EXTRA_FIELD_AZURE_ID (mailNickname)
EXTRA_FIELD_AZURE_UID (id ou objectId)
';
+$strings['OrganisationEmail'] = 'E-mail professionnel';
+$strings['AzureId'] = 'ID Azure (mailNickname)';
+$strings['AzureUid'] = 'Azure UID (internal ID)';
+$strings['ManagementLogin'] = 'Login de gestion';
+$strings['InvalidId'] = 'Échec du login - nom d\'utilisateur ou mot de passe incorrect. Errocode: AZMNF';
+$strings['provisioning'] = 'Création automatisée';
+$strings['provisioning_help'] = 'Créer les utilisateurs automatiquement (en tant qu\'apprenants) depuis Azure s\'ils n\'existent pas encore dans Chamilo.';
+$strings['update_users'] = 'Actualiser les utilisateurs';
+$strings['update_users_help'] = 'Permettre d\'actualiser les données de l\'utilisateur lors du démarrage de la session.';
+$strings['group_id_admin'] = 'ID du groupe administrateur';
+$strings['group_id_admin_help'] = 'L\'id du groupe peut être trouvé dans les détails du groupe, et ressemble à ceci : ae134eef-cbd4-4a32-ba99-49898a1314b6. Si ce champ est laissé vide, aucun utilisateur ne sera créé en tant qu\'administrateur.';
+$strings['group_id_session_admin'] = 'ID du groupe administrateur de sessions';
+$strings['group_id_session_admin_help'] = 'The group ID for session admins. Si ce champ est laissé vide, aucun utilisateur ne sera créé en tant qu\'administrateur de sessions.';
+$strings['group_id_teacher'] = 'ID du groupe enseignant';
+$strings['group_id_teacher_help'] = 'The group ID for teachers. Si ce champ est laissé vide, aucun utilisateur ne sera créé en tant qu\'enseignant.';
+$strings['additional_interaction_required'] = 'Une interaction supplémentaire est nécessaire pour vous authentifier. Veuillez vous connecter directement auprès de votre système d\'authentification, puis revenir ici pour vous connecter.';
+$strings['tenant_id'] = 'ID du client';
+$strings['tenant_id_help'] = 'Nécessaire pour exécuter des scripts.';
+$strings['deactivate_nonexisting_users'] = 'Deactivate non-existing users';
+$strings['deactivate_nonexisting_users_help'] = 'Compare registered users in Chamilo with those in Azure and deactivate accounts in Chamilo that do not exist in Azure.';
+$strings['script_users_delta'] = 'Requête delta pour les utilisateurs';
+$strings['script_users_delta_help'] = 'Get newly created, updated, or deleted users without having to perform a full read of the entire user collection. By default, is No.';
+$strings['script_usergroups_delta'] = 'Requête delta pour les groupes d\'utilisateurs';
+$strings['script_usergroups_delta_help'] = 'Get newly created, updated, or deleted groups, including group membership changes, without having to perform a full read of the entire group collection. By default, is No.';
+$strings['group_filter_regex'] = 'Group filter RegEx';
+$strings['group_filter_regex_help'] = 'Regular expression to filter groups (only matches will be synchronized), e.g. .*-FIL-.*.*-PAR-.*.*(FIL|PAR).*^(FIL|PAR).*';
diff --git a/plugin/azure_active_directory/lang/spanish.php b/plugin/azure_active_directory/lang/spanish.php
new file mode 100644
index 000000000..f2b9f093f
--- /dev/null
+++ b/plugin/azure_active_directory/lang/spanish.php
@@ -0,0 +1,54 @@
+
+ *
+ * @package chamilo.plugin.azure_active_directory
+ */
+$strings['plugin_title'] = 'Azure Active Directory';
+$strings['plugin_comment'] = 'Permite la autenticación de usuarios por Azure Active Directory de Microsoft';
+
+$strings['enable'] = 'Activar';
+$strings['app_id'] = 'ID de la aplicación';
+$strings['app_id_help'] = 'Introduzca el ID de la aplicación asignado a su app en el portal de Azure, p.ej. 580e250c-8f26-49d0-bee8-1c078add1609';
+$strings['app_secret'] = 'Clave secreta de la aplicación';
+$strings['force_logout'] = 'Botón de logout';
+$strings['force_logout_help'] = 'Muestra un botón para hacer logout de Azure.';
+$strings['block_name'] = 'Nombre del bloque';
+$strings['management_login_enable'] = 'Login de gestión';
+$strings['management_login_enable_help'] = 'Desactivar el login de Chamilo y activar una página de login alternativa para los usuarios de administración. '
+ .'Para ello, tendrá que copiar el archivo /plugin/azure_active_directory/layout/login_form.tpl en la carpeta /main/template/overrides/layout/.';
+$strings['management_login_name'] = 'Nombre del bloque de login de gestión';
+$strings['management_login_name_help'] = 'El nombre por defecto es "Login de gestión".';
+$strings['existing_user_verification_order'] = 'Orden de verificación de usuario existente';
+$strings['existing_user_verification_order_help'] = 'Este valor indica el orden en que el usuario serña buscado en Chamilo para verificar su existencia. '
+ .'Por defecto es 1, 2, 3.'
+ .'
EXTRA_FIELD_ORGANISATION_EMAIL (mail)
EXTRA_FIELD_AZURE_ID (mailNickname)
EXTRA_FIELD_AZURE_UID (id o objectId)
';
+$strings['OrganisationEmail'] = 'E-mail profesional';
+$strings['AzureId'] = 'ID Azure (mailNickname)';
+$strings['AzureUid'] = 'UID Azure (ID interno)';
+$strings['ManagementLogin'] = 'Login de gestión';
+$strings['InvalidId'] = 'Problema en el login - nombre de usuario o contraseña incorrecto. Errocode: AZMNF';
+$strings['provisioning'] = 'Creación automatizada';
+$strings['provisioning_help'] = 'Crear usuarios automáticamente (como alumnos) desde Azure si no existen en Chamilo todavía.';
+$strings['update_users'] = 'Actualizar los usuarios';
+$strings['update_users_help'] = 'Permite actualizar los datos del usuario al iniciar sesión.';
+$strings['group_id_admin'] = 'ID de grupo administrador';
+$strings['group_id_admin_help'] = 'El ID de grupo se encuentra en los detalles del grupo en Azure, y parece a: ae134eef-cbd4-4a32-ba99-49898a1314b6. Si deja este campo vacío, ningún usuario será creado como administrador.';
+$strings['group_id_session_admin'] = 'ID de grupo admin de sesiones';
+$strings['group_id_session_admin_help'] = 'El ID de grupo para administradores de sesiones. Si deja este campo vacío, ningún usuario será creado como administrador de sesiones.';
+$strings['group_id_teacher'] = 'ID de grupo profesor';
+$strings['group_id_teacher_help'] = 'El ID de grupo para profesores. Si deja este campo vacío, ningún usuario será creado como profesor.';
+$strings['additional_interaction_required'] = 'Alguna interacción adicional es necesaria para identificarlo/a. Por favor conéctese primero a través de su sistema de autenticación, luego regrese aquí para logearse.';
+$strings['tenant_id'] = 'Id. del inquilino';
+$strings['tenant_id_help'] = 'Necesario para ejecutar scripts.';
+$strings['deactivate_nonexisting_users'] = 'Desactivar usuarios no existentes';
+$strings['deactivate_nonexisting_users_help'] = 'Compara los usuarios registrados en Chamilo con los de Azure y desactiva las cuentas en Chamilo que no existan en Azure.';
+$strings['script_users_delta'] = 'Consula delta para usuarios';
+$strings['script_users_delta_help'] = 'Obtiene usuarios recién creados, actualizados o eliminados sin tener que realizar una lectura completa de toda la colección de usuarios. De forma predeterminada, es No.';
+$strings['script_usergroups_delta'] = 'Consulta delta para grupos de usuarios';
+$strings['script_usergroups_delta_help'] = 'Obtiene grupos recién creados, actualizados o eliminados, incluidos los cambios de membresía del grupo, sin tener que realizar una lectura completa de toda la colección de grupos. De forma predeterminada, es No';
+$strings['group_filter_regex'] = 'Group filter RegEx';
+$strings['group_filter_regex_help'] = 'Expresión regular para filtrar grupos (solo las coincidencias serán sincronizadas), p.ej. .*-FIL-.*.*-PAR-.*.*(FIL|PAR).*^(FIL|PAR).*';
diff --git a/plugin/azure_active_directory/layout/login_form.tpl b/plugin/azure_active_directory/layout/login_form.tpl
new file mode 100644
index 000000000..498422b23
--- /dev/null
+++ b/plugin/azure_active_directory/layout/login_form.tpl
@@ -0,0 +1,51 @@
+{% if _u.logged == 0 %}
+ {% if login_form %}
+
+
+ {{ login_language_form }}
+ {% if plugin_login_top is not null %}
+
+ {{ plugin_login_top }}
+
+ {% endif %}
+
+ {{ login_failed }}
+
+ {% set azure_plugin_enabled = 'azure_active_directory'|api_get_plugin_setting('enable') %}
+ {% set azure_plugin_manage_login = 'azure_active_directory'|api_get_plugin_setting('manage_login_enable') %}
+
+ {% if 'false' == azure_plugin_enabled or 'false' == azure_plugin_manage_login %}
+ {{ login_form }}
+
+ {% if "allow_lostpassword" | api_get_setting == 'true' or "allow_registration"|api_get_setting == 'true' %}
+
+ {% if "allow_registration"|api_get_setting != 'false' %}
+
+ {% if meeting.show_links.record %}
+ {# Record list #}
+ {% for link in meeting.show_links %}
+ {% if link is not iterable %}
+ {{ link }}
+ {% endif %}
+ {% endfor %}
+ {% else %}
+ {{ 'NoRecording'|get_plugin_lang('BBBPlugin') }}
+ {% endif %}
+
+
+{% endif %}
+
diff --git a/plugin/before_login/README.md b/plugin/before_login/README.md
new file mode 100644
index 000000000..6c3609867
--- /dev/null
+++ b/plugin/before_login/README.md
@@ -0,0 +1,14 @@
+Before login plugin
+===
+
+This plugins shows a page before showing the login page. It is a bit rough and requires theming work to make anything appealing.
+
+This is useful if you want to ask a question before a user has the opportunity to login.
+
+Example:
+You're old enough to enter this site? If you accept the "option 1" then you can enter to the campus (the login will appear).
+
+If you select "option 2" then you're redirected to another page.
+
+To enable, install the plugin then add it to the content_top or main_top region.
+Theme it by creating custom.template.tpl from template.tpl in the plugin/before_login/ directory.
\ No newline at end of file
diff --git a/plugin/before_login/index.php b/plugin/before_login/index.php
new file mode 100644
index 000000000..d171a7547
--- /dev/null
+++ b/plugin/before_login/index.php
@@ -0,0 +1,76 @@
+defaultRenderer();
+ $renderer->setFormTemplate('');
+ $renderer->setCustomElementTemplate('
');
+
+ $form2->addElement('html', $option2);
+ $form2->addElement('checkbox', 'right', null, get_lang('Yes'));
+ $form2->addElement('button', 'submit', get_lang('Send'));
+ $formHtml2 = $form2->returnForm();
+
+ if ($form2->validate()) {
+ $result = $form2->getSubmitValues();
+ if (isset($result['right']) && $result['right']) {
+ header('Location: '.$urlOption2);
+ exit;
+ }
+ }
+ }
+
+ $_template['form_option1'] = $formHtml;
+ $_template['form_option2'] = $formHtml2;
+}
diff --git a/plugin/before_login/plugin.php b/plugin/before_login/plugin.php
new file mode 100644
index 000000000..f93f91898
--- /dev/null
+++ b/plugin/before_login/plugin.php
@@ -0,0 +1,53 @@
+Plugins).
+ *
+ * @package chamilo.plugin
+ *
+ * @author Julio Montoya
+ */
+
+/* Plugin config */
+
+// The plugin title.
+$plugin_info['title'] = 'Show HTML before login';
+// The comments that go with the plugin.
+$plugin_info['comment'] = "Show a content before loading the login page.";
+// The plugin version.
+$plugin_info['version'] = '1.0';
+// The plugin author.
+$plugin_info['author'] = 'Julio Montoya';
+
+// The plugin configuration.
+$form = new FormValidator('form');
+$form->addElement('select', 'language', get_lang('Language'), api_get_languages_to_array());
+
+$form->addElement('header', 'Option 1');
+$form->addElement('textarea', 'option1', get_lang('Description'), ['rows' => 10, 'class' => 'span6']);
+$form->addElement('text', 'option1_url', get_lang('RedirectTo'));
+
+$form->addElement('header', 'Option 2');
+$form->addElement('textarea', 'option2', get_lang('Description'), ['rows' => 10, 'class' => 'span6']);
+$form->addElement('text', 'option2_url', get_lang('RedirectTo'));
+$form->addElement('button', 'submit_button', get_lang('Save'));
+
+// Get default value for form
+
+$defaults = [];
+$defaults['language'] = api_get_plugin_setting('before_login', 'language');
+$defaults['option1'] = api_get_plugin_setting('before_login', 'option1');
+$defaults['option2'] = api_get_plugin_setting('before_login', 'option2');
+
+$defaults['option1_url'] = api_get_plugin_setting('before_login', 'option1_url');
+$defaults['option2_url'] = api_get_plugin_setting('before_login', 'option2_url');
+
+$plugin_info['templates'] = ['template.tpl'];
+if (file_exists(__DIR__.'/custom.template.tpl')) {
+ $plugin_info['templates'] = ['custom.template.tpl'];
+}
+$form->setDefaults($defaults);
+
+// Display form
+$plugin_info['settings_form'] = $form;
diff --git a/plugin/before_login/template.tpl b/plugin/before_login/template.tpl
new file mode 100644
index 000000000..77fa85e4b
--- /dev/null
+++ b/plugin/before_login/template.tpl
@@ -0,0 +1,14 @@
+{% if before_login.form_option1 %}
+
+
+
+ {{ before_login.form_option1 }}
+
+
+ {{ before_login.form_option2 }}
+
+
+
+{% else %}
+
+{% endif %}
\ No newline at end of file
diff --git a/plugin/buycourses/CHANGELOG.md b/plugin/buycourses/CHANGELOG.md
new file mode 100644
index 000000000..6deee5944
--- /dev/null
+++ b/plugin/buycourses/CHANGELOG.md
@@ -0,0 +1,134 @@
+v7.4 - 2022-04-28
+====
+Add subscriptions support.
+
+If the plugin has already been installed, the update.php script must be executed (load plugin/buycourses/update.php in your browser) to update the database structure by adding the plugin_buycourses_subscription, plugin_buycourses_subscription_rel_sale, plugin_buycourses_subscription_period and plugin_buycourses_coupon_rel_subscription_sale.
+
+v7.3 - 2022-04-28
+====
+Add Cecabank payments support.
+
+If the plugin has already been installed, the update.php script must be executed (load plugin/buycourses/update.php in your browser) to update the structure of the tables in the database.
+
+v7.2 - 2021-11-22
+====
+Add Stripe payments support.
+
+If the plugin has already been installed, the update.php script must be executed (load plugin/buycourses/update.php in your browser) to update the structure of the tables in the database.
+
+v7.1 - 2021-10-26
+====
+Fix install issue with DB field type.
+
+v7.0 - 2021-08-12
+====
+Added support for discount coupons.
+Added a better table view for the sales report.
+Multiple fixes to navigation-related issues.
+
+WARNING: Updating this plugin (or Chamilo) without going through the specific update procedure for this plugin will break your sales pages.
+
+The file [your-host]/plugin/buycourses/update.php *MUST* be executed to update the structure of the tables
+in the database.
+
+v6.0 - 2020-11-29
+====
+Added support for purchase instructions e-mail customization (although this
+does not support multiple languages at the moment).
+This requires changes to the DB tables:
+```sql
+ALTER TABLE plugin_buycourses_global_config ADD COLUMN info_email_extra TEXT;
+```
+
+v5.0 - 2019-02-06
+====
+
+This version includes two additional modules (taxes and invoices),
+which can be enabled from the configuration.
+
+The file update.php must be executed to update the structure of the tables
+ in the database.
+
+
+v4.0 - 2017-04-25
+====
+
+This version includes the Culqi payment gateway v1.0 (now expired) and introduces
+an additional option to show the Buy Courses tab to anonymous users.
+
+To enable these features, if you have already installed this plugin on your
+portal prior to this version, you will need to add the corresponding settings
+to your settings_current table. No documentation is available at this time on
+how to do that, so please check up the code. Sorry about that.
+
+
+v3.0 - 2015-09-25
+====
+
+This version has been fixed and improved for Chamilo LMS 1.10.x.
+
+- A new user interface for platform admins and users.
+- Avoid data redundancy by adding courses/sessions to catalog
+- The catalog of sessions can be configured to offer some courses or sessions
+in a currency other than the others courses or sessions
+- The sales have a status: Pending, Completed, Canceled
+- The Peding Orders pages has been replaced by a Sales Report.
+Allowing filter the sales by its status
+- The plugin Registration page was removed. Instead the Chamilo LMS
+registrarion page is used.
+- Added the ability to record beneficiaries with the sale of courses/sessions
+
+##Changes in database structure
+
+The database structure has been changed totally. The previous database
+structure was formed for the tables:
+
+- `plugin_buy_course` The registered courses in the platform
+- `plugin_buy_course_country` The list of countries with their currencies
+- `plugin_buy_course_paypal` The PayPal account info
+- `plugin_buy_course_sale` The sales of courses that were made
+- `plugin_buy_course_temporal` The pending orders of courses that were made
+- `plugin_buy_course_transfer` The bank accounts for transfers
+- `plugin_buy_session` The registered courses in the platform
+- `plugin_buy_session_course` The courses in sessions
+- `plugin_buy_session_sale` The sales of session that were made
+- `plugin_buy_session_temporary` The pending orders of session that were made
+
+To avoid the data redundancy, the `plugin_buy_course`, `plugin_buy_session`
+and `plugin_buy_session_course` tables were replaced for the
+`plugin_buycourses_item` table. And the `plugin_buy_course_sale`,
+`plugin_buy_course_temporal`, `plugin_buy_session_sale` and
+`plugin_buy_session_temporary` tables were replaced for the
+ `plugin_buycourses_item` table.
+
+The __new database__ structure is formed for the tables:
+
+- `plugin_buycourses_currency` The list of countries with their currencies
+- `plugin_buycourses_item` The registered courses and sessions in the platform
+- `plugin_buycourses_item_re_beneficiary` The beneficiaries users with the sale of courses
+- `plugin_buycourses_paypal_account` The PayPal account info
+- `plugin_buycourses_sale` The sales of courses and sessions that were made
+- `plugin_buycourses_transfer` The bank accounts for transfers
+
+---
+
+v2.0 - 2014-10-14
+=================
+This version adds support for sales of sessions access.
+A session can be purchased as soon as it is given a price, granted the current
+date is either previous to the session start date, between the start and end,
+or no date has been defined for the session.
+Students are subscribed automatically once they have paid. There is no
+intermediary step.
+This version does not work (yet) with the session period defined by user
+(a special feature introduced in Chamilo 1.9.10).
+
+Upgrade procedure
+-----------------
+If you are working with this plugin since earlier versions, you will have to
+look at the installer to *fix* your plugin tables (add a few fields).
+
+v1.0 - 2014-06-30 (or something)
+=================
+This is the first release of the plugin, with only the PayPal payment method
+in working state and only for courses.
diff --git a/plugin/buycourses/README.md b/plugin/buycourses/README.md
new file mode 100644
index 000000000..0aca81db4
--- /dev/null
+++ b/plugin/buycourses/README.md
@@ -0,0 +1,37 @@
+Buy Courses (course sales) plugin
+=================================
+This plugin transforms your Chamilo installation in an online shop by adding a catalogue
+ of courses and sessions that you have previously configured for sales.
+
+If the user is not registered or logged in, he/she will be requested to register/login
+before he/she can resume buying items.
+
+Do not enable this plugin in any "Region". This is a known issue, but it works without
+region assignation.
+
+Once the course or session is chosen, the plugin displays the available payment methods
+and lets the user proceed with the purchase.
+Currently, the plugin allows users to pay through:
+ - PayPal (requires a merchant account on PayPal at configuration time)
+ - Bank payments (requires manual confirmation of payments' reception)
+ - RedSys payments (Spanish payment gateway) (requires the download of an external file)
+ - Stripe payments (requieres a merchant account oin Stripe at configuration time)
+ - Cecabank payments (Spanish payment gateway)
+
+The user receives an e-mail confirming the purchase and she/he can immediately
+access to the course or session.
+
+We recommend using sessions as this gives you more time-related availability options
+(in the session configuration).
+
+Updates
+=========
+
+You must load the update.php script for installations that were in
+production before updating the code, as it will update the database structure to
+enable new features.
+
+Please note that updating Chamilo does *NOT* automatically update the plugins
+structure.
+
+You can find a history of changes in the [CHANGELOG.md file](../../plugin/buycourses/CHANGELOG.md)
diff --git a/plugin/buycourses/admin.php b/plugin/buycourses/admin.php
new file mode 100644
index 000000000..f1a1f640f
--- /dev/null
+++ b/plugin/buycourses/admin.php
@@ -0,0 +1,3 @@
+getConnection();
+$platform = $connection->getDatabasePlatform();
+$sm = $connection->getSchemaManager();
+
+// Create tables
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_PAYPAL)) {
+ $paypalTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_PAYPAL);
+ $paypalTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $paypalTable->addColumn('username', Types::STRING);
+ $paypalTable->addColumn('password', Types::STRING);
+ $paypalTable->addColumn('signature', Types::STRING);
+ $paypalTable->addColumn('sandbox', Types::BOOLEAN);
+ $paypalTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_TRANSFER)) {
+ $transferTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_TRANSFER);
+ $transferTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $transferTable->addColumn('name', Types::STRING);
+ $transferTable->addColumn('account', Types::STRING);
+ $transferTable->addColumn('swift', Types::STRING);
+ $transferTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_TPV_REDSYS)) {
+ $tpvRedsysTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_TPV_REDSYS);
+ $tpvRedsysTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $tpvRedsysTable->addColumn('merchantcode', Types::STRING);
+ $tpvRedsysTable->addColumn('terminal', Types::STRING);
+ $tpvRedsysTable->addColumn('currency', Types::STRING);
+ $tpvRedsysTable->addColumn('kc', Types::STRING);
+ $tpvRedsysTable->addColumn('url_redsys', Types::STRING);
+ $tpvRedsysTable->addColumn('url_redsys_sandbox', Types::STRING);
+ $tpvRedsysTable->addColumn('sandbox', Types::BOOLEAN);
+ $tpvRedsysTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_CURRENCY)) {
+ $currencyTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_CURRENCY);
+ $currencyTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $currencyTable->addColumn(
+ 'country_code',
+ Types::STRING,
+ ['length' => 2]
+ );
+ $currencyTable->addColumn(
+ 'country_name',
+ Types::STRING,
+ ['length' => 255]
+ );
+ $currencyTable->addColumn(
+ 'iso_code',
+ Types::STRING,
+ ['length' => 3]
+ );
+ $currencyTable->addColumn('status', Types::BOOLEAN);
+ $currencyTable->addUniqueIndex(['country_code']);
+ $currencyTable->addIndex(['iso_code']);
+ $currencyTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_ITEM)) {
+ $itemTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_ITEM);
+ $itemTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $itemTable->addColumn('product_type', Types::INTEGER);
+ $itemTable->addColumn(
+ 'product_id',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $itemTable->addColumn(
+ 'price',
+ Types::DECIMAL,
+ ['scale' => 2]
+ );
+ $itemTable->addColumn(
+ 'currency_id',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $itemTable->addColumn(
+ 'tax_perc',
+ Types::INTEGER,
+ ['unsigned' => true, 'notnull' => false]
+ );
+ $itemTable->setPrimaryKey(['id']);
+ $itemTable->addForeignKeyConstraint(
+ $currencyTable,
+ ['currency_id'],
+ ['id'],
+ ['onDelete' => 'CASCADE']
+ );
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_ITEM_BENEFICIARY)) {
+ $itemBeneficiary = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_ITEM_BENEFICIARY);
+ $itemBeneficiary->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $itemBeneficiary->addColumn(
+ 'item_id',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $itemBeneficiary->addColumn(
+ 'user_id',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $itemBeneficiary->addColumn(
+ 'commissions',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $itemBeneficiary->setPrimaryKey(['id']);
+ $itemBeneficiary->addForeignKeyConstraint(
+ $itemTable,
+ ['item_id'],
+ ['id'],
+ ['onDelete' => 'CASCADE']
+ );
+}
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_COMMISSION)) {
+ $commissions = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_COMMISSION);
+ $commissions->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $commissions->addColumn(
+ 'commission',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $commissions->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_PAYPAL_PAYOUTS)) {
+ $saleCommissions = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_PAYPAL_PAYOUTS);
+ $saleCommissions->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $saleCommissions->addColumn('date', Types::DATETIME_MUTABLE);
+ $saleCommissions->addColumn('payout_date', Types::DATETIME_MUTABLE);
+ $saleCommissions->addColumn(
+ 'sale_id',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $saleCommissions->addColumn(
+ 'user_id',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $saleCommissions->addColumn(
+ 'commission',
+ Types::DECIMAL,
+ ['scale' => 2]
+ );
+ $saleCommissions->addColumn(
+ 'status',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $saleCommissions->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_SALE)) {
+ $saleTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_SALE);
+ $saleTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $saleTable->addColumn('reference', Types::STRING);
+ $saleTable->addColumn('date', Types::DATETIME_MUTABLE);
+ $saleTable->addColumn(
+ 'user_id',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $saleTable->addColumn('product_type', Types::INTEGER);
+ $saleTable->addColumn('product_name', Types::STRING);
+ $saleTable->addColumn(
+ 'product_id',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $saleTable->addColumn(
+ 'price',
+ Types::DECIMAL,
+ ['scale' => 2]
+ );
+ $saleTable->addColumn(
+ 'price_without_tax',
+ Types::DECIMAL,
+ ['scale' => 2, 'notnull' => false]
+ );
+ $saleTable->addColumn(
+ 'tax_perc',
+ Types::INTEGER,
+ ['unsigned' => true, 'notnull' => false]
+ );
+ $saleTable->addColumn(
+ 'tax_amount',
+ Types::DECIMAL,
+ ['scale' => 2, 'notnull' => false]
+ );
+ $saleTable->addColumn(
+ 'currency_id',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $saleTable->addColumn('status', Types::INTEGER);
+ $saleTable->addColumn('payment_type', Types::INTEGER);
+ $saleTable->addColumn('invoice', Types::INTEGER);
+ $saleTable->addColumn(
+ 'price_without_discount',
+ Types::DECIMAL,
+ ['scale' => 2, 'notnull' => false]
+ );
+ $saleTable->addColumn(
+ 'discount_amount',
+ Types::DECIMAL,
+ ['scale' => 2, 'notnull' => false]
+ );
+ $saleTable->setPrimaryKey(['id']);
+ $saleTable->addForeignKeyConstraint(
+ $currencyTable,
+ ['currency_id'],
+ ['id'],
+ ['onDelete' => 'CASCADE']
+ );
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_SERVICES)) {
+ $servicesTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_SERVICES);
+ $servicesTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $servicesTable->addColumn('name', Types::STRING);
+ $servicesTable->addColumn('description', Types::TEXT);
+ $servicesTable->addColumn(
+ 'price',
+ Types::DECIMAL,
+ ['scale' => 2]
+ );
+ $servicesTable->addColumn('duration_days', Types::INTEGER);
+ $servicesTable->addColumn('applies_to', Types::INTEGER);
+ $servicesTable->addColumn('owner_id', Types::INTEGER);
+ $servicesTable->addColumn('visibility', Types::INTEGER);
+ $servicesTable->addColumn('video_url', Types::STRING);
+ $servicesTable->addColumn('image', Types::STRING);
+ $servicesTable->addColumn('service_information', Types::TEXT);
+ $servicesTable->addColumn('tax_perc', Types::INTEGER);
+ $servicesTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_SERVICES_SALE)) {
+ $servicesNodeTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_SERVICES_SALE);
+ $servicesNodeTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $servicesNodeTable->addColumn(
+ 'service_id',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $servicesNodeTable->addColumn('reference', Types::STRING);
+ $servicesNodeTable->addColumn('currency_id', Types::INTEGER);
+ $servicesNodeTable->addColumn(
+ 'price',
+ Types::DECIMAL,
+ ['scale' => 2]
+ );
+ $servicesNodeTable->addColumn(
+ 'price_without_tax',
+ Types::DECIMAL,
+ ['scale' => 2, 'notnull' => false]
+ );
+ $servicesNodeTable->addColumn(
+ 'tax_perc',
+ Types::INTEGER,
+ ['unsigned' => true, 'notnull' => false]
+ );
+ $servicesNodeTable->addColumn(
+ 'tax_amount',
+ Types::DECIMAL,
+ ['scale' => 2, 'notnull' => false]
+ );
+ $servicesNodeTable->addColumn('node_type', Types::INTEGER);
+ $servicesNodeTable->addColumn('node_id', Types::INTEGER);
+ $servicesNodeTable->addColumn('buyer_id', Types::INTEGER);
+ $servicesNodeTable->addColumn('buy_date', Types::DATETIME_MUTABLE);
+ $servicesNodeTable->addColumn(
+ 'date_start',
+ Types::DATETIME_MUTABLE,
+ ['notnull' => false]
+ );
+ $servicesNodeTable->addColumn(
+ 'date_end',
+ Types::DATETIME_MUTABLE
+ );
+ $servicesNodeTable->addColumn('status', Types::INTEGER);
+ $servicesNodeTable->addColumn('payment_type', Types::INTEGER);
+ $servicesNodeTable->addColumn('invoice', Types::INTEGER);
+ $servicesNodeTable->addColumn(
+ 'price_without_discount',
+ Types::DECIMAL,
+ ['scale' => 2, 'notnull' => false]
+ );
+ $servicesNodeTable->addColumn(
+ 'discount_amount',
+ Types::DECIMAL,
+ ['scale' => 2, 'notnull' => false]
+ );
+ $servicesNodeTable->setPrimaryKey(['id']);
+ $servicesNodeTable->addForeignKeyConstraint(
+ $servicesTable,
+ ['service_id'],
+ ['id'],
+ ['onDelete' => 'CASCADE']
+ );
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_CULQI)) {
+ $culqiTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_CULQI);
+ $culqiTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $culqiTable->addColumn('commerce_code', Types::STRING);
+ $culqiTable->addColumn('api_key', Types::STRING);
+ $culqiTable->addColumn('integration', Types::INTEGER);
+ $culqiTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_GLOBAL_CONFIG)) {
+ $globalTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_GLOBAL_CONFIG);
+ $globalTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $globalTable->addColumn('terms_and_conditions', Types::TEXT);
+ $globalTable->addColumn('global_tax_perc', Types::INTEGER);
+ $globalTable->addColumn('tax_applies_to', Types::INTEGER);
+ $globalTable->addColumn('tax_name', Types::STRING);
+ $globalTable->addColumn('seller_name', Types::STRING);
+ $globalTable->addColumn('seller_id', Types::STRING);
+ $globalTable->addColumn('seller_address', Types::STRING);
+ $globalTable->addColumn('seller_email', Types::STRING);
+ $globalTable->addColumn('next_number_invoice', Types::INTEGER);
+ $globalTable->addColumn('invoice_series', Types::STRING);
+ $globalTable->addColumn('sale_email', Types::STRING);
+ $globalTable->addColumn('info_email_extra', Types::TEXT);
+ $globalTable->setPrimaryKey(['id']);
+} else {
+ $globalTable = $pluginSchema->getTable(BuyCoursesPlugin::TABLE_GLOBAL_CONFIG);
+
+ if (!$globalTable->hasColumn('info_email_extra')) {
+ $globalTable->addColumn('info_email_extra', Types::TEXT);
+ }
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_INVOICE)) {
+ $invoiceTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_INVOICE);
+ $invoiceTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $invoiceTable->addColumn('sale_id', Types::INTEGER);
+ $invoiceTable->addColumn('is_service', Types::INTEGER);
+ $invoiceTable->addColumn(
+ 'num_invoice',
+ Types::INTEGER,
+ ['unsigned' => true, 'notnull' => false]
+ );
+ $invoiceTable->addColumn(
+ 'year',
+ Types::INTEGER,
+ ['unsigned' => true, 'notnull' => false]
+ );
+ $invoiceTable->addColumn('serie', Types::STRING);
+ $invoiceTable->addColumn('date_invoice', Types::DATETIME_MUTABLE);
+ $invoiceTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_COUPON)) {
+ $couponTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_COUPON);
+ $couponTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $couponTable->addColumn('code', Types::STRING);
+ $couponTable->addColumn('discount_type', Types::INTEGER);
+ $couponTable->addColumn('discount_amount', Types::INTEGER);
+ $couponTable->addColumn('valid_start', Types::DATETIME_MUTABLE);
+ $couponTable->addColumn('valid_end', Types::DATETIME_MUTABLE);
+ $couponTable->addColumn('delivered', Types::INTEGER);
+ $couponTable->addColumn('active', Types::BOOLEAN);
+ $couponTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_COUPON_ITEM)) {
+ $couponItemTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_COUPON_ITEM);
+ $couponItemTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $couponItemTable->addColumn('coupon_id', Types::INTEGER);
+ $couponItemTable->addColumn('product_type', Types::INTEGER);
+ $couponItemTable->addColumn('product_id', Types::INTEGER);
+ $couponItemTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_COUPON_SERVICE)) {
+ $couponService = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_COUPON_SERVICE);
+ $couponService->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $couponService->addColumn('coupon_id', Types::INTEGER);
+ $couponService->addColumn('service_id', Types::INTEGER);
+ $couponService->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_SUBSCRIPTION)) {
+ $subscriptionTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_SUBSCRIPTION);
+ $subscriptionTable->addColumn(
+ 'product_type',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $subscriptionTable->addColumn(
+ 'product_id',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $subscriptionTable->addColumn(
+ 'duration',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $subscriptionTable->addColumn('currency_id', Types::INTEGER);
+ $subscriptionTable->addColumn('price', Types::DECIMAL);
+ $subscriptionTable->addColumn('tax_perc', Types::INTEGER);
+ $subscriptionTable->setPrimaryKey(['product_type', 'product_id', 'duration']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_SUBSCRIPTION_SALE)) {
+ $subscriptionSaleTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_SUBSCRIPTION_SALE);
+ $subscriptionSaleTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $subscriptionSaleTable->addColumn('currency_id', Types::INTEGER);
+ $subscriptionSaleTable->addColumn('reference', Types::STRING);
+ $subscriptionSaleTable->addColumn('date', Types::DATETIME_MUTABLE);
+ $subscriptionSaleTable->addColumn('user_id', Types::INTEGER);
+ $subscriptionSaleTable->addColumn('product_type', Types::INTEGER);
+ $subscriptionSaleTable->addColumn('product_name', Types::STRING);
+ $subscriptionSaleTable->addColumn('product_id', Types::INTEGER);
+ $subscriptionSaleTable->addColumn('price', Types::DECIMAL);
+ $subscriptionSaleTable->addColumn('price_without_tax', Types::DECIMAL, ['notnull' => false]);
+ $subscriptionSaleTable->addColumn('tax_perc', Types::INTEGER, ['notnull' => false]);
+ $subscriptionSaleTable->addColumn('tax_amount', Types::DECIMAL, ['notnull' => false]);
+ $subscriptionSaleTable->addColumn('status', Types::INTEGER);
+ $subscriptionSaleTable->addColumn('payment_type', Types::INTEGER);
+ $subscriptionSaleTable->addColumn('invoice', Types::INTEGER);
+ $subscriptionSaleTable->addColumn('price_without_discount', Types::DECIMAL);
+ $subscriptionSaleTable->addColumn('discount_amount', Types::DECIMAL);
+ $subscriptionSaleTable->addColumn('subscription_end', Types::DATETIME_MUTABLE);
+ $subscriptionSaleTable->addColumn('expired', Types::BOOLEAN);
+ $subscriptionSaleTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_SUBSCRIPTION_PERIOD)) {
+ $subscriptionPeriodTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_SUBSCRIPTION_PERIOD);
+ $subscriptionPeriodTable->addColumn(
+ 'duration',
+ Types::INTEGER,
+ ['unsigned' => true]
+ );
+ $subscriptionPeriodTable->addColumn('name', Types::STRING);
+ $subscriptionPeriodTable->setPrimaryKey(['duration']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_COUPON_SALE)) {
+ $couponSaleTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_COUPON_SALE);
+ $couponSaleTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $couponSaleTable->addColumn('coupon_id', Types::INTEGER);
+ $couponSaleTable->addColumn('sale_id', Types::INTEGER);
+ $couponSaleTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_COUPON_SERVICE_SALE)) {
+ $couponSaleTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_COUPON_SERVICE_SALE);
+ $couponSaleTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $couponSaleTable->addColumn('coupon_id', Types::INTEGER);
+ $couponSaleTable->addColumn('service_sale_id', Types::INTEGER);
+ $couponSaleTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_STRIPE)) {
+ $stripeTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_STRIPE);
+ $stripeTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $stripeTable->addColumn('account_id', Types::STRING);
+ $stripeTable->addColumn('secret_key', Types::STRING);
+ $stripeTable->addColumn('endpoint_secret', Types::STRING);
+ $stripeTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_TPV_CECABANK)) {
+ $tpvCecabankTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_TPV_CECABANK);
+ $tpvCecabankTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $tpvCecabankTable->addColumn('crypto_key', Types::STRING);
+ $tpvCecabankTable->addColumn('merchant_id', Types::STRING);
+ $tpvCecabankTable->addColumn('acquirer_bin', Types::STRING);
+ $tpvCecabankTable->addColumn('terminal_id', Types::STRING);
+ $tpvCecabankTable->addColumn('cypher', Types::STRING);
+ $tpvCecabankTable->addColumn('exponent', Types::STRING);
+ $tpvCecabankTable->addColumn('supported_payment', Types::STRING);
+ $tpvCecabankTable->addColumn('url', Types::STRING);
+ $tpvCecabankTable->setPrimaryKey(['id']);
+}
+
+if (false === $sm->tablesExist(BuyCoursesPlugin::TABLE_COUPON_SUBSCRIPTION_SALE)) {
+ $couponSubscriptionSaleTable = $pluginSchema->createTable(BuyCoursesPlugin::TABLE_COUPON_SUBSCRIPTION_SALE);
+ $couponSubscriptionSaleTable->addColumn(
+ 'id',
+ Types::INTEGER,
+ ['autoincrement' => true, 'unsigned' => true]
+ );
+ $couponSubscriptionSaleTable->addColumn('coupon_id', Types::INTEGER);
+ $couponSubscriptionSaleTable->addColumn('sale_id', Types::INTEGER);
+ $couponSubscriptionSaleTable->setPrimaryKey(['id']);
+}
+
+$queries = $pluginSchema->toSql($platform);
+
+foreach ($queries as $query) {
+ Database::query($query);
+}
+
+// Insert data
+$paypalTable = Database::get_main_table(BuyCoursesPlugin::TABLE_PAYPAL);
+$currencyTable = Database::get_main_table(BuyCoursesPlugin::TABLE_CURRENCY);
+$itemTable = Database::get_main_table(BuyCoursesPlugin::TABLE_ITEM);
+$saleTable = Database::get_main_table(BuyCoursesPlugin::TABLE_SALE);
+$commissionTable = Database::get_main_table(BuyCoursesPlugin::TABLE_COMMISSION);
+$extraFieldTable = Database::get_main_table(TABLE_EXTRA_FIELD);
+$culqiTable = Database::get_main_table(BuyCoursesPlugin::TABLE_CULQI);
+$globalTable = Database::get_main_table(BuyCoursesPlugin::TABLE_GLOBAL_CONFIG);
+$tpvRedsysTable = Database::get_main_table(BuyCoursesPlugin::TABLE_TPV_REDSYS);
+$stripeTable = Database::get_main_table(BuyCoursesPlugin::TABLE_STRIPE);
+
+$paypalExtraField = Database::select(
+ "*",
+ $extraFieldTable,
+ [
+ 'where' => ['variable = ?' => 'paypal'],
+ ],
+ 'first'
+);
+
+if (!$paypalExtraField) {
+ Database::insert(
+ $extraFieldTable,
+ [
+ 'extra_field_type' => 1,
+ 'field_type' => 1,
+ 'variable' => 'paypal',
+ 'display_text' => 'Paypal',
+ 'default_value' => '',
+ 'field_order' => 0,
+ 'visible_to_self' => 1,
+ 'changeable' => 1,
+ 'filter' => 0,
+ 'created_at' => api_get_utc_datetime(),
+ ]
+ );
+}
+
+Database::insert(
+ $paypalTable,
+ [
+ 'username' => '',
+ 'password' => '',
+ 'signature' => '',
+ 'sandbox' => true,
+ ]
+);
+
+Database::insert(
+ $tpvRedsysTable,
+ [
+ 'url_redsys' => 'https://sis.redsys.es/sis/realizarPago',
+ 'url_redsys_sandbox' => 'https://sis-t.redsys.es:25443/sis/realizarPago',
+ ]
+);
+
+Database::insert(
+ $culqiTable,
+ [
+ 'commerce_code' => '',
+ 'api_key' => '',
+ 'integration' => 1,
+ ]
+);
+
+Database::insert(
+ $globalTable,
+ [
+ 'terms_and_conditions' => '',
+ ]
+);
+
+Database::insert(
+ $commissionTable,
+ [
+ 'commission' => 0,
+ ]
+);
+
+Database::insert(
+ $stripeTable,
+ [
+ 'account_id' => '',
+ 'secret_key' => '',
+ 'endpoint_secret' => '',
+ ]
+);
+
+$currencies = [
+ ['AD', 'Andorra', 'EUR', 'AND'],
+ ['AE', 'United Arab Emirates', 'AED', 'ARE'],
+ ['AF', 'Afghanistan', 'AFN', 'AFG'],
+ ['AG', 'Antigua and Barbuda', 'XCD', 'ATG'],
+ ['AI', 'Anguilla', 'XCD', 'AIA'],
+ ['AL', 'Albania', 'ALL', 'ALB'],
+ ['AM', 'Armenia', 'AMD', 'ARM'],
+ ['AO', 'Angola', 'AOA', 'AGO'],
+ ['AR', 'Argentina', 'ARS', 'ARG'],
+ ['AS', 'American Samoa', 'USD', 'ASM'],
+ ['AT', 'Austria', 'EUR', 'AUT'],
+ ['AU', 'Australia', 'AUD', 'AUS'],
+ ['AW', 'Aruba', 'AWG', 'ABW'],
+ ['AX', 'Åland', 'EUR', 'ALA'],
+ ['AZ', 'Azerbaijan', 'AZN', 'AZE'],
+ ['BA', 'Bosnia and Herzegovina', 'BAM', 'BIH'],
+ ['BB', 'Barbados', 'BBD', 'BRB'],
+ ['BD', 'Bangladesh', 'BDT', 'BGD'],
+ ['BE', 'Belgium', 'EUR', 'BEL'],
+ ['BF', 'Burkina Faso', 'XOF', 'BFA'],
+ ['BG', 'Bulgaria', 'BGN', 'BGR'],
+ ['BH', 'Bahrain', 'BHD', 'BHR'],
+ ['BI', 'Burundi', 'BIF', 'BDI'],
+ ['BJ', 'Benin', 'XOF', 'BEN'],
+ ['BL', 'Saint Barthélemy', 'EUR', 'BLM'],
+ ['BM', 'Bermuda', 'BMD', 'BMU'],
+ ['BN', 'Brunei', 'BND', 'BRN'],
+ ['BO', 'Bolivia', 'BOB', 'BOL'],
+ ['BQ', 'Bonaire', 'USD', 'BES'],
+ ['BR', 'Brazil', 'BRL', 'BRA'],
+ ['BS', 'Bahamas', 'BSD', 'BHS'],
+ ['BT', 'Bhutan', 'BTN', 'BTN'],
+ ['BV', 'Bouvet Island', 'NOK', 'BVT'],
+ ['BW', 'Botswana', 'BWP', 'BWA'],
+ ['BY', 'Belarus', 'BYR', 'BLR'],
+ ['BZ', 'Belize', 'BZD', 'BLZ'],
+ ['CA', 'Canada', 'CAD', 'CAN'],
+ ['CC', 'Cocos [Keeling] Islands', 'AUD', 'CCK'],
+ ['CD', 'Congo', 'CDF', 'COD'],
+ ['CF', 'Central African Republic', 'XAF', 'CAF'],
+ ['CG', 'Republic of the Congo', 'XAF', 'COG'],
+ ['CH', 'Switzerland', 'CHF', 'CHE'],
+ ['CI', 'Ivory Coast', 'XOF', 'CIV'],
+ ['CK', 'Cook Islands', 'NZD', 'COK'],
+ ['CL', 'Chile', 'CLP', 'CHL'],
+ ['CM', 'Cameroon', 'XAF', 'CMR'],
+ ['CN', 'China', 'CNY', 'CHN'],
+ ['CO', 'Colombia', 'COP', 'COL'],
+ ['CR', 'Costa Rica', 'CRC', 'CRI'],
+ ['CU', 'Cuba', 'CUP', 'CUB'],
+ ['CV', 'Cape Verde', 'CVE', 'CPV'],
+ ['CW', 'Curacao', 'ANG', 'CUW'],
+ ['CX', 'Christmas Island', 'AUD', 'CXR'],
+ ['CY', 'Cyprus', 'EUR', 'CYP'],
+ ['CZ', 'Czechia', 'CZK', 'CZE'],
+ ['DE', 'Germany', 'EUR', 'DEU'],
+ ['DJ', 'Djibouti', 'DJF', 'DJI'],
+ ['DK', 'Denmark', 'DKK', 'DNK'],
+ ['DM', 'Dominica', 'XCD', 'DMA'],
+ ['DO', 'Dominican Republic', 'DOP', 'DOM'],
+ ['DZ', 'Algeria', 'DZD', 'DZA'],
+ ['EC', 'Ecuador', 'USD', 'ECU'],
+ ['EE', 'Estonia', 'EUR', 'EST'],
+ ['EG', 'Egypt', 'EGP', 'EGY'],
+ ['EH', 'Western Sahara', 'MAD', 'ESH'],
+ ['ER', 'Eritrea', 'ERN', 'ERI'],
+ ['ES', 'Spain', 'EUR', 'ESP'],
+ ['ET', 'Ethiopia', 'ETB', 'ETH'],
+ ['FI', 'Finland', 'EUR', 'FIN'],
+ ['FJ', 'Fiji', 'FJD', 'FJI'],
+ ['FK', 'Falkland Islands', 'FKP', 'FLK'],
+ ['FM', 'Micronesia', 'USD', 'FSM'],
+ ['FO', 'Faroe Islands', 'DKK', 'FRO'],
+ ['FR', 'France', 'EUR', 'FRA'],
+ ['GA', 'Gabon', 'XAF', 'GAB'],
+ ['GB', 'United Kingdom', 'GBP', 'GBR'],
+ ['GD', 'Grenada', 'XCD', 'GRD'],
+ ['GE', 'Georgia', 'GEL', 'GEO'],
+ ['GF', 'French Guiana', 'EUR', 'GUF'],
+ ['GG', 'Guernsey', 'GBP', 'GGY'],
+ ['GH', 'Ghana', 'GHS', 'GHA'],
+ ['GI', 'Gibraltar', 'GIP', 'GIB'],
+ ['GL', 'Greenland', 'DKK', 'GRL'],
+ ['GM', 'Gambia', 'GMD', 'GMB'],
+ ['GN', 'Guinea', 'GNF', 'GIN'],
+ ['GP', 'Guadeloupe', 'EUR', 'GLP'],
+ ['GQ', 'Equatorial Guinea', 'XAF', 'GNQ'],
+ ['GR', 'Greece', 'EUR', 'GRC'],
+ ['GS', 'South Georgia and the South Sandwich Islands', 'GBP', 'SGS'],
+ ['GT', 'Guatemala', 'GTQ', 'GTM'],
+ ['GU', 'Guam', 'USD', 'GUM'],
+ ['GW', 'Guinea-Bissau', 'XOF', 'GNB'],
+ ['GY', 'Guyana', 'GYD', 'GUY'],
+ ['HK', 'Hong Kong', 'HKD', 'HKG'],
+ ['HM', 'Heard Island and McDonald Islands', 'AUD', 'HMD'],
+ ['HN', 'Honduras', 'HNL', 'HND'],
+ ['HR', 'Croatia', 'HRK', 'HRV'],
+ ['HT', 'Haiti', 'HTG', 'HTI'],
+ ['HU', 'Hungary', 'HUF', 'HUN'],
+ ['ID', 'Indonesia', 'IDR', 'IDN'],
+ ['IE', 'Ireland', 'EUR', 'IRL'],
+ ['IL', 'Israel', 'ILS', 'ISR'],
+ ['IM', 'Isle of Man', 'GBP', 'IMN'],
+ ['IN', 'India', 'INR', 'IND'],
+ ['IO', 'British Indian Ocean Territory', 'USD', 'IOT'],
+ ['IQ', 'Iraq', 'IQD', 'IRQ'],
+ ['IR', 'Iran', 'IRR', 'IRN'],
+ ['IS', 'Iceland', 'ISK', 'ISL'],
+ ['IT', 'Italy', 'EUR', 'ITA'],
+ ['JE', 'Jersey', 'GBP', 'JEY'],
+ ['JM', 'Jamaica', 'JMD', 'JAM'],
+ ['JO', 'Jordan', 'JOD', 'JOR'],
+ ['JP', 'Japan', 'JPY', 'JPN'],
+ ['KE', 'Kenya', 'KES', 'KEN'],
+ ['KG', 'Kyrgyzstan', 'KGS', 'KGZ'],
+ ['KH', 'Cambodia', 'KHR', 'KHM'],
+ ['KI', 'Kiribati', 'AUD', 'KIR'],
+ ['KM', 'Comoros', 'KMF', 'COM'],
+ ['KN', 'Saint Kitts and Nevis', 'XCD', 'KNA'],
+ ['KP', 'North Korea', 'KPW', 'PRK'],
+ ['KR', 'South Korea', 'KRW', 'KOR'],
+ ['KW', 'Kuwait', 'KWD', 'KWT'],
+ ['KY', 'Cayman Islands', 'KYD', 'CYM'],
+ ['KZ', 'Kazakhstan', 'KZT', 'KAZ'],
+ ['LA', 'Laos', 'LAK', 'LAO'],
+ ['LB', 'Lebanon', 'LBP', 'LBN'],
+ ['LC', 'Saint Lucia', 'XCD', 'LCA'],
+ ['LI', 'Liechtenstein', 'CHF', 'LIE'],
+ ['LK', 'Sri Lanka', 'LKR', 'LKA'],
+ ['LR', 'Liberia', 'LRD', 'LBR'],
+ ['LS', 'Lesotho', 'LSL', 'LSO'],
+ ['LT', 'Lithuania', 'LTL', 'LTU'],
+ ['LU', 'Luxembourg', 'EUR', 'LUX'],
+ ['LV', 'Latvia', 'LVL', 'LVA'],
+ ['LY', 'Libya', 'LYD', 'LBY'],
+ ['MA', 'Morocco', 'MAD', 'MAR'],
+ ['MC', 'Monaco', 'EUR', 'MCO'],
+ ['MD', 'Moldova', 'MDL', 'MDA'],
+ ['ME', 'Montenegro', 'EUR', 'MNE'],
+ ['MF', 'Saint Martin', 'EUR', 'MAF'],
+ ['MG', 'Madagascar', 'MGA', 'MDG'],
+ ['MH', 'Marshall Islands', 'USD', 'MHL'],
+ ['MK', 'Macedonia', 'MKD', 'MKD'],
+ ['ML', 'Mali', 'XOF', 'MLI'],
+ ['MM', 'Myanmar [Burma]', 'MMK', 'MMR'],
+ ['MN', 'Mongolia', 'MNT', 'MNG'],
+ ['MO', 'Macao', 'MOP', 'MAC'],
+ ['MP', 'Northern Mariana Islands', 'USD', 'MNP'],
+ ['MQ', 'Martinique', 'EUR', 'MTQ'],
+ ['MR', 'Mauritania', 'MRO', 'MRT'],
+ ['MS', 'Montserrat', 'XCD', 'MSR'],
+ ['MT', 'Malta', 'EUR', 'MLT'],
+ ['MU', 'Mauritius', 'MUR', 'MUS'],
+ ['MV', 'Maldives', 'MVR', 'MDV'],
+ ['MW', 'Malawi', 'MWK', 'MWI'],
+ ['MX', 'Mexico', 'MXN', 'MEX'],
+ ['MY', 'Malaysia', 'MYR', 'MYS'],
+ ['MZ', 'Mozambique', 'MZN', 'MOZ'],
+ ['NA', 'Namibia', 'NAD', 'NAM'],
+ ['NC', 'New Caledonia', 'XPF', 'NCL'],
+ ['NE', 'Niger', 'XOF', 'NER'],
+ ['NF', 'Norfolk Island', 'AUD', 'NFK'],
+ ['NG', 'Nigeria', 'NGN', 'NGA'],
+ ['NI', 'Nicaragua', 'NIO', 'NIC'],
+ ['NL', 'Netherlands', 'EUR', 'NLD'],
+ ['NO', 'Norway', 'NOK', 'NOR'],
+ ['NP', 'Nepal', 'NPR', 'NPL'],
+ ['NR', 'Nauru', 'AUD', 'NRU'],
+ ['NU', 'Niue', 'NZD', 'NIU'],
+ ['NZ', 'New Zealand', 'NZD', 'NZL'],
+ ['OM', 'Oman', 'OMR', 'OMN'],
+ ['PA', 'Panama', 'PAB', 'PAN'],
+ ['PE', 'Peru', 'PEN', 'PER'],
+ ['PF', 'French Polynesia', 'XPF', 'PYF'],
+ ['PG', 'Papua New Guinea', 'PGK', 'PNG'],
+ ['PH', 'Philippines', 'PHP', 'PHL'],
+ ['PK', 'Pakistan', 'PKR', 'PAK'],
+ ['PL', 'Poland', 'PLN', 'POL'],
+ ['PM', 'Saint Pierre and Miquelon', 'EUR', 'SPM'],
+ ['PN', 'Pitcairn Islands', 'NZD', 'PCN'],
+ ['PR', 'Puerto Rico', 'USD', 'PRI'],
+ ['PS', 'Palestine', 'ILS', 'PSE'],
+ ['PT', 'Portugal', 'EUR', 'PRT'],
+ ['PW', 'Palau', 'USD', 'PLW'],
+ ['PY', 'Paraguay', 'PYG', 'PRY'],
+ ['QA', 'Qatar', 'QAR', 'QAT'],
+ ['RE', 'Réunion', 'EUR', 'REU'],
+ ['RO', 'Romania', 'RON', 'ROU'],
+ ['RS', 'Serbia', 'RSD', 'SRB'],
+ ['RU', 'Russia', 'RUB', 'RUS'],
+ ['RW', 'Rwanda', 'RWF', 'RWA'],
+ ['SA', 'Saudi Arabia', 'SAR', 'SAU'],
+ ['SB', 'Solomon Islands', 'SBD', 'SLB'],
+ ['SC', 'Seychelles', 'SCR', 'SYC'],
+ ['SD', 'Sudan', 'SDG', 'SDN'],
+ ['SE', 'Sweden', 'SEK', 'SWE'],
+ ['SG', 'Singapore', 'SGD', 'SGP'],
+ ['SH', 'Saint Helena', 'SHP', 'SHN'],
+ ['SI', 'Slovenia', 'EUR', 'SVN'],
+ ['SJ', 'Svalbard and Jan Mayen', 'NOK', 'SJM'],
+ ['SK', 'Slovakia', 'EUR', 'SVK'],
+ ['SL', 'Sierra Leone', 'SLL', 'SLE'],
+ ['SM', 'San Marino', 'EUR', 'SMR'],
+ ['SN', 'Senegal', 'XOF', 'SEN'],
+ ['SO', 'Somalia', 'SOS', 'SOM'],
+ ['SR', 'Suriname', 'SRD', 'SUR'],
+ ['SS', 'South Sudan', 'SSP', 'SSD'],
+ ['ST', 'São Tomé and Príncipe', 'STD', 'STP'],
+ ['SV', 'El Salvador', 'USD', 'SLV'],
+ ['SX', 'Sint Maarten', 'ANG', 'SXM'],
+ ['SY', 'Syria', 'SYP', 'SYR'],
+ ['SZ', 'Swaziland', 'SZL', 'SWZ'],
+ ['TC', 'Turks and Caicos Islands', 'USD', 'TCA'],
+ ['TD', 'Chad', 'XAF', 'TCD'],
+ ['TF', 'French Southern Territories', 'EUR', 'ATF'],
+ ['TG', 'Togo', 'XOF', 'TGO'],
+ ['TH', 'Thailand', 'THB', 'THA'],
+ ['TJ', 'Tajikistan', 'TJS', 'TJK'],
+ ['TK', 'Tokelau', 'NZD', 'TKL'],
+ ['TL', 'East Timor', 'USD', 'TLS'],
+ ['TM', 'Turkmenistan', 'TMT', 'TKM'],
+ ['TN', 'Tunisia', 'TND', 'TUN'],
+ ['TO', 'Tonga', 'TOP', 'TON'],
+ ['TR', 'Turkey', 'TRY', 'TUR'],
+ ['TT', 'Trinidad and Tobago', 'TTD', 'TTO'],
+ ['TV', 'Tuvalu', 'AUD', 'TUV'],
+ ['TW', 'Taiwan', 'TWD', 'TWN'],
+ ['TZ', 'Tanzania', 'TZS', 'TZA'],
+ ['UA', 'Ukraine', 'UAH', 'UKR'],
+ ['UG', 'Uganda', 'UGX', 'UGA'],
+ ['UM', 'U.S. Minor Outlying Islands', 'USD', 'UMI'],
+ ['US', 'United States', 'USD', 'USA'],
+ ['UY', 'Uruguay', 'UYU', 'URY'],
+ ['UZ', 'Uzbekistan', 'UZS', 'UZB'],
+ ['VA', 'Vatican City', 'EUR', 'VAT'],
+ ['VC', 'Saint Vincent and the Grenadines', 'XCD', 'VCT'],
+ ['VE', 'Venezuela', 'VEF', 'VEN'],
+ ['VG', 'British Virgin Islands', 'USD', 'VGB'],
+ ['VI', 'U.S. Virgin Islands', 'USD', 'VIR'],
+ ['VN', 'Vietnam', 'VND', 'VNM'],
+ ['VU', 'Vanuatu', 'VUV', 'VUT'],
+ ['WF', 'Wallis and Futuna', 'XPF', 'WLF'],
+ ['WS', 'Samoa', 'WST', 'WSM'],
+ ['XK', 'Kosovo', 'EUR', 'XKX'],
+ ['YE', 'Yemen', 'YER', 'YEM'],
+ ['YT', 'Mayotte', 'EUR', 'MYT'],
+ ['ZA', 'South Africa', 'ZAR', 'ZAF'],
+ ['ZM', 'Zambia', 'ZMK', 'ZMB'],
+ ['ZW', 'Zimbabwe', 'ZWL', 'ZWE'],
+];
+
+foreach ($currencies as $currency) {
+ $value = Database::select(
+ "*",
+ $currencyTable,
+ [
+ 'where' => ['country_code = ?' => $currency[0]],
+ ],
+ 'first'
+ );
+
+ if (!empty($value)) {
+ continue;
+ }
+
+ Database::insert(
+ $currencyTable,
+ [
+ 'country_code' => $currency[0],
+ 'country_name' => $currency[1],
+ 'iso_code' => $currency[2],
+ ]
+ );
+}
+
+$fieldlabel = 'buycourses_company';
+$fieldtype = '1';
+$fieldtitle = BuyCoursesPlugin::get_lang('Company');
+$fielddefault = '';
+$field_id = UserManager::create_extra_field($fieldlabel, $fieldtype, $fieldtitle, $fielddefault);
+
+$fieldlabel = 'buycourses_vat';
+$fieldtype = '1';
+$fieldtitle = BuyCoursesPlugin::get_lang('VAT');
+$fielddefault = '';
+$field_id = UserManager::create_extra_field($fieldlabel, $fieldtype, $fieldtitle, $fielddefault);
+
+$fieldlabel = 'buycourses_address';
+$fieldtype = '1';
+$fieldtitle = BuyCoursesPlugin::get_lang('Address');
+$fielddefault = '';
+$field_id = UserManager::create_extra_field($fieldlabel, $fieldtype, $fieldtitle, $fielddefault);
diff --git a/plugin/buycourses/index.php b/plugin/buycourses/index.php
new file mode 100644
index 000000000..fefacd452
--- /dev/null
+++ b/plugin/buycourses/index.php
@@ -0,0 +1,7 @@
+install();
diff --git a/plugin/buycourses/lang/brazilian.php b/plugin/buycourses/lang/brazilian.php
new file mode 100644
index 000000000..19debc6ca
--- /dev/null
+++ b/plugin/buycourses/lang/brazilian.php
@@ -0,0 +1,99 @@
+configurar pagamento - Defina a moeda com a qual você gostaria de vender seus cursos ou sessões";
+$strings['InstructionsStepThree'] = "Para configurar cursos para vender. Vá em: configurar cursos e preços.";
+$strings['BuyCourses'] = "Comprar cursos";
+$strings['ConfigurationOfCoursesAndPrices'] = "Configurar cursos e preços ";
+$strings['SalesReport'] = "Relatório de vendas";
+$strings['UserInformation'] = "Detalhes do comprador";
+$strings['PaymentMethods'] = "Métodos de pagamento";
+$strings['ConfirmOrder'] = "Confirmar pedido";
+$strings['PurchaseData'] = "Dados de Compra";
+$strings['bc_subject'] = "Confirmação de ordem do curso";
+$strings['PurchaseStatusX'] = "Estado de compra: %s";
+$strings['PendingReasonByTransfer'] = " Pendente Aguardando confirmação da transferência.";
+$strings['CancelOrder'] = "Anular ordem";
+$strings['BankAccountInformation'] = "Detalhes da conta bancária";
+$strings['BankAccount'] = "Contas bancárias";
+$strings['OnceItIsConfirmedYouWillReceiveAnEmailWithTheBankInformationAndAnOrderReference'] = "Uma vez confirmada, você receberá um e-mail com os dados bancários e uma referência de ordem.";
+$strings['SubscriptionToCourseXSuccessful'] = "Sua inscrição para \"%s\" foi concluída com sucesso.";
+$strings['OrderCanceled'] = "Ordem cancelada";
+$strings['OrderStatus'] = "Status do pedido";
+$strings['SearchByStatus'] = "Pesquisar por estado";
+$strings['OrderReference'] = "Código de encomenda";
+$strings['OrderDate'] = "Data do pedido";
+$strings['ProductType'] = "Tipo de produto";
+$strings['SubscribeUser'] = "Inscrever utilizador";
+$strings['DeleteOrder'] = "Excluir a ordem";
+$strings['ErrorContactPlatformAdmin'] = "Por favor entre em contato com o administrador da plataforma de erro desconhecido..";
+$strings['PendingReasonByAddress'] = ". Pendente Você não digitou um endereço de entrega confirmado.";
+$strings['PendingReasonByAuthorization'] = " Pendentes fundos Nós ainda não capturados..";
+$strings['PendingReasonByEcheck'] = " Pendente O pagamento foi feito por um eCheck que ainda não foi eliminada.";
+$strings['PendingReasonByIntl'] = " Pendente Nós não temos um mecanismo de retirada de fundos..";
+$strings['PendingReasonByMulticurrency'] = " Pendente Nós não equilibrar na moeda enviado.";
+$strings['PendingReasonByOrder'] = " Pendente Ordem feita Nós ainda não capturado fundos...";
+$strings['PendingReasonByPaymentReview'] = " Pendente O pagamento está sendo revisto pelo PayPal para o risco.";
+$strings['PendingReasonByRegulatoryReview'] = " Pendente O pagamento está sendo revisado para conformidade com regulamentações governamentais..";
+$strings['PendingReasonByUnilateral'] = " Pendente O e-mail ainda não está registrado o confirmou..";
+$strings['PendingReasonByUpgrade'] = " Pendente O pagamento foi feito por cartão de crédito..";
+$strings['PendingReasonByVerify'] = " Pendente Desculpe Nós ainda não são verificados no PayPal...";
+$strings['PendingReasonByOther'] = " Pendente Por favor, entre em contato com o administrador da plataforma..";
+$strings['PayPalPaymentOKPleaseConfirm'] = "PayPal relata a transação está pronto para ser executado. Para confirmar que você está OK para prosseguir, clique no botão de confirmação abaixo. Uma vez clicado, você será registrado para o curso e os fundos serão transferido da sua conta PayPal para a nossa loja. Você sempre pode acessar seus cursos através da aba 'Meus cursos' Obrigado por seu costume.!";
+$strings['Sandbox'] = "Ambiente de teste";
+$strings['PayPalConfig'] = "Configuração PayPal:";
+$strings['TransfersConfig'] = "Configurar transferências bancárias:";
+$strings['PluginInstruction'] = "Você pode ativar ou desativar a opção de pagar via PayPal ou transferência bancária na seção de configuração do plugin.";
+$strings['ClickHere'] = " Clique aqui para obter mais detalhes";
+$strings['CurrencyType'] = "Tipo de moeda";
+$strings['InfoCurrency'] = "Define a moeda para o pagamento de seus cursos.";
+$strings['ApiUsername'] = "Nome de usuário API";
+$strings['ApiPassword'] = "Senha API";
+$strings['ApiSignature'] = "Assinatura API";
+$strings['InfoApiCredentials'] = "Para gerar as suas credenciais de API para integrar Chamilo com a sua conta PayPal, você deve seguir os seguintes passos";
+$strings['InfoApiStepOne'] = "Vá para a sua conta do PayPal, Resumo , então preferências do vendedor na Ferramentas de Vendas menu (se o fizer não ter este item de menu, você pode precisar de obter a autorização para vender o material através do PayPal em primeiro lugar).";
+$strings['InfoApiStepTwo'] = "No parágrafo Acesso API , clique em Atualizar ";
+$strings['InfoApiStepThree'] = "Na opção 2 (credenciais de solicitação de API para criar o seu próprio nome de usuário e senha API), clique no botão Exibir API Assinatura link , e copiar as credenciais apresentadas à direita no formulário do plugin BuyCourses.";
+$strings['ErrorOccurred'] = " Ocorreu um erro Código: %s. Mensagem: %s. Por favor, entre em contato com o Admin da plataforma.";
+$strings['VisibleInCatalog'] = "Visível no catálogo";
+$strings['Beneficiaries'] = "Beneficiários";
+$strings['AvailableCourse'] = "Campo disponível";
+$strings['ShowOnCourseCatalog'] = "Mostrar no catálogo de cursos";
+$strings['ByStatus'] = "Por estado";
+$strings['ByUser'] = "Por usuário";
+$strings['ByEmail'] = "Por email";
+$strings['PaymentMethod'] = "Método de pagamento";
+$strings['SWIFT'] = "Código SWIFT";
+$strings['SWIFT_help'] = "Formato padrão de Códigos de Identificação Bancária (BIC) e serve como um identificador exclusivo de um banco ou instituição financeira";
+$strings['ExportReport'] = "Exportar Relatório de Vendas";
+$strings['OrderTime'] = "Hora do pedido";
+$strings['SelectDateRange'] = "Selecione uma data de início e uma data de término para o relatório";
diff --git a/plugin/buycourses/lang/dutch.php b/plugin/buycourses/lang/dutch.php
new file mode 100644
index 000000000..d265f6c10
--- /dev/null
+++ b/plugin/buycourses/lang/dutch.php
@@ -0,0 +1,135 @@
+ %s . Deze percentage zal invermindering gebracht worden van de totale prijs van het product. Het verschil in het bedrag zal aan de volgende commissies toegepast worden.";
+$strings['CoursesInSessionsDoesntDisplayHere'] = "De cursussen die binnen een training zitten worden niet weergegeven in de lijst van de cursussen als afzonderlijke producten.";
+$strings['WantToSellCourses'] = "Wilt u leren en wat geld verdienen met uw eigen cursussen? Deze lijst kan worden gevuld met de verkoop van uw cursussen via dit platform. Neem contact op met ons.";
+$strings['SelectOptionToProceed'] = "Selecteer de optie om verder te gaan";
+$strings['VerifyTotalAmountToProceedPayout'] = "Controleer het totale bedrag voor over te gaan tot de betaling van commissies. Dit bedrag heeft geen extra kosten voor PayPal rekening houder. De cursussen verkopen die niet meetellen met een Paypal-account voor de begunstigden niet in aanmerking genomen.";
+$strings['TotalAcounts'] = "Totaal van de rekeningen:";
+$strings['TotalPayout'] = "Totaal te betalen:";
+$strings['PayoutDate'] = "Betaaldatum:";
+$strings['CautionThisProcessCantBeCanceled'] = "Let op:Dit proces kan een paar minuten duren en kan niet worden geannuleerd.";
+$strings['ProcessingPayoutsDontCloseThisWindow'] = "Verder gaan met betalingen. Dit venster niet sluiten totdat dit proces is afgerond.";
+$strings['PayoutSuccess'] = "Betalingen met succes verwerkt";
+$strings['Buyer'] = "Koper";
+$strings['BankTransfer'] = "Bankoverschrijving";
+$strings['SaleInfo'] = "Verkoop Informatie";
+$strings['SaleStatusPending'] = "Verkoop in behandeling";
+$strings['SaleStatusCanceled'] = "Verkoop geannuleerd";
+$strings['SaleStatusCompleted'] = "verkoop afgerond";
+$strings['PayoutStatusPending'] = "Betaling in behandeling";
+$strings['PayoutStatusCanceled'] = "Betaling geannuleerd";
+$strings['PayoutStatusCompleted'] = "Betaling afgerond";
+$strings['PayoutsTotalPending'] = "Onafgehandelde betalingen:";
+$strings['PayoutsTotalCanceled'] = "Geannuleerde betalingen:";
+$strings['PayoutsTotalCompleted'] = "Afgeronde betalingen:";
+$strings['TotalAmount'] = "Totaalbedrag:";
+$strings['CourseListOnSale'] = "Lijst van cursussen in verkoop";
+$strings['AvailableCourses'] = "Beschikbare Cursussen";
+$strings['Price'] = "Prijs";
+$strings['SearchFilter'] = "Zoekmachine";
+$strings['MinimumPrice'] = "Minimuum prijs";
+$strings['MaximumPrice'] = "Maximuumm prijs";
+$strings['AvailableCoursesConfiguration'] = "Beschikbare cursussen overzicht";
+$strings['PaymentsConfiguration'] = "Betalingsoverzicht";
+$strings['TheUserIsAlreadyRegisteredInTheCourse'] = "Je bent geregistreerd voor de cursus.";
+$strings['SeeDescription'] = "Omschrijving";
+$strings['Buy'] = "Koop / inschrijven";
+$strings['WaitingToReceiveThePayment'] = "In afwachting van uw betaling";
+$strings['TheUserIsAlreadyRegisteredInTheSession'] = "Je bent geregistreerd voor de sessie";
+$strings['ItemNotSaved'] = "Item niet opgeslagen";
+$strings['TitlePlugin'] = "Alles wat je nodig hebt om online cursussen te onderwijzen en te verkopen";
+$strings['PluginPresentation'] = "The BuyCourses Plugin gives you the means to sell your courses or sessions and teach online, through only a few simple steps and settings. What are you waiting for? Start selling courses through Chamilo LMS!";
+$strings['Instructions'] = "Instructies";
+$strings['InstructionsStepOne'] = "Maak een cursus of een sessie op het platform.";
+$strings['InstructionsStepTwo'] = "In de betaling , stelt de valuta waarmee u wilt uw cursussen of sessies te verkopen.";
+$strings['InstructionsStepThree'] = "Overzicht van cursussen om te verkopen in de Cursussen en prijzen Instellingen.";
+$strings['BuyCourses'] = "Cursussen Kopen";
+$strings['ConfigurationOfCoursesAndPrices'] = "Cursussen en Prijzen overzicht";
+$strings['SalesReport'] = "Verkoopcijfers";
+$strings['UserInformation'] = "Gegevens van de koper";
+$strings['PaymentMethods'] = "Betaal methodes";
+$strings['ConfirmOrder'] = "Bevestig bestelling";
+$strings['PurchaseData'] = "Aankoopgegevens";
+$strings['bc_subject'] = "Bevestiging cursus order";
+$strings['PurchaseStatusX'] = "Aankoop status: %s";
+$strings['PendingReasonByTransfer'] = " In afwachting . In afwachting van de overdracht bevestiging";
+$strings['CancelOrder'] = "Annuleer order";
+$strings['BankAccountInformation'] = "Bankgegevens";
+$strings['BankAccount'] = "Bankrekening";
+$strings['OnceItIsConfirmedYouWillReceiveAnEmailWithTheBankInformationAndAnOrderReference'] = "Eenmaal bevestigd, ontvangt u een e-mail met de bankgegevens en een orderreferentie.";
+$strings['SubscriptionToCourseXSuccessful'] = "Uw aankoop op \"%s\" werd met succes afgerond.";
+$strings['OrderCanceled'] = "Order geannuleerd";
+$strings['OrderStatus'] = "Bestelstatus";
+$strings['PayoutStatus'] = "Betalingsstatus";
+$strings['SearchByStatus'] = "Zoeken op status";
+$strings['OrderReference'] = "Bestelreferentie";
+$strings['OrderDate'] = "Besteldatum";
+$strings['OrderPrice'] = "Bestel prijs";
+$strings['ProductType'] = "Artikel type";
+$strings['SubscribeUser'] = "Onderteken gebruiker";
+$strings['DeleteOrder'] = "Verwijder bestelling";
+$strings['ErrorContactPlatformAdmin'] = "Onbekende fout. Neem contact op met het platform administrator.";
+$strings['PendingReasonByAddress'] = "in afwachting. Je hebt een bevestigde verzendadres niet in te voeren.";
+$strings['PendingReasonByAuthorization'] = "in afwachting. We hebben nog geen betalingen.";
+$strings['PendingReasonByEcheck'] = "in afwachting. De betaling is verricht door een eCheck die nog niet is vrijgemaakt";
+$strings['PendingReasonByIntl'] = "in afwachting.We hebben geen terugtrekking mechanisme voor betalingen.";
+$strings['PendingReasonByMulticurrency'] = "in afwachting. We hebben niet in evenwicht te brengen in de valuta verzonden.";
+$strings['PendingReasonByOrder'] = "in afwachting. Bestelling. We hebben nog geen betalingen.";
+$strings['PendingReasonByPaymentReview'] = "in afwachting. De betaling wordt beoordeeld door PayPal voor risico.";
+$strings['PendingReasonByRegulatoryReview'] = "in afwachting. De betaling wordt beoordeeld voor de naleving van regelgeving van de overheid.";
+$strings['PendingReasonByUnilateral'] = "in afwachting. Het e-mailadres is nog niet geregistreerd of bevestigd.";
+$strings['PendingReasonByUpgrade'] = "in afwachting. De betaling werd gedaan via credit card.";
+$strings['PendingReasonByVerify'] = "in afwachting. Sorry. We zijn nog niet in Paypal geverifieerd.";
+$strings['PendingReasonByOther'] = "in afwachting. Neem contact op met het platform admin.";
+$strings['PayPalPaymentOKPleaseConfirm'] = "PayPal rapporteert de transactie nu worden gestart. Om te erkennen dat u op OK om verder te gaan, klikt u op de knop bevestiging hieronder. Eenmaal geklikt, wordt u ingeschreven voor de cursus en het geld zal naar onze winkel worden overgedragen van uw PayPal-rekening. U kunt altijd toegang tot uw cursussen via het tabblad 'Mijn cursussen'. Dank u voor uw aankoop";
+$strings['Sandbox'] = "Test omgeving";
+$strings['PayPalAccount'] = "Paypal rekening";
+$strings['NoPayPalAccountDetected'] = "Geen Paypal account gedetecteerd";
+$strings['PayPalConfig'] = "PayPal bevestiging:";
+$strings['TransfersConfig'] = "Bank overschrijving bevestiging:";
+$strings['PluginInstruction'] = "U kunt de optie in- of uitschakelen om te betalen via PayPal of overschrijving instellen in de configuratie sectie van de plugin.";
+$strings['ClickHere'] = "Click here voor meer details";
+$strings['CurrencyType'] = "Valuta type";
+$strings['InfoCurrency'] = "Stel in de valuta voor de betaling van uw cursussen.";
+$strings['ApiUsername'] = "API Gebruikersnaam";
+$strings['ApiPassword'] = "API Paswoord";
+$strings['ApiSignature'] = "API Handtekening";
+$strings['InfoApiCredentials'] = "To generate your API credentials to integrate Chamilo with your PayPal account, you must follow the following steps";
+$strings['InfoApiStepOne'] = "Ga naar je PayPal-account, Overzicht en verkoper voorkeuren in het Verkoop Tools menu (als je niet dit menu-item hebt, om spullen te verkopen moet u eerst via PayPal om de toestemming te krijgen ).";
+$strings['InfoApiStepTwo'] = "In paragraaf API-toegang , klikt u op Bijwerken ";
+$strings['InfoApiStepThree'] = "In optie 2 (API Request referenties aan uw eigen API gebruikersnaam en wachtwoord), klikt u op de Bekijk API Handtekening , en kopieer de getoonde rechtsaf de Cursusshop plugin vorm referenties";
+$strings['ErrorOccurred'] = " Er is een fout opgetreden . Code:% s. Bericht:% s. Neem contact op met naar platform admin";
+$strings['VisibleInCatalog'] = "Zichtbaar in catalogus";
+$strings['Beneficiaries'] = "begunstigden";
+$strings['AvailableCourse'] = "beschikbare cursus";
+$strings['ShowOnCourseCatalog'] = "Toon op de cursusoverzicht";
+$strings['ByStatus'] = "Door de status";
+$strings['ByUser'] = "Door de gebruiker";
+$strings['PaymentMethod'] = "Betaal methodes";
+$strings['SWIFT'] = "SWIFT code";
+$strings['SWIFT_help'] = "Standaardformaat van Bank Identifier Codes (BIC) en dient als een unieke identificatiecode voor een bank of financiële instelling";
diff --git a/plugin/buycourses/lang/english.php b/plugin/buycourses/lang/english.php
new file mode 100644
index 000000000..deb6f0ae3
--- /dev/null
+++ b/plugin/buycourses/lang/english.php
@@ -0,0 +1,321 @@
+ %s . This percentage will be discounted from the total product price. The difference will be the amount to apply the following commissions.";
+$strings['CoursesInSessionsDoesntDisplayHere'] = "The courses which are inside a training session don't appear in the list of courses to configure as individual products.";
+$strings['WantToSellCourses'] = "Do you want to teach and earn some money with your own courses? This list could be filled with your earnings selling courses through this platform. Get in touch with us.";
+$strings['SelectOptionToProceed'] = "Select option to proceed";
+$strings['VerifyTotalAmountToProceedPayout'] = "Please check the total amount to proceed with the payment of commissions. This amount doesn't take extra costs for PayPal into account. The courses sales that do not count with a Paypal account for the beneficiaries will not be considered.";
+$strings['TotalAcounts'] = "Total of accounts:";
+$strings['TotalPayout'] = "Total to be paid:";
+$strings['PayoutDate'] = "Payment date:";
+$strings['CautionThisProcessCantBeCanceled'] = "Caution: this process can take a few minutes and cannot be cancelled.";
+$strings['ProcessingPayoutsDontCloseThisWindow'] = "Proceeding with payments. Don't close this window until this process is finalized.";
+$strings['PayoutSuccess'] = "Payments processed successfully";
+$strings['Buyer'] = "Buyer";
+$strings['BankTransfer'] = "Bank transfer";
+$strings['SaleInfo'] = "Sale information";
+$strings['SaleStatusPending'] = "Sale pending";
+$strings['SaleStatusCanceled'] = "Sale canceled";
+$strings['SaleStatusCompleted'] = "Sale completed";
+$strings['PayoutStatusPending'] = "Payment pending";
+$strings['PayoutStatusCanceled'] = "Payment cancelled";
+$strings['PayoutStatusCompleted'] = "Payment completed";
+$strings['PayoutsTotalPending'] = "Pending payments:";
+$strings['PayoutsTotalCanceled'] = "Cancelled payments:";
+$strings['PayoutsTotalCompleted'] = "Completed payments:";
+$strings['Total'] = "Total";
+$strings['TotalAmount'] = "Total amount:";
+$strings['CourseListOnSale'] = "List of courses on sale";
+$strings['AvailableCourses'] = "Available Courses";
+$strings['Price'] = "Price";
+$strings['SearchFilter'] = "Search filter";
+$strings['MinimumPrice'] = "Minimum price";
+$strings['MaximumPrice'] = "Maximum price";
+$strings['AvailableCoursesConfiguration'] = "Available courses configuration";
+$strings['PaymentsConfiguration'] = "Payments configuration";
+$strings['TheUserIsAlreadyRegisteredInTheCourse'] = "You are already registered in the course.";
+$strings['SeeDescription'] = "Description";
+$strings['Buy'] = "Buy";
+$strings['WaitingToReceiveThePayment'] = "Currently pending payment";
+$strings['TheUserIsAlreadyRegisteredInTheSession'] = "You are already registered in the session";
+$strings['ItemNotSaved'] = "Item not saved";
+$strings['TitlePlugin'] = "Everything you need to teach and sell courses online";
+$strings['PluginPresentation'] = "The BuyCourses Plugin gives you the means to sell your courses or sessions and teach online, through only a few simple steps and settings. What are you waiting for? Start selling courses through Chamilo LMS!";
+$strings['Instructions'] = "Instructions";
+$strings['InstructionsStepOne'] = "Create a course or session on the platform.";
+$strings['InstructionsStepTwo'] = "In the payment settings, set the currency with which you would like to sell your courses or sessions.";
+$strings['InstructionsStepThree'] = "Configure courses to sell in the Courses and prices settings.";
+$strings['BuyCourses'] = "Buy courses";
+$strings['ConfigurationOfCoursesAndPrices'] = "Courses and prices configuration";
+$strings['SalesReport'] = "Sales report";
+$strings['UserInformation'] = "Buyer's details";
+$strings['PaymentMethods'] = "Payment methods";
+$strings['ConfirmOrder'] = "Confirm order";
+$strings['PurchaseData'] = "Purchase data";
+$strings['bc_subject'] = "Confirmation of course order";
+$strings['PurchaseStatusX'] = "Purchase status: %s";
+$strings['PendingReasonByTransfer'] = "Pending. Awaiting for transfer confirmation";
+$strings['CancelOrder'] = "Cancel order";
+$strings['BankAccountInformation'] = "Bank account details";
+$strings['BankAccount'] = "Bank account";
+$strings['OnceItIsConfirmedYouWillReceiveAnEmailWithTheBankInformationAndAnOrderReference'] = "Once confirmed, you will receive an e-mail with the bank details and an order reference.";
+$strings['SubscriptionToCourseXSuccessful'] = "Completed process. Your subscription to \"%s\" was completed successfully.";
+$strings['OrderCanceled'] = "Order canceled";
+$strings['OrderStatus'] = "Order status";
+$strings['PayoutStatus'] = "Payment status";
+$strings['SearchByStatus'] = "Search by status";
+$strings['OrderReference'] = "Order reference";
+$strings['OrderDate'] = "Order date";
+$strings['OrderPrice'] = "Order price";
+$strings['ProductType'] = "Product type";
+$strings['SubscribeUser'] = "Subscribe user";
+$strings['DeleteOrder'] = "Delete order";
+$strings['ErrorContactPlatformAdmin'] = "Unknown error. Please contact the platform administrator.";
+$strings['PendingReasonByAddress'] = "Pending. You did not enter a confirmed shipping address.";
+$strings['PendingReasonByAuthorization'] = "Pending. We have not yet captured funds.";
+$strings['PendingReasonByEcheck'] = "Pending. The payment was made by an eCheck that has not yet cleared";
+$strings['PendingReasonByIntl'] = "Pending. We have no a withdrawal mechanism for funds.";
+$strings['PendingReasonByMulticurrency'] = "Pending. We have not balance in the currency sent.";
+$strings['PendingReasonByOrder'] = "Pending. Order made. We have not yet captured funds.";
+$strings['PendingReasonByPaymentReview'] = "Pending. The payment is being reviewed by PayPal for risk.";
+$strings['PendingReasonByRegulatoryReview'] = "Pending. The payment is being reviewed for compliance with government regulations.";
+$strings['PendingReasonByUnilateral'] = "Pending. The email address is not yet registered o confirmed.";
+$strings['PendingReasonByUpgrade'] = "Pending. The payment was made via credit card.";
+$strings['PendingReasonByVerify'] = "Pending. Sorry. We are not yet verified in PayPal.";
+$strings['PendingReasonByOther'] = "Pending. Please contact with the platform admin.";
+$strings['PayPalPaymentOKPleaseConfirm'] = "PayPal reports the transaction is ready to be executed. To acknowledge that you are OK to proceed, please click the confirmation button below. Once clicked, you will be registered to the course and the funds will be transferred from your PayPal account to our shop. You can always access your courses through the 'My courses' tab. Thank you for your custom!";
+$strings['Sandbox'] = "Test environment";
+$strings['PayPalAccount'] = "Paypal account";
+$strings['NoPayPalAccountDetected'] = "No Paypal account detected";
+$strings['PayPalConfig'] = "PayPal configuration:";
+$strings['TransfersConfig'] = "Bank transfers configuration:";
+$strings['PluginInstruction'] = "You can enable or disable the option to pay via PayPal, Culqi or bank transfer settings in the configuration section of the plugin.";
+$strings['ClickHere'] = "Click here for more details";
+$strings['CurrencyType'] = "Currency type";
+$strings['InfoCurrency'] = "Sets the currency for the payment of your courses.";
+$strings['ApiUsername'] = "API Username";
+$strings['ApiPassword'] = "API Password";
+$strings['ApiSignature'] = "API Signature";
+$strings['InfoApiCredentials'] = "To generate your API credentials to integrate Chamilo with your PayPal account, you must follow the following steps";
+$strings['InfoApiStepOne'] = "Go to your PayPal account, Summary, then Seller preferences in the Selling Tools menu (if you do not have this menu entry, you might need to get the authorization to sell stuff through PayPal first).";
+$strings['InfoApiStepTwo'] = "In paragraph API access, click Update";
+$strings['InfoApiStepThree'] = "In Option 2 (Request API credentials to create your own API username and password), click the View API Signature link, and copy the credentials shown right into the BuyCourses plugin form";
+$strings['ErrorOccurred'] = "An error ocurred. Code: %s. Message: %s. Please contact to platform admin";
+$strings['VisibleInCatalog'] = "Visible in catalog";
+$strings['Beneficiaries'] = "Beneficiaries";
+$strings['AvailableCourse'] = "Available course";
+$strings['ShowOnCourseCatalog'] = "Show on course catalog";
+$strings['ByStatus'] = "By status";
+$strings['ByUser'] = "By user";
+$strings['ByEmail'] = "By email";
+$strings['PaymentMethod'] = "Payment method";
+$strings['SWIFT'] = "SWIFT code";
+$strings['SWIFT_help'] = "Standard format of Bank Identifier Codes (BIC) and serves as a unique identifier for a bank or financial institution";
+$strings['PleaseSelectThePaymentMethodBeforeConfirmYourOrder'] = "Please select your favorite payment method before confirming your order";
+$strings['NoPaymentOptionAvailable'] = 'No payment option available. Please report to the administrator.';
+$strings['XIsOnlyPaymentMethodAvailable'] = '%s is the only payment method available for this purchase.';
+$strings['hide_free_text'] = "Hide 'Free' text";
+$strings['culqi_enable'] = "Enable culqi";
+$strings['include_services'] = "Include services";
+$strings['CurrencyIsNotConfigured'] = "Please, configure a currency before continuing.";
+$strings['Services'] = "Services";
+$strings['Service'] = "Service";
+$strings['NewService'] = "New service";
+$strings['ServiceName'] = "Service name";
+$strings['AppliesTo'] = "Applies to";
+$strings['ServiceInformation'] = "Service information";
+$strings['ListOfServicesOnSale'] = "List of services on sale";
+$strings['GlobalConfig'] = "Global configuration";
+$strings['WriteHereTheTermsAndConditionsOfYourECommerce'] = "Write here the terms and conditions of your e-commerce";
+$strings['EditService'] = "Edit service";
+$strings['DeleteThisService'] = "Delete this service";
+$strings['IConfirmIReadAndAcceptTermsAndCondition'] = "I confirm I read and accept the terms and conditions";
+$strings['PleaseSelectTheCorrectInfoToApplyTheService'] = "Please select the correct info to apply the service";
+$strings['SaleStatusCancelled'] = "Sale cancelled";
+$strings['ServiceSaleInfo'] = "Service sale info";
+$strings['ServiceId'] = "Service Id";
+$strings['BoughtBy'] = "Bought by";
+$strings['PurchaserUser'] = "Purchaser user";
+$strings['Pending'] = "Pending";
+$strings['Names'] = "Names";
+$strings['SellerName'] = "Seller name";
+$strings['SellerId'] = "Seller id";
+$strings['SellerAddress'] = "Seller address";
+$strings['SellerEmail'] = "Seller e-mail";
+$strings['NextNumberInvoice'] = "Next invoice number";
+$strings['NextNumberInvoiceDescription'] = "Number of the following invoice";
+$strings['InvoiceSeries'] = "Invoice series";
+$strings['InvoiceSeriesDescription'] = "Optional parameter: Example invoice Id <series><year>/<number>";
+$strings['InvoiceView'] = "View invoice";
+$strings['NoInvoiceEnable'] = "No invoicing block enable";
+$strings['Company'] = "Company";
+$strings['VAT'] = "VAT";
+$strings['Address'] = "Address";
+$strings['InvoiceNumber'] = "Invoice number";
+$strings['InvoiceDate'] = "Invoice date";
+$strings['Invoice'] = "Invoice";
+$strings['SaleEmail'] = "Sales e-mail";
+$strings['PurchaseDetailsIntro'] = "Purchase details";
+$strings['PurchaseDetailsEnd'] = "Regards";
+$strings['ProductName'] = "Product name";
+$strings['BankAccountIntro'] = "Bank Account Info";
+$strings['AdditionalInfoRequired'] = 'More information needed';
+$strings['SubscriptionToServiceXSuccessful'] = "Subscription to service %s completed.";
+$strings['ClickHereToFinish'] = "Click here to finish";
+$strings['OrderCancelled'] = "Order cancelled";
+$strings['use_currency_symbol'] = "Use currency symbol";
+$strings['ExportReport'] = "Export Sales Report";
+$strings['OrderTime'] = "Order time";
+$strings['SelectDateRange'] = "Select a start date and end date for the report";
+$strings['ServiceAdded'] = "Service added";
+$strings['ServiceEdited'] = "Service updated";
+$strings['ListOfServicesOnSale'] = "List of services for sale";
+$strings['AdditionalInfo'] = "Additional information";
+$strings['culqi_enable'] = "Enable Culqi";
+$strings['CulqiConfig'] = "Culqi configuration:";
+$strings['InfoCulqiCredentials'] = "To obtain your credentials, you will need to create an account on Culqi and enter the development mode, copy the merchant code in your dashboard, then enter the API Keys section and copy the corresponding key to paste it here.";
+$strings['CommerceCode'] = "Merchant code";
+$strings['NoTermsAndConditionsProvided'] = "No defined terms and conditions";
+$strings['GlobalConfig'] = "Global configuration:";
+$strings['MyServices'] = "My services";
+$strings['SalePrice'] = "Sale price";
+$strings['YouNeedToBeRegisteredInAtLeastOneCourse'] = "You need to be registered in at least one course";
+$strings['YouNeedToBeRegisteredInAtLeastOneSession'] = "You need to be registered in at least one session";
+$strings['IfYouWantToGetTheCertificateAndOrSkillsAsociatedToThisCourseYouNeedToBuyTheCertificateServiceYouCanGoToServiceCatalogClickingHere'] = "To obtain the certificate and/or the skills associated to this course, you need to buy the Certificate service. Go to the services catalogue to buy it by clicking here";
+$strings['ServiceDeleted'] = 'Service deleted';
+$strings['YourCoursesNeedAtLeastOneLearningPath'] = 'The courses to which you are subscribed need at least one learning path that contains a final certificate item';
+$strings['GlobalTaxPerc'] = "Global tax rate";
+$strings['GlobalTaxPercDescription'] = "Default tax rate that will be used unless there is a specific tax rate for the course, session or service.";
+$strings['TaxPerc'] = "Tax rate";
+$strings['TaxPercDescription'] = "If left blank, the global tax rate will be used.";
+$strings['ByDefault'] = "by default (global value)";
+$strings['OnlyCourses'] = "Only courses";
+$strings['OnlySessions'] = "Only sessions";
+$strings['OnlyServices'] = "Only services";
+$strings['TaxAppliesTo'] = "Tax applied to";
+$strings['AllCoursesSessionsAndServices'] = "All (courses, sessions and services)";
+$strings['TaxNameCustom'] = "Tax name";
+$strings['TaxNameExamples'] = "VAT, IVA, IGV, TVA, IV ...";
+$strings['ErrorUpdateFieldDB'] = "Error updating the database fields";
+$strings['tpv_redsys_enable'] = "Enable RedSys POS";
+$strings['tpv_redsys_enable_help'] = "In order to use the RedSys POS payment method, it is necessary to download the \"REST INTEGRATION - PHP API \" files at the following link web de RedSys and locate the file apiRedSys.php in the plugin/buycourses/resources directory.";
+$strings['NotFindRedsysFile'] = "The apiRedsys.php file cannot be found in plugin/buycourses/resources directory";
+$strings['TpvPayment'] = "POS payment";
+$strings['TpvRedsysConfig'] = "Redsys POS configuration";
+$strings['DS_MERCHANT_MERCHANTCODE'] = "Trade number (FUC)";
+$strings['DS_MERCHANT_TERMINAL'] = "Terminal number";
+$strings['DS_MERCHANT_CURRENCY'] = "Terminal currency";
+$strings['kc'] = "Secret encryption key";
+$strings['url_redsys'] = "Redsys connection URL";
+$strings['url_redsys_sandbox'] = "Redsys connection URL (Sandbox)";
+$strings['InfoTpvRedsysApiCredentials'] = "You must complete the following form fields with the information provided by the Redsys POS Technical Support:";
+$strings['InfoEmailExtra'] = "Extra info in payment e-mail";
+$strings['Coupon'] = "Coupon";
+$strings['DiscountAmount'] = "Discount amount";
+$strings['RedeemCoupon'] = "Redeem";
+$strings['NeedToAddCouponCode'] = "A coupon code has not been entered";
+$strings['CouponNotValid'] = "This coupon is not valid";
+$strings['CouponRedeemed'] = "Coupon redeemed";
+$strings['CouponDiscount'] = "Discount";
+$strings['CouponStatus'] = "Coupon status";
+$strings['CouponPercentage'] = "Percentage";
+$strings['CouponAmount'] = "Amount";
+$strings['CouponList'] = "Coupon list";
+$strings['CouponCode'] = "Coupon code";
+$strings['CouponDiscountType'] = "Discount type";
+$strings['CouponDateStart'] = "Valid from";
+$strings['CouponDateEnd'] = "Expires on";
+$strings['CouponDelivered'] = "Delivered";
+$strings['CouponDisable'] = "Disable";
+$strings['CouponEnable'] = "Enable";
+$strings['CouponCodeUsed'] = "Coupon code already used";
+$strings['CouponNoExists'] = "Coupon no exists";
+$strings['CouponErrorInsert'] = "Error inserting new coupon";
+$strings['CouponActive'] = "Active";
+$strings['CouponDisabled'] = "Disabled";
+$strings['CouponUpdate'] = "Coupon updated";
+$strings['CouponsConfiguration'] = "Coupons configuration";
+$strings['CouponAdd'] = "New coupon";
+$strings['ConfigureCoupon'] = "Configure coupon";
+$strings['DiscountCoupon'] = "Discount coupons";
+$strings['CouponsCode'] = "Code";
+$strings['DoYouHaveACoupon'] = "Do you have a coupon?";
+$strings['stripe_enable'] = "Enable Stripe";
+$strings['StripeConfig'] = "Stripe configuration:";
+$strings['InfoStripeCredentials'] = "To obtain the credentials you must first create an account in Stripe, copy the account id from your profile, go to the API Keys configuration section and copy the secret key, finally you must go to the Developers button (top right), register a new Endpoint in the Webhooks section, pointing to https://{site}/plugin/buycourses/src/stripe_response.php for the checkout.session.completed event type and copy the secret of the Endpoint.";
+$strings['StripeAccountId'] = "Account id:";
+$strings['StripeSecret'] = "Secret key:";
+$strings['StripeEndpointSecret'] = "Endpoint secret:";
+$strings['PendingReasonByStripe'] = "Pending. Awaiting for payment confirmation ...";
+$strings['cecabank_enable'] = "Enable Cecabank POS";
+$strings['TpvCecabank'] = "POS payment (Cecabank)";
+$strings['CecaSecret'] = "Secret";
+$strings['CecaUrl'] = "Url";
+$strings['CecaMerchanId'] = "Merchant";
+$strings['CecaAcquirerId'] = "Adquirer";
+$strings['CecaTerminalId'] = "Terminal";
+$strings['CecaCypher'] = "Cypher";
+$strings['CecaCurrency'] = "Currency";
+$strings['CecaExponent'] = "Exponent";
+$strings['CecaSupportedPayment'] = "Payment supported";
+$strings['CecabankConfig'] = "Cecabank configuration";
+$strings['Country'] = "Country";
+$strings['PaymentType'] = "Payment type";
+$strings['CountryRelPaymentConfig'] = "Payment type configuration for the country";
+$strings['CountryRelPaymentMessage'] = "To process an order, the type of payment per country must be defined, otherwise the order will not be allowed to go through.";
+$strings['CountryEmpty'] = "To process an order, you need to define the Country field in the user profile.";
+$strings['Duration'] = "Duration";
+$strings['SubscriptionAdd'] = "Add subscription";
+$strings['SubscriptionList'] = "Subscription list";
+$strings['SubscriptionListOnSale'] = "Subscriptions on sale";
+$strings['SelectSubscription'] = "Select duration";
+$strings['SubscriptionNotValid'] = "Subscription not valid";
+$strings['SubscriptionSalesReport'] = "Sales report";
+$strings['BuySubscriptions'] = "Buy subscriptions";
+$strings['ConfigurationOfSubscriptionsAndPrices'] = "Subscriptions and prices configuration";
+$strings['FrequencyConfig'] = "Duration config";
+$strings['Subscriptions'] = "Subscriptions";
+$strings['HasSubscriptions'] = "Has subscriptions";
+$strings['FrequencyRemoved'] = "Period removed";
+$strings['SubscriptionPeriodOnUse'] = "Subscription period in use";
+$strings['FrequencyNotExits'] = "Period does not exist";
+$strings['FrequencyIncorrect'] = "Period incorrect";
+$strings['SubscriptionFrequencyValueDays'] = "Value on days";
+$strings['FrequencyNotUpdated'] = "Period not updated";
+$strings['FrequencyNotSaved'] = "Period not saved";
+$strings['NeedToAddDuration'] = "Need to add duration";
+$strings['SubscriptionNotValid'] = "Subscription not valid";
+$strings['SelecSubscription'] = "Select a subscription";
+$strings['ConfigureSubscriptionsFrequencies'] = "Configure subscriptions periods";
+$strings['FrequencyAdd'] = "Add periods";
+$strings['SubscriptionAlreadyExists'] = "Subscription already exists";
+$strings['SubscriptionPeriodDuration'] = "Subscription duration (in days)";
+$strings['Product'] = "Product name";
+$strings['YouProductIsActivatedYouCanNowAccessIt'] = "Your product is now activated and you can now have access to it.";
+$strings['hide_shopping_cart_from_course_catalogue'] = "Hide shopping cart from the course catalogue and leave the subscribe button";
diff --git a/plugin/buycourses/lang/french.php b/plugin/buycourses/lang/french.php
new file mode 100644
index 000000000..a8eebb865
--- /dev/null
+++ b/plugin/buycourses/lang/french.php
@@ -0,0 +1,250 @@
+ %s . Ce pourcentage sera soustrait du prix total du produit. La différence sera répartie selon les commissions définies ci-dessous.";
+$strings['CoursesInSessionsDoesntDisplayHere'] = "Les cours qui se trouvent dans une session n'apparaissent pas dans la liste de cours individuels.";
+$strings['WantToSellCourses'] = "Envie d'enseigner et de gagner un peu d'argent avec vos propres cours? Cette liste pourraît être pleine de ventes de vos cours. Contactez-nous.";
+$strings['SelectOptionToProceed'] = "Sélectionnez une option pour continuer";
+$strings['VerifyTotalAmountToProceedPayout'] = "Veuillez vérifier le montant total avant de continuer le paiement des commissions. Ce montant ne prend pas en compte le coût additionnel éventuellement perçu par PayPal. Les ventes de cours pour lesquelles les bénéficiaires n'ont pas de compte PayPal configuré ne seront pas prises en compte.";
+$strings['TotalAcounts'] = "Total des comptes:";
+$strings['TotalPayout'] = "Total à payer:";
+$strings['PayoutDate'] = "Date de paiement:";
+$strings['CautionThisProcessCantBeCanceled'] = "Attention: ce processus peut prendre quelques minutes et ne peut pas être interrompu.";
+$strings['ProcessingPayoutsDontCloseThisWindow'] = "Exécution des paiements. Ne fermez pas cette fenêtre avant que le processus ne soit finalisé.";
+$strings['PayoutSuccess'] = "Paiements effectués avec succès";
+$strings['Buyer'] = "Acheteur";
+$strings['BankTransfer'] = "Transfert bancaire";
+$strings['SaleInfo'] = "Information vente";
+$strings['SaleStatusPending'] = "Vente non confirmée";
+$strings['SaleStatusCanceled'] = "Vente annulée";
+$strings['SaleStatusCompleted'] = "Vente finalisée";
+$strings['PayoutStatusPending'] = "Paiement en attente";
+$strings['PayoutStatusCanceled'] = "Paiement annulé";
+$strings['PayoutStatusCompleted'] = "Paiement complété";
+$strings['PayoutsTotalPending'] = "Paiements en attente:";
+$strings['PayoutsTotalCanceled'] = "Paiements annulés:";
+$strings['PayoutsTotalCompleted'] = "Paiements complétés:";
+$strings['TotalAmount'] = "Montant total:";
+$strings['CourseListOnSale'] = "Liste de cours en vente";
+$strings['AvailableCourses'] = "Cours disponibles";
+$strings['Price'] = "Prix";
+$strings['SearchFilter'] = "Filtre de recherche";
+$strings['MinimumPrice'] = "Prix minimum";
+$strings['MaximumPrice'] = "Prix maximum";
+$strings['AvailableCoursesConfiguration'] = "Configuration des cours disponibles";
+$strings['PaymentsConfiguration'] = "Configuration des paiements";
+$strings['TheUserIsAlreadyRegisteredInTheCourse'] = "L'utilisateur est déjà inscrit au cours";
+$strings['SeeDescription'] = "Voir description";
+$strings['Buy'] = "Acheter";
+$strings['WaitingToReceiveThePayment'] = "En attente de réception du paiement";
+$strings['TheUserIsAlreadyRegisteredInTheSession'] = "Vous êtes déjà inscrit à cette session";
+$strings['ItemNotSaved'] = "Article non sauvé";
+$strings['TitlePlugin'] = "Tout le nécessaire pour enseigner et vendre des cours en ligne";
+$strings['PluginPresentation'] = "Le plugin BuyCourses vous donne les moyens nécessaires pour vendre vos cours ou vos sessions existants et d'enseigner en ligne, en quelques étapes très simples. Qu'attendez-vous? Commencez à vendre des cours grâce à Chamilo dès maintenant!";
+$strings['Instructions'] = "Instructions d'utilisation";
+$strings['InstructionsStepOne'] = "Créer un cours ou une session de formation sur la plateforme.";
+$strings['InstructionsStepTwo'] = "Dans la section configuration de paiements, configurer le type de devise dans lequel vous souhaitez vendre vos cours et sessions";
+$strings['InstructionsStepThree'] = "Configurer les cours à vendre dans la section Configuration de cours et prix";
+$strings['BuyCourses'] = "Acheter des cours";
+$strings['ConfigurationOfCoursesAndPrices'] = "Configuration des cours et prix";
+$strings['SalesReport'] = "Rapport des ventes";
+$strings['UserInformation'] = "Fiche acheteur";
+$strings['PaymentMethods'] = "Méthodes de paiement";
+$strings['ConfirmOrder'] = "Confirmer commande";
+$strings['PurchaseData'] = "Détails d'achat";
+$strings['bc_subject'] = "Confirmation de commande de cours";
+$strings['PurchaseStatusX'] = "État de la vente: %s";
+$strings['PendingReasonByTransfer'] = "En attente. Transfert pas encore confirmé";
+$strings['CancelOrder'] = "Annuler la commande";
+$strings['BankAccountInformation'] = "Détails du compte bancaire";
+$strings['BankAccount'] = "Compte bancaire";
+$strings['OnceItIsConfirmedYouWillReceiveAnEmailWithTheBankInformationAndAnOrderReference'] = "Une fois confirmée, vous recevrez un e-mail avec les données bancaires et la référence de la commande";
+$strings['SubscriptionToCourseXSuccessful'] = "Votre inscription au \"%s\" est terminée.";
+$strings['OrderCanceled'] = "Commande annulée";
+$strings['OrderStatus'] = "Statut de commande";
+$strings['PayoutStatus'] = "État du paiement";
+$strings['SearchByStatus'] = "Recherche par statut";
+$strings['OrderReference'] = "Référence de la commande";
+$strings['OrderDate'] = "Date de commande";
+$strings['OrderPrice'] = "Commande Prix";
+$strings['ProductType'] = "Type de produit";
+$strings['SubscribeUser'] = "Inscrire utilisateur";
+$strings['DeleteOrder'] = "Éliminer la commande";
+$strings['ErrorContactPlatformAdmin'] = "Une erreur inconnue s'est produite. Veuillez contacter l'administrateur de la plateforme.";
+$strings['PendingReasonByAddress'] = "En attente. Il manque une adresse de livraison.";
+$strings['PendingReasonByAuthorization'] = "En attente. Nous n'avons pas confirmé la réception des fonds.";
+$strings['PendingReasonByEcheck'] = "En attente. Le paiement a été fait via un chèque qui n'a pas encore été libéré";
+$strings['PendingReasonByIntl'] = "En attente. Aucun moyen de toucher les fonds.";
+$strings['PendingReasonByMulticurrency'] = "En attente. Nous n'avons pas de compte dans la devise indiquée.";
+$strings['PendingReasonByOrder'] = "En attente. Commande enregistrée. Les fonds n'ont pas encore été libérés.";
+$strings['PendingReasonByPaymentReview'] = "En attente. Le paiement est en révision anti-fraude chez PayPal.";
+$strings['PendingReasonByRegulatoryReview'] = "En attente. Le paiement est actuellement revu pour correspondre aux règles mises en vigueur par le gouvernement.";
+$strings['PendingReasonByUnilateral'] = "En attente. L'adresse e-mail n'est pas encore confirmée ou enregistrée.";
+$strings['PendingReasonByUpgrade'] = "En attente. Le paiement a été fait par carte de crédit.";
+$strings['PendingReasonByVerify'] = "En attente. Désolé, nous n'avons pas encore vérifié sur PayPal.";
+$strings['PendingReasonByOther'] = "En attente. Veuillez contacter l'administrateur.";
+$strings['PayPalPaymentOKPleaseConfirm'] = "PayPal nous indique que la transaction est prête à être exécutée. Par mesure de sécurité, nous vous demandons de bien vouloir confirmer une dernière fois la transaction en cliquant sur le bouton de confirmation ci-dessous. Une fois cliqué, vous serez immédiatement enregistré au cours, et les fonds correspondants seront soustraits de votre compte PayPal. Vous pouvez accéder à vos cours à tout moment à partir de l'onglet 'Mes cours'. Merci de votre fidélité!";
+$strings['Sandbox'] = "Environnement de test";
+$strings['PayPalAccount'] = "Compte Paypal";
+$strings['NoPayPalAccountDetected'] = "Pas de compte paypal détecté";
+$strings['PayPalConfig'] = "Configuration PayPal:";
+$strings['TransfersConfig'] = "Configuration des transfers bancaires:";
+$strings['PluginInstruction'] = "Vous pouvez activer ou désactiver l'option de paiements via PayPal, Culqi ou de transferts bancaires dans la section de configuration des plugins.";
+$strings['ClickHere'] = "Cliquez ici pour plus d'infos";
+$strings['CurrencyType'] = "Type de devise";
+$strings['InfoCurrency'] = "Permet de configurer la devise pour l'achat des cours.";
+$strings['ApiUsername'] = "Nom d'utilisateur de l'API";
+$strings['ApiPassword'] = "Mot de passe de l'API";
+$strings['ApiSignature'] = "Signature de l'API";
+$strings['InfoApiCredentials'] = "Pour générer vos données API pour intégrer votre compte PayPal à Chamilo, il vous faudra suivre les étapes suivantes";
+$strings['InfoApiStepOne'] = "Aller dans l'option profil de PayPal, et ensuite dans Outils de vente";
+$strings['InfoApiStepTwo'] = "Dans la section API d'accès, cliquer sur l'option Mettre à jour";
+$strings['InfoApiStepThree'] = "Dans l'option 2 de Configuration des données et permissions API, cliquer sur Voir signature API. Copier ces donées dans le formulaire de configuration de ce plugin";
+$strings['ErrorOccurred'] = "Une erreur est survenue. Code: %s. Message: %s. Veuillez contacter l'administrateur de la plateforme";
+$strings['VisibleInCatalog'] = "Visible dans le catalogue";
+$strings['Beneficiaries'] = "Bénéficiaires";
+$strings['AvailableCourse'] = "Cours disponibles";
+$strings['ShowOnCourseCatalog'] = "Afficher dans le catalogue de cours";
+$strings['ByStatus'] = "Par statut";
+$strings['ByUser'] = "Par utilisateur";
+$strings['ByEmail'] = "Par email";
+$strings['PaymentMethod'] = "Méthodes de paiement";
+$strings['SWIFT'] = "Code SWIFT";
+$strings['SWIFT_help'] = "Format standard des codes d'identification de banque (BIC) et sert un identifiant unique pour une banque ou une institution financière.";
+$strings['PleaseSelectThePaymentMethodBeforeConfirmYourOrder'] = 'Veuillez sélectionner votre méthode de paiement préférée avant de confirmer';
+$strings['NoPaymentOptionAvailable'] = 'Aucune méthode de paiement disponible. Merci de bien vouloir rapporter ce problème à l\'administrateur.';
+$strings['XIsOnlyPaymentMethodAvailable'] = '%s est la seule méthode de paiement disponible pour cet achat.';
+$strings['public_main_menu_tab'] = "Montrer l'onglet dans le menu principal aux utilisateurs anonyme également";
+$strings['culqi_enable'] = "Activé culqi";
+$strings['include_services'] = "Inclure les services";
+$strings['hide_free_text'] = "Cacher le texte 'Free' ou 'Gratuit'";
+$strings['Services'] = "Services";
+$strings['ServiceName'] = "Nom du service";
+$strings['AppliesTo'] = "S'applique à";
+$strings['ListOfServicesOnSale'] = "Liste de services en vente";
+$strings['GlobalConfig'] = "Configuration globale";
+$strings['WriteHereTheTermsAndConditionsOfYourECommerce'] = "Écrire les conditions de ventes de votre e-commerce";
+$strings['NewService'] = "Nouveau service";
+$strings['Service'] = "Service";
+$strings['ServiceInformation'] = "Information sur le service";
+$strings['EditService'] = "Edition du service";
+$strings['DeleteThisService'] = "Supprimer ce service";
+$strings['IConfirmIReadAndAcceptTermsAndCondition'] = "Je confirme que j'ai lu et que j'accepte les termes et conditions";
+$strings['PleaseSelectTheCorrectInfoToApplyTheService'] = "Veuillez choisir l'information à appliquer au service";
+$strings['SaleStatusCancelled'] = "Vente annulée";
+$strings['ServiceSaleInfo'] = "Information de vente";
+$strings['ServiceId'] = "Id du service";
+$strings['BoughtBy'] = "Acheté par";
+$strings['PurchaserUser'] = "Utilisateur acheteur";
+$strings['Pending'] = "En attente";
+$strings['Names'] = "Nom";
+$strings['ExportReport'] = "Export du rapport des ventes";
+$strings['OrderTime'] = "Heure de commande";
+$strings['SelectDateRange'] = "Sélectionnez une date de début et une date de fin pour le rapport";
+$strings['InfoEmailExtra'] = "Informations additionnelles dans l'e-mail de paiement";
+$strings['Coupon'] = "Coupon";
+$strings['DiscountAmount'] = "Ristourne totale";
+$strings['RedeemCoupon'] = "Échanger";
+$strings['NeedToAddCouponCode'] = "Aucun code de coupon n'a été introduit";
+$strings['CouponNotValid'] = "Ce coupon n'est pas valable";
+$strings['CouponRedeemed'] = "Coupon échangé";
+$strings['CouponDiscount'] = "Ristourne";
+$strings['CouponStatus'] = "Statut du coupon";
+$strings['CouponPercentage'] = "Pourcentage";
+$strings['CouponAmount'] = "Montant";
+$strings['CouponList'] = "Liste des coupons";
+$strings['CouponCode'] = "Code du coupon";
+$strings['CouponDiscountType'] = "Type de ristourne";
+$strings['CouponDateStart'] = "Valable apd";
+$strings['CouponDateEnd'] = "Expire le";
+$strings['CouponDelivered'] = "Livré";
+$strings['CouponDisable'] = "Désactivé";
+$strings['CouponEnable'] = "Activé";
+$strings['CouponCodeUsed'] = "Code de coupon déjà utilisé";
+$strings['CouponNoExists'] = "Ce coupon n'existe pas";
+$strings['CouponErrorInsert'] = "Erreur lors de l'insertion du coupon";
+$strings['CouponActive'] = "Actif";
+$strings['CouponDisabled'] = "Désactivé";
+$strings['CouponUpdate'] = "Coupon mis à jour";
+$strings['CouponsConfiguration'] = "Configuration coupons";
+$strings['CouponAdd'] = "Nouveau coupon";
+$strings['ConfigureCoupon'] = "Configurer le coupon";
+$strings['DiscountCoupon'] = "Coupons de ristourne";
+$strings['CouponsCode'] = "Code";
+$strings['DoYouHaveACoupon'] = "Vous avez un coupon?";
+$strings['stripe_enable'] = "Activer Stripe";
+$strings['StripeConfig'] = "Configuration de Stripe:";
+$strings['InfoStripeCredentials'] = "Pour obtenir les données de connexion, vous devez d'abord réer un compte Stripe (ce qui prend un certain temps), puis copier l'ID du compte depuis votre profil et le renseigner ici. Puis aller dans la configuration des clefs API et copier la clef secrète. Enfin, suivre le bouton 'Développeurs' (haut droite), enregistrer un nouveau Endpoint dans la section Webhooks, le pointer vers l'URL https://{site}/plugin/buycourses/src/stripe_response.php et l'associer à l'événement checkout.session.completed et copier le secret du Endpoint ici.";
+$strings['StripeAccountId'] = "ID du compte:";
+$strings['StripeSecret'] = "Clef secrète:";
+$strings['StripeEndpointSecret'] = "Clef secrète du Endpoint:";
+$strings['PendingReasonByStripe'] = "Un instant.... En attente de confirmation du paiement...";
+$strings['cecabank_enable'] = "Activer Cecabank POS";
+$strings['TpvCecabank'] = "Paiement POS (Cecabank)";
+$strings['CecaSecret'] = "Clef secrète";
+$strings['CecaUrl'] = "URL";
+$strings['CecaMerchanId'] = "Marchand";
+$strings['CecaAcquirerId'] = "Acquéreur";
+$strings['CecaTerminalId'] = "Terminal";
+$strings['CecaCypher'] = "Chiffrage";
+$strings['CecaCurrency'] = "Devise";
+$strings['CecaExponent'] = "Exposant";
+$strings['CecaSupportedPayment'] = "Paiement supporté";
+$strings['CecabankConfig'] = "Configuration de Cecabank";
+$strings['Country'] = "Pays";
+$strings['PaymentType'] = "Type de paiement";
+$strings['CountryRelPaymentConfig'] = "Configuration du type de paiement par pays";
+$strings['CountryRelPaymentMessage'] = "Pour faire une commande, il est nécessaire de définir le type de paiement par pays. Sinon, il est impossible de clôturer la commande";
+$strings['CountryEmpty'] = "Pour passer commande, il est nécessaire de définir le champ Pays dans le profil utilisateur.";
+$strings['Duration'] = "Durée";
+$strings['SubscriptionAdd'] = "Ajouter souscription";
+$strings['SubscriptionList'] = "Liste des souscriptions";
+$strings['SubscriptionListOnSale'] = "Souscriptions en promo";
+$strings['SelectSubscription'] = "Sélectionner la durée";
+$strings['SubscriptionNotValid'] = "Souscription non valable";
+$strings['SubscriptionSalesReport'] = "Rapport de ventes";
+$strings['BuySubscriptions'] = "Souscrire";
+$strings['ConfigurationOfSubscriptionsAndPrices'] = "Configuration des souscriptions et prix";
+$strings['FrequencyConfig'] = "Configuration périodes";
+$strings['Subscriptions'] = "Souscriptions";
+$strings['HasSubscriptions'] = "A des souscriptions";
+$strings['FrequencyRemoved'] = "Période supprimée";
+$strings['SubscriptionPeriodOnUse'] = "Période de souscription utilisée";
+$strings['FrequencyNotExits'] = "Cette période n'existe pas";
+$strings['FrequencyIncorrect'] = "Période incorrecte";
+$strings['SubscriptionFrequencyValueDays'] = "Valeur en jours";
+$strings['FrequencyNotUpdated'] = "Période non mise à jour";
+$strings['FrequencyNotSaved'] = "Période non sauvegardée";
+$strings['NeedToAddDuration'] = "Une durée est nécessaire";
+$strings['SubscriptionNotValid'] = "Souscription non valable";
+$strings['SelecSubscription'] = "Sélectionnez une souscription";
+$strings['ConfigureSubscriptionsFrequencies'] = "Configurez des périodes de souscription";
+$strings['FrequencyAdd'] = "Ajouter période";
+$strings['SubscriptionAlreadyExists'] = "La souscription existe déjà";
+$strings['SubscriptionPeriodDuration'] = "Durée de souscription (en jours)";
+$strings['Product'] = "Produit";
+$strings['SalePrice'] = "Prix";
+$strings['YouProductIsActivatedYouCanNowAccessIt'] = "Votre abonnement a été activé, vous avez maintenant accès au module choisi.";
+$strings['hide_shopping_cart_from_course_catalogue'] = "Cacher le chariot de vente dans le catalogue de cours et laisse le bouton s'inscrire";
diff --git a/plugin/buycourses/lang/spanish.php b/plugin/buycourses/lang/spanish.php
new file mode 100644
index 000000000..cfc31902c
--- /dev/null
+++ b/plugin/buycourses/lang/spanish.php
@@ -0,0 +1,321 @@
+ %s , este porcentaje será descontado del total del precio del producto cuya diferencia será el monto base para aplicar estas comisiones.";
+$strings['CoursesInSessionsDoesntDisplayHere'] = "Los cursos que se encuentren dentro de una sesión de formación no aparecerán en la lista de cursos a configurar como productos individuales.";
+$strings['WantToSellCourses'] = "¿Quieres enseñar y ganar dinero con tus propios cursos?, Esta lista puede estar llena de tus ganancias vendiendo cursos a través de esta plataforma!. Informate con nosotros! ";
+$strings['SelectOptionToProceed'] = "Selecciona una opción para proceder";
+$strings['VerifyTotalAmountToProceedPayout'] = "Por favor verificar el monto total para proceder con el pago de comisiones, este monto no considera cargos extras por paypal, las ventas de cursos que no cuenten con una cuenta de Paypal para el o los beneficiarios no serán consideradas.";
+$strings['TotalAcounts'] = "Total de cuentas:";
+$strings['TotalPayout'] = "Total a pagar:";
+$strings['PayoutDate'] = "Fecha del Pago:";
+$strings['CautionThisProcessCantBeCanceled'] = "Precaución: este proceso puede tomar unos minutos y no puede ser cancelado.";
+$strings['ProcessingPayoutsDontCloseThisWindow'] = "Procesando los pagos, no cierre esta ventana hasta que haya finalizado";
+$strings['PayoutSuccess'] = "Los pagos se han realizado con éxito";
+$strings['Buyer'] = "Comprador";
+$strings['BankTransfer'] = "Transferencia Bancaria";
+$strings['SaleInfo'] = "Información de la venta";
+$strings['SaleStatusPending'] = "Venta pendiente";
+$strings['SaleStatusCanceled'] = "Venta cancelada";
+$strings['SaleStatusCompleted'] = "Venta completada";
+$strings['PayoutStatusPending'] = "Pago pendiente";
+$strings['PayoutStatusCanceled'] = "Pago cancelado";
+$strings['PayoutStatusCompleted'] = "Pago completado";
+$strings['PayoutsTotalPending'] = "Pagos pendientes:";
+$strings['PayoutsTotalCanceled'] = "Pagos cancelados:";
+$strings['PayoutsTotalCompleted'] = "Pagos completados:";
+$strings['Total'] = "Total";
+$strings['TotalAmount'] = "Monto total:";
+$strings['CourseListOnSale'] = "Lista de cursos a la venta";
+$strings['AvailableCourses'] = "Cursos disponibles";
+$strings['Price'] = "Precio";
+$strings['SearchFilter'] = "Filtro de búsqueda";
+$strings['MinimumPrice'] = "Precio mínimo";
+$strings['MaximumPrice'] = "Precio máximo";
+$strings['AvailableCoursesConfiguration'] = "Configuración de cursos disponibles";
+$strings['PaymentsConfiguration'] = "Configuración de Pagos";
+$strings['TheUserIsAlreadyRegisteredInTheCourse'] = "Usted ya está registrado en el curso.";
+$strings['SeeDescription'] = "Ver descripción";
+$strings['Buy'] = "Comprar";
+$strings['WaitingToReceiveThePayment'] = "Se encuentra a la espera de recibir el pago";
+$strings['TheUserIsAlreadyRegisteredInTheSession'] = "Usted ya está registrado en la sesión";
+$strings['ItemNotSaved'] = "Elemento no guardado";
+$strings['TitlePlugin'] = "Todo lo que necesita para enseñar y vender cursos en línea";
+$strings['PluginPresentation'] = "El Plugin BuyCourses le da los recursos para vender sus cursos o sus sesiones ya creados y enseñar en línea, todo en unos simples pasos y configuraciones. ¿Qué espera? Empiece a vender cursos a través de Chamilo ahora!";
+$strings['Instructions'] = "Instrucciones de uso";
+$strings['InstructionsStepOne'] = "Crea un curso o una sesión de formación en la plataforma.";
+$strings['InstructionsStepTwo'] = "En la sección configuración de pagos, configure el tipo de moneda con el que venderá sus cursos o sesiones.";
+$strings['InstructionsStepThree'] = "Configure los cursos a vender en la sección Configuración de cursos y precios";
+$strings['BuyCourses'] = "Comprar cursos";
+$strings['ConfigurationOfCoursesAndPrices'] = "Configuración de cursos y precios";
+$strings['SalesReport'] = "Reporte de ventas";
+$strings['UserInformation'] = "Ficha del comprador";
+$strings['PaymentMethods'] = "Métodos de pago";
+$strings['ConfirmOrder'] = "Confirmar Orden";
+$strings['PurchaseData'] = "Datos de la compra";
+$strings['bc_subject'] = "Confirmación pedido de cursos";
+$strings['PurchaseStatusX'] = "Estado de la compra: %s";
+$strings['PendingReasonByTransfer'] = "Pendiente. Esperando la confirmación de la transferencia";
+$strings['CancelOrder'] = "Cancelar Orden";
+$strings['BankAccountInformation'] = "Información de la Cuenta Bancaria";
+$strings['BankAccount'] = "Cuenta Bancaria";
+$strings['OnceItIsConfirmedYouWillReceiveAnEmailWithTheBankInformationAndAnOrderReference'] = "Una vez confirmado, recibira un e-mail con los datos bancarios y una referencia del pedido.";
+$strings['SubscriptionToCourseXSuccessful'] = "
Proceso completado. Tu subscripción a \"%s\" se realizó correctamente.
";
+$strings['OrderCanceled'] = "Pedido cancelado";
+$strings['OrderStatus'] = "Estado del pedido";
+$strings['PayoutStatus'] = "Estado del pago";
+$strings['SearchByStatus'] = "Buscar por estado";
+$strings['OrderReference'] = "Referencia del pedido";
+$strings['OrderDate'] = "Fecha del pedido";
+$strings['OrderPrice'] = "Precio del pedido";
+$strings['ProductType'] = "Tipo de producto";
+$strings['SubscribeUser'] = "Inscribir usuario";
+$strings['DeleteOrder'] = "Eliminar pedido";
+$strings['ErrorContactPlatformAdmin'] = "Se ha producido un error desconocido. Por favor, póngase en contacto con el administrador de la plataforma.";
+$strings['PendingReasonByAddress'] = "Pendiente. No has ingresado una dirección de correo confirmada.";
+$strings['PendingReasonByAuthorization'] = "Pendiente. No hemos recibido los fondos.";
+$strings['PendingReasonByEcheck'] = "Pendiente. El pago fue hecho a través de un eCheck que aún no está declarado";
+$strings['PendingReasonByIntl'] = "Pendiente. No tenemos un mecanismo de retiro de fondos.";
+$strings['PendingReasonByMulticurrency'] = "Pending. No podemos realizar el cambio de moneda.";
+$strings['PendingReasonByOrder'] = "Pendiente. Pedido realizado. No hemos recibido los fondos.";
+$strings['PendingReasonByPaymentReview'] = "Pendiente. El pago está siendo revisado por PayPal por riesgo.";
+$strings['PendingReasonByRegulatoryReview'] = "Pendiente. El pago está siendo revisado por cumplimiento con regulaciones gubernamentales.";
+$strings['PendingReasonByUnilateral'] = "Pendiente. La dirección de correo electrónico aún no está registrada o confirmada.";
+$strings['PendingReasonByUpgrade'] = "Pendiente. El pago fue realizado a través de una tarjeta de crédito.";
+$strings['PendingReasonByVerify'] = "Pendiente. Lo sentimos. Todavía no estamos verificados en PayPal.";
+$strings['PendingReasonByOther'] = "Pendiente. Por favor contacta al administrador de la plataforma.";
+$strings['PayPalPaymentOKPleaseConfirm'] = "PayPal nos indicó que todo estaba listo para ejecutar el pago. Por seguridad, le pedimos confirme una última vez su pedido dando clic en el botón de confirmación a bajo. Una vez le haya dado clic, será registrado al curso y el monto correspondiente será retirado de su cuenta PayPal. Siempre puede acceder a sus cursos a partir de la pestaña 'Mis cursos'. Gracias por su compra!";
+$strings['Sandbox'] = "Entorno de Pruebas";
+$strings['PayPalAccount'] = "Cuenta de Paypal";
+$strings['NoPayPalAccountDetected'] = "No se ingresó una cuenta de Paypal";
+$strings['PayPalConfig'] = "Configuración PayPal:";
+$strings['TransfersConfig'] = "Configuración de transferencias:";
+$strings['PluginInstruction'] = "Puede activar o desactivar la opción de pagos vía PayPal, Culqi o configuración de transferencias bancarias, en la sección del configuración del plugin.";
+$strings['ClickHere'] = "Clic aquí para más detalles";
+$strings['CurrencyType'] = "Tipo de moneda";
+$strings['InfoCurrency'] = "Permite configurar el tipo de moneda (de pago) para sus cursos.";
+$strings['ApiUsername'] = "Nombre de usuario de API";
+$strings['ApiPassword'] = "Contraseña de API";
+$strings['ApiSignature'] = "Firma";
+$strings['InfoApiCredentials'] = "Para generar tu credenciales API para integrar su cuenta PayPal con Chamilo LMS, deberá seguir los siguientes pasos";
+$strings['InfoApiStepOne'] = "Ir a la opción de Perfil de PayPal, luego en Mis herramientas de venta";
+$strings['InfoApiStepTwo'] = "En el apartado Acceso API, dar clic en la opción Actualizar";
+$strings['InfoApiStepThree'] = "En la opción 2, de Configuración de credenciales y permisos de API, dar clic en Ver firma de API. Copiar los datos de las credenciales en el formulario de configuración de este plugin";
+$strings['ErrorOccurred'] = "Ocurrio un error. Codigo: %s. Mensaje: %s. Por favor, contacta al administrador de la plataforma.";
+$strings['VisibleInCatalog'] = "Visible en el catálogo";
+$strings['Beneficiaries'] = "Beneficiarios";
+$strings['AvailableCourse'] = "Curso disponible";
+$strings['ShowOnCourseCatalog'] = "Mostrar en el catálogo de cursos";
+$strings['ByStatus'] = "Por estado";
+$strings['ByUser'] = "Por usuario";
+$strings['ByEmail'] = "Por email";
+$strings['PaymentMethod'] = "Método de pago";
+$strings['SWIFT'] = "Código SWIFT";
+$strings['SWIFT_help'] = "Formato estándar de los Códigos de Identificación Bancaria (BIC) que sirve como identificador único para un banco o institución financiera.";
+$strings['PleaseSelectThePaymentMethodBeforeConfirmYourOrder'] = "Seleccione su método de pago preferido antes de confirmar su pedido";
+$strings['NoPaymentOptionAvailable'] = 'No hay opción de pago disponible. Por favor reporte este problema al administrador.';
+$strings['XIsOnlyPaymentMethodAvailable'] = '%s es la única opción de pago disponible para esta compra.';
+$strings['hide_free_text'] = "Esconder texto 'Gratis'";
+$strings['culqi_enable'] = "Activar culqi";
+$strings['include_services'] = "Incluir Servicios";
+$strings['CurrencyIsNotConfigured'] = "Configure una moneda antes de seguir.";
+$strings['Services'] = "Servicios";
+$strings['Service'] = "Servicio";
+$strings['NewService'] = "Nuevo servicio";
+$strings['ServiceName'] = "Nombre de servicio";
+$strings['AppliesTo'] = "Aplicado a";
+$strings['ServiceInformation'] = "Información del servicio";
+$strings['ListOfServicesOnSale'] = "Lista de servicios en venta";
+$strings['GlobalConfig'] = "Configuración global";
+$strings['WriteHereTheTermsAndConditionsOfYourECommerce'] = "Escriba aquí los términos y condiciones de su portal e-commerce";
+$strings['EditService'] = "Editar servicio";
+$strings['DeleteThisService'] = "Borrar servicio";
+$strings['IConfirmIReadAndAcceptTermsAndCondition'] = "He leído y acepto los términos y condiciones";
+$strings['PleaseSelectTheCorrectInfoToApplyTheService'] = "Porfavor Seleccione la información correcta para aplicar el servicio";
+$strings['SaleStatusCancelled'] = "Venta anulada";
+$strings['ServiceSaleInfo'] = "Información del servicio";
+$strings['ServiceId'] = "Id de servicio";
+$strings['BoughtBy'] = "Comprado por";
+$strings['PurchaserUser'] = "Usuario comprador";
+$strings['Pending'] = "Pendiente";
+$strings['Names'] = "Nombres";
+$strings['SellerName'] = "Nombre vendedor";
+$strings['SellerId'] = "Identificador vendedor";
+$strings['SellerAddress'] = "Dirección vendedor";
+$strings['SellerEmail'] = "E-mail vendedor";
+$strings['NextNumberInvoice'] = "Número siguiente factura";
+$strings['NextNumberInvoiceDescription'] = "Número de la siguiente factura asignado de forma manual";
+$strings['InvoiceSeries'] = "Serie factura";
+$strings['InvoiceSeriesDescription'] = "Parámetro opcional: Ejemplo de numeración factura <serie><año>/<número>";
+$strings['InvoiceView'] = "Ver factura";
+$strings['NoInvoiceEnable'] = "No está habilitado el bloque de facturación";
+$strings['Company'] = "Empresa";
+$strings['VAT'] = "CIF";
+$strings['Address'] = "Dirección";
+$strings['InvoiceNumber'] = "Num. factura";
+$strings['InvoiceDate'] = "Fecha de emisión";
+$strings['Invoice'] = "Factura";
+$strings['SaleEmail'] = "E-mail de ventas";
+$strings['PurchaseDetailsIntro'] = "Detalles de la comprar";
+$strings['PurchaseDetailsEnd'] = "Atentamente";
+$strings['ProductName'] = "Nombre producto";
+$strings['BankAccountIntro'] = "Información cuentas bancarias";
+$strings['AdditionalInfoRequired'] = 'Se requiere que se elija la información adicional antes de proceder';
+$strings['SubscriptionToServiceXSuccessful'] = "La subscripción al servicio %s ha sido satisfactoria";
+$strings['ClickHereToFinish'] = "De clic aquí para terminar";
+$strings['OrderCancelled'] = "Pedido anulado";
+$strings['use_currency_symbol'] = "Usar símbolo de la moneda";
+$strings['ExportReport'] = "Exportar reporte de ventas";
+$strings['OrderTime'] = "Fecha del pedido";
+$strings['SelectDateRange'] = "Seleccione una fecha de inicio y una fecha de fin para el reporte";
+$strings['ServiceAdded'] = "Servicio agregado";
+$strings['ServiceEdited'] = "Servicio editado";
+$strings['ListOfServicesOnSale'] = "Lista de servicios a la venta";
+$strings['AdditionalInfo'] = "Información adicional";
+$strings['culqi_enable'] = "Habilitar Culqi";
+$strings['CulqiConfig'] = "Configuración de CULQI:";
+$strings['InfoCulqiCredentials'] = "Para obtener las credenciales es necesario crearse una cuenta en Culqi e ingresar en modo desarrollo, copiar el código de comercio que se encuentra en su panel de control, luego ingresar al apartado de API Keys y generar la Key correspondiente para copiarla aquí";
+$strings['CommerceCode'] = "Codigo de comercio";
+$strings['NoTermsAndConditionsProvided'] = "Terminos y condiciones no establecidos";
+$strings['GlobalConfig'] = "Configuración global:";
+$strings['MyServices'] = "Mis servicios";
+$strings['SalePrice'] = "Precio de venta";
+$strings['YouNeedToBeRegisteredInAtLeastOneCourse'] = "Necesitas estar registrado en al menos un curso";
+$strings['YouNeedToBeRegisteredInAtLeastOneSession'] = "Necesitas estar registrado en al menos una sesión";
+$strings['IfYouWantToGetTheCertificateAndOrSkillsAsociatedToThisCourseYouNeedToBuyTheCertificateServiceYouCanGoToServiceCatalogClickingHere'] = "Si quieres obtener el certificado y/o las competencias asociadas a este curso, necesitas comprar el servicio de Certificado , puedes ir al catálogo de servicios para comprarlo haciendo click AQUÍ";
+$strings['ServiceDeleted'] = 'Servicio eliminado';
+$strings['YourCoursesNeedAtLeastOneLearningPath'] = 'Los cursos en los que estás registrado necesitan tener al menos una lección que contenga un item de cerficado final';
+$strings['GlobalTaxPerc'] = "Porcentaje del impuesto global";
+$strings['GlobalTaxPercDescription'] = "Porcentaje por defecto que se usará, excepto si existe un impuesto específico en el curso, sesión o servicio.";
+$strings['TaxPerc'] = "Porcentaje del impuesto";
+$strings['TaxPercDescription'] = "Si se deja vacío se usará valor global por defecto.";
+$strings['ByDefault'] = "por defecto (valor global)";
+$strings['OnlyCourses'] = "Solo Cursos";
+$strings['OnlySessions'] = "Solo Sesiones";
+$strings['OnlyServices'] = "Solo Servicios";
+$strings['TaxAppliesTo'] = "Impuestos aplicados a";
+$strings['AllCoursesSessionsAndServices'] = "Todos (Cursos, sesiones y servicios)";
+$strings['TaxNameCustom'] = "Nombre del impuesto";
+$strings['TaxNameExamples'] = "VAT, IVA, IGV, TVA, IV ...";
+$strings['ErrorUpdateFieldDB'] = "Error al actualizar los campos de la base de datos";
+$strings['tpv_redsys_enable'] = "Habilitar TPV RedSys";
+$strings['tpv_redsys_enable_help'] = "Para poder utilizar la modalidad de pago del TPV de RedSys es necesario descargar los ficheros de \"INTEGRACIÓN REST - API PHP\" en el siguiente enlace web de RedSys y ubicar el fichero el fichero apiRedSys.php en el directorio plugin/buycourses/resources.";
+$strings['NotFindRedsysFile'] = "No se encuentra en el directorio plugin/buycourses/resources el fichero apiRedsys.php";
+$strings['TpvPayment'] = "Pago con tarjeta";
+$strings['TpvRedsysConfig'] = "Configuración TPV Redsys";
+$strings['DS_MERCHANT_MERCHANTCODE'] = "Número de comercio (FUC)";
+$strings['DS_MERCHANT_TERMINAL'] = "Número de terminal";
+$strings['DS_MERCHANT_CURRENCY'] = "Moneda del terminal";
+$strings['kc'] = "Clave secreta de encriptación";
+$strings['url_redsys'] = "URL conexión Redsys";
+$strings['url_redsys_sandbox'] = "URL conexión Redsys (Pruebas)";
+$strings['InfoTpvRedsysApiCredentials'] = "Deberá completar los siguientes campos del formulario con la información que les facilite el Soporte Técnico del TPV Redsys:";
+$strings['InfoEmailExtra'] = "Información extra en e-mail";
+$strings['Coupon'] = "Cupón";
+$strings['DiscountAmount'] = "Descuento total";
+$strings['RedeemCoupon'] = "Canjear";
+$strings['NeedToAddCouponCode'] = "No se ha introducido un código de cupón";
+$strings['CouponNotValid'] = "El cupón no es valido";
+$strings['CouponRedeemed'] = "Cupón canjeado";
+$strings['CouponDiscount'] = "Descuento";
+$strings['CouponStatus'] = "Estado cupón";
+$strings['CouponPercentage'] = "Porcentaje";
+$strings['CouponAmount'] = "Valor";
+$strings['CouponList'] = "Lista de cupones";
+$strings['CouponCode'] = "Código del cupón";
+$strings['CouponDiscountType'] = "Tipo de descuento";
+$strings['CouponDateStart'] = "Valido desde";
+$strings['CouponDateEnd'] = "Caduca el";
+$strings['CouponDelivered'] = "Canjeados";
+$strings['CouponDisable'] = "Desactivar";
+$strings['CouponEnable'] = "Activar";
+$strings['CouponCodeUsed'] = "Código de cupón ya usado";
+$strings['CouponNoExists'] = "El cupón no existe";
+$strings['CouponErrorInsert'] = "Error añadiendo nuevo cupón";
+$strings['CouponActive'] = "Activo";
+$strings['CouponDisabled'] = "Desactivado";
+$strings['CouponUpdate'] = "Cupón actualizado";
+$strings['CouponsConfiguration'] = "Configuración de cupones";
+$strings['CouponAdd'] = "Nuevo cupón";
+$strings['ConfigureCoupon'] = "Configurar cupón";
+$strings['DiscountCoupons'] = "Cupones descuento";
+$strings['CouponsCode'] = "Código";
+$strings['DoYouHaveACoupon'] = "¿Tienes un cupón?";
+$strings['stripe_enable'] = "Activar Stripe";
+$strings['StripeConfig'] = "Configuración de Stripe:";
+$strings['InfoStripeCredentials'] = "Para obtener las credenciales debes crear una cuenta en Stripe, copiar el id de cuenta de tu perfil, ir a la sección de configuración de las claves API y copiar la secret key, por último deberá registrar un nuevo Endpoint en la sección de webhooks a https://{site}/plugin/buycourses/src/stripe_response.php para el tipo de evento checkout.session.completed y copiar el secret del Endpoint.";
+$strings['StripeAccountId'] = "Id de la cuenta:";
+$strings['StripeSecret'] = "Secret key:";
+$strings['StripeEndpointSecret'] = "Endpoint secret:";
+$strings['PendingReasonByStripe'] = "Pendiente. Esperando confirmación del pago ...";
+$strings['cecabank_enable'] = "Habilitar TPV Cecabank";
+$strings['TpvCecabank'] = "Pago con tarjeta (Cecabank)";
+$strings['CecaSecret'] = "Secret";
+$strings['CecaUrl'] = "Url";
+$strings['CecaMerchanId'] = "Merchant";
+$strings['CecaAcquirerId'] = "Adquirer";
+$strings['CecaTerminalId'] = "Terminal";
+$strings['CecaCypher'] = "Cypher";
+$strings['CecaCurrency'] = "Moneda";
+$strings['CecaExponent'] = "Exponent";
+$strings['CecaSupportedPayment'] = "Pago soportado";
+$strings['CecabankConfig'] = "Configuración Cecabank";
+$strings['Country'] = "País";
+$strings['PaymentType'] = "Tipo de pago";
+$strings['CountryRelPaymentConfig'] = "Configuración del tipo de pago por país";
+$strings['CountryRelPaymentMessage'] = "Para realizar un pedido es necesario definir el tipo de pago por país, en caso contrario no se permite la realización del pedido";
+$strings['CountryEmpty'] = "Para realizar un pedido es necesario definir en el perfil del usuario el campo país";
+$strings['Duration'] = "Duración";
+$strings['SubscriptionAdd'] = "Añadir suscripción";
+$strings['SubscriptionList'] = "Lista de suscripciones";
+$strings['SubscriptionListOnSale'] = "Lista de suscripciones a la venta";
+$strings['SelectSubscription'] = "Seleciona duración";
+$strings['SubscriptionNotValid'] = "Suscripción no valida";
+$strings['SubscriptionSalesReport'] = "Reporte de ventas";
+$strings['BuySubscriptions'] = "Compra suscripciones";
+$strings['ConfigurationOfSubscriptionsAndPrices'] = "Configuración de suscripciones y precios";
+$strings['FrequencyConfig'] = "Configuración periodos";
+$strings['Subscriptions'] = "Suscripciones";
+$strings['HasSubscriptions'] = "Tiene suscripciones";
+$strings['FrequencyRemoved'] = "Periodo eliminado";
+$strings['SubscriptionPeriodOnUse'] = "Periodo de suscription en uso";
+$strings['FrequencyNotExits'] = "El periodo no existe";
+$strings['FrequencyIncorrect'] = "Periodo incorrecto";
+$strings['SubscriptionFrequencyValueDays'] = "Valor de la suscripción en días";
+$strings['FrequencyNotUpdated'] = "Periodo no actualizado";
+$strings['FrequencyNotSaved'] = "Periodo no guardado";
+$strings['NeedToAddDuration'] = "Hay que especificar una duración";
+$strings['SubscriptionNotValid'] = "Suscripción no valida";
+$strings['SelecSubscription'] = "Seleccione una suscripción";
+$strings['ConfigureSubscriptionsFrequencies'] = "Configurar los períodos de suscripciones";
+$strings['FrequencyAdd'] = "Agregar período";
+$strings['SubscriptionAlreadyExists'] = "La suscripción ya existe";
+$strings['SubscriptionPeriodDuration'] = "Duración de suscripción (en días)";
+$strings['Product'] = "Producto";
+$strings['YouProductIsActivatedYouCanNowAccessIt'] = "Tu producto esta activado y puedes ahora acceder a este.";
+$strings['hide_shopping_cart_from_course_catalogue'] = "Esconder el carito de compra del catalogo de curso y dejar el boton de subscripcion a curso";
diff --git a/plugin/buycourses/plugin.php b/plugin/buycourses/plugin.php
new file mode 100644
index 000000000..f77cd31ed
--- /dev/null
+++ b/plugin/buycourses/plugin.php
@@ -0,0 +1,13 @@
+Plugins).
+ *
+ * @package chamilo.plugin.buycourses
+ */
+/**
+ * Plugin details (must be present).
+ */
+require_once __DIR__.'/config.php';
+$plugin_info = BuyCoursesPlugin::create()->get_info();
diff --git a/plugin/buycourses/resources/css/style.css b/plugin/buycourses/resources/css/style.css
new file mode 100644
index 000000000..40b07a1c5
--- /dev/null
+++ b/plugin/buycourses/resources/css/style.css
@@ -0,0 +1,421 @@
+.buy-courses-tabs {
+ margin-bottom: 15px;
+}
+
+.buy-courses-page-header {
+ padding-bottom: 9px;
+ margin: 40px 0 20px;
+ border-bottom: 1px solid #eee;
+}
+
+.buy-courses-block-button {
+ display: block;
+ width: 100%;
+ height: 50px;
+ line-height: 35px;
+ font-size: 16px;
+}
+
+.buy-courses-cross-out {
+ text-decoration: line-through;
+}
+
+.buy-courses-title-color {
+ color: #5DC3E1;
+ padding-bottom: 10px;
+}
+
+.buy-courses-description-service {
+ text-align: justify;
+ font-size: 18px;
+ line-height: 1.42857;
+}
+
+/* PROCESS BUY */
+.buy-info .price, .service-buy .price {
+ font-size: 24px;
+ line-height: 32px;
+ font-weight: bold;
+ padding-bottom: 10px;
+ padding-top: 10px;
+ letter-spacing: -0.020em;
+}
+
+.buy-info .price-details-tax, .service-buy .price-details-tax {
+ font-size: 18px;
+ line-height: 24px;
+ font-weight: bold;
+ padding-bottom: 0px;
+ padding-top: 10px;
+ letter-spacing: -0.020em;
+}
+
+.buy-info .buy-item .title {
+ margin-top: 5px;
+ font-weight: bold;
+ line-height: 24px;
+ font-size: 18px;
+}
+
+.buy-info .buy-item .description, .buy-info .buy-item .coaches {
+ margin-bottom: 20px;
+ color: #666666;
+}
+
+.buy-info .buy-item .list-description {
+ color: #666666;
+}
+
+.buy-info .buy-item .date {
+ margin-bottom: 10px;
+ color: #666666;
+}
+
+.buy-info .coupon-question, .service-buy .coupon {
+ font-size: 16px;
+ line-height: 6px;
+ font-weight: bold;
+ padding-bottom: 10px;
+ padding-top: 24px;
+ letter-spacing: -0.020em;
+}
+
+.buy-info .coupon, .service-buy .coupon {
+ line-height: 32px;
+ font-weight: bold;
+ padding-bottom: 10px;
+ padding-top: 10px;
+ letter-spacing: -0.1em;
+}
+
+.buy-summary {
+ margin-top: 10px;
+ border-top: 1px solid #CCCCCC;
+}
+
+.panel-box-buy {
+ box-shadow: 0 15px 30px rgba(0, 0, 0, .1);
+ padding: 20px;
+}
+
+.buy-summary .alert-info {
+ color: #31708f;
+ background-color: #e8f2f8;
+ border-color: #bce8f1;
+}
+
+.btn.btn-default.btn-sm {
+ white-space: unset;
+}
+
+tr.sale-row {
+ font-size: 12px;
+}
+tr.sale-columns {
+ font-size: 10px;
+ text-transform: uppercase;
+ background-color: #e6e6e6;
+}
+tr.sale-columns th{
+ vertical-align: middle !important;
+ text-align: center;
+}
+@-moz-keyframes wobblebar-loader {
+ 0% {
+ left: 4px;
+ }
+ 3% {
+ left: 204px;
+ }
+ 6% {
+ left: 4px;
+ }
+ 9% {
+ left: 204px;
+ }
+ 12% {
+ left: 4px;
+ }
+ 15% {
+ left: 204px;
+ }
+ 18% {
+ left: 32px;
+ }
+ 27% {
+ left: 32px;
+ }
+ 30% {
+ left: 204px;
+ }
+ 33% {
+ left: 4px;
+ }
+ 36% {
+ left: 204px;
+ }
+ 39% {
+ left: 4px;
+ }
+ 42% {
+ left: 204px;
+ }
+ 45% {
+ left: 4px;
+ }
+ 48% {
+ left: 204px;
+ }
+ 51% {
+ left: 152px;
+ }
+ 63% {
+ left: 152px;
+ }
+ 66% {
+ left: 4px;
+ }
+ 69% {
+ left: 204px;
+ }
+ 72% {
+ left: 4px;
+ }
+ 75% {
+ left: 204px;
+ }
+ 78% {
+ left: 4px;
+ }
+ 81% {
+ left: 204px;
+ }
+ 84% {
+ left: 72px;
+ }
+ 94% {
+ left: 72px;
+ }
+ 97% {
+ left: 204px;
+ }
+}
+
+@-webkit-keyframes wobblebar-loader {
+ 0% {
+ left: 4px;
+ }
+ 3% {
+ left: 204px;
+ }
+ 6% {
+ left: 4px;
+ }
+ 9% {
+ left: 204px;
+ }
+ 12% {
+ left: 4px;
+ }
+ 15% {
+ left: 204px;
+ }
+ 18% {
+ left: 32px;
+ }
+ 27% {
+ left: 32px;
+ }
+ 30% {
+ left: 204px;
+ }
+ 33% {
+ left: 4px;
+ }
+ 36% {
+ left: 204px;
+ }
+ 39% {
+ left: 4px;
+ }
+ 42% {
+ left: 204px;
+ }
+ 45% {
+ left: 4px;
+ }
+ 48% {
+ left: 204px;
+ }
+ 51% {
+ left: 152px;
+ }
+ 63% {
+ left: 152px;
+ }
+ 66% {
+ left: 4px;
+ }
+ 69% {
+ left: 204px;
+ }
+ 72% {
+ left: 4px;
+ }
+ 75% {
+ left: 204px;
+ }
+ 78% {
+ left: 4px;
+ }
+ 81% {
+ left: 204px;
+ }
+ 84% {
+ left: 72px;
+ }
+ 94% {
+ left: 72px;
+ }
+ 97% {
+ left: 204px;
+ }
+}
+
+@keyframes wobblebar-loader {
+ 0% {
+ left: 4px;
+ }
+ 3% {
+ left: 204px;
+ }
+ 6% {
+ left: 4px;
+ }
+ 9% {
+ left: 204px;
+ }
+ 12% {
+ left: 4px;
+ }
+ 15% {
+ left: 204px;
+ }
+ 18% {
+ left: 32px;
+ }
+ 27% {
+ left: 32px;
+ }
+ 30% {
+ left: 204px;
+ }
+ 33% {
+ left: 4px;
+ }
+ 36% {
+ left: 204px;
+ }
+ 39% {
+ left: 4px;
+ }
+ 42% {
+ left: 204px;
+ }
+ 45% {
+ left: 4px;
+ }
+ 48% {
+ left: 204px;
+ }
+ 51% {
+ left: 152px;
+ }
+ 63% {
+ left: 152px;
+ }
+ 66% {
+ left: 4px;
+ }
+ 69% {
+ left: 204px;
+ }
+ 72% {
+ left: 4px;
+ }
+ 75% {
+ left: 204px;
+ }
+ 78% {
+ left: 4px;
+ }
+ 81% {
+ left: 204px;
+ }
+ 84% {
+ left: 72px;
+ }
+ 94% {
+ left: 72px;
+ }
+ 97% {
+ left: 204px;
+ }
+}
+
+/* Landscape phones and down */
+@media (max-width: 480px) {
+ .panel-box-buy {
+ padding: 0;
+ }
+
+ .buy-summary .pull-right {
+ float: left !important;
+ }
+
+ .buy-summary .btn-success {
+ width: 100%;
+ }
+}
+
+/* :not(:required) hides this rule from IE9 and below */
+.wobblebar-loader:not(:required) {
+ background: #2E6DA4;
+ -moz-border-radius: 10.66667px;
+ -webkit-border-radius: 10.66667px;
+ border-radius: 10.66667px;
+ display: inline-block;
+ overflow: hidden;
+ text-indent: -9999px;
+ width: 228px;
+ height: 21.33333px;
+ position: relative;
+}
+
+.wobblebar-loader:not(:required)::after {
+ -moz-animation: wobblebar-loader 15000ms infinite ease;
+ -webkit-animation: wobblebar-loader 15000ms infinite ease;
+ animation: wobblebar-loader 15000ms infinite ease;
+ background: white;
+ display: block;
+ -moz-border-radius: 7.11111px;
+ -webkit-border-radius: 7.11111px;
+ border-radius: 7.11111px;
+ content: '';
+ position: absolute;
+ top: 3.55556px;
+ left: 4px;
+ width: 21.33333px;
+ height: 14.22222px;
+}
+
+div.items-course-info h4.title {
+ font-weight: bold;
+ height: 2.8em;
+}
+
+@media (min-width: 991px) {
+ .col-sm-6 .items-course .items-course-image figure {
+ max-height: 135px;
+ }
+}
\ No newline at end of file
diff --git a/plugin/buycourses/resources/img/128/backlogs.png b/plugin/buycourses/resources/img/128/backlogs.png
new file mode 100644
index 000000000..522a1cfa0
Binary files /dev/null and b/plugin/buycourses/resources/img/128/backlogs.png differ
diff --git a/plugin/buycourses/resources/img/128/buycourses.png b/plugin/buycourses/resources/img/128/buycourses.png
new file mode 100644
index 000000000..cba3a6492
Binary files /dev/null and b/plugin/buycourses/resources/img/128/buycourses.png differ
diff --git a/plugin/buycourses/resources/img/128/buysubscriptions.png b/plugin/buycourses/resources/img/128/buysubscriptions.png
new file mode 100644
index 000000000..97a4eed52
Binary files /dev/null and b/plugin/buycourses/resources/img/128/buysubscriptions.png differ
diff --git a/plugin/buycourses/resources/img/128/discount.png b/plugin/buycourses/resources/img/128/discount.png
new file mode 100644
index 000000000..9c1f011f7
Binary files /dev/null and b/plugin/buycourses/resources/img/128/discount.png differ
diff --git a/plugin/buycourses/resources/img/128/paymentsettings.png b/plugin/buycourses/resources/img/128/paymentsettings.png
new file mode 100644
index 000000000..24516f22f
Binary files /dev/null and b/plugin/buycourses/resources/img/128/paymentsettings.png differ
diff --git a/plugin/buycourses/resources/img/128/settings.png b/plugin/buycourses/resources/img/128/settings.png
new file mode 100644
index 000000000..ae508a49a
Binary files /dev/null and b/plugin/buycourses/resources/img/128/settings.png differ
diff --git a/plugin/buycourses/resources/img/128/subscriptionssettings.png b/plugin/buycourses/resources/img/128/subscriptionssettings.png
new file mode 100644
index 000000000..9c5050713
Binary files /dev/null and b/plugin/buycourses/resources/img/128/subscriptionssettings.png differ
diff --git a/plugin/buycourses/resources/img/32/backlogs.png b/plugin/buycourses/resources/img/32/backlogs.png
new file mode 100644
index 000000000..7054fc702
Binary files /dev/null and b/plugin/buycourses/resources/img/32/backlogs.png differ
diff --git a/plugin/buycourses/resources/img/32/buycourses.png b/plugin/buycourses/resources/img/32/buycourses.png
new file mode 100644
index 000000000..eac4dfa95
Binary files /dev/null and b/plugin/buycourses/resources/img/32/buycourses.png differ
diff --git a/plugin/buycourses/resources/img/32/buysubscription.png b/plugin/buycourses/resources/img/32/buysubscription.png
new file mode 100644
index 000000000..359169ef6
Binary files /dev/null and b/plugin/buycourses/resources/img/32/buysubscription.png differ
diff --git a/plugin/buycourses/resources/img/32/buysubscriptions.png b/plugin/buycourses/resources/img/32/buysubscriptions.png
new file mode 100644
index 000000000..359169ef6
Binary files /dev/null and b/plugin/buycourses/resources/img/32/buysubscriptions.png differ
diff --git a/plugin/buycourses/resources/img/32/discount.png b/plugin/buycourses/resources/img/32/discount.png
new file mode 100644
index 000000000..ec27b0093
Binary files /dev/null and b/plugin/buycourses/resources/img/32/discount.png differ
diff --git a/plugin/buycourses/resources/img/32/paymentsettings.png b/plugin/buycourses/resources/img/32/paymentsettings.png
new file mode 100644
index 000000000..7f0e022e0
Binary files /dev/null and b/plugin/buycourses/resources/img/32/paymentsettings.png differ
diff --git a/plugin/buycourses/resources/img/32/settings.png b/plugin/buycourses/resources/img/32/settings.png
new file mode 100644
index 000000000..00ded5c27
Binary files /dev/null and b/plugin/buycourses/resources/img/32/settings.png differ
diff --git a/plugin/buycourses/resources/img/32/subscriptionsettings.png b/plugin/buycourses/resources/img/32/subscriptionsettings.png
new file mode 100644
index 000000000..21f37ff01
Binary files /dev/null and b/plugin/buycourses/resources/img/32/subscriptionsettings.png differ
diff --git a/plugin/buycourses/resources/img/32/subscriptionssettings.png b/plugin/buycourses/resources/img/32/subscriptionssettings.png
new file mode 100644
index 000000000..21f37ff01
Binary files /dev/null and b/plugin/buycourses/resources/img/32/subscriptionssettings.png differ
diff --git a/plugin/buycourses/resources/img/42/backlogs.png b/plugin/buycourses/resources/img/42/backlogs.png
new file mode 100644
index 000000000..c7e3cdeeb
Binary files /dev/null and b/plugin/buycourses/resources/img/42/backlogs.png differ
diff --git a/plugin/buycourses/resources/img/42/buycourses.png b/plugin/buycourses/resources/img/42/buycourses.png
new file mode 100644
index 000000000..80cfb5e07
Binary files /dev/null and b/plugin/buycourses/resources/img/42/buycourses.png differ
diff --git a/plugin/buycourses/resources/img/42/buysubscriptions.png b/plugin/buycourses/resources/img/42/buysubscriptions.png
new file mode 100644
index 000000000..359169ef6
Binary files /dev/null and b/plugin/buycourses/resources/img/42/buysubscriptions.png differ
diff --git a/plugin/buycourses/resources/img/42/discount.png b/plugin/buycourses/resources/img/42/discount.png
new file mode 100644
index 000000000..9846e3eff
Binary files /dev/null and b/plugin/buycourses/resources/img/42/discount.png differ
diff --git a/plugin/buycourses/resources/img/42/paymentsettings.png b/plugin/buycourses/resources/img/42/paymentsettings.png
new file mode 100644
index 000000000..1048f830a
Binary files /dev/null and b/plugin/buycourses/resources/img/42/paymentsettings.png differ
diff --git a/plugin/buycourses/resources/img/42/settings.png b/plugin/buycourses/resources/img/42/settings.png
new file mode 100644
index 000000000..975dc44df
Binary files /dev/null and b/plugin/buycourses/resources/img/42/settings.png differ
diff --git a/plugin/buycourses/resources/img/42/subscriptionssettings.png b/plugin/buycourses/resources/img/42/subscriptionssettings.png
new file mode 100644
index 000000000..21f37ff01
Binary files /dev/null and b/plugin/buycourses/resources/img/42/subscriptionssettings.png differ
diff --git a/plugin/buycourses/resources/img/64/backlogs.png b/plugin/buycourses/resources/img/64/backlogs.png
new file mode 100644
index 000000000..f0c83f66c
Binary files /dev/null and b/plugin/buycourses/resources/img/64/backlogs.png differ
diff --git a/plugin/buycourses/resources/img/64/buycourses.png b/plugin/buycourses/resources/img/64/buycourses.png
new file mode 100644
index 000000000..2f42065bd
Binary files /dev/null and b/plugin/buycourses/resources/img/64/buycourses.png differ
diff --git a/plugin/buycourses/resources/img/64/buysubscriptions.png b/plugin/buycourses/resources/img/64/buysubscriptions.png
new file mode 100644
index 000000000..be1bed82d
Binary files /dev/null and b/plugin/buycourses/resources/img/64/buysubscriptions.png differ
diff --git a/plugin/buycourses/resources/img/64/discount.png b/plugin/buycourses/resources/img/64/discount.png
new file mode 100644
index 000000000..80de02b68
Binary files /dev/null and b/plugin/buycourses/resources/img/64/discount.png differ
diff --git a/plugin/buycourses/resources/img/64/paymentsettings.png b/plugin/buycourses/resources/img/64/paymentsettings.png
new file mode 100644
index 000000000..cbb1bcdd0
Binary files /dev/null and b/plugin/buycourses/resources/img/64/paymentsettings.png differ
diff --git a/plugin/buycourses/resources/img/64/settings.png b/plugin/buycourses/resources/img/64/settings.png
new file mode 100644
index 000000000..6f2afb97d
Binary files /dev/null and b/plugin/buycourses/resources/img/64/settings.png differ
diff --git a/plugin/buycourses/resources/img/64/subscriptionssettings.png b/plugin/buycourses/resources/img/64/subscriptionssettings.png
new file mode 100644
index 000000000..2e2d327e7
Binary files /dev/null and b/plugin/buycourses/resources/img/64/subscriptionssettings.png differ
diff --git a/plugin/buycourses/resources/js/commissions.js b/plugin/buycourses/resources/js/commissions.js
new file mode 100644
index 000000000..757a6c1ee
--- /dev/null
+++ b/plugin/buycourses/resources/js/commissions.js
@@ -0,0 +1,145 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+function showSliders(maxPercentage, type, defaultValues) {
+
+ defaultValues = defaultValues || "0";
+
+ var sliderCounter = 1;
+ var percentage = 0;
+ var minPercentage = 0;
+
+ $("#selectBox option:selected").each(function() {
+
+ var count = $("#selectBox option:selected").length;
+
+ percentage = maxPercentage / count;
+ percentage = parseInt(percentage);
+
+ verifyMaxPercentage = percentage * count;
+ if (verifyMaxPercentage !== maxPercentage && sliderCounter === 1) {
+ percentage = percentage + (maxPercentage - verifyMaxPercentage);
+ }
+
+ beneficiaryId = $(this).val();
+ beneficiaryName = $(this).text();
+
+ var verify;
+
+ var slidersValue = defaultValues.toString().split(',');
+
+ if (type === 'default') {
+
+ percentage = slidersValue[sliderCounter - 1];
+ percentage = parseInt(percentage);
+ $("#panelSliders").append("" + beneficiaryName + " - [ " + percentage + " % ] ");
+
+ } else if (type === 'renew') {
+
+ $("#panelSliders").append("" + beneficiaryName + " - [ " + percentage + " % ] ");
+
+ }
+
+ verifyPaypalAccountByBeneficiary(beneficiaryId);
+
+ sliderCounter++;
+ stepSlide = count - 1;
+
+ if (stepSlide === 0) {
+ (type === 'default') ? minPercentage = 0 : minPercentage = 100;
+ (type === 'default') ? stepSlide = 100 : stepSlide = 1;
+ }
+
+
+ $( "#panelSliders .panelSliders" ).slider({
+ value: percentage,
+ min: minPercentage,
+ max: maxPercentage,
+ step: stepSlide,
+ animate: true,
+ slide: function( event, ui ) {
+
+ $(this).prev(".value").text(ui.value);
+
+ var total = 0;
+ var sliders = $( "#panelSliders .panelSliders" );
+
+ sliders.not(this).each(function() {
+ value = $(this).slider("option", "value");
+ total += value;
+ });
+
+ total += ui.value;
+ var delta = 100 - total;
+
+ sliders.not(this).each(function() {
+
+ var t = $(this);
+ value = t.slider("option", "value");
+
+ var newValue = value + (delta/stepSlide);
+
+ if (newValue < 0 || ui.value == 100)
+ newValue = 0;
+ if (newValue > 100)
+ newValue = 100;
+
+ t.prev('.value').text(newValue);
+ t.slider('value', newValue);
+
+ });
+
+ $("[name=\'commissions\']").val(getSlidersValues());
+ }
+ });
+ });
+
+ $("[name=\'commissions\']").val(getSlidersValues());
+ showCorrectSliderHandler();
+};
+
+function getSlidersValues() {
+ var commissions = "";
+
+ $( "#panelSliders .panelSliders" ).each(function() {
+ commissions += $(this).prev(".value").text() + ',';
+ });
+
+ commissions = commissions.substring(0, commissions.length-1);
+
+ return commissions;
+}
+
+function showCorrectSliderHandler() {
+ var correctHandler = [];
+
+ $("#panelSliders > span").each(function () {
+ if ($(this).hasClass('value')) {
+ correctHandler.push($(this).text());
+ }
+ });
+
+ var counter = 0;
+
+ $("#panelSliders .panelSliders > span").each(function () {
+ if ($(this).hasClass('ui-slider-handle')) {
+ $(this).css('left', correctHandler[counter]+'%');
+ counter++;
+ }
+ });
+}
+
+function verifyPaypalAccountByBeneficiary(userId) {
+
+ return $.ajax({
+ data: 'id='+userId,
+ url: 'buycourses.ajax.php?a=verifyPaypal',
+ type: 'POST',
+ success: function(response) {
+ $("#"+userId).append(' '+response);
+ }
+ });
+}
diff --git a/plugin/buycourses/resources/js/modals.js b/plugin/buycourses/resources/js/modals.js
new file mode 100644
index 000000000..90e677d1d
--- /dev/null
+++ b/plugin/buycourses/resources/js/modals.js
@@ -0,0 +1 @@
+!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["jquery"],b):"object"==typeof exports?module.exports=b(require("jquery")):a.bootbox=b(a.jQuery)}(this,function a(b,c){"use strict";function d(a){var b=q[o.locale];return b?b[a]:q.en[a]}function e(a,c,d){a.stopPropagation(),a.preventDefault();var e=b.isFunction(d)&&d.call(c,a)===!1;e||c.modal("hide")}function f(a){var b,c=0;for(b in a)c++;return c}function g(a,c){var d=0;b.each(a,function(a,b){c(a,b,d++)})}function h(a){var c,d;if("object"!=typeof a)throw new Error("Please supply an object of options");if(!a.message)throw new Error("Please specify a message");return a=b.extend({},o,a),a.buttons||(a.buttons={}),c=a.buttons,d=f(c),g(c,function(a,e,f){if(b.isFunction(e)&&(e=c[a]={callback:e}),"object"!==b.type(e))throw new Error("button with key "+a+" must be an object");e.label||(e.label=a),e.className||(e.className=2>=d&&f===d-1?"btn-primary":"btn-default")}),a}function i(a,b){var c=a.length,d={};if(1>c||c>2)throw new Error("Invalid argument length");return 2===c||"string"==typeof a[0]?(d[b[0]]=a[0],d[b[1]]=a[1]):d=a[0],d}function j(a,c,d){return b.extend(!0,{},a,i(c,d))}function k(a,b,c,d){var e={className:"bootbox-"+a,buttons:l.apply(null,b)};return m(j(e,d,c),b)}function l(){for(var a={},b=0,c=arguments.length;c>b;b++){var e=arguments[b],f=e.toLowerCase(),g=e.toUpperCase();a[f]={label:d(g)}}return a}function m(a,b){var d={};return g(b,function(a,b){d[b]=!0}),g(a.buttons,function(a){if(d[a]===c)throw new Error("button key "+a+" is not allowed (options are "+b.join("\n")+")")}),a}var n={dialog:"
",header:"
",footer:"",closeButton:"",form:"",inputs:{text:"",textarea:"",email:"",select:"",checkbox:"",date:"",time:"",number:"",password:""}},o={locale:"en",backdrop:"static",animate:!0,className:null,closeButton:!0,show:!0,container:"body"},p={};p.alert=function(){var a;if(a=k("alert",["ok"],["message","callback"],arguments),a.callback&&!b.isFunction(a.callback))throw new Error("alert requires callback property to be a function when provided");return a.buttons.ok.callback=a.onEscape=function(){return b.isFunction(a.callback)?a.callback.call(this):!0},p.dialog(a)},p.confirm=function(){var a;if(a=k("confirm",["cancel","confirm"],["message","callback"],arguments),a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,!1)},a.buttons.confirm.callback=function(){return a.callback.call(this,!0)},!b.isFunction(a.callback))throw new Error("confirm requires a callback");return p.dialog(a)},p.prompt=function(){var a,d,e,f,h,i,k;if(f=b(n.form),d={className:"bootbox-prompt",buttons:l("cancel","confirm"),value:"",inputType:"text"},a=m(j(d,arguments,["title","callback"]),["cancel","confirm"]),i=a.show===c?!0:a.show,a.message=f,a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,null)},a.buttons.confirm.callback=function(){var c;switch(a.inputType){case"text":case"textarea":case"email":case"select":case"date":case"time":case"number":case"password":c=h.val();break;case"checkbox":var d=h.find("input:checked");c=[],g(d,function(a,d){c.push(b(d).val())})}return a.callback.call(this,c)},a.show=!1,!a.title)throw new Error("prompt requires a title");if(!b.isFunction(a.callback))throw new Error("prompt requires a callback");if(!n.inputs[a.inputType])throw new Error("invalid prompt type");switch(h=b(n.inputs[a.inputType]),a.inputType){case"text":case"textarea":case"email":case"date":case"time":case"number":case"password":h.val(a.value);break;case"select":var o={};if(k=a.inputOptions||[],!b.isArray(k))throw new Error("Please pass an array of input options");if(!k.length)throw new Error("prompt with select requires options");g(k,function(a,d){var e=h;if(d.value===c||d.text===c)throw new Error("given options in wrong format");d.group&&(o[d.group]||(o[d.group]=b("").attr("label",d.group)),e=o[d.group]),e.append("")}),g(o,function(a,b){h.append(b)}),h.val(a.value);break;case"checkbox":var q=b.isArray(a.value)?a.value:[a.value];if(k=a.inputOptions||[],!k.length)throw new Error("prompt with checkbox requires options");if(!k[0].value||!k[0].text)throw new Error("given options in wrong format");h=b(""),g(k,function(c,d){var e=b(n.inputs[a.inputType]);e.find("input").attr("value",d.value),e.find("label").append(d.text),g(q,function(a,b){b===d.value&&e.find("input").prop("checked",!0)}),h.append(e)})}return a.placeholder&&h.attr("placeholder",a.placeholder),a.pattern&&h.attr("pattern",a.pattern),a.maxlength&&h.attr("maxlength",a.maxlength),f.append(h),f.on("submit",function(a){a.preventDefault(),a.stopPropagation(),e.find(".btn-primary").click()}),e=p.dialog(a),e.off("shown.bs.modal"),e.on("shown.bs.modal",function(){h.focus()}),i===!0&&e.modal("show"),e},p.dialog=function(a){a=h(a);var d=b(n.dialog),f=d.find(".modal-dialog"),i=d.find(".modal-body"),j=a.buttons,k="",l={onEscape:a.onEscape};if(b.fn.modal===c)throw new Error("$.fn.modal is not defined; please double check you have included the Bootstrap JavaScript library. See http://getbootstrap.com/javascript/ for more details.");if(g(j,function(a,b){k+="",l[a]=b.callback}),i.find(".bootbox-body").html(a.message),a.animate===!0&&d.addClass("fade"),a.className&&d.addClass(a.className),"large"===a.size?f.addClass("modal-lg"):"small"===a.size&&f.addClass("modal-sm"),a.title&&i.before(n.header),a.closeButton){var m=b(n.closeButton);a.title?d.find(".modal-header").prepend(m):m.css("margin-top","-10px").prependTo(i)}return a.title&&d.find(".modal-title").html(a.title),k.length&&(i.after(n.footer),d.find(".modal-footer").html(k)),d.on("hidden.bs.modal",function(a){a.target===this&&d.remove()}),d.on("shown.bs.modal",function(){d.find(".btn-primary:first").focus()}),"static"!==a.backdrop&&d.on("click.dismiss.bs.modal",function(a){d.children(".modal-backdrop").length&&(a.currentTarget=d.children(".modal-backdrop").get(0)),a.target===a.currentTarget&&d.trigger("escape.close.bb")}),d.on("escape.close.bb",function(a){l.onEscape&&e(a,d,l.onEscape)}),d.on("click",".modal-footer button",function(a){var c=b(this).data("bb-handler");e(a,d,l[c])}),d.on("click",".bootbox-close-button",function(a){e(a,d,l.onEscape)}),d.on("keyup",function(a){27===a.which&&d.trigger("escape.close.bb")}),b(a.container).append(d),d.modal({backdrop:a.backdrop?"static":!1,keyboard:!1,show:!1}),a.show&&d.modal("show"),d},p.setDefaults=function(){var a={};2===arguments.length?a[arguments[0]]=arguments[1]:a=arguments[0],b.extend(o,a)},p.hideAll=function(){return b(".bootbox").modal("hide"),p};var q={bg_BG:{OK:"Ок",CANCEL:"Отказ",CONFIRM:"Потвърждавам"},br:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Sim"},cs:{OK:"OK",CANCEL:"Zrušit",CONFIRM:"Potvrdit"},da:{OK:"OK",CANCEL:"Annuller",CONFIRM:"Accepter"},de:{OK:"OK",CANCEL:"Abbrechen",CONFIRM:"Akzeptieren"},el:{OK:"Εντάξει",CANCEL:"Ακύρωση",CONFIRM:"Επιβεβαίωση"},en:{OK:"OK",CANCEL:"Cancel",CONFIRM:"OK"},es:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Aceptar"},et:{OK:"OK",CANCEL:"Katkesta",CONFIRM:"OK"},fa:{OK:"قبول",CANCEL:"لغو",CONFIRM:"تایید"},fi:{OK:"OK",CANCEL:"Peruuta",CONFIRM:"OK"},fr:{OK:"OK",CANCEL:"Annuler",CONFIRM:"D'accord"},he:{OK:"אישור",CANCEL:"ביטול",CONFIRM:"אישור"},hu:{OK:"OK",CANCEL:"Mégsem",CONFIRM:"Megerősít"},hr:{OK:"OK",CANCEL:"Odustani",CONFIRM:"Potvrdi"},id:{OK:"OK",CANCEL:"Batal",CONFIRM:"OK"},it:{OK:"OK",CANCEL:"Annulla",CONFIRM:"Conferma"},ja:{OK:"OK",CANCEL:"キャンセル",CONFIRM:"確認"},lt:{OK:"Gerai",CANCEL:"Atšaukti",CONFIRM:"Patvirtinti"},lv:{OK:"Labi",CANCEL:"Atcelt",CONFIRM:"Apstiprināt"},nl:{OK:"OK",CANCEL:"Annuleren",CONFIRM:"Accepteren"},no:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},pl:{OK:"OK",CANCEL:"Anuluj",CONFIRM:"Potwierdź"},pt:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Confirmar"},ru:{OK:"OK",CANCEL:"Отмена",CONFIRM:"Применить"},sq:{OK:"OK",CANCEL:"Anulo",CONFIRM:"Prano"},sv:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},th:{OK:"ตกลง",CANCEL:"ยกเลิก",CONFIRM:"ยืนยัน"},tr:{OK:"Tamam",CANCEL:"İptal",CONFIRM:"Onayla"},zh_CN:{OK:"OK",CANCEL:"取消",CONFIRM:"确认"},zh_TW:{OK:"OK",CANCEL:"取消",CONFIRM:"確認"}};return p.addLocale=function(a,c){return b.each(["OK","CANCEL","CONFIRM"],function(a,b){if(!c[b])throw new Error("Please supply a translation for '"+b+"'")}),q[a]={OK:c.OK,CANCEL:c.CANCEL,CONFIRM:c.CONFIRM},p},p.removeLocale=function(a){return delete q[a],p},p.setLocale=function(a){return p.setDefaults("locale",a)},p.init=function(c){return a(c||b)},p});
\ No newline at end of file
diff --git a/plugin/buycourses/src/buy_course_plugin.class.php b/plugin/buycourses/src/buy_course_plugin.class.php
new file mode 100644
index 000000000..fca64a42d
--- /dev/null
+++ b/plugin/buycourses/src/buy_course_plugin.class.php
@@ -0,0 +1,6146 @@
+
+ * @author Imanol Losada
+ * @author Alex Aragón
+ * @author Angel Fernando Quiroz Campos
+ * @author José Loguercio Silva
+ * @author Julio Montoya
+ */
+class BuyCoursesPlugin extends Plugin
+{
+ public const TABLE_PAYPAL = 'plugin_buycourses_paypal_account';
+ public const TABLE_CURRENCY = 'plugin_buycourses_currency';
+ public const TABLE_ITEM = 'plugin_buycourses_item';
+ public const TABLE_ITEM_BENEFICIARY = 'plugin_buycourses_item_rel_beneficiary';
+ public const TABLE_SALE = 'plugin_buycourses_sale';
+ public const TABLE_TRANSFER = 'plugin_buycourses_transfer';
+ public const TABLE_COMMISSION = 'plugin_buycourses_commission';
+ public const TABLE_PAYPAL_PAYOUTS = 'plugin_buycourses_paypal_payouts';
+ public const TABLE_SERVICES = 'plugin_buycourses_services';
+ public const TABLE_SERVICES_SALE = 'plugin_buycourses_service_sale';
+ public const TABLE_CULQI = 'plugin_buycourses_culqi';
+ public const TABLE_GLOBAL_CONFIG = 'plugin_buycourses_global_config';
+ public const TABLE_INVOICE = 'plugin_buycourses_invoices';
+ public const TABLE_TPV_REDSYS = 'plugin_buycourses_tpvredsys_account';
+ public const TABLE_COUPON = 'plugin_buycourses_coupon';
+ public const TABLE_COUPON_ITEM = 'plugin_buycourses_coupon_rel_item';
+ public const TABLE_COUPON_SERVICE = 'plugin_buycourses_coupon_rel_service';
+ public const TABLE_SUBSCRIPTION = 'plugin_buycourses_subscription';
+ public const TABLE_SUBSCRIPTION_SALE = 'plugin_buycourses_subscription_rel_sale';
+ public const TABLE_SUBSCRIPTION_PERIOD = 'plugin_buycourses_subscription_period';
+ public const TABLE_COUPON_SALE = 'plugin_buycourses_coupon_rel_sale';
+ public const TABLE_COUPON_SERVICE_SALE = 'plugin_buycourses_coupon_rel_service_sale';
+ public const TABLE_COUPON_SUBSCRIPTION_SALE = 'plugin_buycourses_coupon_rel_subscription_sale';
+ public const TABLE_STRIPE = 'plugin_buycourses_stripe_account';
+ public const TABLE_TPV_CECABANK = 'plugin_buycourses_cecabank_account';
+ public const PRODUCT_TYPE_COURSE = 1;
+ public const PRODUCT_TYPE_SESSION = 2;
+ public const PRODUCT_TYPE_SERVICE = 3;
+ public const PAYMENT_TYPE_PAYPAL = 1;
+ public const PAYMENT_TYPE_TRANSFER = 2;
+ public const PAYMENT_TYPE_CULQI = 3;
+ public const PAYMENT_TYPE_TPV_REDSYS = 4;
+ public const PAYMENT_TYPE_STRIPE = 5;
+ public const PAYMENT_TYPE_TPV_CECABANK = 6;
+ public const PAYOUT_STATUS_CANCELED = 2;
+ public const PAYOUT_STATUS_PENDING = 0;
+ public const PAYOUT_STATUS_COMPLETED = 1;
+ public const SALE_STATUS_CANCELED = -1;
+ public const SALE_STATUS_PENDING = 0;
+ public const SALE_STATUS_COMPLETED = 1;
+ public const SERVICE_STATUS_PENDING = 0;
+ public const SERVICE_STATUS_COMPLETED = 1;
+ public const SERVICE_STATUS_CANCELLED = -1;
+ public const SERVICE_TYPE_USER = 1;
+ public const SERVICE_TYPE_COURSE = 2;
+ public const SERVICE_TYPE_SESSION = 3;
+ public const SERVICE_TYPE_LP_FINAL_ITEM = 4;
+ public const CULQI_INTEGRATION_TYPE = 'INTEG';
+ public const CULQI_PRODUCTION_TYPE = 'PRODUC';
+ public const TAX_APPLIES_TO_ALL = 1;
+ public const TAX_APPLIES_TO_ONLY_COURSE = 2;
+ public const TAX_APPLIES_TO_ONLY_SESSION = 3;
+ public const TAX_APPLIES_TO_ONLY_SERVICES = 4;
+ public const PAGINATION_PAGE_SIZE = 6;
+ public const COUPON_DISCOUNT_TYPE_PERCENTAGE = 1;
+ public const COUPON_DISCOUNT_TYPE_AMOUNT = 2;
+ public const COUPON_STATUS_ACTIVE = 1;
+ public const COUPON_STATUS_DISABLE = 0;
+
+ public $isAdminPlugin = false;
+
+ /**
+ * BuyCoursesPlugin constructor.
+ */
+ public function __construct()
+ {
+ parent::__construct(
+ '7.1',
+ "
+ Jose Angel Ruiz - NoSoloRed (original author)
+ Francis Gonzales and Yannick Warnier - BeezNest (integration)
+ Alex Aragón - BeezNest (Design icons and css styles)
+ Imanol Losada - BeezNest (introduction of sessions purchase)
+ Angel Fernando Quiroz Campos - BeezNest (cleanup and new reports)
+ José Loguercio Silva - BeezNest (Payouts and buy Services)
+ Julio Montoya
+ ",
+ [
+ 'show_main_menu_tab' => 'boolean',
+ 'public_main_menu_tab' => 'boolean',
+ 'include_sessions' => 'boolean',
+ 'include_services' => 'boolean',
+ 'paypal_enable' => 'boolean',
+ 'transfer_enable' => 'boolean',
+ 'culqi_enable' => 'boolean',
+ 'commissions_enable' => 'boolean',
+ 'unregistered_users_enable' => 'boolean',
+ 'hide_free_text' => 'boolean',
+ 'hide_shopping_cart_from_course_catalogue' => 'boolean',
+ 'invoicing_enable' => 'boolean',
+ 'tax_enable' => 'boolean',
+ 'use_currency_symbol' => 'boolean',
+ 'tpv_redsys_enable' => 'boolean',
+ 'stripe_enable' => 'boolean',
+ 'cecabank_enable' => 'boolean',
+ ]
+ );
+ }
+
+ /**
+ * @return BuyCoursesPlugin
+ */
+ public static function create()
+ {
+ static $result = null;
+
+ return $result ? $result : $result = new self();
+ }
+
+ /**
+ * Check if plugin is enabled.
+ *
+ * @param bool $checkEnabled Check if, additionnally to being installed, the plugin is enabled
+ */
+ public function isEnabled(bool $checkEnabled = false): bool
+ {
+ return $this->get('paypal_enable') || $this->get('transfer_enable') || $this->get('culqi_enable') || $this->get('stripe_enable') || $this->get('cecabank_enable');
+ }
+
+ /**
+ * This method creates the tables required to this plugin.
+ */
+ public function install()
+ {
+ $tablesToBeCompared = [
+ self::TABLE_PAYPAL,
+ self::TABLE_TRANSFER,
+ self::TABLE_CULQI,
+ self::TABLE_ITEM_BENEFICIARY,
+ self::TABLE_ITEM,
+ self::TABLE_SALE,
+ self::TABLE_CURRENCY,
+ self::TABLE_COMMISSION,
+ self::TABLE_PAYPAL_PAYOUTS,
+ self::TABLE_SERVICES,
+ self::TABLE_SERVICES_SALE,
+ self::TABLE_GLOBAL_CONFIG,
+ self::TABLE_INVOICE,
+ self::TABLE_TPV_REDSYS,
+ self::TABLE_COUPON,
+ self::TABLE_COUPON_ITEM,
+ self::TABLE_COUPON_SERVICE,
+ self::TABLE_SUBSCRIPTION,
+ self::TABLE_SUBSCRIPTION_SALE,
+ self::TABLE_SUBSCRIPTION_PERIOD,
+ self::TABLE_COUPON_SALE,
+ self::TABLE_COUPON_SERVICE_SALE,
+ self::TABLE_COUPON_SUBSCRIPTION_SALE,
+ self::TABLE_STRIPE,
+ self::TABLE_TPV_CECABANK,
+ ];
+ $em = Database::getManager();
+ $cn = $em->getConnection();
+ $sm = $cn->getSchemaManager();
+ $tables = $sm->tablesExist($tablesToBeCompared);
+
+ if ($tables) {
+ return false;
+ }
+
+ require_once api_get_path(SYS_PLUGIN_PATH).'buycourses/database.php';
+ }
+
+ /**
+ * This method drops the plugin tables.
+ */
+ public function uninstall()
+ {
+ $tablesToBeDeleted = [
+ self::TABLE_PAYPAL,
+ self::TABLE_TRANSFER,
+ self::TABLE_CULQI,
+ self::TABLE_ITEM_BENEFICIARY,
+ self::TABLE_ITEM,
+ self::TABLE_SALE,
+ self::TABLE_CURRENCY,
+ self::TABLE_COMMISSION,
+ self::TABLE_PAYPAL_PAYOUTS,
+ self::TABLE_SERVICES_SALE,
+ self::TABLE_SERVICES,
+ self::TABLE_GLOBAL_CONFIG,
+ self::TABLE_INVOICE,
+ self::TABLE_TPV_REDSYS,
+ self::TABLE_COUPON,
+ self::TABLE_COUPON_ITEM,
+ self::TABLE_COUPON_SERVICE,
+ self::TABLE_SUBSCRIPTION,
+ self::TABLE_SUBSCRIPTION_SALE,
+ self::TABLE_SUBSCRIPTION_PERIOD,
+ self::TABLE_COUPON_SALE,
+ self::TABLE_COUPON_SERVICE_SALE,
+ self::TABLE_COUPON_SUBSCRIPTION_SALE,
+ self::TABLE_STRIPE,
+ ];
+
+ foreach ($tablesToBeDeleted as $tableToBeDeleted) {
+ $table = Database::get_main_table($tableToBeDeleted);
+ $sql = "DROP TABLE IF EXISTS $table";
+ Database::query($sql);
+ }
+ $this->manageTab(false);
+ }
+
+ public function update()
+ {
+ $table = self::TABLE_GLOBAL_CONFIG;
+ $sql = "SHOW COLUMNS FROM $table WHERE Field = 'global_tax_perc'";
+ $res = Database::query($sql);
+
+ if (Database::num_rows($res) === 0) {
+ $sql = "ALTER TABLE $table ADD (
+ sale_email varchar(255) NOT NULL,
+ global_tax_perc int unsigned NOT NULL,
+ tax_applies_to int unsigned NOT NULL,
+ tax_name varchar(255) NOT NULL,
+ seller_name varchar(255) NOT NULL,
+ seller_id varchar(255) NOT NULL,
+ seller_address varchar(255) NOT NULL,
+ seller_email varchar(255) NOT NULL,
+ next_number_invoice int unsigned NOT NULL,
+ invoice_series varchar(255) NOT NULL
+ )";
+ $res = Database::query($sql);
+ if (!$res) {
+ echo Display::return_message($this->get_lang('ErrorUpdateFieldDB'), 'warning');
+ }
+ }
+
+ $sql = "SHOW COLUMNS FROM $table WHERE Field = 'info_email_extra'";
+ $res = Database::query($sql);
+
+ if (Database::num_rows($res) === 0) {
+ $sql = "ALTER TABLE $table ADD (info_email_extra TEXT NOT NULL)";
+ $res = Database::query($sql);
+ if (!$res) {
+ echo Display::return_message($this->get_lang('ErrorUpdateFieldDB'), 'warning');
+ }
+ }
+
+ $table = self::TABLE_ITEM;
+ $sql = "SHOW COLUMNS FROM $table WHERE Field = 'tax_perc'";
+ $res = Database::query($sql);
+
+ if (Database::num_rows($res) === 0) {
+ $sql = "ALTER TABLE $table ADD tax_perc int unsigned NULL";
+ $res = Database::query($sql);
+ if (!$res) {
+ echo Display::return_message($this->get_lang('ErrorUpdateFieldDB'), 'warning');
+ }
+ }
+
+ $table = self::TABLE_SERVICES;
+ $sql = "SHOW COLUMNS FROM $table WHERE Field = 'tax_perc'";
+ $res = Database::query($sql);
+
+ if (Database::num_rows($res) === 0) {
+ $sql = "ALTER TABLE $table ADD tax_perc int unsigned NULL";
+ $res = Database::query($sql);
+ if (!$res) {
+ echo Display::return_message($this->get_lang('ErrorUpdateFieldDB'), 'warning');
+ }
+ }
+
+ $table = self::TABLE_SALE;
+ $sql = "SHOW COLUMNS FROM $table WHERE Field = 'tax_perc'";
+ $res = Database::query($sql);
+
+ if (Database::num_rows($res) === 0) {
+ $sql = "ALTER TABLE $table ADD (
+ price_without_tax decimal(10,2) NULL,
+ tax_perc int unsigned NULL,
+ tax_amount decimal(10,2) NULL,
+ invoice int unsigned NULL
+ )";
+ $res = Database::query($sql);
+ if (!$res) {
+ echo Display::return_message($this->get_lang('ErrorUpdateFieldDB'), 'warning');
+ }
+ }
+
+ $sql = "SHOW COLUMNS FROM $table WHERE Field = 'price_without_discount'";
+ $res = Database::query($sql);
+
+ if (Database::num_rows($res) === 0) {
+ $sql = "ALTER TABLE $table ADD (
+ price_without_discount decimal(10,2) NULL,
+ discount_amount decimal(10,2) NULL
+ )";
+ $res = Database::query($sql);
+ if (!$res) {
+ echo Display::return_message($this->get_lang('ErrorUpdateFieldDB'), 'warning');
+ }
+ }
+
+ $table = self::TABLE_SERVICES_SALE;
+ $sql = "SHOW COLUMNS FROM $table WHERE Field = 'tax_perc'";
+ $res = Database::query($sql);
+
+ if (Database::num_rows($res) === 0) {
+ $sql = "ALTER TABLE $table ADD (
+ price_without_tax decimal(10,2) NULL,
+ tax_perc int unsigned NULL,
+ tax_amount decimal(10,2) NULL,
+ invoice int unsigned NULL
+ )";
+ $res = Database::query($sql);
+ if (!$res) {
+ echo Display::return_message($this->get_lang('ErrorUpdateFieldDB'), 'warning');
+ }
+ }
+
+ $sql = "SHOW COLUMNS FROM $table WHERE Field = 'price_without_discount'";
+ $res = Database::query($sql);
+
+ if (Database::num_rows($res) === 0) {
+ $sql = "ALTER TABLE $table ADD (
+ price_without_discount decimal(10,2) NULL,
+ discount_amount decimal(10,2) NULL
+ )";
+ $res = Database::query($sql);
+ if (!$res) {
+ echo Display::return_message($this->get_lang('ErrorUpdateFieldDB'), 'warning');
+ }
+ }
+
+ $table = self::TABLE_INVOICE;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ sale_id int unsigned NOT NULL,
+ is_service int unsigned NOT NULL,
+ num_invoice int unsigned NOT NULL,
+ year int(4) unsigned NOT NULL,
+ serie varchar(255) NOT NULL,
+ date_invoice datetime NOT NULL,
+ PRIMARY KEY (id)
+ )";
+ Database::query($sql);
+
+ $table = self::TABLE_TPV_REDSYS;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ merchantcode varchar(255) NOT NULL,
+ terminal varchar(255) NOT NULL,
+ currency varchar(255) NOT NULL,
+ kc varchar(255) NOT NULL,
+ url_redsys varchar(255) NOT NULL,
+ url_redsys_sandbox varchar(255) NOT NULL,
+ sandbox int unsigned NULL,
+ PRIMARY KEY (id)
+ )";
+ Database::query($sql);
+
+ $sql = "SELECT * FROM $table";
+ $res = Database::query($sql);
+ if (Database::num_rows($res) == 0) {
+ Database::insert($table, [
+ 'url_redsys' => 'https://sis.redsys.es/sis/realizarPago',
+ 'url_redsys_sandbox' => 'https://sis-t.redsys.es:25443/sis/realizarPago',
+ ]);
+ }
+
+ $table = self::TABLE_COUPON;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ code varchar(255) NOT NULL,
+ discount_type int unsigned NOT NULL,
+ discount_amount decimal(10, 2) NOT NULL,
+ valid_start datetime NOT NULL,
+ valid_end datetime NOT NULL,
+ delivered varchar(255) NOT NULL,
+ active tinyint NOT NULL,
+ PRIMARY KEY (id)
+ )";
+ Database::query($sql);
+
+ $table = self::TABLE_COUPON_ITEM;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ coupon_id int unsigned NOT NULL,
+ product_type int unsigned NOT NULL,
+ product_id int unsigned NOT NULL,
+ PRIMARY KEY (id)
+ )";
+ Database::query($sql);
+
+ $table = self::TABLE_COUPON_SERVICE;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ coupon_id int unsigned NOT NULL,
+ service_id int unsigned NOT NULL,
+ PRIMARY KEY (id)
+ )";
+ Database::query($sql);
+
+ $table = self::TABLE_SUBSCRIPTION;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ product_type int unsigned NOT NULL,
+ product_id int unsigned NOT NULL,
+ duration int unsigned NOT NULL,
+ currency_id int unsigned NOT NULL,
+ price decimal(10, 2) NOT NULL,
+ tax_perc int unsigned,
+ PRIMARY KEY (product_type, product_id, duration)
+ )";
+ Database::query($sql);
+
+ $table = self::TABLE_SUBSCRIPTION_SALE;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ currency_id int unsigned NOT NULL,
+ reference varchar(255) NOT NULL,
+ date datetime NOT NULL,
+ user_id int unsigned NOT NULL,
+ product_type int NOT NULL,
+ product_name varchar(255) NOT NULL,
+ product_id int unsigned NOT NULL,
+ price decimal(10,2) NOT NULL,
+ price_without_tax decimal(10,2) NULL,
+ tax_perc int unsigned NULL,
+ tax_amount decimal(10,2) NULL,
+ status int NOT NULL,
+ payment_type int NOT NULL,
+ invoice int NOT NULL,
+ price_without_discount decimal(10,2),
+ discount_amount decimal(10,2),
+ subscription_end datetime NOT NULL,
+ expired tinyint NULL,
+ PRIMARY KEY (id)
+ )";
+ Database::query($sql);
+
+ $table = self::TABLE_SUBSCRIPTION_PERIOD;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ duration int unsigned NOT NULL,
+ name varchar(50) NOT NULL,
+ PRIMARY KEY (duration)
+ )";
+ Database::query($sql);
+
+ $table = self::TABLE_COUPON_SALE;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ coupon_id int unsigned NOT NULL,
+ sale_id int unsigned NOT NULL,
+ PRIMARY KEY (id)
+ )";
+ Database::query($sql);
+
+ $table = self::TABLE_COUPON_SERVICE_SALE;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ coupon_id int unsigned NOT NULL,
+ service_sale_id int unsigned NOT NULL,
+ PRIMARY KEY (id)
+ )";
+ Database::query($sql);
+
+ $table = self::TABLE_COUPON_SUBSCRIPTION_SALE;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ coupon_id int unsigned NOT NULL,
+ sale_id int unsigned NOT NULL,
+ PRIMARY KEY (id)
+ )";
+ Database::query($sql);
+
+ $table = self::TABLE_STRIPE;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ account_id varchar(255) NOT NULL,
+ secret_key varchar(255) NOT NULL,
+ endpoint_secret varchar(255) NOT NULL,
+ PRIMARY KEY (id)
+ )";
+ Database::query($sql);
+
+ $sql = "SELECT * FROM $table";
+ $res = Database::query($sql);
+ if (Database::num_rows($res) == 0) {
+ Database::insert($table, [
+ 'account_id' => '',
+ 'secret_key' => '',
+ 'endpoint_secret' => '',
+ ]);
+ }
+
+ $table = self::TABLE_TPV_CECABANK;
+ $sql = "CREATE TABLE IF NOT EXISTS $table (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ crypto_key varchar(255) NOT NULL,
+ merchant_id varchar(255) NOT NULL,
+ acquirer_bin varchar(255) NOT NULL,
+ terminal_id varchar(255) NOT NULL,
+ cypher varchar(255) NOT NULL,
+ exponent varchar(255) NOT NULL,
+ supported_payment varchar(255) NOT NULL,
+ url varchar(255) NOT NULL,
+ PRIMARY KEY (id)
+ )";
+ Database::query($sql);
+
+ Display::addFlash(
+ Display::return_message(
+ $this->get_lang('Updated'),
+ 'info',
+ false
+ )
+ );
+
+ $fieldlabel = 'buycourses_company';
+ $fieldtype = '1';
+ $fieldtitle = $this->get_lang('Company');
+ $fielddefault = '';
+ UserManager::create_extra_field($fieldlabel, $fieldtype, $fieldtitle, $fielddefault);
+
+ $fieldlabel = 'buycourses_vat';
+ $fieldtype = '1';
+ $fieldtitle = $this->get_lang('VAT');
+ $fielddefault = '';
+ UserManager::create_extra_field($fieldlabel, $fieldtype, $fieldtitle, $fielddefault);
+
+ $fieldlabel = 'buycourses_address';
+ $fieldtype = '1';
+ $fieldtitle = $this->get_lang('Address');
+ $fielddefault = '';
+ UserManager::create_extra_field($fieldlabel, $fieldtype, $fieldtitle, $fielddefault);
+
+ header('Location: '.api_get_path(WEB_PLUGIN_PATH).'buycourses');
+ exit;
+ }
+
+ /**
+ * This function verify if the plugin is enable and return the price info for a course or session in the new grid
+ * catalog for 1.11.x , the main purpose is to show if a course or session is in sale it shows in the main platform
+ * course catalog so the old buycourses plugin catalog can be deprecated.
+ *
+ * @param int $productId course or session id
+ * @param int $productType course or session type
+ *
+ * @return mixed bool|string html
+ */
+ public function buyCoursesForGridCatalogValidator(int $productId, int $productType)
+ {
+ $return = [];
+ $paypal = $this->get('paypal_enable') === 'true';
+ $transfer = $this->get('transfer_enable') === 'true';
+ $stripe = $this->get('stripe_enable') === 'true';
+ $culqi = $this->get('culqi_enable') === 'true';
+ $cecabank = $this->get('cecabank_enable') === 'true';
+ $tpv_redsys = $this->get('tpv_redsys_enable') === 'true';
+ $hideFree = $this->get('hide_free_text') === 'true';
+
+ if ($paypal || $transfer || $stripe || $culqi || $cecabank || $tpv_redsys) {
+ $item = $this->getItemByProduct($productId, $productType);
+ $html = '
\ No newline at end of file
diff --git a/plugin/buycourses/view/configure_subscription.tpl b/plugin/buycourses/view/configure_subscription.tpl
new file mode 100644
index 000000000..556f63e3d
--- /dev/null
+++ b/plugin/buycourses/view/configure_subscription.tpl
@@ -0,0 +1,45 @@
+
\ No newline at end of file
diff --git a/plugin/buycourses/view/course_panel.tpl b/plugin/buycourses/view/course_panel.tpl
new file mode 100644
index 000000000..e142ae6b6
--- /dev/null
+++ b/plugin/buycourses/view/course_panel.tpl
@@ -0,0 +1,48 @@
+
\ No newline at end of file
diff --git a/plugin/buycourses/view/list_subscription.tpl b/plugin/buycourses/view/list_subscription.tpl
new file mode 100644
index 000000000..e69de29bb
diff --git a/plugin/buycourses/view/message_confirm.tpl b/plugin/buycourses/view/message_confirm.tpl
new file mode 100644
index 000000000..7f10c4a73
--- /dev/null
+++ b/plugin/buycourses/view/message_confirm.tpl
@@ -0,0 +1,14 @@
+
+
+
\ No newline at end of file
diff --git a/plugin/buycourses/view/process.tpl b/plugin/buycourses/view/process.tpl
new file mode 100644
index 000000000..862ce8d19
--- /dev/null
+++ b/plugin/buycourses/view/process.tpl
@@ -0,0 +1,167 @@
+
+
+{{ form }}
diff --git a/plugin/card_game/LICENSE.txt b/plugin/card_game/LICENSE.txt
new file mode 100644
index 000000000..9e419e042
--- /dev/null
+++ b/plugin/card_game/LICENSE.txt
@@ -0,0 +1,674 @@
+GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
\ No newline at end of file
diff --git a/plugin/card_game/README.md b/plugin/card_game/README.md
new file mode 100644
index 000000000..f7fd5ce33
--- /dev/null
+++ b/plugin/card_game/README.md
@@ -0,0 +1,26 @@
+Card reveal game Chamilo plugin
+===============================
+This plugin adds a little game to the interface to encourage users to connect
+every day.
+
+# Installation
+
+To install, enable the plugin, then go to "Regions" in the administration panel
+and add the region "pre_footer" to the plugin. Save.
+The plugin should appear as a little icon in the lower-right side of your user
+picture in the left column of the "My courses" list.
+
+## Migrating from a non-official version
+This plugin was initially designed by _Les Compagnons Bâtisseurs_. If you had
+used it before its review and integration into Chamilo, you will need to
+execute the following changes in your database to update its structure.
+
+##### Database changes
+You need execute these SQL queries in your database if you are upgrading to
+Chamilo 1.11.8 and the card_game plugin was already installed in your previous version.
+
+```sql
+ALTER TABLE plugin_card_game CHANGE COLUMN idUser user_id INT NOT NULL;
+ALTER TABLE plugin_card_game CHANGE COLUMN dateAcces access_date date default NULL;
+
+```
\ No newline at end of file
diff --git a/plugin/card_game/card_game.php b/plugin/card_game/card_game.php
new file mode 100644
index 000000000..d685a9519
--- /dev/null
+++ b/plugin/card_game/card_game.php
@@ -0,0 +1,51 @@
+';
+ $fh .= '';
+
+ $fh .= '
'.$cardGame->get_lang('openDeckCardGame').'
';
+ $fh .= '
'.$cardGame->get_lang('engageDeckCardGame').'
';
+ $fh .= '
'.$cardGame->get_lang('cardgameloose').'
';
+
+ $fh .= '
'.$pluginPath.'ajax.card.php
';
+
+ $userId = api_get_user_id();
+
+ // Look if the user can still try playing today
+ $cardGameSession = Session::read('cardgame');
+ if (!empty($cardGameSession)) {
+ // If we've already loaded the cardgame in this session, then there's
+ // a chance we've already played
+ if (isset($userId)) {
+ $sqlCount = "SELECT access_date FROM plugin_card_game WHERE user_id = $userId";
+ $resultCount = Database::query($sqlCount)->rowCount();
+
+ if ($resultCount === 0) {
+ // If there is no database entry for this user, insert one
+ // without the 'parts' field (because he has not played yet)
+ // @todo change date call
+ $sql = "INSERT INTO plugin_card_game (user_id, access_date, pan)
+ VALUES ($userId, DATE_ADD(CURDATE(), INTERVAL -1 DAY), 1);";
+ $resultInsert = Database::query($sql);
+ Session::write('cardgame', 'havedeck');
+ } else {
+ // If there is already one or more records in the database,
+ // get the number of records for today
+ // @todo change date call
+ $sqlDate = "SELECT access_date
+ FROM plugin_card_game
+ WHERE access_date = CURDATE()
+ AND user_id = $userId";
+ $resultDate = Database::query($sqlDate)->rowCount();
+
+ if ($resultDate == 0) {
+ // If there are records, but none for today, set the
+ // 'cardgame' session variable and add the
+ // #havedeckcardgame element to the page (it will get
+ // picked up by JS later on)
+ Session::write('cardgame', 'havedeck');
+ $fh .= '';
+ } else {
+ // If the user already played today, set the session
+ // 'cardgame' variable to 'done' and do not add
+ // an #havedeckcardgame element
+ Session::write('cardgame', 'done');
+ }
+ }
+ }
+ } else {
+ Session::write('cardgame', 'havedeck');
+ $fh .= '';
+ }
+
+ $parts = '1';
+ $pan = '1';
+
+ if (isset($userId)) {
+ try {
+ $sqlParts = "SELECT parts, pan FROM plugin_card_game WHERE user_id = $userId";
+ $resultParts = Database::query($sqlParts);
+ while ($part = Database::fetch_array($resultParts)) {
+ $parts = $part['parts'];
+ $pan = $part['pan'];
+ }
+ } catch (Exception $e) {
+ echo 'Exception: ', $e->getMessage(), "\n";
+ }
+ }
+
+ echo '
'.$parts.'
';
+ echo '
'.$pan.'
';
+
+ echo $fh;
+}
diff --git a/plugin/card_game/install.php b/plugin/card_game/install.php
new file mode 100644
index 000000000..5683e4b96
--- /dev/null
+++ b/plugin/card_game/install.php
@@ -0,0 +1,9 @@
+install();
diff --git a/plugin/card_game/lang/english.php b/plugin/card_game/lang/english.php
new file mode 100644
index 000000000..0130a7117
--- /dev/null
+++ b/plugin/card_game/lang/english.php
@@ -0,0 +1,5 @@
+Plugins).
+ *
+ * @package chamilo.plugin card_game
+ *
+ * @author Damien Renou
+ */
+/* Plugin config */
+
+// The plugin title.
+$plugin_info['title'] = 'Card game';
+// The comments that go with the plugin.
+$plugin_info['comment'] = "Show a card game";
+// The plugin version.
+$plugin_info['version'] = '1.2';
+// The plugin author.
+$plugin_info['author'] = 'Damien Renou';
diff --git a/plugin/card_game/resources/ajax.card.php b/plugin/card_game/resources/ajax.card.php
new file mode 100644
index 000000000..7441207a8
--- /dev/null
+++ b/plugin/card_game/resources/ajax.card.php
@@ -0,0 +1,50 @@
+
+
\ No newline at end of file
diff --git a/plugin/card_game/resources/css/card-acces.svg b/plugin/card_game/resources/css/card-acces.svg
new file mode 100644
index 000000000..13816983f
--- /dev/null
+++ b/plugin/card_game/resources/css/card-acces.svg
@@ -0,0 +1,92 @@
+
+
\ No newline at end of file
diff --git a/plugin/card_game/resources/css/card-back.png b/plugin/card_game/resources/css/card-back.png
new file mode 100644
index 000000000..9b06762ae
Binary files /dev/null and b/plugin/card_game/resources/css/card-back.png differ
diff --git a/plugin/card_game/resources/css/card1.jpg b/plugin/card_game/resources/css/card1.jpg
new file mode 100644
index 000000000..9287dcc15
Binary files /dev/null and b/plugin/card_game/resources/css/card1.jpg differ
diff --git a/plugin/card_game/resources/css/card1.pdn b/plugin/card_game/resources/css/card1.pdn
new file mode 100644
index 000000000..91d6f0ce9
Binary files /dev/null and b/plugin/card_game/resources/css/card1.pdn differ
diff --git a/plugin/card_game/resources/css/cardgame.css b/plugin/card_game/resources/css/cardgame.css
new file mode 100644
index 000000000..07115396e
--- /dev/null
+++ b/plugin/card_game/resources/css/cardgame.css
@@ -0,0 +1,238 @@
+/** CSS used to show a 5x3 table of cells hiding a big image */
+#home-card {
+ overflow: hidden;
+}
+
+.enjoy-cardgame-active {
+ position: relative;
+ left: 50%;
+ top: 90%;
+ width: 40px;
+ height: 40px;
+ margin-top: -35px;
+ margin-left: 10px;
+ background-image: url('card-acces-active.svg');
+ background-color: transparent;
+ background-repeat: no-repeat;
+ background-position: center fixed;
+ -webkit-background-size: cover;
+ background-size: cover;
+ cursor: pointer;
+ z-index: 1000;
+}
+
+.enjoy-cardgame {
+ position: relative;
+ left: 50%;
+ top: 90%;
+ width: 40px;
+ height: 40px;
+ margin-top: -35px;
+ margin-left: 10px;
+ background-image: url('card-acces.svg');
+ background-color: transparent;
+ background-repeat: no-repeat;
+ background-position: center fixed;
+ -webkit-background-size: cover;
+ background-size: cover;
+ cursor: pointer;
+ z-index: 1000;
+}
+
+.card-one {
+ position: absolute;
+ transform: rotate(160deg);
+ background-image: url('card1.jpg');
+ background-position: center center;
+ left: 130%;
+ top: 50px;
+ width: 220px;
+ height: 330px;
+ margin-left: -110px;
+ background-color: #CECEF6;
+ border: solid 2px green;
+ cursor: pointer;
+ border-radius: 20px;
+ -webkit-transition: all 1s ease-in-out;
+ transition: all 1s ease-in-out;
+ opacity: 0.1;
+}
+
+.card-one-2 {
+ transform: rotate(0deg);
+ left: 50%;
+ opacity: 1;
+}
+
+.cardgame-pack {
+ position: relative;
+ width: 100%;
+ height: 450px;
+ background-image: url('cardpack450.jpg');
+ background-color: transparent;
+ background-position: -500px bottom;
+ -webkit-transition: all 1s ease-in-out;
+ transition: all 1s ease-in-out;
+}
+
+.cardgame-open {
+ background-position: 0px 0px;
+}
+
+.linescissors {
+ position: absolute;
+ left: 60px;
+ top: 135px;
+ width: 220px;
+ height: 6px;
+ transform: rotate(-26deg);
+ border-top: dotted 3px gray;
+}
+
+.messagePackDeck {
+ position: absolute;
+ text-align: center;
+ right: 20px;
+ top: 30px;
+ width: 200px;
+ padding: 20px;
+ font-size: 22px;
+ border-radius: 15px;
+ background-color: #8A0886;
+ color: white;
+}
+
+.viewDeckBottom {
+ position: absolute;
+ left: 3px;
+ bottom: 5px;
+ width: 73px;
+ height: 60px;
+ background-image: url('galerie.png');
+ background-color: transparent;
+ background-repeat: no-repeat;
+ background-position: center fixed;
+ -webkit-background-size: cover;
+ background-size: cover;
+ display: none;
+ cursor: pointer;
+}
+
+.messagePackDeckBottom {
+ position: absolute;
+ text-align: center;
+ left: 85px;
+ bottom: 10px;
+ right: 20px;
+ padding: 14px;
+ font-size: 16px;
+ border-radius: 15px;
+ background-color: #8A0886;
+ color: white;
+}
+
+.scissorsrightinit {
+ transform: rotate(160deg);
+ position: absolute;
+ left: 260px;
+ top: 50px;
+ width: 60px;
+ height: 60px;
+ background-image: url('scissorsright.gif');
+ background-color: transparent;
+ background-repeat: no-repeat;
+ background-position: center fixed;
+ -webkit-background-size: cover;
+ background-size: cover;
+ cursor: pointer;
+ z-index: 1000;
+ -webkit-transition: all 3s ease-in-out;
+ transition: all 3s ease-in-out;
+}
+
+.scissorsrightinitfinal {
+ left: 10px;
+ top: 170px;
+}
+
+.puzzlecardone {
+ position: absolute;
+ display: none;
+ left: 5%;
+ top: 2%;
+ width: 90%;
+ height: 80%;
+ background-color: gray;
+ background-repeat: no-repeat;
+ background-position: center fixed;
+ -webkit-background-size: cover;
+ background-size: cover;
+ border: solid 1px green;
+ overflow: hidden;
+}
+
+.puzzleMin {
+ float: left;
+ position: relative;
+ left: 10px;
+ top: 10px;
+ margin-right: 10px;
+ margin-bottom: 10px;
+ width: 150px;
+ height: 100px;
+ -webkit-transition: all 1s ease-in-out;
+ transition: all 1s ease-in-out;
+}
+
+.puzzleMinOther {
+ float: left;
+ position: relative;
+ left: 10px;
+ top: 10px;
+ margin-right: 10px;
+ margin-bottom: 10px;
+ width: 150px;
+ height: 100px;
+ background-image: url('b-back.jpg');
+ background-repeat: no-repeat;
+ background-position: center center;
+ -webkit-background-size: cover;
+ background-size: cover;
+ display: none;
+ -webkit-transition: all 1s ease-in-out;
+ transition: all 1s ease-in-out;
+}
+
+.pimg01 {
+ background-image: url('img01.jpg');
+}
+
+.pimg02 {
+ background-image: url('img02.jpg');
+}
+
+.pimg03 {
+ background-image: url('img03.jpg');
+}
+
+.pimg04 {
+ background-image: url('img04.jpg');
+}
+
+.puzzlepart1 {
+ position: relative;
+ border-right: dotted 3px green;
+ border-bottom: dotted 3px green;
+ float: left;
+ left: 0%;
+ top: 0%;
+ width: 20%;
+ height: 33.34%;
+ background-image: url('b-back.jpg');
+ background-color: transparent;
+ background-position: center center;
+}
+
+.puzzlepartstar {
+ background-image: url('animstar.png');
+}
\ No newline at end of file
diff --git a/plugin/card_game/resources/css/cardpack.jpg b/plugin/card_game/resources/css/cardpack.jpg
new file mode 100644
index 000000000..04e4a56a4
Binary files /dev/null and b/plugin/card_game/resources/css/cardpack.jpg differ
diff --git a/plugin/card_game/resources/css/cardpack450.jpg b/plugin/card_game/resources/css/cardpack450.jpg
new file mode 100644
index 000000000..389ffbd79
Binary files /dev/null and b/plugin/card_game/resources/css/cardpack450.jpg differ
diff --git a/plugin/card_game/resources/css/galerie.png b/plugin/card_game/resources/css/galerie.png
new file mode 100644
index 000000000..ddd200aad
Binary files /dev/null and b/plugin/card_game/resources/css/galerie.png differ
diff --git a/plugin/card_game/resources/css/img01.jpg b/plugin/card_game/resources/css/img01.jpg
new file mode 100644
index 000000000..bbdf4ca60
Binary files /dev/null and b/plugin/card_game/resources/css/img01.jpg differ
diff --git a/plugin/card_game/resources/css/img02.jpg b/plugin/card_game/resources/css/img02.jpg
new file mode 100644
index 000000000..5f4c15387
Binary files /dev/null and b/plugin/card_game/resources/css/img02.jpg differ
diff --git a/plugin/card_game/resources/css/img03.jpg b/plugin/card_game/resources/css/img03.jpg
new file mode 100644
index 000000000..c2221e448
Binary files /dev/null and b/plugin/card_game/resources/css/img03.jpg differ
diff --git a/plugin/card_game/resources/css/img04.jpg b/plugin/card_game/resources/css/img04.jpg
new file mode 100644
index 000000000..803271d93
Binary files /dev/null and b/plugin/card_game/resources/css/img04.jpg differ
diff --git a/plugin/card_game/resources/css/renderchami.jpg b/plugin/card_game/resources/css/renderchami.jpg
new file mode 100644
index 000000000..54e697fa0
Binary files /dev/null and b/plugin/card_game/resources/css/renderchami.jpg differ
diff --git a/plugin/card_game/resources/css/scissorsright.gif b/plugin/card_game/resources/css/scissorsright.gif
new file mode 100644
index 000000000..e0758d006
Binary files /dev/null and b/plugin/card_game/resources/css/scissorsright.gif differ
diff --git a/plugin/card_game/resources/js/cardgame.js b/plugin/card_game/resources/js/cardgame.js
new file mode 100644
index 000000000..843c6ef4d
--- /dev/null
+++ b/plugin/card_game/resources/js/cardgame.js
@@ -0,0 +1,237 @@
+/* For license terms, see /license.txt */
+/**
+ * Whenever the cardgame.js file is included into the loaded JavaScript,
+ * search for the user picture in the left menu and add a div
+ * "enjoy-cardgame" to it to show a little drawing cards icon to enable
+ * this plugin
+ */
+$(document).ready(function ($) {
+
+ if (!document.getElementById('havedeckcardgame')) {
+ $('.sidebar .panel-body .img-circle').parent().parent().after('')
+ } else {
+ $('.sidebar .panel-body .img-circle').parent().parent().after('')
+ }
+})
+
+/**
+ * This function inserts the element 'home-card' for the card game
+ * before the .page-content element on the page (the right side of the
+ * "my courses" page) only if the element with id 'home-card' does not exist
+ * yet.
+ */
+function installCardView () {
+ if (!document.getElementById('home-card')) {
+ var mess1 = $('#cardgamemessage').html()
+ var h = ''
+ h += '
'
+ var panClass = 'pimg0' + $('#pancardgame').html()
+
+ // Setup the 3x5 table of images
+ h += '
'
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+ h += '
'
+
+ h += ''
+ h += ''
+ h += ''
+ h += ''
+
+ h += ''
+ h += ''
+
+ h += ''
+ h += '
' + mess1 + '
'
+
+ h += ''
+ h += '
'
+ h += ''
+
+ $('.page-content').before(h)
+
+ } else {
+ $('#home-card').fadeToggle()
+ }
+
+}
+
+/**
+ * Animate the scissors
+ */
+function animationOpenCardView () {
+
+ $('#scissors').addClass('scissorsrightinitfinal')
+
+ setTimeout(function () {
+ $('#scissors').fadeOut()
+ $('.linescissors').fadeOut()
+ $('#messagePackDeck').css('display', 'none')
+ $('#messagePackDeck').removeClass('messagePackDeck')
+
+ }, 2600)
+
+ setTimeout(function () {
+ $('.cardgame-pack').addClass('cardgame-open')
+ $('.card-one').addClass('card-one-2')
+ }, 3000)
+
+}
+
+function cardOpenCardView () {
+
+ $('.card-one').removeClass('card-one-2')
+
+ setTimeout(function () {
+ $('.puzzlecardone').fadeIn()
+ randomOpenCardView()
+ }, 700)
+
+}
+
+function randomOpenCardView () {
+
+ var part = Math.floor(Math.random() * (15 - 1 + 1)) + 1
+
+ $('.puzzlecardone').css('display', 'block')
+ $('#scissors').fadeOut()
+ $('.linescissors').fadeOut()
+ $('#messagePackDeck').css('display', 'none')
+ $('#messagePackDeck').removeClass('messagePackDeck')
+
+ var memocardgame = $('#memocardgame').html()
+ var parts = memocardgame
+ var arrayOfStrings = parts.split(';')
+
+ var findDouble = false
+ for (var i = 0; i < arrayOfStrings.length; i++) {
+ var idpart = arrayOfStrings[i]
+ idpart = idpart.replace('!!', '')
+ idpart = idpart.replace('!', '')
+ idpart = idpart.replace('!', '')
+ idpart = idpart.replace(';', '')
+ $('#puzzlepart' + idpart).css('opacity', 0)
+
+ if (part == idpart) {
+ findDouble = true
+ }
+
+ }
+ if (findDouble) {
+ part = Math.floor(Math.random() * (15 - 1 + 1)) + 1
+ }
+ if (memocardgame.indexOf('!' + part + ';') != -1) {
+ part = Math.floor(Math.random() * (15 - 1 + 1)) + 1
+ }
+ if (memocardgame.indexOf('!' + part + ';') != -1) {
+ part = Math.floor(Math.random() * (15 - 1 + 1)) + 1
+ }
+
+ if (memocardgame.indexOf('!' + part + ';') != -1) {
+ var mess2 = $('#cardgameloose').html()
+ $('#messagePackDeck').html(mess2)
+ $('#messagePackDeck').css('display', 'block')
+ $('#messagePackDeck').css('backgroundColor', 'red')
+ $('#messagePackDeck').addClass('messagePackDeckBottom')
+ var lk = $('#linkcardgame').html()
+ $.ajax({ url: lk + '?loose=1' }).done(function () { })
+
+ } else {
+ var lk = $('#linkcardgame').html()
+ $.ajax({ url: lk + '?part=' + part }).done(function () { })
+
+ setTimeout(function () {
+ $('#puzzlepart' + part).addClass('puzzlepartstar')
+ }, 600)
+
+ setTimeout(function () {
+ $('#puzzlepart' + part).css('opacity', 0)
+ }, 1200)
+
+ setTimeout(function () {
+ var mess2 = $('#cardgameengage').html()
+ $('#messagePackDeck').html(mess2)
+ $('#messagePackDeck').css('display', 'block')
+ $('#messagePackDeck').addClass('messagePackDeckBottom')
+ $('#viewDeckBottom').css('display', 'block')
+ }, 1500)
+
+ }
+ $('#plugin-cardgame-icon').removeClass('enjoy-cardgame-active')
+ $('#plugin-cardgame-icon').addClass('enjoy-cardgame')
+}
+
+/**
+ * This function adds the cardgame area block (in invisible state), then
+ * changes the visibility of blocks inside the puzzle area to show them
+ */
+function onlyOpenCardView () {
+
+ installCardView();
+
+ var memocardgame = $('#memocardgame').html()
+
+ $('.puzzlecardone').css('display', 'block')
+ $('#scissors').css('display', 'none')
+ $('.linescissors').css('display', 'none')
+ $('#messagePackDeck').css('display', 'none')
+ $('#messagePackDeck').removeClass('messagePackDeck')
+
+ var lk = $('#linkcardgame').html()
+ var parts = memocardgame
+ // alert(parts);
+ var arrayOfStrings = parts.split(';')
+
+ for (var i = 0; i < arrayOfStrings.length; i++) {
+ var idpart = arrayOfStrings[i]
+ idpart = idpart.replace('!!', '')
+ idpart = idpart.replace('!', '')
+ idpart = idpart.replace('!', '')
+ idpart = idpart.replace(';', '')
+ $('#puzzlepart' + idpart).css('opacity', 0)
+ }
+
+ var mess2 = $('#cardgameengage').html()
+ $('#messagePackDeck').html(mess2)
+ $('#messagePackDeck').css('display', 'block')
+ $('#messagePackDeck').addClass('messagePackDeckBottom')
+ $('#viewDeckBottom').css('display', 'block')
+}
+
+function minimizePuzzle () {
+
+ $('.puzzlecardone').addClass('puzzleMin')
+ setTimeout(function () {
+
+ $('.puzzleMinOther').css('display', 'block')
+
+ var panNumber = parseInt($('#pancardgame').html())
+
+ if (panNumber > 1) {
+ $('#puzzleMinOther1').addClass('pimg01')
+ }
+ if (panNumber > 2) {
+ $('#puzzleMinOther2').addClass('pimg02')
+ }
+ if (panNumber > 3) {
+ $('#puzzleMinOther3').addClass('pimg03')
+ }
+ if (panNumber > 4) {
+ $('#puzzleMinOther4').addClass('pimg04')
+ }
+ }, 1000)
+}
+
diff --git a/plugin/card_game/uninstall.php b/plugin/card_game/uninstall.php
new file mode 100644
index 000000000..5d0bc768e
--- /dev/null
+++ b/plugin/card_game/uninstall.php
@@ -0,0 +1,9 @@
+uninstall();
diff --git a/plugin/check_extra_field_author_company/CheckExtraFieldAuthorsCompanyPlugin.php b/plugin/check_extra_field_author_company/CheckExtraFieldAuthorsCompanyPlugin.php
new file mode 100644
index 000000000..3ed6cbb77
--- /dev/null
+++ b/plugin/check_extra_field_author_company/CheckExtraFieldAuthorsCompanyPlugin.php
@@ -0,0 +1,553 @@
+tblExtraField = Database::get_main_table(TABLE_EXTRA_FIELD);
+ $this->tblExtraFieldOption = Database::get_main_table(TABLE_EXTRA_FIELD_OPTIONS);
+ $field = new ExtraField('user');
+ $companyField = $field->get_handler_field_info_by_field_variable('company');
+ $this->companyExists = false;
+ if (!empty($companyField)) {
+ $this->companyExists = true;
+ $this->companyField = $companyField;
+ } else {
+ $this->companyField = [
+ 'field_type' => ExtraField::FIELD_TYPE_RADIO,
+ 'variable' => 'company',
+ 'display_text' => 'Company',
+ 'default_value' => '',
+ 'field_order' => 0,
+ 'visible_to_self' => 0,
+ 'visible_to_others' => 0,
+ 'changeable' => 1,
+ 'filter' => 1,
+ ];
+ }
+ $field = new ExtraField('lp');
+ $authorsField = $field->get_handler_field_info_by_field_variable('authors');
+
+ $this->authorsExists = false;
+ if (empty($authorsField)) {
+ $this->authorsExists = true;
+ $this->authorsField = $authorsField;
+ }
+ }
+
+ /**
+ * Create a new instance of CheckExtraFieldAuthorsCompanyPlugin.
+ *
+ * @return CheckExtraFieldAuthorsCompanyPlugin
+ */
+ public static function create()
+ {
+ static $result = null;
+
+ return $result ? $result : $result = new self();
+ }
+
+ /**
+ * Perform the plugin installation.
+ */
+ public function install()
+ {
+ $this->saveCompanyField();
+ $this->setCompanyExtrafieldData();
+ $this->saveAuthorsField();
+ $this->savePrice();
+ $this->saveAuthorLPItem();
+ $this->saveAuthorLp();
+ }
+
+ /**
+ * Save the arrangement for company, it is adjusted internally so that the values match the necessary ones.
+ */
+ public function saveCompanyField()
+ {
+ $data = $this->companyField;
+ if (!empty($data)) {
+ $data['field_type'] = (int) $data['field_type'];
+ $data['field_order'] = (int) $data['field_order'];
+ $data['visible_to_self'] = (int) $data['visible_to_self'];
+ $data['visible_to_others'] = (int) $data['visible_to_others'];
+ $data['changeable'] = (int) $data['changeable'];
+ $data['filter'] = (int) $data['filter'];
+ $data['default_value'] = '';
+ $data['variable'] = 'company';
+ $data['visible'] = 1;
+ $data['display_text'] = strtolower(Database::escape_string($data['display_text']));
+ $schedule = new ExtraField('user');
+ $this->companyField['id'] = $schedule->save($data);
+ }
+ }
+
+ /**
+ * Insert the option fields for company with the generic values Company 1, company 2 and company 3.
+ */
+ public function setCompanyExtrafieldData()
+ {
+ $companies = [
+ 0 => 'Company 1',
+ 1 => 'Company 2',
+ 2 => 'Company 3',
+ ];
+ $companyId = (int) $this->companyField['id'];
+ if ($companyId != 0) {
+ for ($i = 0; $i < count($companies); $i++) {
+ $order = $i + 1;
+ $extraFieldOptionValue = $companies[$i];
+ if ($companyId != null) {
+ $query = "SELECT *
+ FROM ".$this->tblExtraFieldOption."
+ WHERE
+ option_value = '$extraFieldOptionValue' AND
+ field_id = $companyId";
+ $extraFieldOption = Database::fetch_assoc(Database::query($query));
+ if (isset($extraFieldOption['id']) && $extraFieldOption['id'] && $extraFieldOption['field_id'] == $companyId) {
+ // Update?
+ } else {
+ $query = "
+ INSERT INTO ".$this->tblExtraFieldOption."
+ (`field_id`, `option_value`, `display_text`, `priority`, `priority_message`, `option_order`) VALUES
+ ( '$companyId', '$extraFieldOptionValue', '$extraFieldOptionValue', NULL, NULL, '$order');
+ ";
+ Database::query($query);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Save the arrangement for authors, it is adjusted internally so that the values match the necessary ones.
+ */
+ public function saveAuthorsField()
+ {
+ $data = [
+ 'field_type' => ExtraField::FIELD_TYPE_SELECT_MULTIPLE,
+ 'variable' => 'authors',
+ 'display_text' => 'Authors',
+ 'default_value' => '',
+ 'field_order' => 0,
+ 'visible_to_self' => 1,
+ 'visible_to_others' => 0,
+ 'changeable' => 1,
+ 'filter' => 1,
+ ];
+ $schedule = new ExtraField('lp');
+ $schedule->save($data);
+ }
+
+ /**
+ * Save the arrangement for price, it is adjusted internally so that the values match the necessary ones.
+ */
+ public function savePrice()
+ {
+ $schedule = new ExtraField('lp_item');
+ $data = [];
+ $data['visible_to_self'] = 1;
+ $data['visible_to_others'] = 1;
+ $data['changeable'] = 1;
+ $data['filter'] = 0;
+ $data['variable'] = 'price';
+ $data['display_text'] = 'SalePrice';
+ $data['field_type'] = ExtraField::FIELD_TYPE_INTEGER;
+
+ $schedule->save($data);
+ }
+
+ /**
+ * Save the arrangement for AuthorLPItem, it is adjusted internally so that the values match the necessary ones.
+ */
+ public function saveAuthorLPItem()
+ {
+ $schedule = new ExtraField('lp_item');
+ $data = [];
+ $data['visible_to_self'] = 1;
+ $data['visible_to_others'] = 0;
+ $data['changeable'] = 1;
+ $data['filter'] = 0;
+ $data['variable'] = 'authorlpitem';
+ $data['display_text'] = 'LearningPathItemByAuthor';
+ $data['field_type'] = ExtraField::FIELD_TYPE_SELECT_MULTIPLE;
+ $schedule->save($data);
+ }
+
+ /**
+ * Save the arrangement for authorlp, it is adjusted internally so that the values match the necessary ones.
+ */
+ public function saveAuthorLp()
+ {
+ $schedule = new ExtraField('user');
+ $data = [];
+ $data['variable'] = 'authorlp';
+ $data['display_text'] = 'authors';
+ $data['changeable'] = 1;
+ $data['visible_to_self'] = 1;
+ $data['visible_to_others'] = 0;
+ $data['filter'] = 0;
+ $data['field_type'] = ExtraField::FIELD_TYPE_CHECKBOX;
+ $schedule->save($data);
+ }
+
+ /**
+ * Remove the extra fields set by the plugin.
+ */
+ public function uninstall()
+ {
+ $companyExists = $this->companyFieldExists();
+ if ($companyExists == true) {
+ // $this->removeCompanyField();
+ }
+ $authorsExists = $this->authorsFieldExists();
+ if ($authorsExists == true) {
+ // $this->removeAuthorsField();
+ }
+ $priceExists = $this->priceFieldExists();
+ if ($priceExists == true) {
+ // $this->>removePriceField();
+ }
+ $authorLpItemExists = $this->authorLpItemFieldExists();
+ if ($authorLpItemExists == true) {
+ // $this->removeAuthorLpItemField();
+ }
+ $authorLpExists = $this->authorLpFieldExists();
+ if ($authorLpExists == true) {
+ // $this->removeAuthorLpField();
+ }
+ }
+
+ /**
+ * Verify that the "company" field exists in the database.
+ */
+ public function companyFieldExists(): bool
+ {
+ $this->getCompanyField();
+ $this->companyExists = (isset($this->companyField['id'])) ? true : false;
+
+ return $this->companyExists;
+ }
+
+ /**
+ * Returns the content of the extra field "company" if it exists in the database, if not, it returns an arrangement
+ * with the basic elements for its operation.
+ *
+ * @return array
+ */
+ public function getCompanyField()
+ {
+ $companyField = $this->getInfoExtrafield('company');
+ if (count($companyField) > 1) {
+ $this->companyField = $companyField;
+ } else {
+ $companyField = $this->companyField;
+ }
+
+ return $companyField;
+ }
+
+ /**
+ * Verify that the "authors" field exists in the database.
+ */
+ public function authorsFieldExists(): bool
+ {
+ $this->getAuthorsField();
+ $this->authorsExists = (isset($this->authorsField['id'])) ? true : false;
+
+ return $this->authorsExists;
+ }
+
+ /**
+ * Returns the content of the extra field "authors" if it exists in the database, if not, it returns an arrangement
+ * with the basic elements for its operation.
+ *
+ * @return array
+ */
+ public function getAuthorsField()
+ {
+ $schedule = new ExtraField('lp');
+ $data = $schedule->get_handler_field_info_by_field_variable('authors');
+ if (empty($data)) {
+ $this->authorsField = $data;
+ } else {
+ $data = $this->authorsField;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Verify that the "price" field exists in the database.
+ */
+ public function priceFieldExists(): bool
+ {
+ $this->getPriceField();
+ $this->priceExists = (isset($this->priceField['id'])) ? true : false;
+
+ return $this->priceExists;
+ }
+
+ /**
+ * Returns the content of the extra field "price" if it exists in the database, if not, it returns an arrangement
+ * with the basic elements for its operation.
+ *
+ * @return array
+ */
+ public function getPriceField()
+ {
+ $schedule = new ExtraField('lp_item');
+ $data = $schedule->get_handler_field_info_by_field_variable('price');
+ if (empty($data)) {
+ $this->priceField = $data;
+ } else {
+ $data = $this->priceField;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Verify that the "authorlpitem" field exists in the database.
+ */
+ public function authorLpItemFieldExists(): bool
+ {
+ $this->getAuthorLpItemField();
+ $this->authorLpItemExists = (isset($this->authorLpItemField['id'])) ? true : false;
+
+ return $this->authorLpItemExists;
+ }
+
+ /**
+ * Returns the content of the extra field "authorlpitem" if it exists in the database, if not, it returns an arrangement
+ * with the basic elements for its operation.
+ *
+ * @return array
+ */
+ public function getAuthorLpItemField()
+ {
+ $schedule = new ExtraField('lp_item');
+ $data = $schedule->get_handler_field_info_by_field_variable('authorlpitem');
+ if (empty($data)) {
+ $this->authorLpItemField = $data;
+ } else {
+ $data = $this->authorLpItemField;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Verify that the "authorlp" field exists in the database.
+ */
+ public function authorLpFieldExists(): bool
+ {
+ $this->getAuthorLpField();
+ $this->authorLpExists = (isset($this->authorLpField['id'])) ? true : false;
+
+ return $this->authorLpExists;
+ }
+
+ /**
+ * Returns the content of the extra field "authorlp" if it exists in the database, if not, it returns an arrangement
+ * with the basic elements for its operation.
+ *
+ * @return array
+ */
+ public function getAuthorLpField()
+ {
+ $field = new ExtraField('user');
+ $data = $field->get_handler_field_info_by_field_variable('authorlp');
+ if (empty($data)) {
+ $this->authorLpField = $data;
+ } else {
+ $data = $this->authorLpField;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Remove the extra fields "company".
+ */
+ public function removeCompanyField()
+ {
+ $data = $this->getCompanyField();
+ // $this->deleteQuery($data);
+ }
+
+ /**
+ * Remove the extra fields "authors".
+ */
+ public function removeAuthorsField()
+ {
+ $data = $this->getAuthorsField();
+ // $this->deleteQuery($data);
+ }
+
+ /**
+ * Remove the extra fields "price".
+ */
+ public function removePriceField()
+ {
+ $data = $this->getPriceField();
+ // $this->deleteQuery($data);
+ }
+
+ /**
+ * Remove the extra fields "authorlpitem".
+ */
+ public function removeAuthorLpItemField()
+ {
+ $data = $this->getAuthorLpItemField();
+ // $this->deleteQuery($data);
+ }
+
+ /**
+ * Remove the extra fields "authorlp".
+ */
+ public function removeAuthorLpField()
+ {
+ $data = $this->getAuthorLpField();
+ // $this->deleteQuery($data);
+ }
+
+ /**
+ * Executes fix removal for authors or company.
+ *
+ * @param $data
+ */
+ protected function deleteQuery($data)
+ {
+ $validVariable = false;
+ $variable = $data['variable'];
+ $extraFieldTypeInt = (int) $data['extra_field_type'];
+ $FieldType = (int) $data['field_type'];
+ $id = (int) $data['id'];
+ $extraFieldType = null;
+ switch ($variable) {
+ case 'company':
+ case 'authorlp':
+ $validVariable = true;
+ $extraFieldType = 'user';
+ break;
+ case 'authors':
+ $validVariable = true;
+ $extraFieldType = 'lp';
+ break;
+ case 'price':
+ case 'authorlpitem':
+ $validVariable = true;
+ $extraFieldType = 'lp_item';
+ break;
+ }
+ if ($variable === 'company') {
+ } elseif ($variable === 'authors') {
+ }
+ if ($validVariable == true && $id != 0 && !empty($extraFieldType)) {
+ $query = "SELECT id
+ FROM
+ ".$this->tblExtraField."
+ WHERE
+ id = $id AND
+ variable = '$variable' AND
+ extra_field_type = $extraFieldTypeInt AND
+ field_type = $FieldType
+ ";
+ $data = Database::fetch_assoc(Database::query($query));
+ if (isset($data['id'])) {
+ $obj = new ExtraField($extraFieldType);
+ $obj->delete($data['id']);
+ }
+ }
+ }
+
+ /**
+ * Returns the array of an element in the database that matches the variable.
+ *
+ * @param string $variableName
+ *
+ * @return array
+ */
+ protected function getInfoExtrafield($variableName = null)
+ {
+ if ($variableName == null) {
+ return [];
+ }
+ $variableName = strtolower(Database::escape_string($variableName));
+ $tblExtraField = $this->tblExtraField;
+ $query = "SELECT * FROM $tblExtraField WHERE variable = '$variableName'";
+ $data = Database::fetch_assoc(Database::query($query));
+ if ($data == false || !isset($data['display_text'])) {
+ return [];
+ }
+
+ return $data;
+ }
+}
diff --git a/plugin/check_extra_field_author_company/README.md b/plugin/check_extra_field_author_company/README.md
new file mode 100644
index 000000000..0e58972f0
--- /dev/null
+++ b/plugin/check_extra_field_author_company/README.md
@@ -0,0 +1,30 @@
+Check Extra Fields 'author' and 'company'
+======
+
+The "User by organization" report allows the administrator to select a
+date range to show the number of users who have been subscribed to a learning
+path or a course during this time frame. The number of users are grouped by
+entity/company.
+
+The "Learning path by author" report allows the administrator to define, for
+each user, if (s)he is an author or not. Then, for each item in a Learning
+Path, the administrator can select who is its author from the identified list
+and indicate the cost of that item.
+
+Finally, the reports allow the administrator to select a date range to show
+for each author how many of his/her content (LP item) users have been given
+access to (based on the learning path subscriptions by users) and show the
+amount of money they should be paid based on the number of accesses given
+during this period.
+
+This plugin adds the extra fields necessary to display the reports:
+
+* The "User by organization" report requires the 'company' extra field to be created on user.
+* The "Learning path by author" report requires the 'authors' extra field to be created on lp.
+* The "LP Item by author" report additional reports requires the 'authorlpitem' extra field to be created on lp_item and the 'authorlp' extra field to be created on 'user'.
+* For prices to be adequately shown, the 'price' extra field needs to be created on 'lp_item'.
+
+## Uninstall
+
+When uninstalling this plugin, the extra fields created will not be removed,
+for data persistence reasons.
\ No newline at end of file
diff --git a/plugin/check_extra_field_author_company/index.php b/plugin/check_extra_field_author_company/index.php
new file mode 100644
index 000000000..92a1ce705
--- /dev/null
+++ b/plugin/check_extra_field_author_company/index.php
@@ -0,0 +1,5 @@
+install();
diff --git a/plugin/check_extra_field_author_company/plugin.php b/plugin/check_extra_field_author_company/plugin.php
new file mode 100644
index 000000000..9f110c2fa
--- /dev/null
+++ b/plugin/check_extra_field_author_company/plugin.php
@@ -0,0 +1,17 @@
+Plugins).
+ *
+ * @author Carlos Alvarado
+ */
+$plugin_info['title'] = 'Extra reports: User by organization", "Learning path by author" and "LP item by author"';
+$plugin_info['comment'] = 'To enable these reports, enable user subscription to a learning path through the '.
+ '"Learning path settings" -> "Subscribe users to learning path" '.
+ 'You can then go to /main/mySpace/ (as administrator), then to the "admin" section (star icon) to see the new reports.';
+$plugin_info['version'] = '1.2';
+$plugin_info['author'] = 'Carlos Alvarado, Julio Montoya';
diff --git a/plugin/check_extra_field_author_company/uninstall.php b/plugin/check_extra_field_author_company/uninstall.php
new file mode 100644
index 000000000..13d8d6e56
--- /dev/null
+++ b/plugin/check_extra_field_author_company/uninstall.php
@@ -0,0 +1,8 @@
+uninstall();
diff --git a/plugin/cleandeletedfiles/README.md b/plugin/cleandeletedfiles/README.md
new file mode 100644
index 000000000..781494fda
--- /dev/null
+++ b/plugin/cleandeletedfiles/README.md
@@ -0,0 +1,6 @@
+CleanDeleted Files Maintenance plugin
+===
+
+This plugin allows the administrator to permanently delete files marked as deleted.
+Enable the plugin, then add it to the menu_administrator region and check back the
+'Plugins' box on the administration page to access it.
\ No newline at end of file
diff --git a/plugin/cleandeletedfiles/admin.php b/plugin/cleandeletedfiles/admin.php
new file mode 100644
index 000000000..374f59fae
--- /dev/null
+++ b/plugin/cleandeletedfiles/admin.php
@@ -0,0 +1,192 @@
+get_info();
+$isPlatformAdmin = api_is_platform_admin();
+
+if ($plugin->isEnabled() && $isPlatformAdmin) {
+ $htmlHeadXtra[] = '';
+
+ $nameTools = $plugin->get_lang("FileList");
+ Display::display_header($nameTools);
+ echo Display::page_header($nameTools);
+
+ $pathList = [
+ "app/courses",
+ "app/upload",
+ ];
+
+ function findDeletedFiles($pathRelative)
+ {
+ global $sizePath;
+ $pathAbsolute = api_get_path(SYS_PATH).$pathRelative;
+ $result = [];
+ if (is_dir($pathAbsolute)) {
+ $dir = dir($pathAbsolute);
+ while ($file = $dir->read()) {
+ if (is_file($pathAbsolute.'/'.$file)) {
+ $filesize = round(filesize($pathAbsolute.'/'.$file) / 1024, 1);
+ $pos = strpos($file, "DELETED");
+ if ($pos !== false) {
+ $result[] = [
+ 'path_complete' => $pathAbsolute.'/'.$file,
+ 'path_relative' => $pathRelative.'/'.$file,
+ 'size' => $filesize,
+ ];
+ $sizePath += $filesize;
+ }
+ } else {
+ if ($file != '..' && $file != '.') {
+ $result = array_merge($result, findDeletedFiles($pathRelative.'/'.$file));
+ }
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ $sizeTotal = 0;
+ $i = 0;
+ foreach ($pathList as $pathItem) {
+ $sizePath = 0;
+ $filesDeletedList = findDeletedFiles($pathItem);
+ echo Display::page_subheader($plugin->get_lang("path_dir").": ".$pathItem);
+
+ if (count($filesDeletedList) > 0) {
+ echo "