diff --git a/README.md b/README.md index 79e7c6a2e279c9672c049bea40b35c47f7045fc1..b739b836b2d7ba82b1b729bac2c37d4a2fb6677d 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,28 @@ This module allows you to create DNS Zone, Site, Database User, Database, FTP User, SSH User, Mail Domain and Mailboxes in one form. You just define domain name and using checkboxes decide which services you want to create. Services are created using user defined templates. -This module is compatible with ISPConfig version 3.1 +This module is compatible with ISPConfig version 3.2 ## Installation -This module have to be installed on master node in multiserver setup. +This module has to be installed on master node in multiserver setup. - Download this repo as archive, unpack it to `/usr/local/ispconfig/interface/web/` and rename forlder to `wizard` OR clone repo using git `git clone https://git.ispconfig.org/ispconfig/module-wizard.git /usr/local/ispconfig/interface/web/wizard` -- Create DB table provided in db.sql file `mysql -u root -p < db.sql` +- Create the database table provided in `db.sql` file: `mysql -u root -p < db.sql` - Enable module in user interface System -> CP Users -> Admin user -> Check "wizard" and save. - If it doesn't work, enable module manually by editing admin user in DB table `sys_user` column `modules` - + +On a slave server you only have to create the database table using the instructions above. + +## Upgrade + +When upgrading from an older version: + +- Replace existing files of the module with new ones (this doesn't apply to slave servers in multiserver setup) +- Make sure that the database schema is up to date. Apply `db_update_3.2.sql`, or, should that fail, execute relevant statements from it manually +- Please note that in the database, `fastcgi_php_version` string column has been replaced with `server_php_id`, which is an integer. This means you will have to modify all your saved wizard configuration templates after the update to make sure that they use the correct PHP version + ## Planned features - Export results as PDF @@ -35,4 +45,4 @@ Contributions are welcome, so please contribute your translations. Thank you. ![Template](https://git.ispconfig.org/ispconfig/module-wizard/raw/master/readme_images/template.png) -![Result](https://git.ispconfig.org/ispconfig/module-wizard/raw/master/readme_images/result.png) +![Result](https://git.ispconfig.org/helmo/module-wizard/raw/master/readme_images/result.png) diff --git a/db.sql b/db.sql index dfeb121b37251d5595c822daf62a5eae482546e0..66142e7d9607700a9452add24d2cba1884dab8bc 100644 --- a/db.sql +++ b/db.sql @@ -52,7 +52,7 @@ CREATE TABLE `wizard_template` ( `errordocs` tinyint(1) NOT NULL DEFAULT '1', `subdomain` enum('none','www','*') NOT NULL DEFAULT 'www', `php` varchar(32) NOT NULL DEFAULT 'fast-cgi', - `fastcgi_php_version` varchar(255) DEFAULT NULL, + `server_php_id` int(11) UNSIGNED NOT NULL DEFAULT 0, `ruby` enum('n','y') NOT NULL DEFAULT 'n', `python` enum('n','y') NOT NULL DEFAULT 'n', `perl` enum('n','y') NOT NULL DEFAULT 'n', @@ -61,12 +61,14 @@ CREATE TABLE `wizard_template` ( `allow_override` varchar(255) NOT NULL DEFAULT 'All', `http_port` int(11) NOT NULL DEFAULT '80', `https_port` int(11) NOT NULL DEFAULT '443', + `folder_directive_snippets` text NULL, `log_retention` int(11) NOT NULL DEFAULT '30', - `apache_directives` mediumtext, - `nginx_directives` mediumtext, + `proxy_protocol` ENUM('n','y') NOT NULL DEFAULT 'n', + `apache_directives` mediumtext NULL DEFAULT NULL, + `nginx_directives` mediumtext NULL DEFAULT NULL, `php_fpm_use_socket` enum('n','y') NOT NULL DEFAULT 'n', `php_fpm_chroot` enum('n','y') NOT NULL DEFAULT 'n', - `pm` enum('static','dynamic','ondemand') NOT NULL DEFAULT 'dynamic', + `pm` enum('static','dynamic','ondemand') NOT NULL DEFAULT 'ondemand', `pm_max_children` int(11) unsigned NOT NULL DEFAULT '10', `pm_start_servers` int(11) unsigned NOT NULL DEFAULT '2', `pm_min_spare_servers` int(11) unsigned NOT NULL DEFAULT '1', @@ -74,6 +76,13 @@ CREATE TABLE `wizard_template` ( `pm_process_idle_timeout` int(11) unsigned NOT NULL DEFAULT '10', `pm_max_requests` int(11) unsigned NOT NULL DEFAULT '0', `custom_php_ini` mediumtext, + `backup_interval` varchar(255) NOT NULL DEFAULT 'none', + `backup_copies` int(11) NOT NULL DEFAULT 1, + `backup_format_web` varchar(255) NOT NULL DEFAULT 'default', + `backup_format_db` varchar(255) NOT NULL DEFAULT 'gzip', + `backup_encrypt` enum('n','y') NOT NULL DEFAULT 'n', + `backup_password` varchar(255) NOT NULL DEFAULT '', + `backup_excludes` mediumtext DEFAULT NULL, `database_charset` varchar(64) DEFAULT NULL, `database_remote_access` enum('n','y') NOT NULL DEFAULT 'n', `database_remote_ips` text, @@ -88,5 +97,10 @@ CREATE TABLE `wizard_template` ( `enablesmtp` enum('n','y') NOT NULL DEFAULT 'y', `enableimap` enum('n','y') NOT NULL DEFAULT 'y', `enablepop3` enum('n','y') NOT NULL DEFAULT 'y', + `jailkit_chroot_app_sections` mediumtext NULL DEFAULT NULL, + `jailkit_chroot_app_programs` mediumtext NULL DEFAULT NULL, + `delete_unused_jailkit` enum('n','y') NOT NULL DEFAULT 'n', + `last_jailkit_update` date NULL DEFAULT NULL, + `last_jailkit_hash` varchar(255) DEFAULT NULL, PRIMARY KEY (`template_id`) ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; diff --git a/db_update_3.2.sql b/db_update_3.2.sql new file mode 100644 index 0000000000000000000000000000000000000000..7d27869e729bddeb2afa96d56cc7261e4112b4c9 --- /dev/null +++ b/db_update_3.2.sql @@ -0,0 +1,37 @@ + +ALTER TABLE `wizard_template` + ADD COLUMN `proxy_protocol` ENUM('n','y') NOT NULL DEFAULT 'n' AFTER `log_retention`; +ALTER TABLE `wizard_template` + ADD COLUMN `server_php_id` int(11) UNSIGNED NOT NULL DEFAULT 0; + + +ALTER TABLE `wizard_template` ADD `backup_interval` varchar(255) NOT NULL DEFAULT 'none' AFTER `custom_php_ini`; +ALTER TABLE `wizard_template` ADD `backup_copies` int(11) NOT NULL DEFAULT 1 AFTER `backup_interval`; + +-- backup format +ALTER TABLE `wizard_template` ADD `backup_format_web` VARCHAR( 255 ) NOT NULL default 'default' AFTER `backup_copies`; +ALTER TABLE `wizard_template` ADD `backup_format_db` VARCHAR( 255 ) NOT NULL default 'gzip' AFTER `backup_format_web`; +-- end of backup format + +-- backup encryption +ALTER TABLE `wizard_template` ADD `backup_encrypt` enum('n','y') NOT NULL DEFAULT 'n' AFTER `backup_format_db`; +ALTER TABLE `wizard_template` ADD `backup_password` VARCHAR( 255 ) NOT NULL DEFAULT '' AFTER `backup_encrypt`; +-- end of backup encryption + +ALTER TABLE `wizard_template` ADD `backup_excludes` mediumtext DEFAULT NULL AFTER `backup_password`; + +ALTER TABLE `wizard_template` ADD `jailkit_chroot_app_sections` mediumtext NULL DEFAULT NULL; +ALTER TABLE `wizard_template` ADD `jailkit_chroot_app_programs` mediumtext NULL DEFAULT NULL; +ALTER TABLE `wizard_template` ADD `delete_unused_jailkit` enum('n','y') NOT NULL DEFAULT 'n'; +ALTER TABLE `wizard_template` ADD `last_jailkit_update` date NULL DEFAULT NULL; +ALTER TABLE `wizard_template` ADD `last_jailkit_hash` varchar(255) DEFAULT NULL; + +-- Match incremental/upd_0091.sql +ALTER TABLE `wizard_template` ALTER pm SET DEFAULT 'ondemand'; +ALTER TABLE `wizard_template` DROP COLUMN `enable_spdy`; +ALTER TABLE `wizard_template` ADD `folder_directive_snippets` TEXT NULL AFTER `https_port`; +ALTER TABLE `wizard_template` CHANGE `apache_directives` `apache_directives` mediumtext NULL DEFAULT NULL; +ALTER TABLE `wizard_template` CHANGE `nginx_directives` `nginx_directives` mediumtext NULL DEFAULT NULL; + +-- Match incremental/upd_0092.sql +ALTER TABLE `wizard_template` DROP COLUMN `fastcgi_php_version`; diff --git a/form/new_service.tform.php b/form/new_service.tform.php index 21b961417040f0fd1c70c10bfd0025e727a8efa9..9d42a8b3aecb12993a45fc59abae3cdde8845b1f 100644 --- a/form/new_service.tform.php +++ b/form/new_service.tform.php @@ -79,6 +79,9 @@ $form["auth_preset"]["perm_user"] = 'riud'; //r = read, i = insert, u = update, $form["auth_preset"]["perm_group"] = 'riud'; //r = read, i = insert, u = update, d = delete $form["auth_preset"]["perm_other"] = ''; //r = read, i = insert, u = update, d = delete +// Admin always right? +$wildcard_available = TRUE; + $form["tabs"]['client'] = array( 'title' => "New service", 'description' => 'Create domain, mail domain, site, database and ftp/ssh', @@ -161,6 +164,12 @@ $form["tabs"]['client'] = array( ), 'value' => '', ), + 'subdomain' => array ( + 'datatype' => 'VARCHAR', + 'formtype' => 'SELECT', + 'default' => 'template', + 'value' => ($wildcard_available ? array('template' => 'use_template_txt', 'none' => 'none_txt', 'www' => 'www.', '*' => '*.') : array('none' => 'none_txt', 'www' => 'www.')) + ), 'db' => array ( 'datatype' => 'VARCHAR', 'formtype' => 'CHECKBOX', @@ -243,7 +252,7 @@ $form["tabs"]['client'] = array( 'dns' => array ( 'datatype' => 'VARCHAR', 'formtype' => 'CHECKBOX', - 'default' => 'y', + 'default' => 'n', 'value' => array(0 => 'n', 1 => 'y') ), 'dns_template_id' => array( @@ -258,4 +267,4 @@ $form["tabs"]['client'] = array( 'value' => '', ), ) -); \ No newline at end of file +); diff --git a/lib/lang/cz_new_service.lng b/lib/lang/cz_new_service.lng index 0166b69e5b30dc772d2ec0504aed6d4a17dd763e..d96c56b1ec1c1946fa37a13e08db2fdc5a70c563 100644 --- a/lib/lang/cz_new_service.lng +++ b/lib/lang/cz_new_service.lng @@ -45,4 +45,7 @@ $wb['error_email_empty'] = 'Email prázdné.'; $wb['error_domain_regex'] = 'Doména obsahuje neplatné znaky.'; $wb['error_ns1_regex'] = 'NS1 obsahuje neplatné znaky.'; $wb['error_ns2_regex'] = 'NS2 obsahuje neplatné znaky.'; -$wb['error_email_regex'] = 'Email neosahuje platnou emailovou adresu.'; \ No newline at end of file +$wb['error_email_regex'] = 'Email neosahuje platnou emailovou adresu.'; +$wb['subdomain_txt'] = 'Auto-Subdomain'; +$wb['none_txt'] = 'None'; +$wb['use_template_txt'] = 'Use template value'; diff --git a/lib/lang/cz_template.lng b/lib/lang/cz_template.lng index 5c1c2676a46397a6fc2e191e3f86591f856df5d1..942e9ebd06c7c7d8fc82ff72882b24509106ae8e 100644 --- a/lib/lang/cz_template.lng +++ b/lib/lang/cz_template.lng @@ -16,7 +16,7 @@ $wb["perl_txt"] = 'Perl'; $wb["ruby_txt"] = 'Ruby'; $wb["python_txt"] = 'Python'; $wb["allow_override_txt"] = 'Apache AllowOverride'; -$wb["fastcgi_php_version_txt"] = 'PHP verze'; +$wb["server_php_id_txt"] = 'PHP verze'; $wb["pm_txt"] = 'PHP-FPM Process Manager'; $wb["pm_process_idle_timeout_txt"] = 'PHP-FPM pm.process_idle_timeout'; $wb["pm_max_requests_txt"] = 'PHP-FPM pm.max_requests'; diff --git a/lib/lang/en_new_service.lng b/lib/lang/en_new_service.lng index c7013fd084672d01a08742a19dc9bebffa38e90a..7ef5238993eee5549446eed8c45564fac57ddddd 100644 --- a/lib/lang/en_new_service.lng +++ b/lib/lang/en_new_service.lng @@ -46,4 +46,7 @@ $wb['error_email_empty'] = 'Email empty.'; $wb['error_domain_regex'] = 'Domain contains invalid characters.'; $wb['error_ns1_regex'] = 'NS1 contains invalid characters.'; $wb['error_ns2_regex'] = 'NS2 contains invalid characters.'; -$wb['error_email_regex'] = 'Email does not contain a valid email address.'; \ No newline at end of file +$wb['error_email_regex'] = 'Email does not contain a valid email address.'; +$wb['subdomain_txt'] = 'Auto-Subdomain'; +$wb['none_txt'] = 'None'; +$wb['use_template_txt'] = 'Use template value'; diff --git a/lib/lang/en_template.lng b/lib/lang/en_template.lng index 5247f83fdf51ed6976d1e0f70a7f51a96bf5c0a2..3d9e59cca6007b048da858dd8879f544bfffcb6c 100644 --- a/lib/lang/en_template.lng +++ b/lib/lang/en_template.lng @@ -15,7 +15,7 @@ $wb["perl_txt"] = 'Perl'; $wb["ruby_txt"] = 'Ruby'; $wb["python_txt"] = 'Python'; $wb["allow_override_txt"] = 'Apache AllowOverride'; -$wb["fastcgi_php_version_txt"] = 'PHP Version'; +$wb["server_php_id_txt"] = 'PHP Version'; $wb["pm_txt"] = 'PHP-FPM Process Manager'; $wb["pm_process_idle_timeout_txt"] = 'PHP-FPM pm.process_idle_timeout'; $wb["pm_max_requests_txt"] = 'PHP-FPM pm.max_requests'; diff --git a/lib/lang/es_new_service.lng b/lib/lang/es_new_service.lng index 53ae9eaae88e1e42199346e501905ca0a6d5c066..252d146d82e3a6bc09fd05ac77b3812a5b6c56dd 100644 --- a/lib/lang/es_new_service.lng +++ b/lib/lang/es_new_service.lng @@ -45,3 +45,6 @@ $wb['error_domain_regex'] = 'El dominio contiene caracteres erroneos.'; $wb['error_ns1_regex'] = 'NS1 contiene caracteres erroneos.'; $wb['error_ns2_regex'] = 'NS2 contiene caracteres erroneos.'; $wb['error_email_regex'] = 'El Email no contiene una direccion valida.'; +$wb['subdomain_txt'] = 'Auto-Subdomain'; +$wb['none_txt'] = 'None'; +$wb['use_template_txt'] = 'Use template value'; diff --git a/lib/lang/es_template.lng b/lib/lang/es_template.lng index 55a28db0966ec1f626898b4c5baaf24aa8aab5b2..e95b9128490053ef88e160ddfde0acd85f1e5d2a 100644 --- a/lib/lang/es_template.lng +++ b/lib/lang/es_template.lng @@ -15,7 +15,7 @@ $wb["perl_txt"] = 'Perl'; $wb["ruby_txt"] = 'Ruby'; $wb["python_txt"] = 'Python'; $wb["allow_override_txt"] = 'Apache AllowOverride'; -$wb["fastcgi_php_version_txt"] = 'Version PHP'; +$wb["server_php_id_txt"] = 'Version PHP'; $wb["pm_txt"] = 'PHP-FPM Process Manager'; $wb["pm_process_idle_timeout_txt"] = 'PHP-FPM pm.process_idle_timeout'; $wb["pm_max_requests_txt"] = 'PHP-FPM pm.max_requests'; diff --git a/new_service.php b/new_service.php index 918d5a395a9f884798d08c80a888d34c251eae62..7cdee02aed2da0bad26b0aef19e91e714dc6aa4b 100644 --- a/new_service.php +++ b/new_service.php @@ -83,10 +83,21 @@ class page_action extends tform_actions { global $app, $conf; $fields = $app->tform->encode($this->dataRecord, $app->tform->getCurrentTab(), true); + $client_domain_exists = FALSE; // check for domain unique name - if($app->db->queryOneRecord('SELECT domain_id FROM domain WHERE domain = "'.$fields['domain'].'"')) { - $app->tform->errorMessage = $app->tform->wordbook['domain_error_unique']; + if($app->db->queryOneRecord('SELECT domain_id FROM domain WHERE domain = ?', $fields['domain'])) { + $client_domain_exists = TRUE; + // Check use as web domain. + if($app->db->queryOneRecord('SELECT domain_id FROM web_domain WHERE domain = ?', $fields['domain'])) { + $app->tform->errorMessage = $app->tform->wordbook['domain_error_unique'] . 'b'; + } + elseif($fields['mail'] && $app->db->queryOneRecord('SELECT domain_id FROM mail_domain WHERE domain = ?', $fields['domain'])) { + $app->tform->errorMessage = $app->tform->wordbook['domain_error_unique']. 'a'; + } + else { + // Exists as client domain, not with a website. + } } if($app->tform->errorMessage) @@ -112,9 +123,11 @@ class page_action extends tform_actions { // client prefix and group id $res = $app->db->queryOneRecord("SELECT groupid FROM sys_group WHERE client_id = ".$app->functions->intval($fields['client_id'])); $client_group_id = $res['groupid']; - + // add domain - $domain_id = $remote->insert_query('../client/form/domain.tform.php', $fields['client_id'], array('domain' => $fields['domain'])); + if (!$client_domain_exists) { + $domain_id = $remote->insert_query('../client/form/domain.tform.php', $fields['client_id'], array('domain' => $fields['domain'])); + } $min_password_length = 8; if(isset($server_config_array['misc']['min_password_length'])) $min_password_length = $server_config_array['misc']['min_password_length']; @@ -375,7 +388,9 @@ class page_action extends tform_actions { 'disablepop3' => ($template['enablepop3'] == 'y' ? 'n' : 'y'), 'disablesmtp' => ($template['enablesmtp'] == 'y' ? 'n' : 'y'), 'maildir' => '/var/vmail/'.$fields['domain'].'/'.$alias, + 'maildir_format' => 'maildir', 'homedir' => '/var/vmail', + 'backup_interval' => 'none', ); $remote->insert_query('../mail/form/mail_user.tform.php', $fields['client_id'], $params); @@ -399,9 +414,9 @@ class page_action extends tform_actions { 'python' => $template['python'], 'perl' => $template['perl'], 'errordocs' => $template['errordocs'], - 'subdomain' => $template['subdomain'], + 'subdomain' => $fields['subdomain'] == 'template' ? $template['subdomain'] : $fields['subdomain'], 'php' => $template['php'], - 'fastcgi_php_version' => $template['fastcgi_php_version'], + 'server_php_id' => $template['server_php_id'], 'seo_redirect' => $template['seo_redirect'], 'rewrite_to_https' => $template['rewrite_to_https'], 'allow_override' => $template['allow_override'], @@ -500,7 +515,7 @@ class page_action extends tform_actions { 'server_id' => $template['web_server_id'], 'parent_domain_id' => $site_id, 'username' => $client_ftp_prefix . $generated_username, - 'username_prefix' => $client_prefix, + 'username_prefix' => $client_ftp_prefix, 'password' => $app->auth->get_random_password($min_password_length, true), 'quota_size' => $template['hd_quota'], 'dir' => $site_data['document_root'], @@ -537,7 +552,7 @@ class page_action extends tform_actions { 'shell' => $template['shell'], 'active' => 'y', 'username' => $client_shell_prefix . $generated_username, - 'username_prefix' => $client_prefix, + 'username_prefix' => $client_shell_prefix, ); // username, password @@ -550,7 +565,8 @@ class page_action extends tform_actions { $remote->insert_query('../sites/form/shell_user.tform.php', $fields['client_id'], $ssh_params); } - $dbserver = $app->db->queryOneRecord("SELECT server_name FROM server WHERE web_server = 1 AND server_id = ?", $template['database_server_id']); + $webserver = $app->db->queryOneRecord("SELECT server_name FROM server WHERE web_server = 1 AND server_id = ?", $template['web_server_id']); + $dbserver = $app->db->queryOneRecord("SELECT server_name FROM server WHERE web_server = 1 AND server_id = ?", $template['database_server_id']); $global_config = $app->getconf->get_global_config('sites'); $phpmyadmin_url = $global_config['phpmyadmin_url']; @@ -580,7 +596,7 @@ class page_action extends tform_actions { '.$db_user_params['database_user'].' '.$db_user_params['database_password'].' - '.$phpmyadmin_url.' + https://' . $webserver['server_name'] . '/phpmyadmin/ '; } if(isset($ftp_params)) { @@ -601,12 +617,12 @@ class page_action extends tform_actions { SSH user SSH password - SSH Server + SSH Server '.$ssh_params['username'].' '.$ssh_params['password'].' - '.$webserver['server_name'].' + '.$webserver['server_name'].' '; } if(isset($mailbox_passwords) and (bool)count($mailbox_passwords)) @@ -630,6 +646,28 @@ class page_action extends tform_actions { '; + + // Loading the template + $app->uses('tpl'); + $app->tpl->newTemplate("templates/template_summary_plaintext.htm"); + + $app->tpl->setVar('sitedomain', $fields['domain']); + $app->tpl->setVar('database_username', $db_user_params['database_user']); + $app->tpl->setVar('database_password', $db_user_params['database_password']); + + if(isset($ftp_params)) { + $app->tpl->setVar('ftp_server_name', $webserver['server_name']); + $app->tpl->setVar('ftp_username', $ftp_params['username']); + $app->tpl->setVar('ftp_password', $ftp_params['password']); + } + + $app->tpl->setVar('ssh_server_name', $webserver['server_name']); + $app->tpl->setVar('ssh_username', $ssh_params['username']); + $app->tpl->setVar('ssh_password', $ssh_params['password']); + // TODO +// $app->tpl->setVar('mailbox_passwords', $mailbox_passwords); + + $app->tpl->pparse(); } private function clean_private_key($key) diff --git a/template_edit.php b/template_edit.php index 80ff26d2734fb2053e235895cedded10ea4e083c..29f5913be074d7a2b787bde88c2b034f11e407b9 100644 --- a/template_edit.php +++ b/template_edit.php @@ -99,7 +99,7 @@ class page_action extends tform_actions { unset($tmp); unset($ips); - $app->tpl->setVar("fastcgi_php_version", $this->dataRecord['fastcgi_php_version']); + $app->tpl->setVar("server_php_id", $this->dataRecord['server_php_id']); parent::onShowEnd(); } diff --git a/templates/new_service.htm b/templates/new_service.htm index 4c64065e3748ab35f3c57cf61a9e423a32a66424..f29a931550f054ba0f7d577a18edc2145212a8ff 100644 --- a/templates/new_service.htm +++ b/templates/new_service.htm @@ -33,6 +33,14 @@ +
+ +
+
+ +
@@ -177,6 +185,7 @@
\ No newline at end of file + diff --git a/templates/template_summary_plaintext.htm b/templates/template_summary_plaintext.htm new file mode 100644 index 0000000000000000000000000000000000000000..de7e52c775d64abea427211d321b7b8c9023de6f --- /dev/null +++ b/templates/template_summary_plaintext.htm @@ -0,0 +1,22 @@ +
+Site: {tmpl_var name='sitedomain'}
+
+
+SFTP/SSH Server     {tmpl_var name='ssh_server_name'}
+SSH user       {tmpl_var name='ssh_username'}
+SSH password   {tmpl_var name='ssh_password'}
+
+
+
+DB name/user   {tmpl_var name='database_username'}
+DB password    {tmpl_var name='database_password'}
+PhpMyAdmin     https://{tmpl_var name='ssh_server_name'}/phpmyadmin/
+
+
+
+FTP Server     {tmpl_var name='ftp_server_name'}
+FTP user       {tmpl_var name='ftp_username'}
+FTP password   {tmpl_var name='ftp_password'}
+
+
+