From 7740a0d132d249a49985d38917db02c96798cf09 Mon Sep 17 00:00:00 2001 From: Jacques Deguest Date: Wed, 25 Mar 2020 20:27:01 +0900 Subject: [PATCH 01/12] Initial commit for Autoconfig --- .gitignore | 1 + AUTOCONFIG/AutoconfigHandler.php | 1494 +++++++++++++++++++++++ AUTOCONFIG/INSTALL.md | 157 +++ AUTOCONFIG/autoconfig-host-settings.tpl | 91 ++ AUTOCONFIG/autoconfig-mysql.sql | 116 ++ AUTOCONFIG/autoconfig-postgres.sql | 120 ++ AUTOCONFIG/autoconfig-sqlite.sql | 112 ++ AUTOCONFIG/autoconfig-v2.js | 908 ++++++++++++++ AUTOCONFIG/autoconfig.css | 808 ++++++++++++ AUTOCONFIG/autoconfig.js | 910 ++++++++++++++ AUTOCONFIG/autoconfig.php | 307 +++++ AUTOCONFIG/autoconfig.pl | 1321 ++++++++++++++++++++ AUTOCONFIG/autoconfig.tpl | 302 +++++ AUTOCONFIG/autoconfig_languages.php | 193 +++ AUTOCONFIG/sprintf.js | 212 ++++ config.inc.php | 11 + functions.inc.php | 15 + languages/en.lang | 123 ++ languages/fr.lang | 125 ++ languages/ja.lang | 122 ++ 20 files changed, 7448 insertions(+) create mode 100644 AUTOCONFIG/AutoconfigHandler.php create mode 100644 AUTOCONFIG/INSTALL.md create mode 100644 AUTOCONFIG/autoconfig-host-settings.tpl create mode 100644 AUTOCONFIG/autoconfig-mysql.sql create mode 100644 AUTOCONFIG/autoconfig-postgres.sql create mode 100644 AUTOCONFIG/autoconfig-sqlite.sql create mode 100644 AUTOCONFIG/autoconfig-v2.js create mode 100644 AUTOCONFIG/autoconfig.css create mode 100644 AUTOCONFIG/autoconfig.js create mode 100644 AUTOCONFIG/autoconfig.php create mode 100755 AUTOCONFIG/autoconfig.pl create mode 100644 AUTOCONFIG/autoconfig.tpl create mode 100644 AUTOCONFIG/autoconfig_languages.php create mode 100644 AUTOCONFIG/sprintf.js diff --git a/.gitignore b/.gitignore index 4833c98d..0e813783 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /.php_cs.cache /.idea /composer.lock +**/*.bbprojectd diff --git a/AUTOCONFIG/AutoconfigHandler.php b/AUTOCONFIG/AutoconfigHandler.php new file mode 100644 index 00000000..b4448981 --- /dev/null +++ b/AUTOCONFIG/AutoconfigHandler.php @@ -0,0 +1,1494 @@ +struct = array( + # field name allow display in... type $PALANG label $PALANG description default / options / ... + # editing? form list + 'encoding' => pacol( 0, 1, 1, 'text', 'Autoconfig_encoding' , 'XML encoding' , 'utf-8' ), + 'provider_id' => pacol( 1, 1, 1, 'text', 'Autoconfig_provider_id' , 'Provider ID' , '' ), + 'provider_domain' => pacol( 1, 1, 1, 'text', 'Autoconfig_provider_domain' , 'Applicable domain name' , '' ), + 'provider_name' => pacol( 1, 1, 1, 'text', 'Autoconfig_provider_name' , '' , '' ), + 'provider_short' => pacol( 1, 1, 1, 'text', 'Autoconfig_provider_short' , '' , '' ), + 'incoming_server' => pacol( 1, 1, 1, 'text', 'Autoconfig_incoming_server' , '' , '' ), + 'outgoing_server' => pacol( 1, 1, 1, 'text', 'Autoconfig_outgoing_server' , '' , '' ), + 'type' => pacol( 1, 1, 1, 'text', 'Autoconfig_type' , '' , '' ), + 'hostname' => pacol( 1, 1, 1, 'text', 'Autoconfig_hostname' , '' , '' ), + 'port' => pacol( 1, 1, 1, 'integer', 'Autoconfig_port' , '' , '' ), + 'socket_type' => pacol( 1, 1, 1, 'text', 'Autoconfig_socket_type' , '' , '' ), + 'auth' => pacol( 1, 1, 1, 'text', 'Autoconfig_auth' , '' , '' ), + 'username' => pacol( 1, 1, 1, 'text', 'Autoconfig_username' , '' , '' ), + 'leave_messages_on_server' => pacol( 1, 1, 1, 'boolean', 'Autoconfig_leave_messages_on_server' , '' , '' ), + 'download_on_biff' => pacol( 1, 1, 1, 'boolean', 'Autoconfig_download_on_biff' , '' , '' ), + 'days_to_leave_messages_on_server' => pacol( 1, 1, 1, 'integer', 'Autoconfig_days_to_leave_messages_on_server' , '' , '' ), + 'check_interval' => pacol( 1, 1, 1, 'integer', 'Autoconfig_check_interval' , '' , '' ), + 'enable' => pacol( 1, 1, 1, 'text', 'Autoconfig_enable' , '' , '' ), + 'enable_status' => pacol( 1, 1, 1, 'text', '' , '' , '' ), + 'enable_url' => pacol( 1, 1, 1, 'text', 'Autoconfig_enable_url' , '' , '' ), + 'enable_instruction' => pacol( 1, 1, 1, 'text', 'Autoconfig_enable_instruction' , '' , '' ), + 'documentation' => pacol( 1, 1, 1, 'text', 'Autoconfig_documentation' , '' , '' ), + 'documentation_status' => pacol( 1, 1, 1, 'text', '' , '' , '' ), + 'documentation_url' => pacol( 1, 1, 1, 'text', 'Autoconfig_documentation_url' , '' , '' ), + 'documentation_desc' => pacol( 1, 1, 1, 'text', 'Autoconfig_documentation_desc' , '' , '' ), + 'webmail' => pacol( 1, 1, 1, 'text', 'Autoconfig_webmail' , '' , '' ), + 'webmail_login_page' => pacol( 1, 1, 1, 'text', 'Autoconfig_webmail_login_page' , '' , '' ), + 'webmail_login_page_info' => pacol( 1, 1, 1, 'text', 'Autoconfig_webmail_login_page_info' , '' , '' ), + 'lp_info_url' => pacol( 1, 1, 1, 'text', 'Autoconfig_lp_info_url' , '' , '' ), + 'lp_info_username' => pacol( 1, 1, 1, 'text', 'Autoconfig_lp_info_username' , '' , '' ), + 'lp_info_username_field_id' => pacol( 1, 1, 1, 'text', 'Autoconfig_lp_info_username_field_id' , '' , '' ), + 'lp_info_username_field_name' => pacol( 1, 1, 1, 'text', 'Autoconfig_lp_info_username_field_name' , '' , '' ), + 'lp_info_login_button_id' => pacol( 1, 1, 1, 'text', 'Autoconfig_lp_info_login_button_id' , '' , '' ), + 'lp_info_login_button_name' => pacol( 1, 1, 1, 'text', 'Autoconfig_lp_info_login_button_name' , '' , '' ), + // Mac Mail specific fields + 'account_name' => pacol( 1, 1, 1, 'text', 'Autoconfig_account_name' , '' , '' ), + // Typically 'email' + 'account_type' => pacol( 1, 1, 1, 'text', 'Autoconfig_account_type' , '' , '' ), + 'email' => pacol( 1, 1, 1, 'text', 'Autoconfig_email' , '' , '' ), + 'ssl_enabled' => pacol( 1, 1, 1, 'boolean', 'Autoconfig_ssl' , '' , '' ), + // Will be empty obviously unless the user enters it in the form + 'password' => pacol( 1, 1, 1, 'text', 'Autoconfig_password' , '' , '' ), + // Used for payload_description + 'description' => pacol( 1, 1, 1, 'text', 'Autoconfig_description' , '' , '' ), + 'organisation' => pacol( 1, 1, 1, 'text', 'Autoconfig_organisation' , '' , '' ), + // regular account, or Microsoft Exchange + 'type' => pacol( 1, 1, 1, 'text', 'Autoconfig_type' , '' , '' ), + 'prevent_app_sheet' => pacol( 1, 1, 1, 'boolean', 'Autoconfig_prevent_app_sheet' , '' , '' ), + 'prevent_move' => pacol( 1, 1, 1, 'boolean', 'Autoconfig_prevent_move' , '' , '' ), + 'smime_enabled' => pacol( 1, 1, 1, 'boolean', 'Autoconfig_smime_enabled' , '' , '' ), + 'payload_remove_ok' => pacol( 1, 1, 1, 'boolean', 'Autoconfig_payload_remove_ok' , '' , '' ), + // Outlook specific fields + // domain_required -> Not sure this should be an option; false by default + 'spa' => pacol( 1, 1, 1, 'text', 'Autoconfig_spa' , '' , '' ), + ); + } + + /** + * @param string $username + */ + public function __construct( $username ) + { + $this->username = $username; + if( authentication_has_role('admin') ) + { + $this->is_admin = true; + $this->all_domains = list_domains_for_admin( $username ); + // Get the list of configuration ids, if any, this admin is allowed to access + $this->allowed_config_ids = $this->get_config_ids_for_user( $username ); + } + } + + protected function initMsg() + { + // Need to develop this part + } + + /** + * @return array + */ + public function webformConfig() + { + return array( + # $PALANG labels + 'formtitle_create' => 'pAutoconfig_page_title', + 'formtitle_edit' => 'pAutoconfig_page_title', + 'create_button' => 'save', + + # various settings + 'required_role' => 'admin', + 'listview' => 'list-virtual.php', + 'early_init' => 1, # 0 for create-domain + ); + } + + protected function validate_new_id() + { + # autoconfig can only be enabled if a domain name exists + if( $this->is_admin ) + { + if( count( $this->all_domains ) > 0 ) + { + return( true ); + } + } + else + { + // Need to develop this part + return( true ); + } + + # still here? This means the mailbox doesn't exist or the admin/user doesn't have permissions to view it + $this->errormsg[] = Config::Lang('invalid_parameter'); + return( false ); + } + + public function get_config_ids_for_user( $user ) + { + $table_autoconfig = table_by_key('autoconfig'); + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $table_domain_admins = table_by_key('domain_admins'); + $table_domain = table_by_key('domain'); + // This is a super admin, so he/she has access to all configs + if( authentication_has_role( 'global-admin' ) ) + { + // $sql = "SELECT DISTINCT ad.config_id FROM $table_autoconfig_domains ad LEFT JOIN $table_domain d ON ad.domain = d.domain WHERE d.domain != 'ALL AND d.active IS TRUE'"; + // global admin has access to all config + $sql = "SELECT c.config_id FROM $table_autoconfig c"; + } + // This is a per-domain admin, so we use the table domain_admis to cross check which configuration he/she has access + elseif( authentication_has_role( 'admin' ) ) + { + $E_username = escape_string( $user ); + $sql = "SELECT DISTINCT ad.config_id FROM $table_domain d LEFT JOIN $table_autoconfig_domains ad ON ad.domain = d.domain WHERE d.active IS TUE AND d.username='$E_username'"; + } + $res = db_query( $sql ); + if( !empty( $res['error'] ) ) + { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + if( DEBUG ) error_log( "get_config_ids_for_user() \$sth = " . print_r( $sth, true ) ); + $all = $this->db_fetchall( $sth ); + $list = []; + foreach( $all as $row ) + { + $list[] = $row['config_id']; + } + return( $list ); + } + + public function has_permission_over_config_id( $user, $this_config_id ) + { + if( empty( $user ) || empty( $this_config_id ) ) + { + if( $this->debug ) error_log( "has_permission_over_config_id() user is empty, or no config was provided." ); + return( false ); + } + $table_admin = table_by_key('admin'); + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $table_domain_admins = table_by_key('domain_admins'); + $E_username = escape_string( $user ); + $E_config_id = escape_string( $this_config_id ); + $sql_admin = "SELECT a.* FROM $table_admin a WHERE a.username = '$E_username'"; + $res = db_query( $sql_admin ); + if( !empty( $res['error'] ) ) + { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $row = $this->db_assoc( $sth ); + if( !empty( $row ) ) + { + // Admin status is not active + if( !$row['active'] ) + { + if( $this->debug ) error_log( "has_permission_over_config_id() config $this_config_id is not active." ); + return( false ); + } + // Admin is a super admin, so he has access to everything + elseif( $row['superadmin'] ) + { + if( $this->debug ) error_log( "has_permission_over_config_id() user $user is a super admin, returning true." ); + return( true ); + } + else + { + $true = db_get_boolean( true ); + $sql = "SELECT c.config_id FROM $table_autoconfig_domains c LEFT JOIN $table_domain_admins da ON da.domain = c.domain WHERE da.username = '$E_username' AND da.active = '$true' AND c.config_id = '$E_config_id'"; + if( $this->debug ) error_log( "has_permission_over_config_id() checking user '$user' permission over config '$this_config_id' with sql query: $sql" ); + $res = db_query( $sql ); + if( !empty( $res['error'] ) ) + { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $row = $this->db_assoc( $sth ); + return( !empty( $row ) ); + } + } + // This is a regular user + else + { + return( false ); + } + } + + public function allowed_ids() + { + return( $this->allowed_config_ids ); + } + + public function config_id( $id ) + { + if( isset( $id ) ) + { + if( $this->debug ) error_log( "config_id() checking config id \"$id\"." ); + $table_autoconfig = table_by_key('autoconfig'); + $E_id = escape_string( $id ); + $sql = "SELECT config_id FROM $table_autoconfig WHERE config_id = '$E_id'"; + $res = db_query( $sql ); + if( !empty( $res['error'] ) ) + { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $row = $this->db_assoc( $sth ); + if( empty( $row ) ) + { + if( $this->debug ) error_log( "config_id() could not find config id \"$id\"." ); + return( false ); + } + if( $this->debug ) error_log( "config_id() config id \"$id\" found." ); + $this->config_id = $row['config_id']; + } + return( $this->config_id ); + } + + public function db_assoc( $sth ) + { + if( empty( $sth ) ) throw( "No statement handler was provided." ); + try + { + return( $sth->fetch( PDO::FETCH_ASSOC ) ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + return( false ); + } + } + + public function db_fetchall( $sth ) + { + if( empty( $sth ) ) throw( "No statement handler was provided." ); + // if( DEBUG ) error_log( "db_fetchall() \$sth = " . print_r( $sth, true ) ); + try + { + return( $sth->fetchAll(PDO::FETCH_ASSOC) ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + return( false ); + } + } + + public function db_rows( $sth ) + { + if( empty( $sth ) ) throw( "No statement handler was provided." ); + try + { + return( $sth->rowCount() ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + return( false ); + } + } + + public function error_as_string() + { + if( is_array( $this->error ) ) + { + return( implode( ', ', $this->error ) ); + } + else + { + return( $this->error ); + } + } + + private function get_config( $id ) + { + if( !isset( $id ) ) $id = $this->config_id; + $table_autoconfig = table_by_key('autoconfig'); + $E_config_id = escape_string( $id ); + $sql = "SELECT * FROM $table_autoconfig WHERE config_id = '$E_config_id'"; + $res = db_query( $sql ); + if( !empty( $res['error'] ) ) + { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $row = $this->db_assoc( $sth ); + if( empty( $row ) ) + { + return( false ); + } + return( $row ); + } + + // Get the list of config id that this user has access + public function get_config_ids() + { + $table_autoconfig = table_by_key('autoconfig'); + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $table_domain_admins = table_by_key('domain_admins'); + $true = db_get_boolean( true ); + $E_username = escape_string( $this->username ); + $sql = "SELECT distinct c.config_id, c.provider_id FROM $table_autoconfig_domains d LEFT JOIN $table_autoconfig c ON c.config_id = d.config_id LEFT JOIN $table_domain_admins da ON da.username = '$E_username' AND da.active = '$true' AND (da.domain = 'ALL' OR da.domain = d.domain)"; + $res = db_query( $sql ); + if( !empty( $res['error'] ) ) + { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $all = $this->db_fetchall( $sth ); + $list = []; + foreach( $all as $row ) + { + $list[$row['config_id']] = $row['provider_id']; + } + if( $this->debug ) error_log( "get_config_ids() returning '" . print_r( $list, true ) . "'" ); + return( $list ); + } + + public function get_id_by_domain( $thisDomain ) + { + if( !isset( $thisDomain ) ) + { + // Are the domain names for this autoconfig set ? + if( !isset( $this->domains ) ) + { + return( false ); + } + // Pick one + $thisDomain = $this->domains[0]; + } + $E_domain = escape_string( $thisDomain ); + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $sql = "SELECT config_id FROM $table_autoconfig_domains WHERE domain = '$E_domain'"; + $res = db_query( $sql ); + if( !empty( $res['error'] ) ) + { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $row = $this->db_assoc( $sth ); + if( empty( $row ) ) + { + return( false ); + } + return( $row['config_id'] ); + } + + private function get_domains( $id ) + { + if( count( $this->domains ) ) + { + return( $this->domains ); + } + elseif( !isset( $id ) ) + { + if( !empty( $this->config_id ) ) + { + $id = $this->config_id; + } + else + { + return( false ); + } + } + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $table_domain_admins = table_by_key('domain_admins'); + $E_config_id = escape_string( $id ); + $E_username = escape_string( $this->username ); + // Make sure the admin can only get the list of domain names he is in charge of + $sql = "SELECT d.domain FROM $table_autoconfig_domains AS d LEFT JOIN $table_domain_admins AS da ON da.username = '$E_username' AND (da.domain = 'ALL' OR da.domain = d.domain) WHERE d.config_id = '$E_config_id'"; + if( $this->debug ) error_log( "get_domains() executing following query to get the list of authorised domains: $sql" ); + $res = db_query( $sql ); + if( !empty( $res['error'] ) ) + { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $all = $this->db_fetchall( $sth ); + $list = []; + foreach( $all as $row ) + { + $list[] = $row['domain']; + } + if( $this->debug ) error_log( "get_domains() returning '" . print_r( $list, true ) . "'" ); + return( $list ); + } + + public function get_other_config_domains( $id ) + { + if( is_null( $id ) ) + { + $id = $this->config_id; + } + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $table_domain_admins = table_by_key('domain_admins'); + $true = db_get_boolean( true ); + $E_username = escape_string( $this->username ); + if( !empty( $id ) ) + { + $E_config_id = escape_string( $id ); + $sql = "SELECT d.domain FROM autoconfig_domains d LEFT JOIN domain_admins da ON da.username = '$E_username' AND da.active = '$true' AND (da.domain = 'ALL' OR da.domain = d.domain) WHERE d.config_id != '$E_config_id'"; + } + else + { + $sql = "SELECT d.domain FROM autoconfig_domains d LEFT JOIN domain_admins da ON da.username = '$E_username' AND da.active = '$true' AND (da.domain = 'ALL' OR da.domain = d.domain)"; + } + $res = db_query( $sql ); + if( !empty( $res['error'] ) ) + { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $all = $this->db_fetchall( $sth ); + $list = []; + foreach( $all as $row ) + { + $list[] = $row['domain']; + } + return( $list ); + } + + private function get_hosts( $type, $id ) + { + if( empty( $type ) ) + { + return( false ); + } + elseif( $type != 'in' && $type != 'out' ) + { + return( false ); + } + + if( !isset( $id ) ) + { + if( !empty( $this->config_id ) ) + { + $id = $this->config_id; + } + else + { + return( false ); + } + } + + $table_autoconfig_hosts = table_by_key('autoconfig_hosts'); + $E_config_id = escape_string( $id ); + if( $type == 'in' ) + { + $sql = "SELECT *, id AS \"host_id\" FROM $table_autoconfig_hosts WHERE (type = 'imap' OR type = 'pop3') AND config_id = '$E_config_id' ORDER BY priority"; + } + else + { + $sql = "SELECT *, id AS \"host_id\" FROM $table_autoconfig_hosts WHERE type = 'smtp' AND config_id = '$E_config_id' ORDER BY priority"; + } + $res = db_query( $sql ); + if( !empty( $res['error'] ) ) + { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $all = $this->db_fetchall( $sth ); + $list = []; + foreach( $all as $row ) + { + $list[] = $row; + } + return( $list ); + } + + private function get_text( $type, $id ) + { + // Must provide explicitely the type + if( empty( $type ) ) + { + return( false ); + } + elseif( $type != 'instruction' && $type != 'documentation' ) + { + return( false ); + } + if( !isset( $id ) ) + { + if( !empty( $this->config_id ) ) + { + $id = $this->config_id; + } + else + { + return( false ); + } + } + $table_autoconfig_text = table_by_key('autoconfig_text'); + $E_type = escape_string( $type ); + $E_config_id = escape_string( $id ); + $sql = "SELECT * FROM $table_autoconfig_text WHERE type = '$E_type' AND config_id = '$E_config_id'"; + $res = db_query( $sql ); + if( !empty( $res['error'] ) ) + { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $all = $this->db_fetchall( $sth ); + $list = []; + foreach( $all as $row ) + { + $list[] = $row; + } + return( $list ); + } + + public function get_details( $id ) + { + if( !$this->is_admin ) return( false ); + if( empty( $id ) ) + { + if( $this->debug ) error_log( "No id provided, returning false." ); + return( false ); + } + // Some security: do not get the details for this configuration if the user does not have permission + if( !$this->has_permission_over_config_id( $this->username, $id ) ) + { + if( $this->debug ) error_log( sprintf( "Admin %s has no permission over this config %s, returning false.", $this->username, $id ) ); + return( false ); + } + $config = []; + $conf_domains = []; + $conf_hosts_in = []; + $conf_hosts_out = []; + $conf_texts_inst = []; + $conf_texts_doc = []; + if( ( $config = $this->get_config( $id ) ) === false ) + { + if( $this->debug ) error_log( "Unable to get basic config data, returning false." ); + return( false ); + } + // If active is null, set it to true by default + if( is_null( $config['active'] ) ) $config['active'] = true; + $booleanProperties = array( 'enable_status', 'documentation_status', 'ssl_enabled', 'prevent_app_sheet', 'prevent_move', 'smime_enabled', 'payload_remove_ok', 'active', 'spa' ); + $this->encode_boolean( $config, $booleanProperties ); + if( ( $conf_domains = $this->get_domains( $id ) ) === false ) + { + if( $this->debug ) error_log( "Unable to get config domains, returning false." ); + return( false ); + } + if( $this->debug ) error_log( sprintf( "Found %d domains for config $id", count( $conf_domains ) ) ); + $config['provider_domain'] = $conf_domains; + // To build the domain names html menu + $config['provider_domain_options'] = $this->all_domains; + if( $this->debug ) error_log( sprintf( "Found %d total domains for config $id", count( $conf_domains ) ) ); + + // Get incoming servers + if( ( $conf_hosts_in = $this->get_hosts( 'in', $id ) ) === false ) + { + if( $this->debug ) error_log( "Unable to get config incoming hosts, returning false." ); + return( false ); + } + if( $this->debug ) error_log( sprintf( "Found %d incoming servers for config $id", count( $conf_hosts_in ) ) ); + $booleanProperties = array( 'leave_messages_on_server', 'download_on_biff' ); + // foreach( $conf_hosts_in as $conf_hosts_ref ) + for( $j = 0; $j < count( $conf_hosts_in ); $j++ ) + { + $conf_hosts_in[$j] = $this->encode_boolean( $conf_hosts_in[$j], $booleanProperties ); + if( $this->debug ) error_log( "get_details(): incoming host data is now: " . print_r( $conf_hosts_in[$j], true ) ); + } + $config['incoming_server'] = $conf_hosts_in; + + // Get outgoing servers + if( ( $conf_hosts_out = $this->get_hosts( 'out', $id ) ) === false ) + { + if( $this->debug ) error_log( "Unable to get config outgoing hosts, returning false." ); + return( false ); + } + if( $this->debug ) error_log( sprintf( "Found %d outgoing servers for config $id", count( $conf_hosts_out ) ) ); + // foreach( $conf_hosts_out as $conf_hosts_ref ) + for( $j = 0; $j < count( $conf_hosts_out ); $j++ ) + { + $conf_hosts_out[$j] = $this->encode_boolean( $conf_hosts_out[$j], $booleanProperties ); + } + $config['outgoing_server'] = $conf_hosts_out; + + // Get enabling instructions, if any + if( ( $conf_texts_inst = $this->get_text( 'instruction', $id ) ) === false ) + { + if( $this->debug ) error_log( "Unable to get enabling instrucctions, returning false." ); + return( false ); + } + if( $this->debug ) error_log( sprintf( "Found %d enable instruction(s) for config $id", count( $conf_texts_inst ) ) ); +// $langs = array(); +// foreach( $conf_texts_inst as $ref ) +// { +// $langs[ $ref['lang'] ] = $ref['phrase']; +// } + $config['enable'] = array( + 'url' => $config['enable_url'], + 'instruction' => $conf_texts_inst, + ); + + // Configuration support documentation + if( ( $conf_texts_doc = $this->get_text( 'documentation', $id ) ) === false ) + { + if( $this->debug ) error_log( "Unable to get documentation description, returning false." ); + return( false ); + } + if( $this->debug ) error_log( sprintf( "Found %d documentatoin description(s) for config $id", count( $conf_texts_doc ) ) ); +// $langs = array(); +// foreach( $conf_texts_doc as $ref ) +// { +// $langs[ $ref['lang'] ] = $ref['phrase']; +// } + $config['documentation'] = array( + 'url' => $config['documentation_url'], + 'description' => $conf_texts_doc, + ); + if( $this->debug ) error_log( sprintf( "Returning hash ref with %d keys", count( array_keys( $config ) ) ) ); + return( $config ); + } + + public function remove_config( $id ) + { + global $CONF, $PALANG; + $conf_data = array(); + if( empty( $id ) ) + { + $this->error = $PALANG['pAutoconfig_no_config_id_provded']; + return( false ); + } + elseif( ( $conf_data = $this->get_config( $id ) ) === false ) + { + $this->error = sprintf( $PALANG['pAutoconfig_config_id_not_found'], $id ); + return( false ); + } + elseif( !$this->has_permission_over_config_id( $this->username, $id ) ) + { + $this->error = sprintf( $PALANG['pAutoconfig_lack_permission_over_config_id'], $id ); + return( false ); + } + $table_autoconfig = table_by_key('autoconfig'); + $ok = 0; + try + { + // $ok = db_delete( $table_autoconfig, 'config_id', $id ); + // I need this to throw an exception so I can report the issue + $ok = db_execute( "DELETE FROM $table_autoconfig WHERE config_id = ?", array($id), true ); + } + catch( Exception $e ) + { + if( DEBUG ) error_log( "remove_config(): An error occurred while trying to remove config id $id: " . $e->getMessage() ); + $this->error = $e->getMessage(); + return( false ); + } + return( $ok ); + } + + public function save_config( &$data ) + { + global $CONF, $PALANG; + // Number of rows changed + $ok = 0; + if( !is_array( $data ) ) + { + $this->error = $PALANG['pAutoconfig_save_no_data_provided']; + return( false ); + } + elseif( !isset( $data['provider_domain'] ) ) + { + $this->error = $PALANG['pAutoconfig_no_domain_names_have_been_selected']; + return( false ); + } + // Should not happen + elseif( !is_array( $data['provider_domain'] ) ) + { + $this->error = $PALANG['pAutoconfig_domain_data_provided_is_not_an_array']; + return( false ); + } + elseif( count( $data['provider_domain'] ) == 0 ) + { + $this->error = $PALANG['pAutoconfig_no_domain_names_have_been_selected']; + return( false ); + } + $config_data = array( + 'encoding' => @$data['encoding'], + 'provider_id' => @$data['provider_id'], + 'provider_name' => @$data['provider_name'], + 'provider_short' => @$data['provider_short'], + 'enable_status' => @$data['enable_status'], + 'enable_url' => @$data['enable_url'], + 'documentation_status' => @$data['documentation_status'], + 'documentation_url' => @$data['documentation_url'], + 'webmail_login_page' => @$data['webmail_login_page'], + 'lp_info_url' => @$data['lp_info_url'], + 'lp_info_username_field_id' => @$data['lp_info_username_field_id'], + 'lp_info_username_field_name' => @$data['lp_info_username_field_name'], + 'lp_info_login_button_id' => @$data['lp_info_login_button_id'], + 'lp_info_login_button_name' => @$data['lp_info_login_button_name'], + 'account_name' => @$data['account_name'], + 'account_type' => @$data['account_type'], + 'email' => @$data['email'], + 'ssl_enabled' => @$data['ssl_enabled'], + 'description' => @$data['description'], + 'organisation' => @$data['organisation'], + 'payload_type' => @$data['payload_type'], + 'prevent_app_sheet' => @$data['prevent_app_sheet'], + 'prevent_move' => @$data['prevent_move'], + 'smime_enabled' => @$data['smime_enabled'], + 'payload_remove_ok' => @$data['payload_remove_ok'], + 'spa' => @$data['spa'], + 'active' => @$data['active'], + 'sign_option' => @$data['sign_option'], + 'cert_filepath' => @$data['cert_filepath'], + 'privkey_filepath' => @$data['privkey_filepath'], + 'chain_filepath' => @$data['chain_filepath'], + ); + $dataError = null; + if( ( $dataError = $this->check_autoconfig_data( $config_data ) ) != null ) + { + $this->error = $dataError; + return( false ); + } + // In case of update + elseif( !empty( $data['config_id'] ) ) + { + // Should not be happening, but let's not assume anything + if( empty( $this->config_id ) ) + { + $this->error = $PALANG['pAutoconfig_no_config_id_declared']; + return( false ); + } + elseif( $data['config_id'] != $this->config_id ) + { + if( $this->debug ) error_log( sprintf( "save_config() config id submitted \"%s\" is not the same as our current id \"%s\"", $data['config_id'], $this->config_id ) ); + $this->error = sprintf( $PALANG['pAutoconfig_config_id_submitted_is_unauthorised'], $data['config_id'] ); + return( false ); + } + } + // For the rest, there could be no imap, pop3 or smtp declared. That's up to the user who is always right + // Likewise, there could be no login enable instruction or support documentation, so we don't make them mandatory + if( DEBUG ) error_log( "Base config data are: " . print_r( $config_data, true ) ); + + $table_autoconfig = table_by_key('autoconfig'); + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $table_autoconfig_hosts = table_by_key('autoconfig_hosts'); + $table_autoconfig_text = table_by_key('autoconfig_text'); + + // Start sql transaction + db_begin(); + try + { + $is_new = empty( $this->config_id ); + if( !$is_new ) + { + try + { + $ok = db_update( 'autoconfig', 'config_id', $this->config_id, $config_data, array('modified'), true ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + $config_data['config_id'] = $this->config_id; + } + // New entry + else + { + $this->config_id = $config_data['config_id'] = $data['config_id'] = $this->generate_uuid_v4(); + try + { + $ok = db_insert( 'autoconfig', $config_data, array('created', 'modified'), true ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + if( $ok == 0 ) + { + $this->error = $PALANG['pAutoconfig_failed_to_add_config']; + db_rollback(); + return( false ); + } + } + + // Process domain names. We get the current list, first remove the ones that have been removed and add the new ones + $selected_domains = $data['provider_domain']; + if( !$is_new ) + { + if( $this->debug ) error_log( "save_config() get current domain names for this update." ); + $current_domains = array(); + if( ( $current_domains = $this->get_domains( $this->config_id ) ) === false ) + { + $this->error = $PALANG['pAutoconfig_no_domain_authorised_for_this_admin']; + db_rollback(); + return( false ); + } + // First remove the ones that are not anymore in our selection + // $E_config_id = escape_string( $this->config_id ); + foreach( $current_domains as $domain ) + { + if( !in_array( $domain, $selected_domains ) ) + { + try + { + // $ok += db_delete( $table_autoconfig_domains, 'domain', $domain, "AND config_id = '$E_config_id'" ); + $ok += db_execute( "DELETE FROM $table_autoconfig_domains WHERE domain = ? AND config_id = ?", array( $domain, $this->config_id ), true ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + } + } + } + // Now, add the ones selected that are not in the current domains + $current_domains = array(); + if( !empty( $this->config_id ) ) + { + if( $this->debug ) error_log( "save_config() get current domain names for this config id \"" . $this->config_id . "\"." ); + if( ( $current_domains = $this->get_domains( $this->config_id ) ) === false ) + { + if( $this->debug ) error_log( "save_config() get_domains returned: '" . print_r( $current_domains, true ) . "'." ); + $this->error = $PALANG['pAutoconfig_no_domain_authorised_for_this_admin']; + db_rollback(); + return( false ); + } + } + + foreach( $selected_domains as $domain ) + { + if( !in_array( $domain, $current_domains ) ) + { + $this_data = array( + 'config_id' => $this->config_id, + 'domain' => $domain, + ); + try + { + $added = db_insert( 'autoconfig_domains', $this_data, [], true ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + if( $added == 0 ) + { + $this->error = sprintf( $PALANG['pAutoconfig_failed_to_add_domain_to_config'], $domain ); + db_rollback(); + return( false ); + } + else + { + $ok += $added; + } + } + } + $config_data['provider_domain'] = $selected_domains; + $config_data['provider_domain_options'] = $this->all_domains; + + // Process hosts, if any + // First, get current host, and remove the ones that have been removed + if( !$is_new ) + { + $host_types = ['in','out']; + foreach( $host_types as $this_type ) + { + $current_servers = $this->get_hosts( $this_type, $this->config_id ); + // There must be at least one host for each type, even if blank + if( !array_key_exists( 'host_id', $data ) ) + { + error_log( "No host id could be found at all from the web data submitted for config id \"" . $this->config_id . "\" which is" . ( $is_new ? '' : ' not' ) . " new." ); + $this->error = "No host id could be found at all from the web data submitted for config id \"" . $this->config_id . "\" which is" . ( $is_new ? '' : ' not' ) . " new."; + db_rollback(); + return( false ); + } + foreach( $current_servers as $ref ) + { + if( !in_array( $ref['host_id'], $data['host_id'] ) ) + { + try + { + // $deleted = db_delete( $table_autoconfig_hosts, 'id', $ref['host_id'] ); + $deleted = db_execute( "DELETE FROM $table_autoconfig_hosts WHERE id = ?", array( $ref['host_id'] ), true ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + } + } + } + } + + if( count( $data['hostname'] ) > 0 ) + { + // counter by type + $counter = []; + // To check for duplicates + $processed = []; + for( $i = 0; $i < count( $data['hostname'] ); $i++ ) + { + $host_data = array( + 'host_id' => @$data['host_id'][$i], + 'type' => @$data['type'][$i], + 'hostname' => @$data['hostname'][$i], + 'port' => @$data['port'][$i], + 'socket_type' => @$data['socket_type'][$i], + 'auth' => @$data['auth'][$i], + 'username' => @$data['username'][$i], + 'leave_messages_on_server' => @$data['leave_messages_on_server'][$i], + 'download_on_biff' => @$data['download_on_biff'][$i], + 'days_to_leave_messages_on_server' => @$data['days_to_leave_messages_on_server'][$i], + 'check_interval' => @$data['check_interval'][$i], + 'priority' => ++$counter[$data['type'][$i]], + ); + if( ( $dataError = $this->check_autoconfig_host_data( $host_data ) ) != null ) + { + $this->error = $dataError; + db_rollback(); + return( false ); + } + elseif( array_key_exists( $host_data['hostname'], $processed ) && + $processed[ $host_data['hostname'] ]['type'] == $host_data['type'] && + $processed[ $host_data['hostname'] ]['port'] == $host_data['port'] ) + { + $this->error = sprintf( $PALANG['pAutoconfig_duplicate_host'], $host_data['hostname'], $host_data['type'], $host_data['port'] ); + db_rollback(); + return( false ); + } + $processed[ $host_data['hostname'] ] = array( 'type' => $host_data['type'], 'port' => $host_data['port'] ); + + if( !empty( $host_data['host_id'] ) ) + { + // This was just temporary for checking. There is no host_id field + $this_id = $host_data['host_id']; + unset( $host_data['host_id'] ); + try + { + $ok += db_update( 'autoconfig_hosts', 'id', $this_id, $host_data, [], true ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + } + else + { + unset( $host_data['host_id'] ); + $host_data['config_id'] = $config_data['config_id']; + try + { + $added = db_insert( 'autoconfig_hosts', $host_data, [], true ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + if( $added == 0 ) + { + $this->error = sprintf( $PALANG['pAutoconfig_failed_to_add_host_to_config'], $host_data['hostname'] ); + db_rollback(); + return( false ); + } + else + { + $ok += $added; + } + } + } + } + $host_types = ['in','out']; + foreach( $host_types as $this_type ) + { + $current_servers = $this->get_hosts( $this_type, $this->config_id ); + if( $this_type == 'in' ) + { + $config_data['incoming_server'] = $current_servers; + } + else + { + $config_data['outgoing_server'] = $current_servers; + } + } + + // First remove instructions or documentation that have been removed from the interface + $textTypes = array( 'instruction', 'documentation' ); + if( !$is_new ) + { + foreach( $textTypes as $textType ) + { + $all_text = []; + // No need to bother checking one by one, if there are no text at all + if( !array_key_exists( "${textType}_id", $data ) || + ( is_array( $data["${textType}_id"] ) && count( $data["${textType}_id"] ) == 0 ) ) + { + try + { + // $ok += db_delete( $table_autoconfig_text, 'config_id', $this->config_id ); + $ok += db_execute( "DELETE FROM $table_autoconfig_text WHERE config_id = ?", array( $this->config_id ), true ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + continue; + } + // An error occurred. Need to report it: TODO + if( ( $all_text = $this->get_text( $textType, $this->config_id ) ) === false ) + { + error_log( "An error occurred. Could not get all the text type ${textType} for config id \"" . $this->config_id . "\"." ); + db_rollback(); + $this->error = "An error occurred. Could not get all the text type ${textType} for config id \"" . $this->config_id . "\"."; + return( false ); + } + + // One by one. If our existing text id for this type is not in the array of ids, then remove it + foreach( $all_text as $ref ) + { + if( empty( $ref['id'] ) ) + { + error_log( "Somehow, I got an empty text id from function get_text() for config \"" . $this->config_id . "\"." ); + db_rollback(); + $this->error = "Somehow, I got an empty text id from function get_text() for config \"" . $this->config_id . "\"."; + return( false ); + } + if( !in_array( $ref['id'], $data["${textType}_id"] ) ) + { + try + { + // $ok += db_delete( $table_autoconfig_text, 'id', $ref['id'] ); + $ok += db_execute( "DELETE FROM $table_autoconfig_text WHERE id = ?", array( $ref['id'] ), true ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + } + } + } + } + + // Now, do the additions + // Process login enable instruction and support documentation + foreach( $textTypes as $textType ) + { + $existing_langs = []; + if( array_key_exists( "${textType}_lang", $data ) && + is_array( $data["${textType}_lang"] ) && + count( $data["${textType}_lang"] ) ) + { + for( $i = 0; $i < count( $data["${textType}_lang"] ); $i++ ) + { + $text_data = array( + 'type' => $textType, + 'id' => @$data["${textType}_id"][$i], + 'lang' => @$data["${textType}_lang"][$i], + 'phrase' => @$data["${textType}_text"][$i], + ); + // The text is empty: no need to go further + if( preg_match( '/^[[:blank:]\r\n]*$/', $text_data['phrase'] ) ) + { + continue; + } + // Found a language duplicate + elseif( in_array( $text_data['lang'], $existing_langs ) ) + { + $this->error = sprintf( $PALANG['pAutoconfig_text_language_already_used'], $text_data['lang'] ); + db_rollback(); + return( false ); + } + $existing_langs[] = $text_data['lang']; + + if( ( $dataError = $this->check_autoconfig_text_data( $text_data ) ) != null ) + { + $this->error = $dataError; + db_rollback(); + return( false ); + } + if( empty( $text_data['id'] ) ) + { + $text_data['config_id'] = $config_data['config_id']; + unset( $text_data['id'] ); + try + { + $added = db_insert( 'autoconfig_text', $text_data, [], true ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + if( $added == 0 ) + { + $this->error = sprintf( $PALANG['pAutoconfig_failed_to_add_text_to_config'], mb_substr( $text_data['phrase'], 0, 12 ) ); + db_rollback(); + return( false ); + } + else + { + $ok += $added; + } + } + else + { + $textId = $text_data['id']; + unset( $text_data['id'] ); + try + { + $ok += db_update( 'autoconfig_text', 'id', $textId, $text_data, [], true ); + } + catch( Exception $e ) + { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + } + } + } + else + { + if( $this->debug ) error_log( "save_config() No lang found for text $textType" ); + } + + $all_text = []; + if( ( $all_text = $this->get_text( $textType, $this->config_id ) ) === false ) + { + error_log( "An error occurred. Could not get all the text type ${textType} for config id \"" . $this->config_id . "\"." ); + db_rollback(); + $this->error = "An error occurred. Could not get all the text type ${textType} for config id \"" . $this->config_id . "\"."; + return( false ); + } + + if( $textType == 'instruction' ) + { + $config_data['enable'] = array( + 'url' => $config_data['enable_url'], + 'instruction' => $all_text, + ); + } + // Otherwise this is the suppport documentation + else + { + $config_data['documentation'] = array( + 'url' => $config_data['documentation_url'], + 'description' => $all_text, + ); + } + } + // All clear, we commit the changes + db_commit(); + return( $config_data ); + } + catch( Exception $e ) + { + db_rollback(); + $this->error = $e->getMessage(); + return( false ); + } + } + + // Taken from StackOverflow: https://stackoverflow.com/a/44504979/4814971 + private function generate_uuid_v4() + { + if (function_exists('com_create_guid') === true) + return trim(com_create_guid(), '{}'); + + $data = PHP_MAJOR_VERSION < 7 ? openssl_random_pseudo_bytes(16) : random_bytes(16); + $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100 + $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10 + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); + } + + public function check_autoconfig_data( &$data ) + { + global $CONF, $PALANG; + $errorList = []; + if( !empty( $data['config_id'] ) ) + { + if( !$this->get_config( $data['config_id'] ) ) + { + $errorList[] = sprintf( $PALANG['pAutoconfig_config_id_not_found'], $data['config_id'] ); + } + elseif( !$this->has_permission_over_config_id( $this->username, $data['config_id'] ) ) + { + $errorList[] = sprintf( $PALANG['pAutoconfig_lack_permission_over_config_id'], $data['config_id'] ); + } + } + if( !empty( $data['encoding'] ) && !preg_match( '/^[a-zA-Z][\w\-]+$/', $data['encoding'] ) ) + { + $errorList[] = sprintf( $PALANG['pAutoconfig_invalid_encoding'], $data['encoding'] ); + } + if( empty( $data['provider_id'] ) ) + { + $errorList[] = $PALANG['pAutoconfig_empty_provider_id']; + } + // I do not check on purpose the file path of the cert, private key and chain (if any), because the user may have provided the information before setting up those files, and I do not want to annoy the user + $booleanProperties = array( 'enable_status', 'documentation_status', 'ssl_enabled', 'prevent_app_sheet', 'prevent_move', 'smime_enabled', 'payload_remove_ok', 'active', 'spa' ); + $this->decode_boolean( $data, $booleanProperties ); + if( count( $errorList ) == 0 ) + { + $this->empty2null( $data ); + return( null ); + } + else + { + return( $errorList ); + } + } + + public function check_autoconfig_domains( $domain_list ) + { + global $CONF, $PALANG; + $errorList = []; + if( count( $this->all_domains ) == 0 ) + { + $errorList[] = $PALANG['pAutoconfig_no_domain_allocated_to_admin']; + return( $errorList ); + } + elseif( !is_array( $domain_list ) ) + { + $errorList[] = $PALANG['pAutoconfig_data_provided_is_not_array']; + return( $errorList ); + } + // Nothing to check + elseif( !count( $domain_list ) ) + { + return( null ); + } + + $bad_domains = []; + foreach( $domain_list as $domain ) + { + if( !in_array( $domain, $this->all_domains ) ) + { + $bad_domains[] = $domain; + } + } + if( count( $bad_domains ) > 0 ) + { + $errorList[] = sprintf( $PALANG['pAutoconfig_unauthorised_domains'], join( ', ', $bad_domains ) ); + } + + if( count( $errorList ) == 0 ) + { + return( null ); + } + else + { + return( $errorList ); + } + } + + public function check_autoconfig_host_data( &$data ) + { + global $PALANG; + $errorList = []; + if( empty( $data['type'] ) ) + { + $errorList[] = $PALANG['pAutoconfig_host_no_type_provided']; + } + elseif( !preg_match( '/^imap|pop3|smtp$/i', $data['type'] ) ) + { + $errorList[] = $PALANG['pAutoconfig_host_invalid_type_value']; + } + else + { + $data['type'] = strtolower( $data['type'] ); + } + + if( !empty( $data['host_id'] ) ) + { + if( !preg_match( '/^\d+$/', $data['host_id'] ) ) + { + $errorList[] = sprintf( $PALANG['pAutoconfig_host_id_is_not_an_integer'], $data['host_id'] ); + } + } + if( empty( $data['hostname'] ) ) + { + $errorList[] = $PALANG['pAutoconfig_host_no_hostname_provided']; + } + if( strlen( $data['port'] ) == 0 ) + { + $errorList[] = $PALANG['pAutoconfig_host_no_port_provided']; + } + elseif( !preg_match( '/^\d+$/', $data['port'] ) ) + { + $errorList[] = $PALANG['pAutoconfig_host_port_is_not_an_integer']; + } + if( !empty( $data['socket_type'] ) && !preg_match( '/^SSL|STARTTLS|TLS$/', $data['socket_type'] ) ) + { + $errorList[] = sprintf( $PALANG['pAutoconfig_host_invalid_socket_type'], $data['socket_type'] ); + } + if( empty( $data['auth'] ) ) + { + $data['auth'] = 'none'; + } + // password-cleartext, password-encrypted (CRAM-MD5 or DIGEST-MD5), NTLM (Windows), GSSAPI (Kerberos), client-IP-address, TLS-client-cert, none, smtp-after-pop (for smtp), OAuth2 + elseif( !preg_match( '/^(password-cleartext|password-encrypted|NTLM|GSSAPI|client-IP-address|TLS-client-cert|smtp-after-pop|oauth2|none)$/i', $data['auth'] ) ) + { + $errorList[] = sprintf( $PALANG['pAutoconfig_host_invalid_auth_scheme'], $data['auth'] ); + } + // username may be blank in the case of authentication by ip for example + $booleanProperties = array( 'leave_messages_on_server', 'download_on_biff' ); + $this->decode_boolean( $data, $booleanProperties ); + if( strlen( $data['days_to_leave_messages_on_server'] ) > 0 && !preg_match( '/^\d+$/', $data['days_to_leave_messages_on_server'] ) ) + { + $errorList[] = $PALANG['pAutoconfig_host_days_on_server_is_not_an_integer']; + } + if( strlen( $data['check_interval'] ) > 0 && !preg_match( '/^\d+$/', $data['check_interval'] ) ) + { + $errorList[] = $PALANG['pAutoconfig_host_check_interval_is_not_an_integer']; + } + + if( count( $errorList ) == 0 ) + { + $this->empty2null( $data ); + return( null ); + } + else + { + return( $errorList ); + } + } + + public function check_autoconfig_text_data( &$data ) + { + global $PALANG; + $errorList = []; + if( !empty( $data['id'] ) && !preg_match( '//', $data['id'] ) ) + { + $errorList[] = sprintf( $PALANG['pAutoconfig_text_id_is_not_an_integer'], $data['id'] ); + } + if( empty( $data['type'] ) ) + { + $errorList[] = $PALANG['pAutoconfig_text_type_not_provided']; + } + elseif( !preg_match( '/^(instruction|documentation)$/', $data['type'] ) ) + { + $errorList[] = sprintf( $PALANG['pAutoconfig_text_type_invalid'], $data['type'] ); + } + if( empty( $data['lang'] ) ) + { + $errorList[] = $PALANG['pAutoconfig_text_lang_not_provided']; + } + elseif( !preg_match( '/^[a-zA-Z]{2}$/', $data['lang'] ) ) + { + $errorList[] = sprintf( $PALANG['pAutoconfig_text_lang_invalid'], $data['lang'] ); + } + if( empty( $data['phrase'] ) ) + { + $errorList[] = $PALANG['pAutoconfig_text_text_not_provided']; + } + + if( count( $errorList ) == 0 ) + { + $this->empty2null( $data ); + return( null ); + } + else + { + return( $errorList ); + } + } + + // Set the boolean value for web + private function encode_boolean( &$data, $booleanProperties ) + { + foreach( $booleanProperties as $prop ) + { + if( strlen( $data[ $prop ] ) > 0 ) + { + $data[ $prop ] = ( $data[ $prop ] == true ? 1 : 0 ); + } + } + return( $data ); + } + + private function decode_boolean( &$data, $booleanProperties ) + { + foreach( $booleanProperties as $prop ) + { + // if( DEBUG ) error_log( "decode_boolean() checking property '$prop' with value \"" . $data[ $prop ] . "\"." ); + if( strlen( $data[ $prop ] ) > 0 ) + { + // if( DEBUG ) error_log( "decode_boolean() is property '$prop' value equal to 1 ? " . ( $data[ $prop ] == 1 ? true : false ) ); + $data[ $prop ] = db_get_boolean( $data[ $prop ] == 1 ? true : false ); + // if( DEBUG ) error_log( "decode_boolean() property '$prop' now has value \"" . $data[ $prop ] . "\"." ); + } + // Remove the boolean field since it is null. Null is different from false + else + { + // unset( $data[ $prop ] ); + $data[ $prop ] = db_get_boolean( false ); + } + } + // Since we are dealing with data reference we should not need to return anything, but just in case. + return( $data ); + } + + // Set to null empty strings, so they can be stored as NULL in sql + private function empty2null( &$data ) + { + foreach( $data as $key => $val ) + { + if( empty( $val ) && $val !== 0 ) + { + $data[ $key ] = null; + } + } + } +}; +?> diff --git a/AUTOCONFIG/INSTALL.md b/AUTOCONFIG/INSTALL.md new file mode 100644 index 00000000..7aca1c58 --- /dev/null +++ b/AUTOCONFIG/INSTALL.md @@ -0,0 +1,157 @@ +Installation procedure for Autodiscovery configuration tool +============================================================ +* Author: [Jacques Deguest](mailto:jack@deguest.jp) +* Created: 2020-03-10 +* License: Same as Postfix Admin itself + +## Quick background & overview + +Autodiscovery is a somewhat standardised features that makes it possible for mail client to find out the proper configuration for a mail account, and to prevent the every day user from guessing the right parameters. + +Let's take the example of joe@example.com. + +When creating an account on Thurderbird and other who use the same configuration, the mail client will make a http query to + +See this page from Mozilla for more information: + +If a dns record exist + +For Outlook, the mail client will attempt a POST request to: and submit a xml-based request + +For Mac mail and iOS, the user needs to download a `mobileconfig` file, which is basically an xml file, that can be signed. + +Unfortunately, there is no auto discovery system for Mac/iOS mail, so you need to redirect your users to the `autoconfig.pl` cgi script under the Postfix Admin web root. You need to pass a `emailaddress` parameter for example + +## Installation + +### Dependencies + +#### SQL + +You need to activate the `uuid-ossp` PostgreSQL extension to use the UUID_V4. You can do that, as an admin logged on PostgreSQL, with `CREATE EXTENSION IF NOT EXISTS "uuid-ossp";` + +If you cannot or do not want to do that, edit the sql script for PostgreSQL and comment line 9 and uncomment line 11, comment line 72 and uncoment line 74, comment line 84 and uncomment line 86, comment line 107 and uncomment line 109 + +#### Perl + +The following perl modules are required. Most are standard core modules. + +* strict +* IO::File +* CGI v4.44 +* Email::Valid v1.202 +* Email::Address v1.912 +* XML::LibXML v2.0201 +* XML::LibXML::PrettyPrint v0.006 +* Data::Dumper v2.174 +* Scalar::Util v1.50 +* Data::UUID v1.224 +* File::Basename v2.85 +* Cwd v3.78 +* File::Temp v0.2309 +* File::Spec v3.78 +* File::Which v1.23 +* JSON v2.34 +* DBI v1.642 +* TryCatch v1.003002 +* Devel::StackTrace v2.04 + +For PostgreSQL you need `DBD::Pg`. I use version 3.8.1. + +For MySQL you need `DBD::mysql` any recent version should do. + +For SQLite, you need `DBD::SQLite`. I used version 1.62. + +You can install those module using `cpanm` (https://metacpan.org/pod/App::cpanminus) like: + +`cpanm --interactive IO::File CGI Email::Valid Email::Address XML::LibXML XML::LibXML::PrettyPrint Data::Dumper Scalar::Util Data::UUID File::Basename Cwd File::Temp File::Spec File::Which JSON DBI TryCatch Devel::StackTrace` + +#### Web + +* jQuery v3.3.1 (loaded automatically from within the template by calling ) + +#### Signature of mobileconfig files for Mac/iOS + +You need to have `openssl` installed. I used version 1.0.2g. You would also need ssl certificates installed for server wide or per domain. I recommend using Let's Encrypt by installing their command line too `certbot` + +### SQL + +Load the sql script `autoconfig.sql` into your Postfix Admin database. For exaple: + +* PostgreSQL : psql -U postfixadmin postfixadmin < autoconfig.sql + +* MySQL : mysql -u postfixadmin postfixadmin < autoconfig.sql + +* SQLite : sqlite3 /path/to/database.sqlite < autoconfig.sql + +This will create 4 separate tables. Rest assured that `autoconfig` does not alter in any way other areas of Postfix Admin database. + +### PHP, perl and other web files + +Move `AutoconfigHandler.php` under the `model` sub directory in the Postfix Admin root folder, and `autoconfig.php`, `autoconfig.pl`, `autoconfig.css`, `autoconfig.js` and `sprintf.js` under the Postfix Admin web root `public`: + +``` +mv ./AUTOCONFIG/autoconfig.{css,js,php,pl} ./public/ +mv ./AUTOCONFIG/{autoconfig_languages.php,sprintf.js} ./public/ +mv ./AUTOCONFIG/AutoconfigHandler.php ./model +mv ./AUTOCONFIG/*.tpl ./templates/ +``` + +#### Additional notes : + +`autoconfig.js` is a small file containing event handlers to make the use of the admin interface smooth, and also makes use of Ajax with jQuery 3.3.1. jQuery 3.3.1 is used, and not the latest 3.3.2, because the pseudo selector `:first` and `:last` have been deprecated and are needed here, at least until I can find an alternative solution. If you have one, please let me know! + +The general use of javascript is light and only to support workflow, nothing more. Pure css is used whenever possible (such as the switch button). No other framework is used to keep things light. + +FontAwesome version 5.12.1 is loaded as import in the css file + +`autoconfig.pl` will guess the location of the `config.inc.php` based on the file path. You can change that, such as by specifiying `config.local.php` instead by editing the perl script and change the line `our $POSTFIXADMIN_CONF_FILE = File::Basename::dirname( __FILE__ ) . '/../config.inc.php';` for example into `our $POSTFIXADMIN_CONF_FILE = '/var/www/postfix/config.inc.php';` + +`autoconfig.pl` will read the php file by converting it into a json file and save that conversion into a temporary file on the server until the modification time of `config.inc.php` changes. + +### DNS + +Not required, but to take full advaantage of the idea of auto discovery, you should set up the following dns record in your domain name zones: + +```bind +_submission._tcp IN SRV 0 1 587 mail.example.com. +_imap._tcp IN SRV 0 0 143 mail.example.com. +_imaps._tcp IN SRV 0 0 993 mail.example.com. +_pop3._tcp IN SRV 0 0 110 mail.example.com. +``` + +If you want to use a dedicated autodisvoer sub domain, you could set up yoru dns zone with the following record: + +```bind +_autodiscover._tcp IN SRV 0 10 443 autoconfig.example.com. +``` + +### Apache + +Add the following tp the general config file or to the relevant Vitual Hosts. You can also add it as a conf file under `/etc/apache2/conf-available` if it exists and then issue `a2enconf autoconfig.conf` to activate it (assuming the file name was `autoconfig.conf`) + +(Here I presumed Postfix Admin is installed under /var/www/postfixadmin) + +```conf +Alias /autoconfig /var/www/postfixadmin/public + + + AllowOverride None + Options Indexes FollowSymLinks ExecCGI + Require all granted + Allow from all + AddHandler cgi-script .cgi .pl + + +RewriteEngine On +# For Thunderbird and others +RewriteRule "^/.well-known/autoconfig/mail/config-v1.1.xml" "/autoconfig/autoconfig.pl" [PT,L] + +# For Outlook; POST request +RewriteRule "^/autodiscover/autodiscover.xml" "/autoconfig/autoconfig.pl?outlook=1" [PT,L] + +# For autodiscovery settings in dns +RewriteRule "^/mail/config-v1\.1\.xml(.*)$" "/autoconfig/autoconfig.pl" [PT,L] +``` + + diff --git a/AUTOCONFIG/autoconfig-host-settings.tpl b/AUTOCONFIG/autoconfig-host-settings.tpl new file mode 100644 index 00000000..262e8697 --- /dev/null +++ b/AUTOCONFIG/autoconfig-host-settings.tpl @@ -0,0 +1,91 @@ + + + + + + {if $server.type == "imap" || $server.type == "pop3"} + + {else} + + {/if} + + + + + + + + + + + + + + + + + + + + + + + + + + + + {if $server.type == "pop3" || $server.type == "imap"} + + {if isset( $server.host_id )} + {assign var=host_unique_id value=$server.host_id} + {else} + {assign var=host_unique_id value=10|mt_rand:20} + {/if} + + + + + + + + + + + + + + + + + + + + + {/if} +
smtp
+
 
 
 
{$PALANG.pAutoconfig_username_template} 
 
 
 
 
+ + + diff --git a/AUTOCONFIG/autoconfig-mysql.sql b/AUTOCONFIG/autoconfig-mysql.sql new file mode 100644 index 00000000..82e09d31 --- /dev/null +++ b/AUTOCONFIG/autoconfig-mysql.sql @@ -0,0 +1,116 @@ +/* +Created on 2020-03-07 +Copyright 2020 Jacques Deguest +Distributed under the same licence as Postfix Admin +*/ +CREATE TABLE IF NOT EXISTS autoconfig ( + id SERIAL + -- https://stackoverflow.com/questions/43056220/store-uuid-v4-in-mysql + ,config_id CHAR(36) NOT NULL + -- If you prefer you can also use CHAR(36) + -- ,config_id CHAR(36) NOT NULL + ,encoding VARCHAR(12) + ,provider_id VARCHAR(255) NOT NULL + -- Nice feature but not enough standard across other db. Instead, we'll use a separate table + -- ,provider_domain VARCHAR(255)[] + ,provider_name VARCHAR(255) + ,provider_short VARCHAR(120) + -- enable section + ,enable_status BOOLEAN + ,enable_url VARCHAR(2048) + -- documentation section + ,documentation_status BOOLEAN + ,documentation_url VARCHAR(2048) + ,webmail_login_page VARCHAR(2048) + -- webmail login page info + ,lp_info_url VARCHAR(2048) + ,lp_info_username VARCHAR(255) + ,lp_info_username_field_id VARCHAR(255) + ,lp_info_username_field_name VARCHAR(255) + ,lp_info_password_field VARCHAR(255) + ,lp_info_login_button_id VARCHAR(255) + ,lp_info_login_button_name VARCHAR(255) + -- Mac Mail specific fields + ,account_name VARCHAR(255) + -- Typically 'email' + ,account_type VARCHAR(42) + -- could be empty or could be a placeholder like %EMAILADDRESS% + ,email VARCHAR(255) + -- If not explicitly set, this will be guessed from host socket_type + ,ssl_enabled BOOLEAN + -- Will be empty obviously unless the user enters it in the form + -- password may be provided by the user on the web interface, but not stored here + -- Used for payload_description + ,description TEXT + ,organisation VARCHAR(255) + -- payload type : regular account, or Microsoft Exchange, e.g. com.apple.mail.managed for mail account or com.apple.eas.account for exchange server + ,payload_type VARCHAR(100) + ,prevent_app_sheet BOOLEAN + ,prevent_move BOOLEAN + ,smime_enabled BOOLEAN + ,payload_remove_ok BOOLEAN + -- Outlook specific fields + -- domain_required -> Not sure this should be an option; false by default + ,spa BOOLEAN + -- payload_enabled + ,active BOOLEAN + -- For signing of the Mac/iOS mobileconfig settings + -- none, local or global + -- none: do not sign + -- local: use this configuration's certificate information + -- global: use the server wide one in config.inc.php + ,sign_option VARCHAR(7) + ,cert_filepath VARCHAR(1024) + ,privkey_filepath VARCHAR(1024) + ,chain_filepath VARCHAR(1024) + ,created TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ,modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ,CONSTRAINT pk_autoconfig PRIMARY KEY (id) + ,CONSTRAINT idx_autoconfig UNIQUE (config_id) +) ENGINE = InnoDB; + +CREATE TABLE IF NOT EXISTS autoconfig_domains ( + id SERIAL + ,config_id CHAR(36) NOT NULL + -- COLLATE here is crucial for the foreign key to work. It must be the same as the target + ,domain VARCHAR(255) NOT NULL COLLATE latin1_general_ci + ,CONSTRAINT pk_autoconfig_domains PRIMARY KEY (id) + ,CONSTRAINT idx_autoconfig_domains UNIQUE (config_id, domain) + ,CONSTRAINT fk_autoconfig_domains_domain FOREIGN KEY (domain) REFERENCES domain(domain) ON DELETE CASCADE + ,CONSTRAINT fk_autoconfig_domains_config_id FOREIGN KEY (config_id) REFERENCES autoconfig(config_id) ON DELETE CASCADE +) ENGINE = InnoDB; + +CREATE TABLE IF NOT EXISTS autoconfig_hosts ( + id SERIAL + ,config_id CHAR(36) NOT NULL + -- imap, smtp, pop3 + ,type VARCHAR(12) NOT NULL + ,hostname VARCHAR(255) NOT NULL + ,port INTEGER NOT NULL + ,socket_type VARCHAR(42) + ,auth VARCHAR(42) DEFAULT 'none' NOT NULL + -- possibly to contain some placeholder like %EMAILADDRESS% + ,username VARCHAR(255) + ,leave_messages_on_server BOOLEAN DEFAULT FALSE + ,download_on_biff BOOLEAN DEFAULT FALSE + ,days_to_leave_messages_on_server INTEGER + ,check_interval INTEGER + ,priority INTEGER + ,CONSTRAINT pk_autoconfig_hosts PRIMARY KEY (id) + ,CONSTRAINT idx_autoconfig_hosts UNIQUE (config_id, type, hostname, port) + ,CONSTRAINT fk_autoconfig_hosts_config_id FOREIGN KEY (config_id) REFERENCES autoconfig(config_id) ON DELETE CASCADE +) ENGINE = InnoDB; + +CREATE TABLE IF NOT EXISTS autoconfig_text ( + id SERIAL + ,config_id CHAR(36) NOT NULL + -- instruction or documentation + ,type VARCHAR(17) NOT NULL + -- iso 639 2-letters code + ,lang CHAR(2) NOT NULL + ,phrase TEXT + ,CONSTRAINT pk_autoconfig_text PRIMARY KEY (id) + ,CONSTRAINT idx_autoconfig_text UNIQUE (config_id, type, lang) + ,CONSTRAINT fk_autoconfig_text_config_id FOREIGN KEY (config_id) REFERENCES autoconfig(config_id) ON DELETE CASCADE +) ENGINE = InnoDB; + diff --git a/AUTOCONFIG/autoconfig-postgres.sql b/AUTOCONFIG/autoconfig-postgres.sql new file mode 100644 index 00000000..354bc4f0 --- /dev/null +++ b/AUTOCONFIG/autoconfig-postgres.sql @@ -0,0 +1,120 @@ +/* +Created on 2020-03-07 +Copyright 2020 Jacques Deguest +Distributed under the same licence as Postfix Admin +*/ +CREATE TABLE IF NOT EXISTS autoconfig ( + id SERIAL + ,config_id UUID NOT NULL + -- If you prefer not using a UUID field + -- ,config_id CHAR(36) NOT NULL + ,encoding VARCHAR(12) + ,provider_id VARCHAR(255) NOT NULL + -- Nice feature but not enough standard across other db. Instead, we'll use a separate table + -- ,provider_domain VARCHAR(255)[] + ,provider_name VARCHAR(255) + ,provider_short VARCHAR(120) + -- enable section + ,enable_status BOOLEAN + ,enable_url VARCHAR(2048) + -- documentation section + ,documentation_status BOOLEAN + ,documentation_url VARCHAR(2048) + ,webmail_login_page VARCHAR(2048) + -- webmail login page info + ,lp_info_url VARCHAR(2048) + ,lp_info_username VARCHAR(255) + ,lp_info_username_field_id VARCHAR(255) + ,lp_info_username_field_name VARCHAR(255) + ,lp_info_password_field VARCHAR(255) + ,lp_info_login_button_id VARCHAR(255) + ,lp_info_login_button_name VARCHAR(255) + -- Mac Mail specific fields + ,account_name VARCHAR(255) + -- Typically 'email' + ,account_type VARCHAR(42) + -- could be empty or could be a placeholder like %EMAILADDRESS% + ,email VARCHAR(255) + -- If not explicitly set, this will be guessed from host socket_type + ,ssl_enabled BOOLEAN + -- Will be empty obviously unless the user enters it in the form + -- password may be provided by the user on the web interface, but not stored here + -- Used for payload_description + ,description TEXT + ,organisation VARCHAR(255) + -- payload type : regular account, or Microsoft Exchange, e.g. com.apple.mail.managed for mail account or com.apple.eas.account for exchange server + ,payload_type VARCHAR(100) + ,prevent_app_sheet BOOLEAN + ,prevent_move BOOLEAN + ,smime_enabled BOOLEAN + ,payload_remove_ok BOOLEAN + -- Outlook specific fields + -- domain_required -> Not sure this should be an option; false by default + ,spa BOOLEAN + -- payload_enabled + ,active BOOLEAN + -- For signing of the Mac/iOS mobileconfig settings + -- none, local or global + -- none: do not sign + -- local: use this configuration's certificate information + -- global: use the server wide one in config.inc.php + ,sign_option VARCHAR(7) + ,cert_filepath VARCHAR(1024) + ,privkey_filepath VARCHAR(1024) + ,chain_filepath VARCHAR(1024) + ,created TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ,modified TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ,CONSTRAINT pk_autoconfig PRIMARY KEY (id) + ,CONSTRAINT idx_autoconfig UNIQUE (config_id) +); + +CREATE TABLE IF NOT EXISTS autoconfig_domains ( + id SERIAL + ,config_id UUID NOT NULL + -- If you prefer not to install uuid-ossp: + --- ,config_id CHAR(36) NOT NULL + ,domain VARCHAR(255) NOT NULL + ,CONSTRAINT pk_autoconfig_domains PRIMARY KEY (id) + ,CONSTRAINT idx_autoconfig_domains UNIQUE (config_id, domain) + ,CONSTRAINT fk_autoconfig_domains_domain FOREIGN KEY (domain) REFERENCES public.domain(domain) ON DELETE CASCADE + ,CONSTRAINT fk_autoconfig_domains_config_id FOREIGN KEY (config_id) REFERENCES public.autoconfig(config_id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS autoconfig_hosts ( + id SERIAL + ,config_id UUID NOT NULL + -- If you prefer not to install uuid-ossp: + --- ,config_id CHAR(36) NOT NULL + -- imap, smtp, pop3 + ,type VARCHAR(12) NOT NULL + ,hostname VARCHAR(255) NOT NULL + ,port INTEGER NOT NULL + ,socket_type VARCHAR(42) + ,auth VARCHAR(42) DEFAULT 'none' NOT NULL + -- possibly to contain some placeholder like %EMAILADDRESS% + ,username VARCHAR(255) + ,leave_messages_on_server BOOLEAN DEFAULT FALSE + ,download_on_biff BOOLEAN DEFAULT FALSE + ,days_to_leave_messages_on_server INTEGER + ,check_interval INTEGER + ,priority INTEGER + ,CONSTRAINT pk_autoconfig_hosts PRIMARY KEY (id) + ,CONSTRAINT idx_autoconfig_hosts UNIQUE (config_id, type, hostname, port) + ,CONSTRAINT fk_autoconfig_hosts_config_id FOREIGN KEY (config_id) REFERENCES public.autoconfig(config_id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS autoconfig_text ( + id SERIAL + ,config_id UUID NOT NULL + -- If you prefer not to install uuid-ossp: + --- ,config_id CHAR(36) NOT NULL + -- instruction or documentation + ,type VARCHAR(17) NOT NULL + -- iso 639 2-letters code + ,lang CHAR(2) NOT NULL + ,phrase TEXT + ,CONSTRAINT pk_autoconfig_text PRIMARY KEY (id) + ,CONSTRAINT idx_autoconfig_text UNIQUE (config_id, type, lang) + ,CONSTRAINT fk_autoconfig_text_config_id FOREIGN KEY (config_id) REFERENCES public.autoconfig(config_id) ON DELETE CASCADE +); + diff --git a/AUTOCONFIG/autoconfig-sqlite.sql b/AUTOCONFIG/autoconfig-sqlite.sql new file mode 100644 index 00000000..1adb0f70 --- /dev/null +++ b/AUTOCONFIG/autoconfig-sqlite.sql @@ -0,0 +1,112 @@ +/* +Created on 2020-03-07 +Copyright 2020 Jacques Deguest +Distributed under the same licence as Postfix Admin +*/ +CREATE TABLE IF NOT EXISTS autoconfig ( + id INTEGER NOT NULL + ,config_id CHAR(36) NOT NULL + ,encoding VARCHAR(12) + ,provider_id VARCHAR(255) NOT NULL + -- Nice feature but not enough standard across other db. Instead, we'll use a separate table + -- ,provider_domain VARCHAR(255)[] + ,provider_name VARCHAR(255) + ,provider_short VARCHAR(120) + -- enable section + ,enable_url VARCHAR(2048) + ,enable_status BOOLEAN + -- documentation section + ,documentation_status BOOLEAN + ,documentation_url VARCHAR(2048) + ,webmail_login_page VARCHAR(2048) + -- webmail login page info + ,lp_info_url VARCHAR(2048) + ,lp_info_username VARCHAR(255) + ,lp_info_username_field_id VARCHAR(255) + ,lp_info_username_field_name VARCHAR(255) + ,lp_info_password_field VARCHAR(255) + ,lp_info_login_button_id VARCHAR(255) + ,lp_info_login_button_name VARCHAR(255) + -- Mac Mail specific fields + ,account_name VARCHAR(255) + -- Typically 'email' + ,account_type VARCHAR(42) + -- could be empty or could be a placeholder like %EMAILADDRESS% + ,email VARCHAR(255) + -- If not explicitly set, this will be guessed from host socket_type + ,ssl_enabled BOOLEAN + -- Will be empty obviously unless the user enters it in the form + -- password may be provided by the user on the web interface, but not stored here + -- Used for payload_description + ,description TEXT + ,organisation VARCHAR(255) + -- payload type : regular account, or Microsoft Exchange, e.g. com.apple.mail.managed for mail account or com.apple.eas.account for exchange server + ,payload_type VARCHAR(100) + ,prevent_app_sheet BOOLEAN + ,prevent_move BOOLEAN + ,smime_enabled BOOLEAN + ,payload_remove_ok BOOLEAN + -- Outlook specific fields + -- domain_required -> Not sure this should be an option; false by default + ,spa BOOLEAN + -- payload_enabled + ,active BOOLEAN + -- For signing of the Mac/iOS mobileconfig settings + -- none, local or global + -- none: do not sign + -- local: use this configuration's certificate information + -- global: use the server wide one in config.inc.php + ,sign_option VARCHAR(7) + ,cert_filepath VARCHAR(1024) + ,privkey_filepath VARCHAR(1024) + ,chain_filepath VARCHAR(1024) + ,created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + ,modified TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + ,CONSTRAINT pk_autoconfig PRIMARY KEY (id) + ,CONSTRAINT idx_autoconfig UNIQUE (config_id) +); + +CREATE TABLE IF NOT EXISTS autoconfig_domains ( + id INTEGER NOT NULL + ,config_id CHAR(36) NOT NULL + ,domain VARCHAR(255) NOT NULL + ,CONSTRAINT pk_autoconfig_domains PRIMARY KEY (id) + ,CONSTRAINT idx_autoconfig_domains UNIQUE (config_id, domain) + ,CONSTRAINT fk_autoconfig_domains_domain FOREIGN KEY (domain) REFERENCES domain(domain) ON DELETE CASCADE + ,CONSTRAINT fk_autoconfig_domains_config_id FOREIGN KEY (config_id) REFERENCES autoconfig(config_id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS autoconfig_hosts ( + id INTEGER NOT NULL + ,config_id CHAR(36) NOT NULL + -- imap, smtp, pop3 + ,type VARCHAR(12) NOT NULL + ,hostname VARCHAR(255) NOT NULL + ,port INTEGER NOT NULL + ,socket_type VARCHAR(42) + ,auth VARCHAR(42) DEFAULT 'none' NOT NULL + -- possibly to contain some placeholder like %EMAILADDRESS% + ,username VARCHAR(255) + ,leave_messages_on_server BOOLEAN DEFAULT FALSE + ,download_on_biff BOOLEAN DEFAULT FALSE + ,days_to_leave_messages_on_server INTEGER + ,check_interval INTEGER + ,priority INTEGER + ,CONSTRAINT pk_autoconfig_hosts PRIMARY KEY (id) + ,CONSTRAINT idx_autoconfig_hosts UNIQUE (config_id, type, hostname, port) + ,CONSTRAINT fk_autoconfig_hosts_config_id FOREIGN KEY (config_id) REFERENCES autoconfig(config_id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS autoconfig_text ( + id INTEGER NOT NULL + ,config_id CHAR(36) NOT NULL + -- instruction or documentation + ,type VARCHAR(17) NOT NULL + -- iso 639 2-letters code + ,lang CHAR(2) NOT NULL + ,phrase TEXT + ,CONSTRAINT pk_autoconfig_text PRIMARY KEY (id) + ,CONSTRAINT idx_autoconfig_text UNIQUE (config_id, type, lang) + ,CONSTRAINT fk_autoconfig_text_config_id FOREIGN KEY (config_id) REFERENCES autoconfig(config_id) ON DELETE CASCADE +); + diff --git a/AUTOCONFIG/autoconfig-v2.js b/AUTOCONFIG/autoconfig-v2.js new file mode 100644 index 00000000..2034a46c --- /dev/null +++ b/AUTOCONFIG/autoconfig-v2.js @@ -0,0 +1,908 @@ +/* +Created on 2020-03-12 +Copyright 2020 Jacques Deguest +Distributed under the same licence as Postfix Admin +*/ +$(document).ready(function() +{ + const DEBUG = true; + + // Credits to: https://tdanemar.wordpress.com/2010/08/24/jquery-serialize-method-and-checkboxes/ + // Modified by Jacques Deguest to include other form elements: + // http://www.w3schools.com/tags/tag_input.asp + (function($) + { + $.fn.serializeAll = function(options) + { + return $.param(this.serializeArrayAll(options)); + }; + + $.fn.serializeArrayAll = function (options) + { + var o = $.extend({ + checkboxesAsBools: false + }, options || {}); + + var rselectTextarea = /select|textarea/i; + var rinput = /text|hidden|password|search|date|time|number|color|datetime|email|file|image|month|range|tel|url|week/i; + + return this.map(function () + { + return this.elements ? $.makeArray(this.elements) : this; + }) + .filter(function () + { + return this.name && !this.disabled && + (this.checked + || (o.checkboxesAsBools && this.type === 'checkbox') + || rselectTextarea.test(this.nodeName) + || rinput.test(this.type)); + }) + .map(function (i, elem) + { + var val = $(this).val(); + return val == null ? + null : + $.isArray(val) ? + $.map(val, function (val, i) + { + return { name: elem.name, value: val }; + }) : + { + name: elem.name, + value: (o.checkboxesAsBools && this.type === 'checkbox') ? + (this.checked ? 1 : 0) : + val + }; + }).get(); + }; + })(jQuery); + + window.makeMessage = function( type, mesg ) + { + return( sprintf( '
%s
', type, mesg ) ); + }; + + window.showMessage = function() + { + if( DEBUG ) console.log( "Called from " + ( arguments.callee.caller === null ? 'void' : arguments.callee.caller.name ) ); + var opts = { + div: $('#message'), + append: false, + dom: null, + timeout: null, + scroll: false, + timeoutCallback: null, + }; + var msgDiv = $('#message'); + if( arguments.length == 3 && typeof( arguments[2] ) === 'object' ) + { + var param = arguments[2]; + opts.type = arguments[0]; + opts.message = arguments[1]; + if( typeof( param.append ) !== 'undefined' ) opts.append = param.append; + if( typeof( param.dom ) !== 'undefined' ) opts.dom = param.dom; + if( typeof( param.timeout ) !== 'undefined' ) opts.timeout = param.timeout; + if( typeof( param.timeoutCallback ) !== 'undefined' ) opts.timeoutCallback = param.timeoutCallback; + if( typeof( param.scroll ) !== 'undefined' ) opts.scroll = param.scroll; + if( typeof( param.speak ) !== 'undefined' ) opts.speak = param.speak; + } + // Backward compatibility + else if( arguments.length >= 2 ) + { + opts.type = arguments[0]; + opts.message = arguments[1]; + if( arguments.length > 2 ) opts.append = arguments[2]; + if( arguments.length > 3 ) opts.dom = arguments[3]; + if( arguments.length > 4 ) opts.timeout = arguments[4]; + } + else + { + msgDiv.append( makeMessage( 'warning', "showMessage called with only " + arguments.length + " parameters while 2 at minimum are required. Usage: showMessage( type, message, append, domObject, timeout ) or showMessage( type, message, options )" ) ); + return( false ); + } + if( typeof( opts.dom ) === 'object' && opts.dom != null ) + { + msgDiv = opts.dom; + } + // Check if message is an array of messages + // https://stackoverflow.com/a/4775741/4814971 + if( Array.isArray ) + { + if( Array.isArray( opts.message ) ) + { + opts.messages = opts.message; + } + } + else if( opts.message instanceof( Array ) ) + { + opts.messages = opts.message; + } + else if( $.isArray( opts.message ) ) + { + opts.messages = opts.message; + } + + // messages has been set with previous check + // Make this array a list of errors + if( opts.hasOwnProperty( 'messages' ) ) + { + opts.message = sprintf( "
    \n%s\n
", opts.messages.map(function(e){ return('
  • ' + e + '
  • '); }).join( "\n" ) ); + } + + if( opts.append ) + { + msgDiv.append(makeMessage(opts.type, opts.message)); + } + else + { + msgDiv.html(makeMessage(opts.type, opts.message)); + } + + if( opts.type == 'error' ) + { + msgDiv.addClass( 'error-shake' ); + setTimeout(function() + { + msgDiv.removeClass( 'error-shake' ); + }, 70000); + } + + if( parseInt( opts.timeout ) > 0 ) + { + var thisTimeout = parseInt( opts.timeout ); + setTimeout(function() + { + msgDiv.html( '' ); + if( typeof( opts.timeoutCallback ) === 'function' ) + { + opts.timeoutCallback(); + } + },thisTimeout); + } + else + { + setTimeout(function() + { + msgDiv.html( '' ); + },15000); + } + if( opts.scroll ) + { + if( DEBUG ) console.log( "Scrolling to the top of the page..." ); + $('html, body').animate( { scrollTop: 0 }, 500 ); + } + else + { + if( DEBUG ) console.log( "No scrolling..." ); + } + }; + + window.postfixAdminProgressBar = function() + { + var xhr = new window.XMLHttpRequest(); + if( DEBUG ) console.log( "Initiating the progress bar." ); + if( DEBUG ) console.log( "Called from:\n" + (new Error).stack ); + $('#postfixadmin-progress').show().removeClass('done'); + xhr.upload.addEventListener('progress', function(evt) + { + if( evt.lengthComputable ) + { + var percentComplete = evt.loaded / evt.total; + if( DEBUG ) console.log(percentComplete); + $('#postfixadmin-progress').css({ + width: percentComplete * 100 + '%' }); + if( DEBUG ) console.log( "upload.addEventListener: " + percentComplete ); + if( percentComplete === 1 ) + { + $('#postfixadmin-progress').addClass('done').hide(); + } + } + }, false); + xhr.addEventListener('progress', function(evt) + { + if( evt.lengthComputable ) + { + var percentComplete = evt.loaded / evt.total; + if( DEBUG ) console.log("addEventListener: " + percentComplete); + $('#postfixadmin-progress').css({ + width: percentComplete * 100 + '%' }); + if( percentComplete === 1 ) + { + $('#postfixadmin-progress').addClass('done').hide(); + } + } + }, false); + return( xhr ); + }; + + window.postfixAdminProgressBarStart = function() + { +// $('#postfixadmin-progress').show().addClass('done'); + $('#postfixadmin-progress').show(); + $({property: 0}).animate({property: 85}, + { + // Arbitrary time, which should well cover the time it takes to get response from server + // Otherwise, well our progress bar will hang at 85% until we get a call to kill it + duration: 4000, + step: function() + { + var _percent = Math.round( this.property ); + $('#postfixadmin-progress').css( 'width', _percent + '%' ); + } + }); + }; + + window.postfixAdminProgressBarStop = function() + { + $({property: 85}).animate({property: 105}, + { + duration: 1000, + step: function() + { + var _percent = Math.round( this.property ); + $('#postfixadmin-progress').css( 'width', _percent + '%' ); + if( _percent == 105 ) + { + $('#postfixadmin-progress').addClass('done'); + } + }, + complete: function() + { + $('#postfixadmin-progress').hide(); + $('#postfixadmin-progress').removeClass('done'); + $('#postfixadmin-progress').css( 'width', '0%' ); + } + }); + }; + + window.autoconfigShowHideArrow = function() + { + // Reset. Show them all + $('table.server .autoconfig-command').show(); + // Hide first and last ones + // This requires jQUery up to v3.3. Version 3.4 onward do not support :first and :last anymore + $('.autoconfig-incoming:first .autoconfig-move-up').hide(); + $('.autoconfig-incoming:last .autoconfig-move-down').hide(); + $('.autoconfig-outgoing:first .autoconfig-move-up').hide(); + $('.autoconfig-outgoing:last .autoconfig-move-down').hide(); + }; + + window.autoconfig_ajax_call = function( postData ) + { + var prom = $.Deferred(); + $this = $(this); + $.ajax({ + xhr: postfixAdminProgressBar(), + type: "POST", + url: "autoconfig.php", + dataType: "json", + data: postData, + beforeSend: function(xhr) + { + xhr.overrideMimeType( "application/json; charset=utf-8" ); + postfixAdminProgressBarStart(); + }, + error: function(xhr, errType, ExceptionObject) + { + postfixAdminProgressBarStop(); + if( DEBUG ) console.log( "Returned error " + xhr.status + " with error type " + errType + " and exception object " + JSON.stringify( ExceptionObject ) ); + if( DEBUG ) console.log( "Current url is: " + xhr.responseURL ); + if( DEBUG ) console.log( "Http headers are: " + xhr.getAllResponseHeaders() ); + if( DEBUG ) console.log( "Response raw data is: \n" + xhr.responseText ); + // There was a redirect most likely due to some timeout +// if( xhr.getResponseHeader( 'Content-Type' ).toLowerCase().indexOf( 'text/html' ) >= 0 ) +// { +// window.location.reload(); +// return( true ); +// } + var msg = 'An unexpected error has occurred'; + showMessage( 'error', msg, { scroll: true }); + $this.addClass( 'error-shake' ); + prom.reject(); + }, + success: function(data, status, xhr) + { + postfixAdminProgressBarStop(); + if( data.error ) + { + showMessage( 'error', data.error, { scroll: true }); + $this.addClass( 'error-shake' ); + setTimeout(function() + { + $this.removeClass( 'error-shake' ); + },5000); + prom.reject(); + } + else + { + if( data.success ) + { + prom.resolve(data); + showMessage( 'success', data.success, { scroll: true }); + if( DEBUG ) console.log( "save(): " + data.success ); + } + else if( data.info ) + { + showMessage( 'info', data.info, { scroll: true } ); + prom.resolve(); + } + else + { + showMessage( 'info', data.msg, { scroll: true } ); + prom.resolve(); + } + } + } + }); + return( prom.promise() ); + }; + + $(document).on('click','#autoconfig_save', function(e) + { + e.preventDefault(); + $this = $(this); + var data = {handler: 'autoconfig_save'}; + $('#autoconfig_form').serializeArrayAll().map(function(item) + { + if( data[ item.name ] !== undefined ) + { + if( !data[ item.name ].push ) + { + data[ item.name ] = [ data[item.name] ]; + } + data[ item.name ].push( item.value ); + } + else + { + data[ item.name ] = item.value; + } + }); + if( DEBUG ) console.log( "serialized data is: " + JSON.stringify( data ) ); + autoconfig_ajax_call( data ).done(function(data) + { + // Since this shared function is used for both saving (adding and updating) as well as deleting + // we check if those data properties are returned by the server. + // Those here are only returned if this is the result of an addition + if( data.hasOwnProperty('config_id') ) + { + if( $('#autoconfig_form input[name="config_id"]').length == 0 ) + { + showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); + throw( "Unable to find the field \"config_id\" !" ); + } + if( typeof( data.config_id ) !== 'undefined' && data.config_id.length > 0 ) + { + // We force trigger change, because this is a hidden field and it hidden field do not trigger change + $('#autoconfig_form input[name="config_id"]').val( data.config_id ).trigger('change'); + } + } + // Do the hosts + var hostTypes = [ 'incoming', 'outgoing' ]; + for( var j = 0; j < hostTypes.length; j++ ) + { + var hostType = hostTypes[j]; + if( DEBUG ) console.log( "Checking host of type " + hostType ); + if( data.hasOwnProperty(hostType + '_server') && Array.isArray( data[hostType + '_server'] ) ) + { + var thoseHosts = $('.autoconfig-' + hostType); + var dataHosts = data[hostType + '_server']; + if( thoseHosts.length == 0 ) + { + showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); + throw( "Unable to find any hosts block in our form!" ); + } + else if( dataHosts.length != thoseHosts.length ) + { + + showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); + throw( "Total of " + hostType + " servers returned from the server (" + dataHosts.length + ") do not match the total hosts we have in our form (" + thoseHosts.length + ")." ); + } + dataHosts.forEach(function(def, index) + { + if( DEBUG ) console.log( "def contains: " + JSON.stringify( def ) ); + if( !def.hasOwnProperty('config_id') || + !def.hasOwnProperty('hostname') || + !def.hasOwnProperty('port') ) + { + if( DEBUG ) console.error( "Something is wrong. Data received is missing fields config_id, or hostname or port" ); + return( false ); + } + thoseHosts.each(function(offset, obj) + { + var dom = $(obj); + var hostId = dom.find('input[name="host_id[]"]'); + var hostName = dom.find('input[name="hostname[]"]'); + var hostPort = dom.find('input[name="port[]"]'); + if( !hostId.length ) + { + showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); + throw( "Unable to find host id field for host type \"" + hostType + "\" at offset " + i ); + } + if( !hostName.length ) + { + showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); + throw( "Unable to find host name field for host type \"" + hostType + "\" at offset " + i ); + } + if( !hostPort.length ) + { + showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); + throw( "Unable to find host port field for host type \"" + hostType + "\" at offset " + i ); + } + // We found our match: no id, hostname and port match + if( hostId.val().length == 0 && hostName.val() == def.hostname && hostPort.val() == def.port ) + { + if( DEBUG ) console.log( "Setting host id " + def.host_id + " to host name " + hostName.val() + " with port " + hostPort.val() ); + hostId.val( def.host_id ); + // exit the loop + return( false ); + } + }); + }); + } + else + { + if( DEBUG ) console.error( "Something is wrong. Data received for hosts of type " + hostType + " does not exist or is not an array." ); + } + } + + // Now, do the texts + var textTypes = ['instruction', 'documentation']; + for( var j = 0; j < textTypes.length; j++ ) + { + var textType = textTypes[j]; + if( DEBUG ) console.log( "Checking text of type " + textType ); + if( data.hasOwnProperty(textType) && Array.isArray( data[textType] ) ) + { + var dataTexts = data[textType]; + var thoseTextIds = $('input[name="' + textType + '_id[]"]'); + var thoseTextLang = $('select[name="' + textType + '_lang[]"]'); + var thoseTextData = $('textarea[name="' + textType + '_text[]"]'); + // The array could very well be empty + dataTexts.forEach(function(def, index) + { + if( !def.hasOwnProperty('id') || + !def.hasOwnProperty('type') || + !def.hasOwnProperty('lang') ) + { + if( DEBUG ) console.error( "Something is wrong. Data received is missing fields id, or type or lang" ); + return( false ); + } + for( var k = 0; k < thoseTextIds.length; k++ ) + { + var textId = thoseTextIds.eq(k); + var textLang = thoseTextLang.eq(k); + // Found a match + if( textId.val().length == 0 && textLang.val() == def.lang ) + { + if( DEBUG ) console.log( "Setting text id " + def.id + " to text with type " + textType + " and language " + def.lang ); + textId.val( def.id ); + return( false ); + } + } + }); + } + else + { + if( DEBUG ) console.error( "Something is wrong. Data received for " + textType + " text does not exist or is not an array." ); + } + } + }).fail(function() + { + // Nothing for now + }); + }); + + $(document).on('click','#autoconfig_remove', function(e) + { + e.preventDefault(); + var data = + { + handler: 'autoconfig_remove', + config_id: $('input[name="config_id"]').val(), + token: $('input[name="token"]').val(), + }; + if( DEBUG ) console.log( "Data to be sent is: " + JSON.stringify( data ) ); + autoconfig_ajax_call( data ).done(function(data) + { + // Reload the page, but without the query string at the end + window.location.href = window.location.pathname; + }).fail(function() + { + // Nothing for now + }); + }); + + $(document).on('click', '#autoconfig_cancel', function(e) + { + e.preventDefault(); + window.location.href = 'list.php?table=domain'; + return( true ); + }); + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random + window.getRandomInt = function(min, max) + { + return( Math.floor( Math.random() * Math.floor((max - min) + min) ) ); + } + /* + I could have just created one function and called it + */ + // Add and remove hosts + $(document).on('click','.autoconfig-server-add',function(e) + { + e.preventDefault(); + var row = $(this).closest('table.server').closest('tr'); + if( !row.length ) + { + throw( "Unable to find the current enclosing row." ); + } + var clone = row.clone(); + clone.find('select,input[type!="hidden"],textarea').each(function(i,item) + { + $(item).val( '' ); + }); + // We need to remove the host_id so it can be treated as a new host and not an update of an existing one + clone.find('input[name="host_id[]"]').val( '' ); + // Set default value and trigger change, which will call an event handler that will hide/show the section on pop3 + clone.find('.host_type').val('imap').trigger('change'); + var optionLabels = ['leave_messages_on_server', 'download_on_biff', 'days_to_leave_messages_on_server', 'check_interval']; + optionLabels.forEach(function(fieldName, index) + { + if( DEBUG ) console.log( "Checking field name " + fieldName ); + var thisField = clone.find('input[name="' + fieldName + '[]"]'); + if( thisField.length > 0 ) + { + var forThisField = $('label[for="' + thisField.attr('id') + '"]', clone); + if( forThisField.length > 0 ) + { + if( DEBUG ) console.log( "Field is " + thisField.html() + "\nLabel field is: " + forThisField.html() ); + thisField.attr('id', 'autoconfig_' + fieldName + '_' + getRandomInt(100,1000)); + maxAttempt = 10; + if( DEBUG ) console.log( "Checking if generated id exists: " + thisField.attr('id') ); + while( $('#' + thisField.attr('id'), clone).length > 0 && ++maxAttempt < 10 ) + { + thisField.attr('id', 'autoconfig_' + fieldName + '_' + getRandomInt(100,1000)); + } + forThisField.attr('for', thisField.attr('id')); + if( DEBUG ) console.log( "Final generated id is: " + thisField.attr('id') ); + } + else + { + if( DEBUG ) console.error( "Unable to find label element for field name." ); + } + } + }); + + clone.insertAfter( row ); + autoconfigShowHideArrow(); + $('html, body').animate( { scrollTop: clone.offset().top }, 500 ); + return( true ); + }); + + $(document).on('click','.autoconfig-server-remove',function(e) + { + e.preventDefault(); + var row = $(this).closest('table.server').closest('tr'); + if( !row.length ) + { + throw( "Unable to find the current enclosing row." ); + } + // Check if there are at least 2 elements, so that after at least one remain + var re = new RegExp('(autoconfig-(?:incoming|outgoing))-server'); + var res = $(this).attr('class').match( re ); + console.log( res ); + if( res == null ) + { + throw( "Cannot find class \"autoconfig-incoming-server\" or class \"autoconfig-outgoing-server\" in our clicked element." ); + } + // Now find how many elements we have with this class + var total = $('tr.' + res[1]).length; + if( total < 2 ) + { + row.addClass('autoconfig-error-shake'); + setTimeout(function() + { + row.removeClass('autoconfig-error-shake'); + },1000); + return( false ); + } + row.remove(); + autoconfigShowHideArrow(); + }); + + // Add and remove account enable instructions or support documentation + $(document).on('click','.autoconfig-locale-text-add',function(e) + { + e.preventDefault(); + var row = $(this).closest('tr'); + if( !row.length ) + { + throw( "Unable to find the current enclosing row." ); + } + var clone = row.clone(); + clone.find('select,input[type!="hidden"],textarea').each(function(i,item) + { + $(item).val( '' ); + }); + // We need to remove the host_id so it can be treated as a new text and not an update of an existing one + clone.find('input[name$="_id[]"]').val( '' ); + clone.insertAfter( row ); + $('html, body').animate( { scrollTop: clone.offset().top }, 500 ); + }); + + $(document).on('click','.autoconfig-locale-text-remove',function(e) + { + e.preventDefault(); + var row = $(this).closest('tr'); + if( !row.length ) + { + throw( "Unable to find the current enclosing row." ); + } + var re = new RegExp('(autoconfig-(instruction|documentation))'); + var res = $(this).attr('class').match( re ); + if( res == null ) + { + throw( "Cannot find class \"autoconfig-instruction\" or class \"autoconfig-documentation\" in our clicked element." ); + } + var textType = res[2]; + var total = $('tr.' + res[1]).length; + if( DEBUG ) console.log( total + " rows found for text type " + textType ); + if( total < 2 ) + { + if( DEBUG ) console.log( "text remove: one left" ); + if( DEBUG ) console.log( "Getting lang object with " + '[name="' + textType + '_lang[]"]' ); + var textLang = row.find('[name="' + textType + '_lang[]"]'); + if( DEBUG ) console.log( "Getting text object with " + '[name="' + textType + '_text[]"]' ); + var textData = row.find('[name="' + textType + '_text[]"]'); + if( DEBUG ) console.log( "text remove: found lang and text field? " + ( ( textLang.length > 0 && textData.length > 0 ) ? "Yes" : "No" ) ); + // This is remaining default tet row and there is no more data + if( ( textLang.val() === null || ( textLang.val() !== null && textLang.val() == '' ) ) && $.trim(textData.val()) == '' ) + { + if( DEBUG ) console.log( "text remove: lang and text fields are empty already, error shake it" ); + row.addClass('autoconfig-error-shake'); + setTimeout(function() + { + row.removeClass('autoconfig-error-shake'); + },1000); + } + else + { + if( DEBUG ) console.log( "text remove: empty fields" ); + textLang.val( '' ); + textData.val( '' ); + } + return( false ); + } + row.remove(); + }); + + $(document).on('click', '#copy_provider_value', function(e) + { + e.preventDefault(); + var orgField = $('input[name="organisation"]'); + var providerNameField = $('input[name="provider_name"]'); + if( !orgField.length || !providerNameField.length ) + { + throw( "Unable to find either the provider name field or the organisation field!" ); + } + if( providerNameField.val().length == 0 ) + { + return( false ); + } + orgField.val( providerNameField.val() ); + return( true ); + }); + + $(document).on('click', '#autoconfig_toggle_select_all_domains', function(e) + { + e.preventDefault(); + // if( DEBUG ) console.log( "provider_domain options length: " + $('#autoconfig_provider_domain option').length + " and disabled options are: " + $('#autoconfig_provider_domain option:disabled').length + " and selected options are: " + $('#autoconfig_provider_domain option:selected').length ); + if( $('#autoconfig_provider_domain option:selected').length > 0 ) + { + $('#autoconfig_provider_domain option').prop('selected', false); + } + else + { + if( $('#autoconfig_provider_domain option:disabled').length == $('#autoconfig_provider_domain option').length ) + { + var row = $(this).closest('tr'); + row.addClass('error-shake'); + setTimeout(function() + { + row.removeClass('error-shake'); + },500); + return( false ); + } + else + { + $('#autoconfig_provider_domain option:not(:disabled)').prop('selected', true); + } + } + }); + + $(document).on('change', '#autoconfig_form .host_type', function(e) + { + var typeValue = $(this).val(); + // We get the enclosing table object to set some limiting context to the host_pop3 selector below + var tbl = $(this).closest('table'); + if( typeValue == 'imap' ) + { + $('.host_pop3', tbl).hide(); + } + else + { + $('.host_pop3', tbl).show(); + } + }); + + $(document).on('change', '#autoconfig_form .username_template', function(e) + { + var usernameField = $(this).closest('.server').find('input[name="username[]"]'); + if( usernameField.length == 0 ) + { + throw( "Unable to find the username field!" ); + } + usernameField.val( $(this).val() ); + }); + + $(document).on('change', '#autoconfig_form select[name="jump_to"]', function(e) + { + var id = $(this).val(); + var re = new RegExp( '([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})' ); + // This will display an empty form + if( id.length == 0 ) + { + window.location.href = 'autoconfig.php'; + } + else if( id.match( re ) ) + { + window.location.href = 'autoconfig.php?config_id=' + encodeURIComponent( id ); + } + }); + + // Upon change in the content of the hidden config_id, enable or disable the "Remove" buttton + // The Remove button can only be used if this is for an existing configuration obviously + $(document).on('change', '#autoconfig_form input[name="config_id"]', function(e) + { + if( DEBUG ) console.log( "Config id field contains \"" + $(this).val() + "\"." ); + if( $(this).val().length == 0 || $(this).val().match( /^[[:blank:]\t]*$/ ) ) + { + $('#autoconfig_remove').attr( 'disabled', true ); + } + else + { + $('#autoconfig_remove').attr( 'disabled', false ); + } + }); + + $(document).on('click', '.autoconfig-move-up', function(e) + { + e.preventDefault(); + var row = $(this).closest('.server').closest('tr'); + if( row.prev().length == 0 || ( !row.prev().hasClass('autoconfig-incoming') && !row.prev().hasClass('autoconfig-outgoing') ) ) + { + return( false ); + } + row.insertBefore( row.prev() ); + $('html, body').animate( { scrollTop: row.offset().top }, 500 ); + autoconfigShowHideArrow(); + }); + + $(document).on('click', '.autoconfig-move-down', function(e) + { + e.preventDefault(); + var row = $(this).closest('.server').closest('tr'); + if( row.next().length == 0 || ( !row.next().hasClass('autoconfig-incoming') && !row.next().hasClass('autoconfig-outgoing') ) ) + { + return( false ); + } + row.insertAfter( row.next() ); + $('html, body').animate( { scrollTop: row.offset().top }, 500 ); + autoconfigShowHideArrow(); + }); + + window.checkExistingTextLanguage = function(opts = {}) + { + if( typeof( opts ) !== 'object' ) + { + throw( "Parameters provided is not an object. Call checkExistingTextLanguage like this: checkExistingTextLanguage({ type: 'instruction', lang: 'fr', caller: $(this) })" ); + } + var textType = opts.type; + var callerMenu = opts.caller; + var langToCheck = opts.lang; + if( DEBUG ) console.log( "checkExistingTextLanguage() checking text type " + textType + " for language " + langToCheck ); + if( typeof( textType ) === 'undefined' ) + { + throw( "No text type was provided." ); + } + var langMenu = $('select[name="' + textType + '_lang[]"]'); + if( DEBUG ) console.log( "checkExistingTextLanguage() Found " + langMenu.length + " language menu(s)." ); + if( langMenu.length == 0 ) + { + throw( "Could not find any language menu for this text type " + textType ); + } + if( langMenu.length == 1 ) + { + return( true ); + } + var alreadySelected = false; + if( DEBUG ) console.log( "checkExistingTextLanguage() checking each existing language menu." ); + langMenu.each(function(offset, menuObject) + { + if( $(menuObject).is( callerMenu ) ) + { + if( DEBUG ) console.log( "checkExistingTextLanguage() skipping because this is our caller menu." ); + return( true ); + } + // Found a match, stop there + else if( $(menuObject).val() == langToCheck ) + { + if( DEBUG ) console.log( "checkExistingTextLanguage() Found match with this menu value (" + $(menuObject).val() + ") matching the language to check \"" + langToCheck + "\"." ); + alreadySelected = true; + return( false ); + } + }); + if( DEBUG ) console.log( "checkExistingTextLanguage() returning: " + !alreadySelected ); + return( !alreadySelected ); + }; + + // Upon selection or change of a language, we check it has not been selected already + $(document).on('change', 'select[name="instruction_lang[]"]', function(e) + { + if( !checkExistingTextLanguage({ type: 'instruction', lang: $(this).val(), caller: $(this) }) ) + { + $(this).addClass('error-shake'); + // + var warning = $('', + { + class: 'fas fa-exclamation-triangle fa-2x', + style: 'color: red; font-size: 20px;', + }); + warning.insertAfter( $(this) ); + var that = $(this); + setTimeout(function() + { + that.removeClass('error-shake'); + warning.remove(); + },5000); + $(this).val(''); + return( false ); + } + return( true ); + }); + + window.toggleCertFiles = function(option) + { + if( typeof( option ) === 'undefined' ) + { + return( false ); + } + if( option == 'local' ) + { + $('.cert_files').show(); + } + else + { + $('.cert_files').hide(); + } + }; + + $(document).on('change', 'select[name="sign_option"]', function(e) + { + toggleCertFiles( $(this).val() ); + }); + + // Need to trigger the change for the host type menu + if( $('#autoconfig_form .host_type').length > 0 ) + { + $('#autoconfig_form .host_type').trigger('change'); + } + if( $('#autoconfig_form input[name="config_id"]').length > 0 ) + { + $('#autoconfig_form input[name="config_id"]').trigger('change'); + } + // Hide useless up/down arrows + autoconfigShowHideArrow(); + toggleCertFiles( $('select[name="sign_option"]').val() ); +}); diff --git a/AUTOCONFIG/autoconfig.css b/AUTOCONFIG/autoconfig.css new file mode 100644 index 00000000..4006e273 --- /dev/null +++ b/AUTOCONFIG/autoconfig.css @@ -0,0 +1,808 @@ +/* +Created on 2020-03-11 +Copyright 2020 Jacques Deguest +Distributed under the same licence as Postfix Admin +*/ +@charset "UTF-8"; +@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.min.css"); + +/* Progress bar */ +#postfixadmin-progress +{ +/* position: absolute; */ + position: fixed; + display: none; + z-index: 2147483647; + top: 0px; + left: 0px; + width: 0px; + height: 2px; + -webkit-transition: width 500ms ease-out,opacity 400ms linear; + -moz-transition: width 500ms ease-out,opacity 400ms linear; + -ms-transition: width 500ms ease-out,opacity 400ms linear; + -o-transition: width 500ms ease-out,opacity 400ms linear; + transition: width 500ms ease-out,opacity 400ms linear; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + background: #b91f1f; +} + +#postfixadmin-progress.done +{ + opacity: 0; +} + +#postfixadmin-progress dd, +#postfixadmin-progress dt +{ + position: absolute; + top: 0px; + height: 2px; + -webkit-border-radius: 100%; + -moz-border-radius: 100%; + border-radius: 100%; + -webkit-box-shadow: #b91f1f 1px 0 6px 1px; + -moz-box-shadow: #b91f1f 1px 0 6px 1px; + -ms-box-shadow: #b91f1f 1px 0 6px 1px; + box-shadow: #b91f1f 1px 0 6px 1px; +} + +#postfixadmin-progress dd +{ + right: 0px; + clip: rect(-6px,22px,14px,10px); + width: 20px; + opacity: 1; +} + +#postfixadmin-progress dt +{ + right: -80px; + clip: rect(-6px,90px,14px,-6px); + width: 180px; + opacity: 1; +} + +@-moz-keyframes legaltech-progress-pulse +{ + 30% + { + opacity: 1; + } + 60% + { + opacity: 0; + } + 100% + { + opacity: 1; + } +} + +@-ms-keyframes legaltech-progress-pulse +{ + 30% + { + opacity: .6; + } + 60% + { + opacity: 0; + } + 100% + { + opacity: .6; + } +} + +@-o-keyframes legaltech-progress-pulse +{ + 30% + { + opacity: 1; + } + 60% + { + opacity: 0; + } + 100% + { + opacity: 1; + } +} + +@-webkit-keyframes legaltech-progress-pulse +{ + 30% + { + opacity: .6; + } + 60% + { + opacity: 0; + } + 100% + { + opacity: .6; + } +} + +@keyframes legaltech-progress-pulse +{ + 30% + { + opacity: 1; + } + 60% + { + opacity: 0; + } + 100% + { + opacity: 1; + } +} + +#postfixadmin-progress.waiting dd, +#postfixadmin-progress.waiting dt +{ + -webkit-animation: legaltech-progress-pulse 2s ease-out 0s infinite; + -moz-animation: legaltech-progress-pulse 2s ease-out 0s infinite; + -ms-animation: legaltech-progress-pulse 2s ease-out 0s infinite; + -o-animation: legaltech-progress-pulse 2s ease-out 0s infinite; + animation: legaltech-progress-pulse 2s ease-out 0s infinite; +} + +.autoconfig-command +{ + position: relative; + display: inline-block; + z-index: 1; + outline: 0; + width: 30px; + height: 30px; + line-height: 30px; + border-radius: 50%; + padding: 3px 3px; +/* padding: 0 16px; */ + margin: 10px 4px; + background-color: #3f51b5; + color: #fff; + border: 0; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); + text-transform: uppercase; + text-decoration: none; + font-size: 10px; + font-weight: 500; + vertical-align: middle; + cursor: pointer; + overflow: hidden; + -webkit-transition: all .15s ease-in; + transition: all .15s ease-in; +} + +.autoconfig-command:hover, +.autoconfig-command:focus +{ + opacity: .9; +} + +@-webkit-keyframes ripple +{ + 0% + { + width: 0px; + height: 0px; + opacity: .5; + } + 100% + { + width: 150px; + height: 150px; + opacity: 0; + } +} + +@keyframes ripple +{ + 0% + { + width: 0px; + height: 0px; + opacity: .5; + } + 100% + { + width: 150px; + height: 150px; + opacity: 0; + } +} + +.ripple:before +{ + content: ''; + z-index: 2; + position: absolute; + visibility: hidden; + top: 50%; + left: 50%; + width: 0px; + height: 0px; + -webkit-transform: translate( -50%, -50% ); + transform: translate( -50%, -50% ); + border-radius: 50%; + background-color: currentColor; +} + +.ripple:not(:active):before +{ + -webkit-animation: ripple 0.4s cubic-bezier( 0, 0, 0.2, 1 ); + animation: ripple 0.4s cubic-bezier( 0, 0, 0.2, 1 ); + -webkit-transition: visibility .4s step-end; + transition: visibility .4s step-end; +} + +.ripple:active:before +{ + visibility: visible; +} + +.autoconfig-server-add, +.autoconfig-locale-text-add +{ + background-color: green; +} + +.autoconfig-server-remove, +.autoconfig-locale-text-remove +{ + background-color: red; +} + +@-webkit-keyframes autoconfig-error +{ + 0% { -webkit-transform: translateX( 0px ); } + 25% { -webkit-transform: translateX( 30px ); } + 45% { -webkit-transform: translateX( -30px ); } + 65% { -webkit-transform: translateX( 30px ); } + 82% { -webkit-transform: translateX( -30px ); } + 94% { -webkit-transform: translateX( 30px ); } + 35%, 55%, 75%, 87%, 97%, 100% { -webkit-transform: translateX( 0px ); } +} + +@keyframes autoconfig-error +{ + 0% { transform: translateX( 0px ); } + 25% { transform: translateX( 30px ); } + 45% { transform: translateX( -30px ); } + 65% { transform: translateX( 30px ); } + 82% { transform: translateX( -30px ); } + 94% { transform: translateX( 30px ); } + 35%, 55%, 75%, 87%, 97%, 100% { transform: translateX( 0px ); } +} + +.autoconfig-error-shake +{ + -webkit-animation: autoconfig-error 0.35s linear; + -moz-animation: autoconfig-error 0.35s linear; + animation: autoconfig-error 0.35s linear; +} + +.autoconfig-error:before +{ + background-position: 0 0; +} + +/* Message formatting */ +#message +{ + max-width: 90%; + display: block; +} + +/* So that any link inside a message can be clickable */ +#message a +{ + cursor: pointer; + pointer-events: all; +} + +.success, +.error, +.info, +.warning, +.edit, +.lock, +.tip, +.download, +.chat, +.task +{ + position: relative; + display: block; + clear: both; + margin-bottom: 2px; + padding: 10px 10px 10px 40px; + min-height: 20px; + font-family: "Lucida Grande", "Lucida Sans Unicode", sans-serif; + font-size: 12px; + font-weight: normal; + line-height: 20px; + -webkit-border-radius: 5px; + -khtml-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + -webkit-box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.5) inset; + -moz-box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.5) inset; + -o-box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.5) inset; + box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.5) inset; + pointer-events: none; + cursor: pointer; +} + +.success:before, +.error:before, +.info:before, +.warning:before, +.edit:before, +.lock:before, +.tip:before, +.download:before, +.chat:before, +.task:before +{ + content: ""; + position: absolute; + top: 14px; + left: 16px; + width: 14px; + height: 15px; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAACWCAYAAADufbn/AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2RpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo5QkZCREFGMkQ5NDFFMTExQUFEMUYxOUI5RkM4REUzOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyM0E3OTNDMjQ2NUYxMUUxQTMxRkIyNkE5NjhGNzcwOSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyM0E3OTNDMTQ2NUYxMUUxQTMxRkIyNkE5NjhGNzcwOSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozM0NDRDBCOTVENDZFMTExOTJFNEYxQUZFREQ5ODZCMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QkZCREFGMkQ5NDFFMTExQUFEMUYxOUI5RkM4REUzOCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pmz0zwcAAAlJSURBVHja7JgLUBRHGoCnZ2Zhl+WdZYGFhUOkFA4C6CW+lYRTcyGA5nEmPjg9E8ipJ4gFxkRFTVQOJV6qLoUYS3MqnuQiENEk5PCxQixNTorkAAkJMQiiiEAE5bHs7tzfyyyZnZ3dnSUmd1V3XfVv93b3N//03393/9OIYRiiIiw0iyCIdJBN879ufpfgJWifANkpkCsgKdBHiz4ePy4T/uRz+q3gwkYIIQ3BML7GCoQqoJxAkhLJWp6CQ9B5uSCEE8PMByaMVD39TAGiKK0A/JoFBMnj4ej35zY0tlFvX7jwtba7u7+3vm4GdKI4fR4HceVDU0+UbIFiOwk/t8O35BSqFy95Q0CzEHTVOFRsVTYpa156Mafz/LlVfEiqUn02R1O13ASZgdgQoLGK0et9LNQhZIBhLANrHzMDBa1nmfTsHBph0hrkEhys4Y0ZG+4w9F9sBBFJvseH3KOiSmZVns0MXvH7rVbgIBKefI0PTSsp2wzFmgkbXnmHD9Nubq1BKSkDoIyJqpo/7xNwPebiwuQT8D8Cj5sjisbcXRsrJoQNVU6Kab7zaXUy1NGmxui+r75aJACNwr2Njc9BPgfECddx51ECMmzDqmbtXNChRIvtuG53BPbdAyCnQTJosRAi0GkYrRT+rgEZIEVAT3AgY5LLvCJJEdBJLuQidf9macKfdtNsh5WQ4X0lZ29WwwBblwTQ+wBJftDk2ZT27P4MtV9kNcrIC38N6t4wmphAn0LHuVDEmorNIa/G1GcL1wX5RZ7B00KTiMoyMPqR7YRgZgDwGdbOhVxdvBtWLTqU4a8IO2+aS3JSRMJfECJ1o3sRwURaQL89mA7QOa4DUCeKqq923W3T3exsmgaYmbHc5Iov05ccS/d9aJwG/urM1jbrOf7FH29efbmudAPDGGgTtPaFo+kKr6AqdhGbJ44j+xVX5Gxfvyfq/rZ98Rc7u1vioI6y4vQWvuoHEgPShdejoKYf6+QkMcY06uSz3yzMgWwryPcXMtO8HNEYx+ae2HMcWY9b3aXSvZODAjTsareduCbedroypr69Yx6U1damwWw6YHzB8Ax8mMYhgmjRZKaF2dl/CBKgaMhrTWP0dJH1YluJMU4ZRZKtXi6yL3FFmFJxCW8NYsCYTb95LLenf+BhXDEtJKgOslt2QZizu7kVmkHjOSihb09Sqzr2VV3WiprHIZ1ulnF8Mtm3q4o/2PG3z2uP2psSkwNgxyZu9fZNHdAOK1+c8ehh+PuIGAf43vSqaTOn7HgmNrId/taJAZcnRE58fsO8ORjoB/kcpNcWyD903Nm1aDc9mEOnan0gjjhwLLMQJICtvgFSCrJ9Vn5bp4VGgHDnQhBjuIIoyV3jItAPe7B9MZQGcOkoCNAyHBSMPg2gKTlXluDy5W2TizgwTikAH6EBCoJwpYDgjJWSSHskcu+PTGUdF0RoPzAakqSd/giQnLdGDZAZhS1zG6WYAc9BSQ6bFJFPkQa9NsRRzqDThpBw4IxlIimacpa36QZ6x5k9cXjAn51TeCvK32Lypa7XSZnPuHMWG5hBLwPrXcFiLPPjV8UvNGTA7Jf2IYruMx87PTg5+9xqLLhs1gZ9A+NeLqB9YpO/6Gupyb1RffB1MPXI+kTEcN3+JfWmMlZiCnj9py7N84lJqjW5nE9z2ZZVNy8eWcfxEj0n1DRq8p+ekh+6YHsBjuPNYvL77Q0zr53akQD5r3RD94xGoZ1db8pVEf8MSdz0odw/HB+yt4WWFV6TahAc+Jq86T5IB0jrmILA0p0on92bnl74KnOXFAmlQpY58hGDiu0erABQICtnp1RrZG4BNSMuI52I662eRiU7CArkOAhTupM80tfVNPvMO9GnbjSW4EgZCY7R+ESCKAJZ9MMyJP+6YKN+DxS/ARmkrUCHuRBFy7omzHi1jLvX0gLQIZDFXChq7t6skNi0SsFdjgMt40KxCQfWqX+5GG9Q9yzODhY6YAZJXDpZ6AQf4mrE0HIuNPmpdzMCwp8rY48Ey9MKa1OGzG1EaOQziJLIO1io1BpketWP3BQRZyPjd693lvvWPZJclM5CNo9zVLqT6oMd8I5v6BMJ0xd96MkGf4N2/bDj209eACeIB5HZi234cY4zPs0d3lq5LpeWlpbJXl0EgVwHeauwsPBNm0EgQK/gqwuapl2VSqUG5/g/Wy+sERplCKFuJyennuzs7NWBgYEtbW1twXl5eW9rtVov6OMDmi09RyKRREOj1MfH5wJA+BalBue+vr5ncT2kCMFXVSgUeBqIwcHBPs6+Mjw0NNRhjH88PS0CX5Samvpn1iD2EjZUxqhGSCtFBe+8fqRxrxY5dQ/kK+A/ACJ85SJmgLx+pEqlqhAD8vthl8NeESqCbQZpeDBB4MltMd6QPQ+CYx4XkC/YrbI2KadW0Abog63R4yGDTsxMXtMAGGQNbCtFAA9ZehIl2WwJGWMPHG3sgTMjWXA64IlJNr67POHBfxAGDXoPG59soJAKFAQpiazDxrTrJTL364Kg3Ft93mrAJnHuUoQ8elwQVEcn7neWe9dbuhg5rAydfjAifm2ZIBg6bdkl76DY4xDxmp0TMg//SzHJWwucXRV3rK2OAXdl2EmICK9xPdFNGVoukbq3Gq8/rC0rvW6oBV7NbPvTDd5rsgaNulx7fQWCaTFpwy7GaPt7SLsLGSzLmIK9kRsWhoJpYuyCqoh5/e5+E/9BO7nchFcewlPkqYpoEnPoIDb4G8/GprfYtTf4036UuS04avaUvrKl6L9oe/zZQeSafESUWfmGIqEiSQSUJPSq5bZgtq3c2hgFYWuQxQ0SSCIeMxZctnuDxEuJprf4SS4Qfn4HoKdkeM+BfBe+dRLJnAXJpklEvmdgDEoHlD1OkfQB0kFoZIenKP8xjRHhff5/YCGTriMHi0PJxdmtlcrL3XOnub1BDVFEv7NE2m0SCs5HnX7YlY0wtK4yj2u4XuHue/XlJzfuwk6OGyNGbxfY1HOvU5b61pMbrnc2PwZv9d2Z3O9+xzbhc7Te1tcMRCRM7Ir8+L/HZaub+e1iPofCXz+2JkvMQhZKStNVxYNbyL+OesjmE/jttKmi8l9dSbZgbjuUzZy83M7bmbWj+EhvhweJNWLjJI7BNuVj1khgkN21Ex0oE//XaEPj2IOHMTvAWJfVvwUYAKkVWW9BNMncAAAAAElFTkSuQmCC); + background-repeat: no-repeat; +} + +.success:after, +.error:after, +.info:after, +.warning:after, +.edit:after, +.lock:after, +.tip:after, +.download:after, +.chat:after, +.task:after +{ + content: "x"; + position: absolute; + top: 10px; + right: 10px; + width: 5px; + height: 6px; + cursor: pointer; + font: normal normal 13px/20px "Lucida Grande", "Lucida Sans Unicode", sans-serif; + font-size: 13px; + -webkit-text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7); + -moz-text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7); + text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7); + /* + Awesome trick ! + http://stackoverflow.com/questions/7478336/only-detect-click-event-on-pseudo-element + http://jsfiddle.net/ZWw3Z/70/ + */ + cursor: pointer; + pointer-events: all; +} + +.success +{ + border: 1px solid #accc5d; + color: #70892b; + background-color: #c8e185; + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #d0e98e), color-stop(100%, #c1da7f)); + background-image: -webkit-linear-gradient( #d0e98e, #c1da7f ); + background-image: -moz-linear-gradient( #d0e98e, #c1da7f ); + background-image: -o-linear-gradient( #d0e98e, #c1da7f ); + background-image: linear-gradient( #d0e98e, #c1da7f ); + -webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.3 ); + -moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.3 ); + text-shadow: 0px 1px rgba( 255, 255, 255, 0.3 ); +} + +.success:before +{ + background-position: 0 -15px; +} + +@-webkit-keyframes error +{ + 0% { -webkit-transform: translateX( 0px ); } + 25% { -webkit-transform: translateX( 30px ); } + 45% { -webkit-transform: translateX( -30px ); } + 65% { -webkit-transform: translateX( 30px ); } + 82% { -webkit-transform: translateX( -30px ); } + 94% { -webkit-transform: translateX( 30px ); } + 35%, 55%, 75%, 87%, 97%, 100% { -webkit-transform: translateX( 0px ); } +} + +.error +{ + border: 1px solid #dc4e4d; + color: #b52525; + background-color: #ec8282; + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f48888), color-stop(100%, #e17575)); + background-image: -webkit-linear-gradient( #f48888, #e17575 ); + background-image: -moz-linear-gradient( #f48888, #e17575 ); + background-image: -o-linear-gradient( #f48888, #e17575 ); + background-image: linear-gradient( #f48888, #e17575 ); + -webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + -moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + -webkit-animation: error 0.35s linear; + -moz-animation: error 0.35s linear; +} + +.error-shake +{ + -webkit-animation: error 0.35s linear; + -moz-animation: error 0.35s linear; + animation: error 0.35s linear; +} + +.error:before +{ + background-position: 0 0; +} + +.info +{ + border: 1px solid #69c0ca; + color: #3d8d98; + background-color: #8aced6; + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #99e2eb), color-stop(100%, #79c6cd)); + background-image: -webkit-linear-gradient( #99e2eb, #79c6cd ); + background-image: -moz-linear-gradient( #99e2eb, #79c6cd ); + background-image: -o-linear-gradient( #99e2eb, #79c6cd ); + background-image: linear-gradient( #99e2eb, #79c6cd ); + -webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + -moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); +} + +.info:before +{ + background-position: 0 -30px; +} + +.warning +{ + color: #c2721b; + border: 1px solid #f9b516; + background-color: #fbb160; + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffd57f), color-stop(100%, #ffa544)); + background-image: -webkit-linear-gradient( #ffd57f, #ffa544 ); + background-image: -moz-linear-gradient( #ffd57f, #ffa544 ); + background-image: -o-linear-gradient( #ffd57f, #ffa544 ); + background-image: linear-gradient( #ffd57f, #ffa544 ); + -webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + -moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); +} + +.warning:before +{ + background-position: 0 -45px; +} + +.edit +{ + color: #ae8500; + border: 1px solid #e9c95f; + background-color: #f3dc8f; + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffeaa7), color-stop(100%, #f3d573)); + background-image: -webkit-linear-gradient( #ffeaa7, #f3d573 ); + background-image: -moz-linear-gradient( #ffeaa7, #f3d573 ); + background-image: -o-linear-gradient( #ffeaa7, #f3d573 ); + background-image: linear-gradient( #ffeaa7, #f3d573 ); + -webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + -moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); +} + +.edit:before +{ + background-position: 0 -60px; +} + +.lock +{ + border: 1px solid #CCC; + color: #666; + background-color: #e8e8e8; + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f4f4f4), color-stop(100%, #e0e0e0)); + background-image: -webkit-linear-gradient( #f4f4f4, #e0e0e0 ); + background-image: -moz-linear-gradient( #f4f4f4, #e0e0e0 ); + background-image: -o-linear-gradient( #f4f4f4, #e0e0e0 ); + background-image: linear-gradient( #f4f4f4, #e0e0e0 ); + -webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + -moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); +} + +.lock:before +{ + background-position: 0 -75px; +} + +.tip +{ + border: 1px solid #e6b96f; + color: #b1802f; + background-color: #f5dcb2; + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffedcf), color-stop(100%, #f8d69e)); + background-image: -webkit-linear-gradient( #ffedcf, #f8d69e ); + background-image: -moz-linear-gradient( #ffedcf, #f8d69e ); + background-image: -o-linear-gradient( #ffedcf, #f8d69e ); + background-image: linear-gradient( #ffedcf, #f8d69e ); + -webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + -moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); +} + +.tip:before +{ + background-position: 0 -90px; +} + +.download +{ + border: 1px solid #3178c0; + color: #0c4fa3; + background-color: #6dacea; + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #8ed0fa), color-stop(100%, #4e95dc)); + background-image: -webkit-linear-gradient( #8ed0fa, #4e95dc ); + background-image: -moz-linear-gradient( #8ed0fa, #4e95dc ); + background-image: -o-linear-gradient( #8ed0fa, #4e95dc ); + background-image: linear-gradient( #8ed0fa, #4e95dc ); + -webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + -moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); +} + +.download:before +{ + background-position: 0 -105px; +} + +.chat +{ + color: #366f11; + border: 1px solid #5d902f; + background-color: #89bc5a; + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #8dcb3d), color-stop(100%, #74a547)); + background-image: -webkit-linear-gradient( #8dcb3d, #74a547 ); + background-image: -moz-linear-gradient( #8dcb3d, #74a547 ); + background-image: -o-linear-gradient( #8dcb3d, #74a547 ); + background-image: linear-gradient( #8dcb3d, #74a547 ); + -webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + -moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); +} + +.chat:before +{ + background-position: 0 -120px; +} + +.task +{ + color: #432c12; + border: 1px solid #71502b; + background-color: #92724e; + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #a58868), color-stop(100%, #886640)); + background-image: -webkit-linear-gradient( #a58868, #886640 ); + background-image: -moz-linear-gradient( #a58868, #886640 ); + background-image: -o-linear-gradient( #a58868, #886640 ); + background-image: linear-gradient( #a58868, #886640 ); + -webkit-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + -moz-text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); + text-shadow: 0px 1px rgba( 255, 255, 255, 0.2 ); +} + +.task:before +{ + background-position: 0 -135px; +} + +#autoconfig_save:hover, +#autoconfig_remove:hover, +#autoconfig_cancel:hover +{ + cursor: pointer; +} + +#autoconfig_provider_domain option:disabled:hover, +#autoconfig_remove:disabled +{ + cursor: not-allowed; +} + +/* If this move up button is in the first host block, then deactivate it */ +.autoconfig-incoming:first-child .autoconfig-move-up, +.autoconfig-incoming:last-child .autoconfig-move-down, +.autoconfig-outgoing:first-child .autoconfig-move-up, +.autoconfig-outgoing:last-child .autoconfig-move-down +{ + display: none; +} + +#autoconfig_form > table +{ + border-collapse: collapse; +} + +.autoconfig-incoming, +.autoconfig-outgoing, +.autoconfig-instruction, +.autoconfig-documentation +{ + border-bottom: 1pt solid black; +} + +/* CSS switch designed and credit to Thibaut Courouble http://thibaut.me */ +.switch +{ + position: relative; + display: inline-block; + width: 56px; + height: 20px; + padding: 3px; + vertical-align: top; + background-color: white; + border-radius: 18px; + box-shadow: inset 0 -1px white, inset 0 1px 1px rgba(0, 0, 0, 0.05); + cursor: pointer; + background-image: -webkit-linear-gradient(top, #eeeeee, white 25px); + background-image: -moz-linear-gradient(top, #eeeeee, white 25px); + background-image: -o-linear-gradient(top, #eeeeee, white 25px); + background-image: linear-gradient(to bottom, #eeeeee, white 25px); +} + +.switch-input +{ +/* + position: absolute; + top: 0; + left: 0; + opacity: 0; + */ + display: none; +} + +.switch-label +{ + position: relative; + display: block; + height: inherit; + font-size: 10px; + text-transform: uppercase; + background: #eceeef; + border-radius: inherit; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12), inset 0 0 2px rgba(0, 0, 0, 0.15); + -webkit-transition: 0.15s ease-out; + -moz-transition: 0.15s ease-out; + -o-transition: 0.15s ease-out; + transition: 0.15s ease-out; + -webkit-transition-property: opacity background; + -moz-transition-property: opacity background; + -o-transition-property: opacity background; + transition-property: opacity background; +} + +.switch-label:before, +.switch-label:after +{ + position: absolute; + top: 50%; + margin-top: -.5em; + line-height: 1; + -webkit-transition: inherit; + -moz-transition: inherit; + -o-transition: inherit; + transition: inherit; +} + +.switch-label:before +{ + content: attr(data-off); + right: 11px; + color: #aaa; + text-shadow: 0 1px rgba(255, 255, 255, 0.5); +} + +.switch-label:after +{ + content: attr(data-on); + left: 11px; + color: white; + text-shadow: 0 1px rgba(0, 0, 0, 0.2); + opacity: 0; +} + +input[name="enable_status"].switch-input:checked ~ table [for="enable_status"].switch > .switch-label, +input[name="documentation_status"].switch-input:checked ~ table [for="documentation_status"].switch > .switch-label +{ + background: #47a8d8; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15), inset 0 0 3px rgba(0, 0, 0, 0.2); +} + +input[name="enable_status"].switch-input:checked ~ table [for="enable_status"].switch > .switch-label:before, +input[name="documentation_status"].switch-input:checked ~ table [for="documentation_status"].switch > .switch-label:before +{ + opacity: 0; +} + +input[name="enable_status"].switch-input:checked ~ table [for="enable_status"].switch > .switch-label:after, +input[name="documentation_status"].switch-input:checked ~ table [for="documentation_status"].switch > .switch-label:after +{ + opacity: 1; +} + +.switch-handle +{ + position: absolute; + top: 4px; + left: 4px; + width: 18px; + height: 18px; + background: white; + border-radius: 10px; + box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.2); + background-image: -webkit-linear-gradient(top, white 40%, #f0f0f0); + background-image: -moz-linear-gradient(top, white 40%, #f0f0f0); + background-image: -o-linear-gradient(top, white 40%, #f0f0f0); + background-image: linear-gradient(to bottom, white 40%, #f0f0f0); + -webkit-transition: left 0.15s ease-out; + -moz-transition: left 0.15s ease-out; + -o-transition: left 0.15s ease-out; + transition: left 0.15s ease-out; +} + +.switch-handle:before +{ + content: ''; + position: absolute; + top: 50%; + left: 50%; + margin: -6px 0 0 -6px; + width: 12px; + height: 12px; + background: #f9f9f9; + border-radius: 6px; + box-shadow: inset 0 1px rgba(0, 0, 0, 0.02); + background-image: -webkit-linear-gradient(top, #eeeeee, white); + background-image: -moz-linear-gradient(top, #eeeeee, white); + background-image: -o-linear-gradient(top, #eeeeee, white); + background-image: linear-gradient(to bottom, #eeeeee, white); +} + +input[name="enable_status"].switch-input:checked ~ table [for="enable_status"].switch > .switch-handle, +input[name="documentation_status"].switch-input:checked ~ table [for="documentation_status"].switch > .switch-handle +{ + left: 40px; + box-shadow: -1px 1px 5px rgba(0, 0, 0, 0.2); +} + +input[name="enable_status"] ~ table tr.autoconfig-instruction, +input[name="documentation_status"] ~ table tr.autoconfig-documentation +{ + display: none; +} + +input[name="enable_status"]:checked ~ table tr.autoconfig-instruction, +input[name="documentation_status"]:checked ~ table tr.autoconfig-documentation +{ + display: table-row; +} + diff --git a/AUTOCONFIG/autoconfig.js b/AUTOCONFIG/autoconfig.js new file mode 100644 index 00000000..b8a1d69a --- /dev/null +++ b/AUTOCONFIG/autoconfig.js @@ -0,0 +1,910 @@ +/* +Created on 2020-03-12 +Copyright 2020 Jacques Deguest +Distributed under the same licence as Postfix Admin +*/ +$(document).ready(function() +{ + const DEBUG = false; + + // Credits to: https://tdanemar.wordpress.com/2010/08/24/jquery-serialize-method-and-checkboxes/ + // Modified by Jacques Deguest to include other form elements: + // http://www.w3schools.com/tags/tag_input.asp + (function($) + { + $.fn.serializeAll = function(options) + { + return $.param(this.serializeArrayAll(options)); + }; + + $.fn.serializeArrayAll = function (options) + { + var o = $.extend({ + checkboxesAsBools: false + }, options || {}); + + var rselectTextarea = /select|textarea/i; + var rinput = /text|hidden|password|search|date|time|number|color|datetime|email|file|image|month|range|tel|url|week/i; + + return this.map(function () + { + return this.elements ? $.makeArray(this.elements) : this; + }) + .filter(function () + { + return this.name && !this.disabled && + (this.checked + || (o.checkboxesAsBools && this.type === 'checkbox') + || rselectTextarea.test(this.nodeName) + || rinput.test(this.type)); + }) + .map(function (i, elem) + { + var val = $(this).val(); + return val == null ? + null : + $.isArray(val) ? + $.map(val, function (val, i) + { + return { name: elem.name, value: val }; + }) : + { + name: elem.name, + value: (o.checkboxesAsBools && this.type === 'checkbox') ? + (this.checked ? 1 : 0) : + val + }; + }).get(); + }; + })(jQuery); + + window.makeMessage = function( type, mesg ) + { + return( sprintf( '
    %s
    ', type, mesg ) ); + }; + + window.showMessage = function() + { + if( DEBUG ) console.log( "Called from " + ( arguments.callee.caller === null ? 'void' : arguments.callee.caller.name ) ); + var opts = { + div: $('#message'), + append: false, + dom: null, + timeout: null, + scroll: false, + timeoutCallback: null, + }; + var msgDiv = $('#message'); + if( arguments.length == 3 && typeof( arguments[2] ) === 'object' ) + { + var param = arguments[2]; + opts.type = arguments[0]; + opts.message = arguments[1]; + if( typeof( param.append ) !== 'undefined' ) opts.append = param.append; + if( typeof( param.dom ) !== 'undefined' ) opts.dom = param.dom; + if( typeof( param.timeout ) !== 'undefined' ) opts.timeout = param.timeout; + if( typeof( param.timeoutCallback ) !== 'undefined' ) opts.timeoutCallback = param.timeoutCallback; + if( typeof( param.scroll ) !== 'undefined' ) opts.scroll = param.scroll; + if( typeof( param.speak ) !== 'undefined' ) opts.speak = param.speak; + } + // Backward compatibility + else if( arguments.length >= 2 ) + { + opts.type = arguments[0]; + opts.message = arguments[1]; + if( arguments.length > 2 ) opts.append = arguments[2]; + if( arguments.length > 3 ) opts.dom = arguments[3]; + if( arguments.length > 4 ) opts.timeout = arguments[4]; + } + else + { + msgDiv.append( makeMessage( 'warning', "showMessage called with only " + arguments.length + " parameters while 2 at minimum are required. Usage: showMessage( type, message, append, domObject, timeout ) or showMessage( type, message, options )" ) ); + return( false ); + } + if( typeof( opts.dom ) === 'object' && opts.dom != null ) + { + msgDiv = opts.dom; + } + // Check if message is an array of messages + // https://stackoverflow.com/a/4775741/4814971 + if( Array.isArray ) + { + if( Array.isArray( opts.message ) ) + { + opts.messages = opts.message; + } + } + else if( opts.message instanceof( Array ) ) + { + opts.messages = opts.message; + } + else if( $.isArray( opts.message ) ) + { + opts.messages = opts.message; + } + + // messages has been set with previous check + // Make this array a list of errors + if( opts.hasOwnProperty( 'messages' ) ) + { + opts.message = sprintf( "
      \n%s\n
    ", opts.messages.map(function(e){ return('
  • ' + e + '
  • '); }).join( "\n" ) ); + } + + if( opts.append ) + { + msgDiv.append(makeMessage(opts.type, opts.message)); + } + else + { + msgDiv.html(makeMessage(opts.type, opts.message)); + } + + if( opts.type == 'error' ) + { + msgDiv.addClass( 'error-shake' ); + setTimeout(function() + { + msgDiv.removeClass( 'error-shake' ); + }, 70000); + } + + if( parseInt( opts.timeout ) > 0 ) + { + var thisTimeout = parseInt( opts.timeout ); + setTimeout(function() + { + msgDiv.html( '' ); + if( typeof( opts.timeoutCallback ) === 'function' ) + { + opts.timeoutCallback(); + } + },thisTimeout); + } + else + { + setTimeout(function() + { + msgDiv.html( '' ); + },15000); + } + if( opts.scroll ) + { + if( DEBUG ) console.log( "Scrolling to the top of the page..." ); + $('html, body').animate( { scrollTop: 0 }, 500 ); + } + else + { + if( DEBUG ) console.log( "No scrolling..." ); + } + }; + + window.postfixAdminProgressBar = function() + { + var xhr = $.ajaxSettings.xhr(); + if( DEBUG ) console.log( "Initiating the progress bar." ); + if( DEBUG ) console.log( "Called from:\n" + (new Error).stack ); + $('#postfixadmin-progress').css( 'width', '0%' ).show().removeClass('done'); + xhr.upload.onprogress = function(evt) + { + if( evt.lengthComputable ) + { + var percentComplete = evt.loaded / evt.total; + if( DEBUG ) console.log(percentComplete); + $('#postfixadmin-progress').css({ + width: percentComplete * 100 + '%' }); + if( DEBUG ) console.log( "upload.addEventListener: " + percentComplete ); + if( percentComplete === 1 ) + { + // $('#postfixadmin-progress').addClass('done').hide(); + } + } + }; + xhr.onprogress = function(evt) + { + if( evt.lengthComputable ) + { + var percentComplete = evt.loaded / evt.total; + if( DEBUG ) console.log("addEventListener: " + percentComplete); + $('#postfixadmin-progress').css({ + width: percentComplete * 100 + '%' }); + if( percentComplete === 1 ) + { + // $('#postfixadmin-progress').addClass('done').hide(); + } + } + }; + return( xhr ); + }; + + window.postfixAdminProgressBarStart = function() + { + $({property: 0}).animate({property: 85}, + { + // Arbitrary time, which should well cover the time it takes to get response from server + // Otherwise, well our progress bar will hang at 85% until we get a call to kill it + duration: 4000, + step: function() + { + var _percent = Math.round( this.property ); + $('#postfixadmin-progress').css( 'width', _percent + '%' ); + } + }); + }; + + window.postfixAdminProgressBarStop = function() + { + $({property: 85}).animate({property: 105}, + { + duration: 1000, + step: function() + { + var _percent = Math.round( this.property ); + $('#postfixadmin-progress').css( 'width', _percent + '%' ); + if( _percent == 105 ) + { + $('#postfixadmin-progress').addClass('done'); + } + }, + complete: function() + { + $('#postfixadmin-progress').hide(); + $('#postfixadmin-progress').css( 'width', '0%' ); + } + }); + }; + + window.autoconfigShowHideArrow = function() + { + // Reset. Show them all + $('table.server .autoconfig-command').show(); + // Hide first and last ones + // This requires jQUery up to v3.3. Version 3.4 onward do not support :first and :last anymore + $('.autoconfig-incoming:first .autoconfig-move-up').hide(); + $('.autoconfig-incoming:last .autoconfig-move-down').hide(); + $('.autoconfig-outgoing:first .autoconfig-move-up').hide(); + $('.autoconfig-outgoing:last .autoconfig-move-down').hide(); + }; + + window.autoconfig_ajax_call = function( postData ) + { + var prom = $.Deferred(); + $this = $(this); + $.ajax({ + xhr: postfixAdminProgressBar, + type: "POST", + url: "autoconfig.php", + dataType: "json", + data: postData, + beforeSend: function(xhr) + { + xhr.overrideMimeType( "application/json; charset=utf-8" ); + if( DEBUG ) console.log( "Initiating the progress bar." ); + if( DEBUG ) console.log( "Called from:\n" + (new Error).stack ); + // $('#postfixadmin-progress').show().removeClass('done'); + postfixAdminProgressBarStart(); + } + }).fail(function(xhr, errType, ExceptionObject) + { + postfixAdminProgressBarStop(); + if( DEBUG ) console.log( "Returned error " + xhr.status + " with error type " + errType + " and exception object " + JSON.stringify( ExceptionObject ) ); + if( DEBUG ) console.log( "Current url is: " + xhr.responseURL ); + if( DEBUG ) console.log( "Http headers are: " + xhr.getAllResponseHeaders() ); + if( DEBUG ) console.log( "Response raw data is: \n" + xhr.responseText ); + // There was a redirect most likely due to some timeout + if( typeof( xhr ) !== 'undefined' ) + { + if( DEBUG ) console.log( "Xhr is: " + JSON.stringify( xhr ) ); + if( xhr.getResponseHeader( 'Content-Type' ).toLowerCase().indexOf( 'text/html' ) >= 0 ) + { + window.location.reload(); + return( true ); + } + } + var msg = 'An unexpected error has occurred'; + showMessage( 'error', msg, { scroll: true }); + $this.addClass( 'error-shake' ); + prom.reject(); + }).done(function(data, status, xhr) + { + postfixAdminProgressBarStop(); + if( data.error ) + { + showMessage( 'error', data.error, { scroll: true }); + $this.addClass( 'error-shake' ); + setTimeout(function() + { + $this.removeClass( 'error-shake' ); + },5000); + prom.reject(); + } + else + { + if( data.success ) + { + prom.resolve(data); + showMessage( 'success', data.success, { scroll: true }); + if( DEBUG ) console.log( "save(): " + data.success ); + } + else if( data.info ) + { + showMessage( 'info', data.info, { scroll: true } ); + prom.resolve(); + } + else + { + showMessage( 'info', data.msg, { scroll: true } ); + prom.resolve(); + } + } + }); + return( prom.promise() ); + }; + + $(document).on('click','#autoconfig_save', function(e) + { + e.preventDefault(); + $this = $(this); + var data = {handler: 'autoconfig_save'}; + $('#autoconfig_form').serializeArrayAll().map(function(item) + { + if( data[ item.name ] !== undefined ) + { + if( !data[ item.name ].push ) + { + data[ item.name ] = [ data[item.name] ]; + } + data[ item.name ].push( item.value ); + } + else + { + data[ item.name ] = item.value; + } + }); + if( DEBUG ) console.log( "serialized data is: " + JSON.stringify( data ) ); + autoconfig_ajax_call( data ).done(function(data) + { + // Since this shared function is used for both saving (adding and updating) as well as deleting + // we check if those data properties are returned by the server. + // Those here are only returned if this is the result of an addition + if( data.hasOwnProperty('config_id') ) + { + if( $('#autoconfig_form input[name="config_id"]').length == 0 ) + { + showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); + throw( "Unable to find the field \"config_id\" !" ); + } + if( typeof( data.config_id ) !== 'undefined' && data.config_id.length > 0 ) + { + // We force trigger change, because this is a hidden field and it hidden field do not trigger change + $('#autoconfig_form input[name="config_id"]').val( data.config_id ).trigger('change'); + } + } + // Do the hosts + var hostTypes = [ 'incoming', 'outgoing' ]; + for( var j = 0; j < hostTypes.length; j++ ) + { + var hostType = hostTypes[j]; + if( DEBUG ) console.log( "Checking host of type " + hostType ); + if( data.hasOwnProperty(hostType + '_server') && Array.isArray( data[hostType + '_server'] ) ) + { + var thoseHosts = $('.autoconfig-' + hostType); + var dataHosts = data[hostType + '_server']; + if( thoseHosts.length == 0 ) + { + showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); + throw( "Unable to find any hosts block in our form!" ); + } + else if( dataHosts.length != thoseHosts.length ) + { + + showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); + throw( "Total of " + hostType + " servers returned from the server (" + dataHosts.length + ") do not match the total hosts we have in our form (" + thoseHosts.length + ")." ); + } + dataHosts.forEach(function(def, index) + { + if( DEBUG ) console.log( "def contains: " + JSON.stringify( def ) ); + if( !def.hasOwnProperty('config_id') || + !def.hasOwnProperty('hostname') || + !def.hasOwnProperty('port') ) + { + if( DEBUG ) console.error( "Something is wrong. Data received is missing fields config_id, or hostname or port" ); + return( false ); + } + thoseHosts.each(function(offset, obj) + { + var dom = $(obj); + var hostId = dom.find('input[name="host_id[]"]'); + var hostName = dom.find('input[name="hostname[]"]'); + var hostPort = dom.find('input[name="port[]"]'); + if( !hostId.length ) + { + showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); + throw( "Unable to find host id field for host type \"" + hostType + "\" at offset " + i ); + } + if( !hostName.length ) + { + showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); + throw( "Unable to find host name field for host type \"" + hostType + "\" at offset " + i ); + } + if( !hostPort.length ) + { + showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); + throw( "Unable to find host port field for host type \"" + hostType + "\" at offset " + i ); + } + // We found our match: no id, hostname and port match + if( hostId.val().length == 0 && hostName.val() == def.hostname && hostPort.val() == def.port ) + { + if( DEBUG ) console.log( "Setting host id " + def.host_id + " to host name " + hostName.val() + " with port " + hostPort.val() ); + hostId.val( def.host_id ); + // exit the loop + return( false ); + } + }); + }); + } + else + { + if( DEBUG ) console.error( "Something is wrong. Data received for hosts of type " + hostType + " does not exist or is not an array." ); + } + } + + // Now, do the texts + var textTypes = ['instruction', 'documentation']; + for( var j = 0; j < textTypes.length; j++ ) + { + var textType = textTypes[j]; + if( DEBUG ) console.log( "Checking text of type " + textType ); + if( data.hasOwnProperty(textType) && Array.isArray( data[textType] ) ) + { + var dataTexts = data[textType]; + var thoseTextIds = $('input[name="' + textType + '_id[]"]'); + var thoseTextLang = $('select[name="' + textType + '_lang[]"]'); + var thoseTextData = $('textarea[name="' + textType + '_text[]"]'); + // The array could very well be empty + dataTexts.forEach(function(def, index) + { + if( !def.hasOwnProperty('id') || + !def.hasOwnProperty('type') || + !def.hasOwnProperty('lang') ) + { + if( DEBUG ) console.error( "Something is wrong. Data received is missing fields id, or type or lang" ); + return( false ); + } + for( var k = 0; k < thoseTextIds.length; k++ ) + { + var textId = thoseTextIds.eq(k); + var textLang = thoseTextLang.eq(k); + // Found a match + if( textId.val().length == 0 && textLang.val() == def.lang ) + { + if( DEBUG ) console.log( "Setting text id " + def.id + " to text with type " + textType + " and language " + def.lang ); + textId.val( def.id ); + return( false ); + } + } + }); + } + else + { + if( DEBUG ) console.error( "Something is wrong. Data received for " + textType + " text does not exist or is not an array." ); + } + } + }).fail(function() + { + // Nothing for now + }); + }); + + $(document).on('click','#autoconfig_remove', function(e) + { + e.preventDefault(); + var data = + { + handler: 'autoconfig_remove', + config_id: $('input[name="config_id"]').val(), + token: $('input[name="token"]').val(), + }; + if( DEBUG ) console.log( "Data to be sent is: " + JSON.stringify( data ) ); + autoconfig_ajax_call( data ).done(function(data) + { + // Reload the page, but without the query string at the end + window.location.href = '/' + window.location.pathname; + }).fail(function() + { + // Nothing for now + }); + }); + + $(document).on('click', '#autoconfig_cancel', function(e) + { + e.preventDefault(); + window.location.href = 'list.php?table=domain'; + return( true ); + }); + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random + window.getRandomInt = function(min, max) + { + return( Math.floor( Math.random() * Math.floor((max - min) + min) ) ); + } + /* + I could have just created one function and called it + */ + // Add and remove hosts + $(document).on('click','.autoconfig-server-add',function(e) + { + e.preventDefault(); + var row = $(this).closest('table.server').closest('tr'); + if( !row.length ) + { + throw( "Unable to find the current enclosing row." ); + } + var clone = row.clone(); + clone.find('select,input[type!="hidden"],textarea').each(function(i,item) + { + $(item).val( '' ); + }); + // We need to remove the host_id so it can be treated as a new host and not an update of an existing one + clone.find('input[name="host_id[]"]').val( '' ); + // Set default value and trigger change, which will call an event handler that will hide/show the section on pop3 + clone.find('.host_type').val('imap').trigger('change'); + var optionLabels = ['leave_messages_on_server', 'download_on_biff', 'days_to_leave_messages_on_server', 'check_interval']; + optionLabels.forEach(function(fieldName, index) + { + if( DEBUG ) console.log( "Checking field name " + fieldName ); + var thisField = clone.find('input[name="' + fieldName + '[]"]'); + if( thisField.length > 0 ) + { + var forThisField = $('label[for="' + thisField.attr('id') + '"]', clone); + if( forThisField.length > 0 ) + { + if( DEBUG ) console.log( "Field is " + thisField.html() + "\nLabel field is: " + forThisField.html() ); + thisField.attr('id', 'autoconfig_' + fieldName + '_' + getRandomInt(100,1000)); + maxAttempt = 10; + if( DEBUG ) console.log( "Checking if generated id exists: " + thisField.attr('id') ); + while( $('#' + thisField.attr('id'), clone).length > 0 && ++maxAttempt < 10 ) + { + thisField.attr('id', 'autoconfig_' + fieldName + '_' + getRandomInt(100,1000)); + } + forThisField.attr('for', thisField.attr('id')); + if( DEBUG ) console.log( "Final generated id is: " + thisField.attr('id') ); + } + else + { + if( DEBUG ) console.error( "Unable to find label element for field name." ); + } + } + }); + + clone.insertAfter( row ); + autoconfigShowHideArrow(); + $('html, body').animate( { scrollTop: clone.offset().top }, 500 ); + return( true ); + }); + + $(document).on('click','.autoconfig-server-remove',function(e) + { + e.preventDefault(); + var row = $(this).closest('table.server').closest('tr'); + if( !row.length ) + { + throw( "Unable to find the current enclosing row." ); + } + // Check if there are at least 2 elements, so that after at least one remain + var re = new RegExp('(autoconfig-(?:incoming|outgoing))-server'); + var res = $(this).attr('class').match( re ); + console.log( res ); + if( res == null ) + { + throw( "Cannot find class \"autoconfig-incoming-server\" or class \"autoconfig-outgoing-server\" in our clicked element." ); + } + // Now find how many elements we have with this class + var total = $('tr.' + res[1]).length; + if( total < 2 ) + { + row.addClass('autoconfig-error-shake'); + setTimeout(function() + { + row.removeClass('autoconfig-error-shake'); + },1000); + return( false ); + } + row.remove(); + autoconfigShowHideArrow(); + }); + + // Add and remove account enable instructions or support documentation + $(document).on('click','.autoconfig-locale-text-add',function(e) + { + e.preventDefault(); + var row = $(this).closest('tr'); + if( !row.length ) + { + throw( "Unable to find the current enclosing row." ); + } + var clone = row.clone(); + clone.find('select,input[type!="hidden"],textarea').each(function(i,item) + { + $(item).val( '' ); + }); + // We need to remove the host_id so it can be treated as a new text and not an update of an existing one + clone.find('input[name$="_id[]"]').val( '' ); + clone.insertAfter( row ); + $('html, body').animate( { scrollTop: clone.offset().top }, 500 ); + }); + + $(document).on('click','.autoconfig-locale-text-remove',function(e) + { + e.preventDefault(); + var row = $(this).closest('tr'); + if( !row.length ) + { + throw( "Unable to find the current enclosing row." ); + } + var re = new RegExp('(autoconfig-(instruction|documentation))'); + var res = $(this).attr('class').match( re ); + if( res == null ) + { + throw( "Cannot find class \"autoconfig-instruction\" or class \"autoconfig-documentation\" in our clicked element." ); + } + var textType = res[2]; + var total = $('tr.' + res[1]).length; + if( DEBUG ) console.log( total + " rows found for text type " + textType ); + if( total < 2 ) + { + if( DEBUG ) console.log( "text remove: one left" ); + if( DEBUG ) console.log( "Getting lang object with " + '[name="' + textType + '_lang[]"]' ); + var textLang = row.find('[name="' + textType + '_lang[]"]'); + if( DEBUG ) console.log( "Getting text object with " + '[name="' + textType + '_text[]"]' ); + var textData = row.find('[name="' + textType + '_text[]"]'); + if( DEBUG ) console.log( "text remove: found lang and text field? " + ( ( textLang.length > 0 && textData.length > 0 ) ? "Yes" : "No" ) ); + // This is remaining default tet row and there is no more data + if( ( textLang.val() === null || ( textLang.val() !== null && textLang.val() == '' ) ) && $.trim(textData.val()) == '' ) + { + if( DEBUG ) console.log( "text remove: lang and text fields are empty already, error shake it" ); + row.addClass('autoconfig-error-shake'); + setTimeout(function() + { + row.removeClass('autoconfig-error-shake'); + },1000); + } + else + { + if( DEBUG ) console.log( "text remove: empty fields" ); + textLang.val( '' ); + textData.val( '' ); + } + return( false ); + } + row.remove(); + }); + + $(document).on('click', '#copy_provider_value', function(e) + { + e.preventDefault(); + var orgField = $('input[name="organisation"]'); + var providerNameField = $('input[name="provider_name"]'); + if( !orgField.length || !providerNameField.length ) + { + throw( "Unable to find either the provider name field or the organisation field!" ); + } + if( providerNameField.val().length == 0 ) + { + return( false ); + } + orgField.val( providerNameField.val() ); + return( true ); + }); + + $(document).on('click', '#autoconfig_toggle_select_all_domains', function(e) + { + e.preventDefault(); + // if( DEBUG ) console.log( "provider_domain options length: " + $('#autoconfig_provider_domain option').length + " and disabled options are: " + $('#autoconfig_provider_domain option:disabled').length + " and selected options are: " + $('#autoconfig_provider_domain option:selected').length ); + if( $('#autoconfig_provider_domain option:selected').length > 0 ) + { + $('#autoconfig_provider_domain option').prop('selected', false); + } + else + { + if( $('#autoconfig_provider_domain option:disabled').length == $('#autoconfig_provider_domain option').length ) + { + var row = $(this).closest('tr'); + row.addClass('error-shake'); + setTimeout(function() + { + row.removeClass('error-shake'); + },500); + return( false ); + } + else + { + $('#autoconfig_provider_domain option:not(:disabled)').prop('selected', true); + } + } + }); + + $(document).on('change', '#autoconfig_form .host_type', function(e) + { + var typeValue = $(this).val(); + // We get the enclosing table object to set some limiting context to the host_pop3 selector below + var tbl = $(this).closest('table'); + if( typeValue == 'imap' ) + { + $('.host_pop3', tbl).hide(); + } + else + { + $('.host_pop3', tbl).show(); + } + }); + + $(document).on('change', '#autoconfig_form .username_template', function(e) + { + var usernameField = $(this).closest('.server').find('input[name="username[]"]'); + if( usernameField.length == 0 ) + { + throw( "Unable to find the username field!" ); + } + usernameField.val( $(this).val() ); + }); + + $(document).on('change', '#autoconfig_form select[name="jump_to"]', function(e) + { + var id = $(this).val(); + var re = new RegExp( '([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})' ); + // This will display an empty form + if( id.length == 0 ) + { + window.location.href = 'autoconfig.php'; + } + else if( id.match( re ) ) + { + window.location.href = 'autoconfig.php?config_id=' + encodeURIComponent( id ); + } + }); + + // Upon change in the content of the hidden config_id, enable or disable the "Remove" buttton + // The Remove button can only be used if this is for an existing configuration obviously + $(document).on('change', '#autoconfig_form input[name="config_id"]', function(e) + { + if( DEBUG ) console.log( "Config id field contains \"" + $(this).val() + "\"." ); + if( $(this).val().length == 0 || $(this).val().match( /^[[:blank:]\t]*$/ ) ) + { + $('#autoconfig_remove').attr( 'disabled', true ); + } + else + { + $('#autoconfig_remove').attr( 'disabled', false ); + } + }); + + $(document).on('click', '.autoconfig-move-up', function(e) + { + e.preventDefault(); + var row = $(this).closest('.server').closest('tr'); + if( row.prev().length == 0 || ( !row.prev().hasClass('autoconfig-incoming') && !row.prev().hasClass('autoconfig-outgoing') ) ) + { + return( false ); + } + row.insertBefore( row.prev() ); + $('html, body').animate( { scrollTop: row.offset().top }, 500 ); + autoconfigShowHideArrow(); + }); + + $(document).on('click', '.autoconfig-move-down', function(e) + { + e.preventDefault(); + var row = $(this).closest('.server').closest('tr'); + if( row.next().length == 0 || ( !row.next().hasClass('autoconfig-incoming') && !row.next().hasClass('autoconfig-outgoing') ) ) + { + return( false ); + } + row.insertAfter( row.next() ); + $('html, body').animate( { scrollTop: row.offset().top }, 500 ); + autoconfigShowHideArrow(); + }); + + window.checkExistingTextLanguage = function(opts = {}) + { + if( typeof( opts ) !== 'object' ) + { + throw( "Parameters provided is not an object. Call checkExistingTextLanguage like this: checkExistingTextLanguage({ type: 'instruction', lang: 'fr', caller: $(this) })" ); + } + var textType = opts.type; + var callerMenu = opts.caller; + var langToCheck = opts.lang; + if( DEBUG ) console.log( "checkExistingTextLanguage() checking text type " + textType + " for language " + langToCheck ); + if( typeof( textType ) === 'undefined' ) + { + throw( "No text type was provided." ); + } + var langMenu = $('select[name="' + textType + '_lang[]"]'); + if( DEBUG ) console.log( "checkExistingTextLanguage() Found " + langMenu.length + " language menu(s)." ); + if( langMenu.length == 0 ) + { + throw( "Could not find any language menu for this text type " + textType ); + } + if( langMenu.length == 1 ) + { + return( true ); + } + var alreadySelected = false; + if( DEBUG ) console.log( "checkExistingTextLanguage() checking each existing language menu." ); + langMenu.each(function(offset, menuObject) + { + if( $(menuObject).is( callerMenu ) ) + { + if( DEBUG ) console.log( "checkExistingTextLanguage() skipping because this is our caller menu." ); + return( true ); + } + // Found a match, stop there + else if( $(menuObject).val() == langToCheck ) + { + if( DEBUG ) console.log( "checkExistingTextLanguage() Found match with this menu value (" + $(menuObject).val() + ") matching the language to check \"" + langToCheck + "\"." ); + alreadySelected = true; + return( false ); + } + }); + if( DEBUG ) console.log( "checkExistingTextLanguage() returning: " + !alreadySelected ); + return( !alreadySelected ); + }; + + // Upon selection or change of a language, we check it has not been selected already + $(document).on('change', 'select[name="instruction_lang[]"]', function(e) + { + if( !checkExistingTextLanguage({ type: 'instruction', lang: $(this).val(), caller: $(this) }) ) + { + $(this).addClass('error-shake'); + // + var warning = $('', + { + class: 'fas fa-exclamation-triangle fa-2x', + style: 'color: red; font-size: 20px;', + }); + warning.insertAfter( $(this) ); + var that = $(this); + setTimeout(function() + { + that.removeClass('error-shake'); + warning.remove(); + },5000); + $(this).val(''); + return( false ); + } + return( true ); + }); + + window.toggleCertFiles = function(option) + { + if( typeof( option ) === 'undefined' ) + { + return( false ); + } + if( option == 'local' ) + { + $('.cert_files').show(); + } + else + { + $('.cert_files').hide(); + } + }; + + $(document).on('change', 'select[name="sign_option"]', function(e) + { + toggleCertFiles( $(this).val() ); + }); + + // Need to trigger the change for the host type menu + if( $('#autoconfig_form .host_type').length > 0 ) + { + $('#autoconfig_form .host_type').trigger('change'); + } + if( $('#autoconfig_form input[name="config_id"]').length > 0 ) + { + $('#autoconfig_form input[name="config_id"]').trigger('change'); + } + // Hide useless up/down arrows + autoconfigShowHideArrow(); + toggleCertFiles( $('select[name="sign_option"]').val() ); +}); diff --git a/AUTOCONFIG/autoconfig.php b/AUTOCONFIG/autoconfig.php new file mode 100644 index 00000000..1c01613c --- /dev/null +++ b/AUTOCONFIG/autoconfig.php @@ -0,0 +1,307 @@ +error_reporting = E_ALL & ~E_NOTICE; +/* +if( authentication_has_role('admin') ) +{ + $Admin_role = 1 ; + $fDomain = safeget('domain'); + // $fUsername = safeget('username'); + // list(null $fDomain) = explode('@', $fUsername); + $Return_url = "list-virtual.php?domain=" . urlencode( $fDomain ); + + if( $fDomain == '' || !check_owner( authentication_get_username(), $fDomain ) ) + { + die( "Invalid username!" ); # TODO: better error message + } +} +else +{ + $Admin_role = 0 ; + $Return_url = "main.php"; + authentication_require_role('user'); +} +*/ + +// is autoconfig support enabled in $CONF ? +if( $CONF['autoconfig'] == 'NO' || !array_key_exists( 'autoconfig', $CONF ) ) +{ + header( "Location: $Return_url" ); + exit( 0 ); +} + +date_default_timezone_set( @date_default_timezone_get() ); # Suppress date.timezone warnings + +$error = 0; + +$fDomain = safeget('domain'); +$ah = new AutoconfigHandler( $fUsername ); +$ah->debug = DEBUG; +$config_id = safeget('config_id'); +if( !empty( $fDomain ) && empty( $config_id ) ) +{ + $config_id = $ah->get_id_by_domain( $fDomain ); +} + +// if( !$config_id ) +// { +// flash_error( $PALANG['pAutoconfig_no_config_found'] ); +// $error = 1; +// } +$form = array(); +if( count( $ah->all_domains ) == 0 ) +{ + if( authentication_has_role( 'global-admin' ) ) + { + flash_error( $PALANG['no_domains_exist'] ); + } + else + { + flash_error( $PALANG['no_domains_for_this_admin'] ); + } + header( "Location: list.php?table=domain" ); # no domains (for this admin at least) - redirect to domain list + exit; +} +else +{ + $form['provider_domain_options'] = $ah->all_domains; +} + +if( $_SERVER['REQUEST_METHOD'] == "GET" || empty( $_SERVER['REQUEST_METHOD'] ) ) +{ + if( DEBUG ) error_log( "config id submitted is: '$config_id'." ); + if( !empty( $config_id ) ) + { + if( DEBUG ) error_log( "Getting configuration details with get_details()" ); + $form = $ah->get_details( $config_id ); + if( DEBUG ) error_log( "get_details() returned: " . print_r( $form, true ) ); + } + if( empty( $form['account_type'] ) ) + { + $form['account_type'] = 'imap'; + } + if( empty( $form['ssl_enabled'] ) ) + { + $form['ssl_enabled'] = 1; + } + if( empty( $form['active'] ) ) + { + $form['active'] = 1; + } + $form['placeholder'] = array( + 'provider_id' => $ah->all_domains[0], + 'provider_name' => $PALANG['pAutoconfig_placeholder_provider_name'], + ); + $form['config_options'] = $ah->get_config_ids(); + if( DEBUG ) error_log( "config_options is: " . print_r( $form['config_options'], true ) ); + // $config_id could be null + $form['provider_domain_disabled'] = $ah->get_other_config_domains( $config_id ); + if( DEBUG ) error_log( "provider_domain_disabled is: " . print_r( $form['provider_domain_disabled'], true ) ); + // Get defaults + if( count( $form['enable']['instruction'] ) == 0 ) + { + $form['enable']['instruction'] = array( + array( 'lang' => 'en', 'phrase' => '' ) + ); + if( strlen( $form['enable_status'] ) == 0 ) $form['enable_status'] = 0; + } + else + { + if( strlen( $form['enable_status'] ) == 0 ) $form['enable_status'] = 1; + } + if( count( $form['documentation']['description'] ) == 0 ) + { + $form['documentation']['description'] = array( + array( 'lang' => 'en', 'phrase' => '' ) + ); + if( strlen( $form['documentation_status'] ) == 0 ) $form['documentation_status'] = 0; + } + else + { + if( strlen( $form['documentation_status'] ) == 0 ) $form['documentation_status'] = 1; + } + showAutoconfigForm( $form ); + exit( 0 ); +} +elseif( $_SERVER['REQUEST_METHOD'] == "POST" ) +{ + if( safepost('token') != $_SESSION['PFA_token'] ) + { + die('Invalid token!'); + } + if( !isset( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) || + strtolower( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) != 'xmlhttprequest' ) + { + if( DEBUG ) error_log( "This request is not using Ajax." ); + flash_error( "Request is not using Ajax" ); + showAutoconfigForm( $_POST ); + exit( 0 ); + } + if( isset( $_POST['config_id'] ) && !empty( $_POST['config_id'] ) ) + { + if( DEBUG ) error_log( "Got config_id: " . $_POST['config_id'] ); + if( !$ah->config_id( $_POST['config_id'] ) ) + { + json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_config_id_not_found'], $_POST['config_id'] ) ) ); + exit( 0 ); + } + } + + $handler = null; + if( isset( $_POST['handler'] ) ) + { + if( preg_match( '/^[a-z][a-z_]+$/', $_POST['handler'] ) ) + { + $handler = $_POST['handler']; + } + else + { + if( DEBUG ) error_log( "Illegal character provided in handler \"" . $_POST['handler'] . "\"." ); + json_reply( array( 'error' => "Bad handler provided." ) ); + exit( 0 ); + } + } + + if( DEBUG ) error_log( "handler is \"$handler\"." ); + + if( $handler == 'autoconfig_save' ) + { + if( DEBUG ) error_log( "Got here saving configuration." ); + if( !( $form = $ah->save_config( $_POST ) ) ) + { + if( DEBUG ) error_log( "Failed to save config: " . $ah->error_as_string() ); + json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_server_side_error'], $ah->error_as_string() ) ) ); + } + else + { + if( DEBUG ) error_log( "Ok, config saved." ); + // We return the newly created ids so the user can perform a follow-on update + // The Ajax script will take care of setting those values in the hidden fields + json_reply( array( + 'success' => $PALANG['pAutoconfig_config_saved'], + 'config_id' => $form['config_id'], + 'incoming_server' => $form['incoming_server'], + 'outgoing_server' => $form['outgoing_server'], + 'instruction' => $form['enable']['instruction'], + 'documentation' => $form['documentation']['description'], + ) ); + } + } + elseif( $handler == 'autoconfig_remove' ) + { + if( DEBUG ) error_log( "Got here removing configuration id " . $_POST['config_id'] ); + if( empty( $_POST['config_id'] ) ) + { + json_reply( array( 'error' => $PALANG['pAutoconfig_no_config_yet_to_remove'] ) ); + exit( 0 ); + } + if( !$ah->remove_config( $_POST['config_id'] ) ) + { + if( DEBUG ) error_log( "Failed to remove config: " . $ah->error_as_string() ); + json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_server_side_error'], $ah->error_as_string() ) ) ); + } + else + { + if( DEBUG ) error_log( "Ok, config removed." ); + json_reply( array( 'success' => $PALANG['pAutoconfig_config_removed'] ) ); + } + exit( 0 ); + } + else + { + json_reply( array( 'error' => 'Unknown handler provided "' . $handler . '".' ) ); + } + exit( 0 ); +} + +function json_reply( $data ) +{ + if( !array_key_exists( 'error', $data ) && + !array_key_exists( 'info', $data ) && + !array_key_exists( 'success', $data ) ) + { + error_log( "json_reply() missing message type: error, info or success" ); + return( false ); + } + $allowed_domain = 'http' + . ( ( array_key_exists( 'HTTPS', $_SERVER ) + && $_SERVER[ 'HTTPS' ] + && strtolower( $_SERVER[ 'HTTPS' ] ) !== 'off' ) + ? 's' + : null ) + . '://' . $_SERVER[ 'HTTP_HOST' ]; + header( "Access-Control-Allow-Origin: $allowed_domain" ); + header( 'Content-Type: application/json;charset=utf-8' ); + if( DEBUG ) error_log( "Returning to client the payload: " . json_encode( $data ) ); + echo json_encode( $data ); + return( true ); +} + +function showAutoconfigForm( &$form ) +{ + global $PALANG, $CONF, $languages, $smarty; + if( DEBUG ) error_log( "showAutoconfigForm() received form data: " + print_r( $form, true ) ); + if( $form == null ) $form = array(); + if( array_key_exists( 'enable', $form ) ) + { + if( array_key_exists( 'instruction', $form['enable'] ) ) + { + if( count( $form['enable']['instruction'] ) == 0 ) + { + $form['enable']['instruction'][] = array( 'lang' => 'en' ); + } + } + } + + if( array_key_exists( 'documentation', $form ) ) + { + if( array_key_exists( 'description', $form['documentation'] ) ) + { + if( count( $form['documentation']['description'] ) == 0 ) + { + $form['documentation']['description'][] = array( 'lang' => 'en' ); + } + } + } + $smarty->assign( 'form', $form ); + $smarty->assign( 'language_options', $languages ); + $smarty->assign( 'default_lang', 'en' ); + $smarty->assign( 'smarty_template', 'autoconfig' ); + $smarty->display( 'index.tpl'); + exit( 0 ); +} + +/* vim: set expandtab softtabstop=3 tabstop=3 shiftwidth=3: */ + +?> diff --git a/AUTOCONFIG/autoconfig.pl b/AUTOCONFIG/autoconfig.pl new file mode 100755 index 00000000..bf1a0a1b --- /dev/null +++ b/AUTOCONFIG/autoconfig.pl @@ -0,0 +1,1321 @@ +#!/usr/bin/env perl +## !/usr/local/bin/perl +## Created on 2020-03-04 +## Copyright 2020 Jacques Deguest +## Distributed under the same licence as Postfix Admin +BEGIN +{ + use strict; + use IO::File; + use CGI qw( :standard ); + use Email::Valid; + use Email::Address; + use XML::LibXML; + use XML::LibXML::PrettyPrint; + use Data::Dumper; + use Scalar::Util; + use Data::UUID; + use File::Basename (); + use Cwd (); + use File::Temp (); + use File::Spec (); + use File::Which (); + use JSON; + use DBI; + use TryCatch; + use Devel::StackTrace; +}; + +{ + our $DEBUG = 0; + our $POSTFIXADMIN_CONF_FILE = File::Basename::dirname( __FILE__ ) . '/../config.inc.php'; + my $tmpdir = File::Spec->tmpdir(); + our $POSTFIXADMIN_PERL_FILE = "$tmpdir/autoconfig.pl"; + + our $ERROR = 0; + use utf8; + our $out = IO::File->new(); + $out->fdopen( fileno( STDOUT ), 'w' ); + $out->binmode( ":utf8" ); + $out->autoflush( 1 ); + our $err = IO::File->new(); + $err->fdopen( fileno( STDERR ), 'w' ); + $err->binmode( ":utf8" ); + $err->autoflush( 1 ); + our $out_xml = IO::File->new(); + $out_xml->fdopen( fileno( STDOUT ), 'w' ); + $out_xml->autoflush( 1 ); + + our $params = + { + with_prefix => 0, + include_comment => 1, + include_top_tag => 1, + lowercase => 1, + }; + + our $q = CGI->new; + our( $form, $post_data ); + $form = $q->Vars; + our $email; + + our $dbh; + + $err->print( "Available drivers: '", join( "', '", DBI->available_drivers ), "'\n" ) if( $DEBUG ); + + $err->print( "Reading config file $POSTFIXADMIN_CONF_FILE to $POSTFIXADMIN_PERL_FILE\n" ) if( $DEBUG ); + our $CONF = &read_config_file({ config_file => $POSTFIXADMIN_CONF_FILE, perl_config => $POSTFIXADMIN_PERL_FILE }); + try + { + ## Not including database_password, because password can be blank + my @required = qw( database_type database_name ); + push( @required, qw( database_host database_user ) ) if( $CONF->{database_type} eq 'mysql' || $CONF->{database_type} eq 'mysql' || $CONF->{database_type} eq 'pgsql' ); + foreach my $prop ( @required ) + { + if( !$CONF->{ $prop } ) + { + die( "Property $prop is not set in $POSTFIXADMIN_CONF_FILE\n" ); + } + } + my $dsn; + $err->print( "Database type is: $CONF->{database_type}\n" ) if( $DEBUG ); + if( $CONF->{database_type} eq 'mysql' || $CONF->{database_type} eq 'mysqli' ) + { + require DBD::mysql; + my @opts = ( 'database=' . $CONF->{database_name} ); + push( @opts, 'host=' . $CONF->{database_host} ) if( $CONF->{database_host} ); + push( @opts, 'port=' . $CONF->{database_port} ) if( $CONF->{database_port} ); + $dsn = sprintf( 'dbi:mysql:%s', join( ';', @opts ) ); + } + elsif( $CONF->{database_type} eq 'pgsql' ) + { + require DBD::Pg; + my @opts = ( 'dbname=' . $CONF->{database_name} ); + push( @opts, 'host=' . $CONF->{database_host} ) if( $CONF->{database_host} ); + push( @opts, 'port=' . $CONF->{database_port} ) if( $CONF->{database_port} ); + $dsn = sprintf( 'dbi:Pg:%s', join( ';', @opts ) ); + } + elsif( $CONF->{database_type} eq 'sqlite' ) + { + require DBD::SQLite; + my @opts = ( 'dbname=' . $CONF->{database_name} ); + $dsn = sprintf( 'dbi:SQLite:%s', join( ';', @opts ) ); + } + else + { + die( "Unknown database type \"$CONF->{database_type}\"\n" ); + } + $dbh = DBI->connect( $dsn, $CONF->{database_user}, $CONF->{database_password}, { RaiseError => 0 } ) || die( "Unable to connect to database server with dsn \"$dsn\": ", DBI->errstr, "\n" ); + $dbh->{ShowErrorStatement} = 1; + $dbh->{HandleError} = sub + { + my $err = shift( @_ ); + my $trace = Devel::StackTrace->new( skip_frames => 1, indent => 1 ); + bailout( "$err\n" . $trace->as_string ); + }; + ## This app is read-only to the database + $dbh->{ReadOnly} = 1; + } + catch( $e ) + { + die( $e ); + } + + ## May be not provided in the case of Outlook + if( $form->{emailaddress} ) + { + if( !Email::Valid->address( $form->{emailaddress} ) ) + { + $form->{emailaddress} = ''; + } + else + { + my @emails = Email::Address->parse( $form->{emailaddress} ); + $email = $emails[0] if( scalar( @emails ) ); + } + } + + my $host = ''; + if( $email ) + { + $host = $email->host; + } + elsif( $form->{domain} ) + { + $host = $form->{domain}; + } + elsif( $ENV{HTTP_HOST} ) + { + $host = $ENV{HTTP_HOST}; + } + + our $data = {}; + my $xml; + # my $mime_type = 'application/xml'; + my $mime_type = 'text/xml'; + my $download = 0; + my $filename = ''; + ## Outlook makes a post request with a xml payload including an e-mail address + if( $form->{outlook} || $q->request_method eq 'POST' ) + { + $err->print( "Received an outlook request.\n" ) if( $DEBUG ); + my $req_xml; + my $hash = {}; + unless( $form->{emailaddress} ) + { + ## For debugging in CLI + if( -t( STDIN ) && !$ENV{HTTP_HOST} ) + { + my $io = IO::File->new( "<$form->{request}" ) || die( "Unable to open request file \"$form->{request}\": $!\n" ); + $req_xml = join( '', $io->getlines ); + $io->close; + ## $err->print( "xml request is:\n$req_xml\n" ); + } + else + { + $req_xml = $q->param('POSTDATA'); + } + my $dom = XML::LibXML->load_xml( string => $req_xml ); + $hash = &xml2hash( $dom ); + } + + if( !$hash->{autodiscover}->{request}->{emailaddress} && !$form->{emailaddress} ) + { + bailout( "No email found in request." ); + } + else + { + $form->{emailaddress} ||= $hash->{autodiscover}->{request}->{emailaddress}; + $err->print( "E-mail address found in outlook request is: \"$form->{emailaddress}\"\n" ) if( $DEBUG ); + ## $err->printf( "Ok, found e-mail \"%s\"\n", $form->{emailaddress} ); + if( !Email::Valid->address( $form->{emailaddress} ) ) + { + ## $err->print( "E-mail address $form->{emailaddress} is invalid\n" ); + $form->{emailaddress} = ''; + $data = get_config_for_host( $host ) || bailout( "Unable to get configuration data for host \"$host\": $ERROR" ); + } + else + { + my @emails = Email::Address->parse( $form->{emailaddress} ); + $email = $emails[0] if( scalar( @emails ) ); + $host = $email->host || $ENV{HTTP_HOST}; + $err->print( "Getting configuration for outlook for host \"$host\"\n" ) if( $DEBUG ); + $data = get_config_for_host( $host ) || bailout( "Unable to get configuration data for host \"$host\": $ERROR" ); + } + } + $xml = &generate_outlook(); + } + # http://www.rootmanager.com/iphone-ota-configuration/iphone-ota-setup-with-signed-mobileconfig.html + # + # AddType application/x-apple-aspen-config .mobileconfig + # + elsif( $form->{mac_mail} ) + { + $data = get_config_for_host( $host ) || bailout( "Unable to get configuration data for host \"$host\": $ERROR" ); + $xml = &generate_mac_mail(); + $mime_type = 'application/x-apple-aspen-config'; + $download++; + $filename = sprintf( '%s.mobileconfig', $data->{provider_id} ); + } + else + { + $data = get_config_for_host( $host ) || bailout( "Unable to get configuration data for host \"$host\": $ERROR" ); + $xml = &generate_thunderbird(); + } + # $out->print( "Content-Type: $mime_type\n\n" ); + # $out->printf( "Content-Disposition: attachment;filename=%s.mobileconfig\n", $data->{provider_id} ); + if( $download && $filename ) + { + my $cert_ref = {}; + if( $CONF->{autoconfig_sign} && $data->{sign_option} ne 'none' ) + { + ## Check the path to openssl + my $openssl; + if( !defined( $openssl = File::Which::which( 'openssl' ) ) ) + { + bailout( "Unable to find the openssl binary anywhere in the PATH: ", join( ',', File::Spec->path() ) ); + } + elsif( $DEBUG ) + { + $err->print( "Ok, found openssl at $openssl\n" ); + } + my @keys = qw( cert_filepath privkey_filepath chain_filepath ); + if( $data->{sign_option} eq 'local' && + $data->{cert_filepath} && + $data->{privkey_filepath} && + $data->{chain_filepath} ) + { + @$cert_ref{ @keys } = @$data{ @keys }; + } + elsif( ( $data->{sign_option} eq 'global' || !length( $data->{sign_option} ) ) && + $CONF->{autoconfig_cert} && + $CONF->{autoconfig_privkey} && + $CONF->{autoconfig_chain} ) + { + my @keys_global = qw( autoconfig_cert autoconfig_privkey autoconfig_chain ); + @$cert_ref{ @keys } = @$CONF{ @keys_global }; + } + # $err->print( Data::Dumper::Dumper( $cert_ref ), "\n" ); exit; + ## Do we have anything? Check the file path + if( scalar( keys( %$cert_ref ) ) ) + { + foreach my $k ( keys( %$cert_ref ) ) + { + my $f = $cert_ref->{ $k }; + ## It's a symbolic link. Resolve it + if( -l( $f ) ) + { + $err->print( "File \"$f\" is a symbolic link. Resolving it.\n" ) if( $DEBUG ); + my $f2; + if( !defined( $f2 = Cwd::abs_path( $f ) ) ) + { + bailout( "Unable to resolve the symbolic link \"$f\": $!" ); + $f = $f2; + } + else + { + $err->print( "Ok, resolved link is \"$f2\".\n" ) if( $DEBUG ); + } + } + if( !-e( $f ) ) + { + bailout( "File \"$f\" does not exist." ); + } + elsif( !-r( $f ) ) + { + bailout( sprintf( "File \"$f\" does not have read permission for uid $>. Current permissions are: %04o", (stat( $f ))[2] & 07777 ) ); + } + } + } + ## By now, we are good and have everything + my $xml_in = File::Temp->new( SUFFIX => '.mobileconfig' ); + my $mobile_config_file = $xml_in->filename; + $xml_in->print( $xml->toString() ); + + my $fh = File::Temp->new( SUFFIX => '.mobileconfig' ); + my $mobile_config_file_out = $fh->filename; + my $res; + # openssl smime \ + # -sign \ + # -signer your-cert.pem \ + # -inkey your-priv-key.pem \ + # -certfile TheCertChain.pem \ + # -nodetach \ + # -outform der \ + # -in ConfigProfile.mobileconfig \ + # -out ConfigProfile_signed.mobileconfig + ## https://www.steveneppler.com/blog/2011/02/09/signing-ios-mobileconfig-files-with-your-certificate + $out->print( $q->header( + -type => $mime_type, + -content_disposition => "attachment;filename=${filename}", + -expires => 'now', + ) ); + ## Failed to sign it. Log an error on stderr and send out the unsigned version + $err->print( "Executing the following command to sign the payload:\n$openssl smime -sign -signer $cert_ref->{cert_filepath} -inkey $cert_ref->{privkey_filepath} -certfile $cert_ref->{chain_filepath} -nodetach -outform der -in $mobile_config_file -out $mobile_config_file_out\n" ) if( $DEBUG ); + if( !defined( qx( $openssl smime -sign -signer $cert_ref->{cert_filepath} -inkey $cert_ref->{privkey_filepath} -certfile $cert_ref->{chain_filepath} -nodetach -outform der -in $mobile_config_file -out $mobile_config_file_out ) ) ) + { + $err->print( "Unable to sign the mobileconfig file $mobile_config_file. An error occured when running the openssl command with binary at $openssl\n" ); + $out_xml->print( $xml->toString(), "\n" ); + exit( 0 ); + } + chmod( 0600, $mobile_config_file_out ); + my $in = IO::File->new( "<$mobile_config_file_out" ) || bailout( "Unable to open signed mobileconfig file \"$mobile_config_file_out\": $!" ); + $in->binmode; + my $bin_out = IO::File->new(); + $bin_out->fdopen( fileno( STDOUT ), 'w' ); + $bin_out->binmode(); + $bin_out->autoflush( 1 ); + while( defined( $bytes = $in->getline ) ) + { + $bin_out-print( $bytes ); + } + $in->close; + exit( 0 ); + } + else + { + $err->print( "Aucoconfig signature is not activated globally ($CONF->{autoconfig_sign}) or locally ($data->{sign_option}). Returning data in command line.\n" ) if( $DEBUG ); + $out->print( $q->header( + -type => $mime_type, + -content_disposition => "attachment;filename=${filename}", + -expires => 'now', + -charset => 'utf-8', + ) ); + $out_xml->print( $xml->toString(), "\n" ); + } + } + else + { + if( -t( STDIN ) && !$ENV{HTTP_HOST} ) + { + $err->print( "Returning data in command line.\n" ) if( $DEBUG ); + my $pretty = XML::LibXML::PrettyPrint->new( + indent_string => ' ' x 4, + ); + my $pretty_xml = $pretty->pretty_print( $xml ); + $out_xml->print( $pretty_xml, "\n" ); + } + else + { + $err->print( "Returning data to http client $ENV{HTTP_USER_AGENT}.\n" ) if( $DEBUG ); + $out->print( $q->header( + -type => $mime_type, + -expires => 'now', + -charset => 'utf-8', + ) ); + $out_xml->print( $xml->toString(), "\n" ); + } + } + exit( 0 ); +} + +sub bailout +{ + my $error = join( '', @_ ); + $out->print( $q->header( + -type => 'text/plain', + -status => "500 Internal Server Error", + -expires => 'now', + -charset => 'utf-8', + ) ); + $out->print( "An unexpected error has occured. Please try again later.\n" ); + ## Print to stderr to log it to web server log file + $err->print( "$error\n" ); + exit( 0 ); +} + +# https://stackoverflow.com/questions/44373314/how-do-i-create-entity-references-in-the-doctype-using-perl-libxml +sub generate_mac_mail +{ + my $property_map = + { + provider_name => 'EmailAccountDescription', + account_name => 'EmailAccountName', + account_type => 'EmailAccountType', + email => 'EmailAddress', + incoming_server => + [ + auth => 'IncomingMailServerAuthentication', + hostname => 'IncomingMailServerHostName', + port => 'IncomingMailServerPortNumber', + ssl_enabled => 'IncomingMailServerUseSSL', + username => 'IncomingMailServerUsername', + password => 'IncomingPassword', + ], + outgoing_server => + [ + auth => 'OutgoingMailServerAuthentication', + hostname => 'OutgoingMailServerHostName', + port => 'OutgoingMailServerPortNumber', + ssl_enabled => 'OutgoingMailServerUseSSL', + username => 'OutgoingMailServerUsername', + password => 'OutgoingPassword', + ], + same_password => 'OutgoingPasswordSameAsIncomingPassword', + payload_description => 'PayloadDescription', + payload_name => 'PayloadDisplayName', + payload_id => 'PayloadIdentifier', + payload_org => 'PayloadOrganization', + ## com.apple.mail.managed + payload_type => 'PayloadType', + payload_uuid => 'PayloadUUID', + payload_version => 'PayloadVersion', + prevent_app_sheet => 'PreventAppSheet', + prevent_move => 'PreventMove', + smime_enabled => 'SMIMEEnabled', + payload_remove_ok => 'PayloadRemovalDisallowed', + payload_enabled => 'PayloadEnabled', + }; + ## password-cleartext, password-encrypted (CRAM-MD5 or DIGEST-MD5), NTLM (Windows), GSSAPI (Kerberos), client-IP-address, TLS-client-cert, none, smtp-after-pop (for smtp), OAuth2 (gmail) + my $auth_map = + { + 'none' => 'EmailAuthNone', + 'password-cleartext' => 'EmailAuthPassword', + 'password-encrypted' => 'EmailAuthCRAMMD5', + 'smtp-after-pop' => '', + 'client-ip-address' => '', + 'ntlm' => 'EmailAuthNTLM', + 'tls-client-cert' => '', + ## Made that one up. Wild guess... + 'oauth2' => 'EmailAuthOauth2', + }; + my $boolean_properties = + { + ssl_enabled => 1, + }; + my $integer_properties = + { + port => 1, + }; + + my $server_type_map = + { + imap => 'EmailTypeIMAP', + pop3 => 'EmailTypePOP', + }; + my $doc = XML::LibXML::Document->new( '1.0', $data->{encoding} ); + local $add_elements = sub + { + my $key = shift( @_ ); + my $val = shift( @_ ); + my $p = {}; + if( ref( $_[0] ) eq 'HASH' ) + { + $p = shift( @_ ); + } + elsif( @_ && !( @_ % 2 ) ) + { + $p = { @_ }; + } + my $type2prop = + { + string => 'string', + integer => 'integer', + boolean => 'boolean', + }; + my $keyProp = $doc->createElement( 'key' ); + $keyProp->appendText( $key ); + my $valProp; + if( $p->{type} eq 'boolean' ) + { + $valProp = $doc->createElement( $val ? 'true' : 'false' ); + } + else + { + $valProp = $doc->createElement( $p->{type} ); + $valProp->appendText( $val ); + } + if( $p->{parent} ) + { + $p->{parent}->addChild( $keyProp ); + $p->{parent}->addChild( $valProp ); + } + return({ key => $keyProp, value => $valProp }); + }; + + my $dtd = $doc->createInternalSubset( 'plist', "-//Apple//DTD PLIST 1.0//EN", "http://www.apple.com/DTDs/PropertyList-1.0.dtd" ); + my $plist = $doc->createElement( 'plist' ); + $plist->setAttribute( version => 1 ); + my $dict = $doc->createElement( 'dict' ); + $plist->addChild( $dict ); + my $def = {}; + + $data->{payload_uuid} ||= &_generate_uuid(); + $data->{payload_enabled} = 1; + $def = $add_elements->( $property_map->{payload_uuid} => $data->{payload_uuid}, { type => 'string', parent => $dict } ); + ## $def = $add_elements->( $property_map->{payload_type} => ( $data->{payload_type} || 'Configuration' ), { type => 'string', parent => $dict } ); + $def = $add_elements->( $property_map->{payload_type} => 'Configuration', { type => 'string', parent => $dict } ); + $def = $add_elements->( $property_map->{payload_org} => ( $data->{payload_org} || $data->{provider_name} ), { type => 'string', parent => $dict } ); + $def = $add_elements->( $property_map->{payload_id} => $data->{payload_uuid}, { type => 'string', parent => $dict } ); + $def = $add_elements->( $property_map->{payload_name} => ( $data->{payload_name} || $data->{provider_short} || 'Mail Account Proflie' ), { type => 'string', parent => $dict } ); + $def = $add_elements->( $property_map->{payload_description} => ( $data->{payload_description} || 'Mail Account Settings' ), { type => 'string', parent => $dict } ); + $def = $add_elements->( $property_map->{payload_version} => ( $data->{payload_version} || 1 ), { type => 'integer', parent => $dict } ); + $def = $add_elements->( $property_map->{payload_enabled} => $data->{payload_enabled}, { type => 'boolean', parent => $dict } ); + $def = $add_elements->( $property_map->{payload_remove_ok} => $data->{payload_remove_ok}, { type => 'boolean', parent => $dict } ); + $plist->addChild( $dict ); + + my $payloadContentKey = $doc->createElement( 'key' ); + $payloadContentKey->appendText( 'PayloadContent' ); + $dict->addChild( $payloadContentKey ); + my $array = $doc->createElement( 'array' ); + my $srv = $doc->createElement( 'dict' ); + $def = {}; + + ## We need a separate uuid for the server details + my $payload_uuid = &_generate_uuid(); + $def = $add_elements->( $property_map->{payload_uuid} => $payload_uuid, { type => 'string', parent => $srv } ); +# $def = $add_elements->( $property_map->{payload_type} => 'com.apple.eas.account', { type => 'string', parent => $srv } ); + $def = $add_elements->( $property_map->{payload_type} => 'com.apple.mail.managed', { type => 'string', parent => $srv } ); + $def = $add_elements->( $property_map->{payload_org} => &_interpolate_vars_thunderbird( $data->{payload_org} || $data->{provider_name} ), { type => 'string', parent => $srv } ); + $def = $add_elements->( $property_map->{payload_id} => $payload_uuid, { type => 'string', parent => $srv } ); + $def = $add_elements->( $property_map->{payload_name} => &_interpolate_vars_thunderbird( $data->{payload_name} || sprintf( "%s Account (%s)", uc( $data->{incoming_server}->[0]->{type} ), $data->{provider_name} ) ), { type => 'string', parent => $srv } ); + $def = $add_elements->( $property_map->{payload_description} => &_interpolate_vars_thunderbird( $data->{payload_description} || "Mail Account Settings" ), { type => 'string', parent => $srv } ); + $def = $add_elements->( $property_map->{payload_version} => ( $data->{payload_version} || 1 ), { type => 'integer', parent => $srv } ); + $def = $add_elements->( $property_map->{payload_enabled} => $data->{payload_enabled}, { type => 'boolean', parent => $srv } ); + $def = $add_elements->( $property_map->{payload_name} => ( $data->{payload_name} || $data->{provider_short} || 'Mail Account Proflie' ), { type => 'string', parent => $srv } ); + + + if( $data->{provider_name} ) + { + $def = $add_elements->( $property_map->{provider_name} => &_interpolate_vars_thunderbird( $data->{provider_name} ), { type => 'string', parent => $srv } ); + } + if( !$data->{account_name} && !$form->{account_name} && $email ) + { + my $local = $email->user; + my @parts = split( /\./, $local ); + my $name = join( ' ', map( ucfirst( lc( $_ ) ), @parts ) ); + $data->{account_name} = $name; + } + if( $data->{account_name} || $form->{account_name} ) + { + $def = $add_elements->( $property_map->{account_name} => &_interpolate_vars_thunderbird( ( $data->{account_name} || $form->{account_name} ) ), { type => 'string', parent => $srv } ); + } + if( ( $data->{incoming_server} && + ref( $data->{incoming_server} ) eq 'ARRAY' && + scalar( @{$data->{incoming_server}} ) ) || + ( $data->{outgoing_server} && + ref( $data->{outgoing_server} ) eq 'ARRAY' && + scalar( @{$data->{outgoing_server}} ) ) ) + { + ## $def = $add_elements->( email => ( $email ? $email->address : 'taro.urashima@' . $data->{provider_domain}->[0] ), { type => 'string', parent => $srv } ); + $def = $add_elements->( $property_map->{email} => ( $email ? $email->address : '' ), { type => 'string', parent => $srv } ); + foreach my $t ( qw( incoming_server outgoing_server ) ) + { + ## Of course there should be an incoming and outgoing server, but since we rely on the data being here, we check its existence and skip it if not there to avoid an untrapped error + if( !$data->{ $t } ) + { + next; + } + my $srv_conf = $data->{ $t }->[0]; + $def = $add_elements->( $property_map->{account_type} => $server_type_map->{ $srv_conf->{type} }, { type => 'string', parent => $srv } ) if( $t eq 'incoming_server' ); + $srv_conf->{ssl_enabled} = $srv_conf->{socket_type} =~ /^(SSL|STARTTLS|TLS)$/i ? 1 : 0; + for( my $i = 0; $i < scalar( @{$property_map->{ $t }} ); $i += 2 ) + { + my $src_prop = $property_map->{ $t }->[$i]; + my $tar_prop = $property_map->{ $t }->[$i + 1]; + ## $err->print( "Processing property \"$tar_prop\" with value \"", $srv_conf->{ $src_prop }, "\"\n" ); + if( $src_prop =~ /^[[:blank:]]*$/ ) + { + warn( "No source property defined for server type \"$t\" with host \"$ref->{hostname}\" !\n" ); + next; + } + if( $tar_prop =~ /^[[:blank:]]*$/ ) + { + warn( "No target property defined for server type \"$t\" with host \"$ref->{hostname}\" !\n" ); + next; + } + + ## Check if password exists and is same as previous one in incoming_server + if( $t eq 'outgoing_server' && + $src_prop eq 'password' && + $data->{incoming_server}->[0]->{password} eq $data->{outgoing_server}->[0]->{password} ) + { + $def = $add_elements->( $property_map->{same_password} => 1, { type => 'boolean', parent => $srv } ); + next; + } + + if( $src_prop eq 'auth' ) + { + $srv_conf->{ $src_prop } = $auth_map->{ $srv_conf->{ $src_prop } }; + } + + if( exists( $boolean_properties->{ $src_prop } ) ) + { + $def = $add_elements->( $tar_prop => $srv_conf->{ $src_prop }, { type => 'boolean', parent => $srv } ); + } + elsif( exists( $integer_properties->{ $src_prop } ) ) + { + $srv_conf->{ $src_prop } = 0 if( !CORE::length( $srv_conf->{ $src_prop } ) ); + $def = $add_elements->( $tar_prop => $srv_conf->{ $src_prop }, { type => 'integer', parent => $srv } ); + } + else + { + $def = $add_elements->( $tar_prop => &_interpolate_vars_thunderbird( $srv_conf->{ $src_prop } ), { type => 'string', parent => $srv } ); + } + } + } + } + $def = $add_elements->( $property_map->{prevent_app_sheet} => $data->{prevent_app_sheet}, { type => 'boolean', parent => $srv } ); + $def = $add_elements->( $property_map->{prevent_move} => $data->{prevent_move}, { type => 'boolean', parent => $srv } ); + $def = $add_elements->( $property_map->{smime_enabled} => $data->{smime_enabled}, { type => 'boolean', parent => $srv } ); + $array->addChild( $srv ); + $dict->addChild( $array ); + $doc->addChild( $plist ); + return( $doc ); +} + +## https://www.ullright.org/ullWiki/show/providing-email-client-autoconfiguration-information-from-moens-ch +sub generate_outlook +{ + my $property_map = + { + server => + [ + hostname => 'Server', + port => 'Port', + domain_required => 'DomainRequired', + spa => 'SPA', + ssl_enabled => 'SSL', + auth_required => 'AuthRequired', + username => 'LoginName', + ], + }; + + my $boolean_properties = + { + auth_required => 1, + domain_required => 1, + spa => 1, + ssl_enabled => 1, + }; + my $doc = XML::LibXML::Document->new( '1.0', $data->{encoding} ); + my $disco = $doc->createElement( 'Autodiscover' ); + $disco->setAttribute( xmlns => 'http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006' ); + my $resp = $doc->createElement( 'Response' ); + $resp->setAttribute( xmlns => 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a' ); + my $provider = $doc->createElement( 'User' ); + + my $providerName = $doc->createElement( 'DisplayName' ); + $providerName->appendText( &_interpolate_vars_thunderbird( $data->{provider_name} ) ); + $provider->addChild( $providerName ); + $resp->addChild( $provider ); + + my $acct = $doc->createElement( 'Account' ); + + my $acctType = $doc->createElement( 'AccountType' ); + $acctType->appendText( 'email' ); + $acct->addChild( $acctType ); + + my $settings = $doc->createElement( 'Action' ); + $settings->appendText( 'settings' ); + $acct->addChild( $settings ); + + foreach my $t ( qw( incoming_server outgoing_server ) ) + { + next if( !exists( $data->{ $t } ) ); + if( ref( $data->{ $t } ) ne 'ARRAY' ) + { + warn( "Data provided for server type \"$t\" is not an array reference.\n" ); + next; + } + foreach my $ref ( @{$data->{ $t }} ) + { + ## Same property whether this is an incoming or outgoing mail server + my $srv = $doc->createElement( 'Protocol' ); + # Generate some data if necessary + if( !length( $ref->{ssl_enabled} ) ) + { + if( $ref->{socket_type} =~ /^(?:SSL|STARTTLS|TLS)$/i ) + { + $ref->{ssl_enabled} = 1; + } + } + if( !length( $ref->{auth_required} ) ) + { + $ref->{auth_required} = lc( $ref->{auth} ) eq 'none' ? 0 : 1; + } + if( !length( $ref->{domain_required} ) ) + { + $ref->{domain_required} = 0; + } + if( !length( $ref->{spa} ) ) + { + $ref->{spa} = 0; + } + + for( my $i = 0; $i < scalar( @{$property_map->{server}} ); $i += 2 ) + { + my $src_prop = $property_map->{server}->[$i]; + my $tar_prop = $property_map->{server}->[$i + 1]; + if( $src_prop =~ /^[[:blank:]]*$/ ) + { + warn( "No source property defined for server type \"$t\" with host \"$ref->{hostname}\" !\n" ); + next; + } + if( $tar_prop =~ /^[[:blank:]]*$/ ) + { + warn( "No target property defined for server type \"$t\" with host \"$ref->{hostname}\" !\n" ); + next; + } + + if( $ref->{ $src_prop } =~ /^[[:blank:]]*$/ ) + { + warn( "Property value for \"$src_prop\" for server type \"$t\" ($ref->{hostname}) is empty, skipping it.\n" ); + next; + } + if( exists( $boolean_properties->{ $src_prop } ) ) + { + $ref->{ $src_prop } = $ref->{ $src_prop } ? 'on' : 'off'; + } + elsif( $src_prop eq 'type' ) + { + $ref->{ $src_prop } = uc( $ref->{ $src_prop } ); + } + my $prop = $doc->createElement( $tar_prop ); + $prop->appendText( &_interpolate_vars_thunderbird( $ref->{ $src_prop } ) ); + $srv->addChild( $prop ); + } + $acct->addChild( $srv ); + } + } + + $resp->addChild( $acct ); + + $disco->addChild( $resp ); + $doc->addChild( $disco ); + return( $doc ); +} + +## https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat +sub generate_thunderbird +{ +# local $Data::Dumper::Sortkeys = 1; +# $err->print( Data::Dumper::Dumper( $data ), "\n" ); +# exit; + my $property_map = + { + incoming_server => 'incomingServer', + outgoing_server => 'outgoingServer', + server => + [ + hostname => 'hostname', + port => 'port', + socket_type => 'socketType', + auth => 'authentication', + username => 'username', + ], + leave_messages_on_server => 'leaveMessagesOnServer', + download_on_biff => 'downloadOnBiff', + days_to_leave_messages_on_server => 'daysToLeaveMessagesOnServer', + check_interval => 'checkInterval', + webmail => 'webMail', + login_page => 'loginPage', + login_page_info => 'loginPageInfo', + username => 'username', + username_field => 'usernameField', + password_field => 'passwordField', + login_button => 'loginButton', + }; + + my $doc = XML::LibXML::Document->new( '1.0', $data->{encoding} ); + ## + my $config = $doc->createElement( 'clientConfig' ); + $config->setAttribute( version => '1.1' ); + $doc->addChild( $config ); + + ## Email provider + my $provider = XML::LibXML::Element->new( 'emailProvider' ); + if( $data->{provider_id} ) + { + $provider->setAttribute( id => &_interpolate_vars_thunderbird( $data->{provider_id} ) ); + } + $config->addChild( $provider ); + + if( $data->{provider_domain} && ref( $data->{provider_domain} ) ) + { + foreach my $domain ( @{$data->{provider_domain}} ) + { + next if( $domain =~ /^[[:blank:]]*$/ ); + my $providerDomain = $doc->createElement( 'domain' ); + $providerDomain->appendText( &_interpolate_vars_thunderbird( $domain ) ); + $provider->addChild( $providerDomain ); + } + } + + if( $data->{provider_name} ) + { + my $providerName = $doc->createElement( 'displayName' ); + $providerName->appendText( &_interpolate_vars_thunderbird( $data->{provider_name} ) ); + $provider->addChild( $providerName ); + } + + if( $data->{provider_short} ) + { + my $providerShort = $doc->createElement( 'displayShortName' ); + $providerShort->appendText( &_interpolate_vars_thunderbird( $data->{provider_short} ) ); + $provider->addChild( $providerShort ); + } + + ## Process incoming and outgoing servers + foreach my $t ( qw( incoming_server outgoing_server ) ) + { + next if( !exists( $data->{ $t } ) ); + if( ref( $data->{ $t } ) ne 'ARRAY' ) + { + warn( "Data provided for server type \"$t\" is not an array reference.\n" ); + next; + } + foreach my $ref ( @{$data->{ $t }} ) + { + my $srv = $doc->createElement( $property_map->{ $t } ); + $srv->setAttribute( type => $ref->{type} ); + for( my $i = 0; $i < scalar( @{$property_map->{server}} ); $i += 2 ) + { + my $src_prop = $property_map->{server}->[$i]; + my $tar_prop = $property_map->{server}->[$i + 1]; + if( $src_prop =~ /^[[:blank:]]*$/ ) + { + warn( "No source property defined for server type \"$t\" with host \"$ref->{hostname}\" !\n" ); + next; + } + if( $tar_prop =~ /^[[:blank:]]*$/ ) + { + warn( "No target property defined for server type \"$t\" with host \"$ref->{hostname}\" !\n" ); + next; + } + + if( $ref->{ $src_prop } =~ /^[[:blank:]]*$/ ) + { + warn( "Property value for \"$src_prop\" for server type \"$t\" ($ref->{hostname}) is empty, skipping it.\n" ); + next; + } + $ref->{ $src_prop } = lc( $ref->{ $src_prop } ) if( $src_prop eq 'auth' ); + my $prop = $doc->createElement( $tar_prop ); + $prop->appendText( &_interpolate_vars_thunderbird( $ref->{ $src_prop } ) ); + $srv->addChild( $prop ); + } + + ## If this is a pop3 and at least there is one pop3 property set, ie not null, we activate this block + if( $ref->{type} eq 'pop3' && + ( length( $ref->{leave_messages_on_server} ) || + length( $ref->{download_on_biff} ) || + length( $ref->{days_to_leave_messages_on_server} ) || + length( $ref->{check_interval} ) + ) ) + { + my $pop3 = $doc->createElement( 'pop3' ); +# leave_messages_on_server => 1, +# download_on_biff => 1, +# days_to_leave_messages_on_server => 14, +# check_interval => { minutes => 15 }, + foreach my $prop_name ( qw( leave_messages_on_server download_on_biff ) ) + { + ## Does the property exists and has a non blank value ? + ## A blank value could be construed as false, which is incorrect. False must be expressed with 0 + if( length( $ref->{ $prop_name } ) ) + { + if( $ref->{ $prop_name } =~ /^[[:blank:]]*$/ ) + { + warn( "Property \"$prop_name\" for this popr3 server \"$ref->{hostname}\" is blank and ignored.\n" ); + next; + } + my $prop = $doc->createElement( $property_map->{ $prop_name } ); + $prop->appendText( $ref->{ $prop_name } ? 'true' : 'false' ); + $pop3->addChild( $prop ); + } + } + foreach my $int_prop ( qw( days_to_leave_messages_on_server check_interval ) ) + { + if( length( $ref->{ $int_prop } ) ) + { + if( $ref->{ $int_prop } !~ /^\d+$/ ) + { + warn( "The property \"$int_prop\" value for this pop3 server \"$ref->{hostname}\" (", $ref->{ $int_prop }, ") is not an integer and is ignored.\n" ); + } + else + { + my $prop = $doc->createElement( $property_map->{ $int_prop } ); + if( $int_prop eq 'days_to_leave_messages_on_server' ) + { + $prop->appendText( $ref->{ $int_prop } ); + } + else + { + $prop->setAttribute( minutes => $ref->{ $int_prop } ); + } + $pop3->addChild( $prop ); + } + } + } + $srv->addChild( $pop3 ); + } + $provider->addChild( $srv ); + } + } + + if( exists( $data->{enable} ) && + ( !length( $data->{enable_status} ) || ( length( $data->{enable_status} ) && $data->{enable_status} ) ) ) + { + my $enable = $doc->createElement( 'enable' ); + ## Not going to check this is a valid url. This is the responsibility of the user + $enable->setAttribute( visiturl => $data->{enable}->{url} ) if( $data->{enable}->{url} ); + if( $data->{enable}->{instruction} && ref( $data->{enable}->{instruction} ) eq 'HASH' ) + { + foreach my $lang ( sort( keys( %{$data->{enable}->{instruction}} ) ) ) + { + if( $data->{enable}->{instruction}->{ $lang } =~ /^[[:blank:]]*$/ ) + { + warn( "Instruction text to enable login for language \"$lang\" is empty, skipping\n" ); + next; + } + my $help = $doc->createElement( 'instruction' ); + $help->setAttribute( lang => $lang ); + $help->appendText( &_interpolate_vars_thunderbird( $data->{enable}->{instruction}->{ $lang } ) ); + $enable->addChild( $help ); + } + } + $provider->addChild( $enable ); + } + + if( $data->{documentation} && ref( $data->{documentation} ) eq 'HASH' && + ( !length( $data->{documentation_status} ) || ( length( $data->{documentation_status} ) && $data->{documentation_status} ) ) ) + { + my $support_data = $data->{documentation}; + my $support = $doc->createElement( 'documentation' ); + ## Not going to check this is a valid url. This is the responsibility of the user + $support->setAttribute( url => $support_data->{url} ) if( $support_data->{url} ); + if( $support_data->{description} && ref( $support_data->{description} ) eq 'HASH' ) + { + foreach my $lang ( sort( keys( %{$support_data->{description}} ) ) ) + { + if( $support_data->{description}->{ $lang } =~ /^[[:blank:]]*$/ ) + { + warn( "Support documentation text for language \"$lang\" is empty, skipping\n" ); + next; + } + my $desc = $doc->createElement( 'descr' ); + $desc->setAttribute( lang => $lang ); + $desc->appendText( &_interpolate_vars_thunderbird( $support_data->{description}->{ $lang } ) ); + $support->addChild( $desc ); + } + } + $provider->addChild( $support ); + } + + if( $data->{webmail} && ref( $data->{webmail} ) eq 'HASH' ) + { + my $webmail = $doc->createElement( $property_map->{webmail} ); + my $this = $data->{webmail}; + if( $this->{login_page} ) + { + my $loginPage = $doc->createElement( $property_map->{login_page} ); + $loginPage->setAttribute( url => &_interpolate_vars_thunderbird( $this->{login_page} ) ); + $webmail->addChild( $loginPage ); + } + if( $this->{login_page_info} && ref( $this->{login_page_info} ) eq 'HASH' ) + { + my $ref = $this->{login_page_info}; + my $loginInfo = $doc->createElement( $property_map->{login_page_info} ); + $loginInfo->setAttribute( url => &_interpolate_vars_thunderbird( $ref->{url} ) ) if( $ref->{url} ); + if( $ref->{username} ) + { + if( $email ) + { + $ref->{username} = &_interpolate_vars_thunderbird( $ref->{username} ); + } + my $username = $doc->createElement( $property_map->{username} ); + $username->appendText( &_interpolate_vars_thunderbird( $ref->{username} ) ); + $loginInfo->addChild( $username ); + } + if( $ref->{username_field} && ref( $ref->{username_field} ) eq 'HASH' ) + { + my $that = $ref->{username_field}; + my $usernameField = $doc->createElement( $property_map->{username_field} ); + $usernameField->setAttribute( id => &_interpolate_vars_thunderbird( $that->{id} ) ) if( $that->{id} ); + $usernameField->setAttribute( name => &_interpolate_vars_thunderbird( $that->{name} ) ) if( $that->{name} ); + $loginInfo->addChild( $usernameField ); + } + if( $ref->{password_field} ) + { + my $pwdField = $doc->createElement( $property_map->{password_field} ); + $pwdField->setAttribute( name => &_interpolate_vars_thunderbird( $ref->{password_field} ) ); + $loginInfo->addChild( $pwdField ); + } + if( $ref->{login_button} && ref( $ref->{login_button} ) eq 'HASH' ) + { + my $that = $ref->{login_button}; + my $loginButton = $doc->createElement( $property_map->{login_button} ); + $loginButton->setAttribute( id => &_interpolate_vars_thunderbird( $that->{id} ) ) if( $that->{id} ); + $loginButton->setAttribute( name => &_interpolate_vars_thunderbird( $that->{name} ) ) if( $that->{name} ); + $loginInfo->addChild( $loginButton ); + } + $webmail->addChild( $loginInfo ); + } + $config->addChild( $webmail ); + } + ## Get the overall xml as string and return it + # my $xml = $doc->toString(); + return( $doc ); +} + +sub get_config_for_host +{ + my $host = shift( @_ ) || return( _error( "No host provided to get its configuration data." ) ); + my $sth = $dbh->prepare_cached( "SELECT c.* FROM autoconfig_domains d LEFT JOIN autoconfig c ON c.config_id = d.config_id WHERE d.domain = ?" ) || bailout( "Unable to prepare sql statement: ", $dbh->errstr ); + my @parts = split( /\./, $host ); + my $ref; + for( my $i = 0; $i < scalar( @parts ); $i++ ) + { + my $this_host = join( '.', @parts[$i..$#parts] ); + $sth->execute( $this_host ) || bailout( "An error occurred while trying to execute query: ", $sth->errstr ); + $ref = $sth->fetchrow_hashref; + if( ref( $ref ) ) + { + last if( !scalar( keys( %$ref ) ) ); + $ref->{domain} = $this_host; + last; + } + } + $sth->finish; + return( _error( "No configuration data found for host $host" ) ) if( !$ref || ( ref( $ref ) && !scalar( keys( %$ref ) ) ) ); + + my $dom_sth = $dbh->prepare_cached( "SELECT domain FROM autoconfig_domains WHERE config_id = ?" ) || bailout( "Unable to prepare sql statement to get all domains for this configurtion: ", $dbh->errstr ); + $dom_sth->execute( $ref->{config_id} ) || bailout( "An error occurred while trying to get the list of all domains for this configuration: ", $dom_sth->errstr ); + my $all_domains = $dom_sth->fetchall_arrayref( {} ); + $dom_sth->finish; + my $domains = [map( $_->{domain}, @$all_domains )]; + $ref->{provider_domain} = $domains; + + my $hosts_in_sth = $dbh->prepare_cached( "SELECT * FROM autoconfig_hosts WHERE config_id = ? AND ( type = 'imap' OR type = 'pop3' ) ORDER BY priority" ) || bailout( "Unable to prepare the sql statements to get hosts configuration details for config id $ref->{config_id}: ", $dbh->errstr ); + $hosts_in_sth->execute( $ref->{config_id} ) || bailout( "An error occurred while trying to execute the sql query to get host details for config id $ref->{config_id}: ", $hosts_in_sth->errstr ); + my $all_hosts_in = $hosts_in_sth->fetchall_arrayref( {} ); + $hosts_in_sth->finish; + $ref->{incoming_server} = $all_hosts_in; + + my $hosts_out_sth = $dbh->prepare_cached( "SELECT * FROM autoconfig_hosts WHERE config_id = ? AND type = 'smtp' ORDER BY priority" ) || bailout( "Unable to prepare the sql statements to get hosts configuration details for config id $ref->{config_id}: ", $dbh->errstr ); + $hosts_out_sth->execute( $ref->{config_id} ) || bailout( "An error occurred while trying to execute the sql query to get host details for config id $ref->{config_id}: ", $hosts_out_sth->errstr ); + my $all_hosts_out = $hosts_out_sth->fetchall_arrayref( {} ); + $hosts_out_sth->finish; + $ref->{outgoing_server} = $all_hosts_out; + + my $text_sth = $dbh->prepare_cached( "SELECT * FROM autoconfig_text WHERE config_id = ? AND type = ?" ) || bailout( "Unable to get the list of text, if any, for enabling login: ", $dbh->errstr ); + foreach my $t ( qw( enable documentation ) ) + { + my $textType = $t eq 'enable' ? 'instruction' : 'description'; + my $sqlType = $t eq 'enable' ? 'instruction' : 'documentation'; + $text_sth->execute( $ref->{config_id}, $sqlType ) || bailout( "An error occurred while executing query to get the list of enabling $sqlType: ", $text_sth->errstr ); + my $all_text = $text_sth->fetchall_arrayref( {} ); + $err->printf( "%d text elements found for type $t and config id $ref->{config_id}: %s\n", scalar( @$all_text ), Data::Dumper::Dumper( $all_text ) ) if( $DEBUG ); + $text_sth->finish; + if( $ref->{ "${t}_url" } ) + { + $ref->{ $t } = + { + url => $ref->{ "${t}_url" }, + $textType => {}, + }; + foreach my $this ( @$all_text ) + { + $ref->{ $t }->{ $textType }->{ $this->{lang} } = $this->{phrase}; + } + } + } + $text_sth->finish; + + ## Tweak the data layout a bit + $ref->{webmail} = + { + login_page => $ref->{webmail_login_page}, + login_page_info => + { + url => $ref->{lp_info_url}, + username => $ref->{lp_info_username}, + username_field => + { + id => $ref->{lp_info_username_field_id}, + name => $ref->{lp_info_username_field_name}, + }, + password_field => $ref->{lp_info_password_field}, + login_button => + { + id => $ref->{lp_info_login_button_id}, + name => $ref->{lp_info_login_button_name}, + }, + }, + }; + return( $ref ); +} + +sub read_config_file +{ + bailout( "Requires an hash reference to be provided as unique argument with key config_file and perl_config." ) if( ref( $_[0] ) ne 'HASH' ); + my $opts = shift( @_ ); + my $file = $opts->{config_file}; + my $save_to = $opts->{perl_config}; + my $file_mtime = ( stat( $file ) )[9]; + if( -e( $save_to ) && !-z( $save_to ) && $file_mtime == ( stat( $save_to ) )[9] ) + { + $err->printf( "PHP config file \"$file\" modification time $file_mtime is not the same as the per file \"%s\".\n", ( stat( $save_to ) )[9] ) if( $DEBUG ); + try + { + local $CONF; + require( $save_to ); + return( $CONF ); + } + catch( $e ) + { + bailout( "Error reading the perl configuration file: $e" ); + } + } + my $fh = File::Temp->new( SUFFIX => '.php' ); + my $fname = $fh->filename; + $fh->print( < +EOT + my $json_data = ''; + my $io = IO::File->new( "php $fname|" ) || bailout( "Unable to execute temporary php script to transcode postfixadmin confi file into json: $!" ); + $json_data .= $_ while( defined( $_ = $io->getline ) ); + $io->close; + my $json = JSON->new->allow_nonref; + my $perl = $json->utf8->decode( $json_data ); + # $out->print( Data::Dumper::Dumper( $perl ), "\n" ); + my $fh2 = IO::File->new( ">$save_to" ) || bailout( "Unable to write to file \"$save_to\": $!" ); + $fh2->binmode( 'utf-8' ); + local $Data::Dumper::Sortkeys = 1; + $fh2->print( Data::Dumper->Dump( [$perl], [qw(CONF)] ), "\n" ); + $fh2->close; + chmod( 0600, $save_to ); + ## Set the last modification time to be the same as the original file, so we can compare next time. + utime( time(), $file_mtime, $save_to ); + return( $perl ); +} + +sub _generate_uuid +{ + return( uc( Data::UUID->new->create_str ) ); +} + +sub _interpolate_vars_thunderbird +{ + my $this = shift( @_ ); + return( '' ) if( !length( $this ) ); + ## No need to bother if there is no sign of placeholder in the string + return( $this ) if( index( $this, '%' ) == -1 ); + ## No need to bother if no email address was provided + return( $this ) if( !$email ); + $this =~ s/\%EMAILADDRESS\%/$email->address/ge; + $this =~ s/\%EMAILLOCALPART\%/$email->user/ge; + $this =~ s/\%EMAILDOMAIN\%/$email->host/ge; + return( $this ); +} + +sub _error +{ + my $err = join( '', @_ ); + $ERROR = $err; + ## Return undef or empty list depending on how we were called + return; +} + +sub xml2hash +{ + my $elem = shift( @_ ); + my $opts = {}; + $opts = pop( @_ ) if( ref( $_[-1] ) eq 'HASH' && !Scalar::Util::blessed( $_[-1] ) ); + if( !Scalar::Util::blessed( $elem ) ) + { + warn( "An unblessed value was provided. I was expected a XML::LibXML object.\n" ); + return; + } + my $doc = $elem->isa( 'XML::LibXML::Document' ) ? $elem->documentElement : $elem; + foreach my $o ( qw( include_comment include_top_tag lowercase with_prefix ) ) + { + $opts->{ $o } = $params->{ $o } if( !exists( $opts->{ $o } ) ); + } + my $ref = &_xml2hash( $doc, 0, $opts ); + my $ref_top = {}; + if( $opts->{include_top_tag} ) + { + my $k = ( $opts->{with_prefix} ? $doc->nodeName : $doc->getLocalName ); + $k = lc( $k ) if( $opts->{lowercase} ); + $ref_top->{ $k } = $ref; + return( $ref_top ); + } + return( $ref ); +} + +sub _xml2hash +{ + my $doc = shift( @_ ) || return( {} ); + my $level = shift( @_ ) || 0; + my $opts = {}; + $opts = pop( @_ ) if( ref( $_[-1] ) eq 'HASH' ); + my $pref = ( "." x $level ) . "L${level} "; + ## $err->print( "${pref}Received an object of type '", ref( $elem ), "' and processing an object of type '", ref( $doc ), "'\n" ); + my $ref = {}; + local $_; + if( $doc->hasChildNodes or $doc->hasAttributes ) + { + ## $err->print( "${pref}Has child nodes or attributes\n" ); + my $attr = {}; + foreach my $a ( $doc->attributes ) + { + my $k = ( $opts->{with_prefix} ? $a->nodeName : ( $a->getLocalName || $a->nodeName ) ); + ## $err->print( "${pref}Found attribute \"$k\" for node \"$a\"\n" ); + $attr->{ $k } = $a->getValue; + } + $ref->{ '_attributes' } = $attr if( scalar( keys( %$attr ) ) ); + + my @childs = $doc->childNodes; + ## $err->print( "${pref}%d child nodes found", scalar( @childs ), "\n" ); + for( $doc->childNodes ) + { + my $class = ref( $_ ); + my $key = ''; + ## my $nn; + if( $class eq 'XML::LibXML::Text' || + $class eq 'XML::LibXML::CDATASection' ) + { + $key = '_text'; + } + elsif( $class eq 'XML::LibXML::Comment' ) + { + if( $opts->{include_comment} ) + { + $key = '_comment'; + } + else + { + next; + } + } + else + { + $key = ( $opts->{with_prefix} ? $_->nodeName : $_->getLocalName ); + $key = lc( $key ) if( $opts->{lowercase} ); + } + ## $err->print( "${pref}${key} calling xml2hash with value '$_'\n" ); + my $child = &_xml2hash( $_, $level + 1, $opts ); + ## $ref->{ $key } = []; + ## if (( $X2A or $X2A{$nn} ) and !$res->{$nn}) { $res->{$nn} = [] } + if( exists( $ref->{ $key } ) ) + { + ## Move previous entry from string to array + $ref->{ $key } = [ $ref->{ $key } ] unless( ref( $ref->{ $key } ) eq 'ARRAY' ); + push( @{ $ref->{ $key } }, $child ) if( defined( $child ) ); + } + else + { + if( $key eq '_text' ) + { + $ref->{ $key } = $child if( length( $child ) ); + } + else + { + $ref->{ $key } = $child; + } + } + } + if( exists( $ref->{ '_text' } ) && ref( $ref->{ '_text' } ) eq 'ARRAY' ) + { + $ref->{ '_text' } = join( '', @{$ref->{ '_text' }} ); + delete( $ref->{ '_text' } ) if( !length( $ref->{ '_text' } ) ); + } + delete( $ref->{ '_text' } ) if( scalar( keys( %$ref ) ) > 1 && exists( $ref->{ '_text' } ) && !length( $ref->{ '_text' } ) ); + return( $ref->{ '_text' } ) if( scalar( keys( %$ref ) ) == 1 && exists( $ref->{ '_text' } ) ); + ## $err->print( "${pref}Returning: ", Dumper( $ref ), "\n" ); + return( $ref ); + } + else + { + my $text = $doc->textContent; + $text =~ s/^[[:blank:]\r\n]+|[[:blank:]\r\n]+$//g; + ## $err->print( "${pref}Returning text '$text'\n" ); + return( $text ); + } +} + +sub xpc +{ + my $xml = shift( @_ ); + return( $xpc ) if( $xpc ); + ## An error ocured + my $top = $xml->firstChild; + my @ns = $top->getNamespaces; + our $xpc = XML::LibXML::XPathContext->new( $xml ); + foreach my $n ( @ns ) + { + my $localName = $n->getLocalName; + my $val = $n->value; + next unless( $localName && $val ); + ## $err->print( "Setting name space $localName => $val\n" ); + $xpc->registerNs( $localName => $val ); + } + return( $xpc ); +} + +__END__ + diff --git a/AUTOCONFIG/autoconfig.tpl b/AUTOCONFIG/autoconfig.tpl new file mode 100644 index 00000000..d4118d5f --- /dev/null +++ b/AUTOCONFIG/autoconfig.tpl @@ -0,0 +1,302 @@ +{literal} + + + + +{/literal} + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {if count($form.incoming_server) == 0} + {assign var="server" value=['type' => 'imap'] scope="global"} + {include file='autoconfig-host-settings.tpl' server=$server} + {else} + {foreach name=outer item=server from=$form.incoming_server} + {include file='autoconfig-host-settings.tpl' server=$server} + {/foreach} + {/if} + + + + + + {if count($form.outgoing_server) == 0} + {assign var="server" value=['type' => 'smtp'] scope="global"} + {include file='autoconfig-host-settings.tpl' server=$server} + {else} + {foreach name=outer item=server from=$form.outgoing_server} + {include file='autoconfig-host-settings.tpl' server=$server} + {/foreach} + {/if} + + + + + + + + + + {foreach name=support item=text from=$form.enable.instruction} + + + + + + {/foreach} + + + + + + + + + {foreach name=support item=text from=$form.documentation.description} + + + + + + {/foreach} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {$PALANG.pAutoconfig_page_title}
     
     
    utf-8 
     

     
     
     
    {$PALANG.pAutoconfig_incoming_server}
    {$PALANG.pAutoconfig_outgoing_server}
    {$PALANG.pAutoconfig_enable} + +
    {$PALANG.pAutoconfig_not_supported} (?)
     
    + {html_options name="instruction_lang[]" options=$language_options selected=$text.lang}
    +
    {$PALANG.pAutoconfig_documentation} + +
     
    + {html_options name="documentation_lang[]" options=$language_options selected=$text.lang}
    +
    {$PALANG.pAutoconfig_webmail}
     
    {$PALANG.pAutoconfig_webmail_login_page_info}
     
    {$PALANG.pAutoconfig_lp_info_username}
     
     
    {$PALANG.pAutoconfig_lp_info_login_button}
     
     
    {$PALANG.pAutoconfig_mac_specific_settings}
     
     
     
     
     
     
     
     
     
     
     
     
    {$PALANG.pAutoconfig_cert_sign}
     
     
     
     
      + + + +
    +
    +
    diff --git a/AUTOCONFIG/autoconfig_languages.php b/AUTOCONFIG/autoconfig_languages.php new file mode 100644 index 00000000..09b70b7f --- /dev/null +++ b/AUTOCONFIG/autoconfig_languages.php @@ -0,0 +1,193 @@ + "Abkhaz", +'aa' => "Afar", +'af' => "Afrikaans", +'ak' => "Akan", +'sq' => "Albanian", +'am' => "Amharic", +'ar' => "Arabic", +'an' => "Aragonese", +'hy' => "Armenian", +'as' => "Assamese", +'av' => "Avaric", +'ae' => "Avestan", +'ay' => "Aymara", +'az' => "Azerbaijani", +'bm' => "Bambara", +'ba' => "Bashkir", +'eu' => "Basque", +'be' => "Belarusian", +'bn' => "Bengali, Bangla", +'bh' => "Bihari", +'bi' => "Bislama", +'bs' => "Bosnian", +'br' => "Breton", +'bg' => "Bulgarian", +'my' => "Burmese", +'ca' => "Catalan", +'ch' => "Chamorro", +'ce' => "Chechen", +'ny' => "Chichewa, Chewa, Nyanja", +'zh' => "Chinese", +'cv' => "Chuvash", +'kw' => "Cornish", +'co' => "Corsican", +'cr' => "Cree", +'hr' => "Croatian", +'cs' => "Czech", +'da' => "Danish", +'dv' => "Divehi, Dhivehi, Maldivian", +'nl' => "Dutch", +'dz' => "Dzongkha", +'en' => "English", +'eo' => "Esperanto", +'et' => "Estonian", +'ee' => "Ewe", +'fo' => "Faroese", +'fj' => "Fijian", +'fi' => "Finnish", +'fr' => "French", +'ff' => "Fula, Fulah, Pulaar, Pular", +'gl' => "Galician", +'lg' => "Ganda", +'ka' => "Georgian", +'de' => "German", +'el' => "Greek (modern)", +'gn' => "Guaraní", +'gu' => "Gujarati", +'ht' => "Haitian, Haitian Creole", +'ha' => "Hausa", +'he' => "Hebrew (modern)", +'hz' => "Herero", +'hi' => "Hindi", +'ho' => "Hiri Motu", +'hu' => "Hungarian", +'is' => "Icelandic", +'io' => "Ido", +'ig' => "Igbo", +'id' => "Indonesian", +'ia' => "Interlingua", +'ie' => "Interlingue", +'iu' => "Inuktitut", +'ik' => "Inupiaq", +'ga' => "Irish", +'it' => "Italian", +'ja' => "Japanese", +'jv' => "Javanese", +'kl' => "Kalaallisut, Greenlandic", +'kn' => "Kannada", +'kr' => "Kanuri", +'ks' => "Kashmiri", +'kk' => "Kazakh", +'km' => "Khmer", +'ki' => "Kikuyu, Gikuyu", +'rw' => "Kinyarwanda", +'rn' => "Kirundi", +'kv' => "Komi", +'kg' => "Kongo", +'ko' => "Korean", +'ku' => "Kurdish", +'kj' => "Kwanyama, Kuanyama", +'ky' => "Kyrgyz", +'lo' => "Lao", +'la' => "Latin", +'lv' => "Latvian", +'li' => "Limburgish, Limburgan, Limburger", +'ln' => "Lingala", +'lt' => "Lithuanian", +'lu' => "Luba-Katanga", +'lb' => "Luxembourgish, Letzeburgesch", +'mk' => "Macedonian", +'mg' => "Malagasy", +'ms' => "Malay", +'ml' => "Malayalam", +'mt' => "Maltese", +'gv' => "Manx", +'mr' => "Marathi (Marāṭhī)", +'mh' => "Marshallese", +'mn' => "Mongolian", +'mi' => "Māori", +'na' => "Nauruan", +'nv' => "Navajo, Navaho", +'ng' => "Ndonga", +'ne' => "Nepali", +'nd' => "Northern Ndebele", +'se' => "Northern Sami", +'no' => "Norwegian", +'nb' => "Norwegian Bokmål", +'nn' => "Norwegian Nynorsk", +'ii' => "Nuosu", +'oc' => "Occitan", +'oj' => "Ojibwe, Ojibwa", +'cu' => "Old Church Slavonic, Church Slavonic, Old Bulgarian", +'or' => "Oriya", +'om' => "Oromo", +'os' => "Ossetian, Ossetic", +'pa' => "Panjabi, Punjabi", +'ps' => "Pashto, Pushto", +'fa' => "Persian (Farsi)", +'pl' => "Polish", +'pt' => "Portuguese", +'pi' => "Pāli", +'qu' => "Quechua", +'ro' => "Romanian", +'rm' => "Romansh", +'ru' => "Russian", +'sm' => "Samoan", +'sg' => "Sango", +'sa' => "Sanskrit (Saṁskṛta)", +'sc' => "Sardinian", +'gd' => "Scottish Gaelic, Gaelic", +'sr' => "Serbian", +'sn' => "Shona", +'sd' => "Sindhi", +'si' => "Sinhala, Sinhalese", +'sk' => "Slovak", +'sl' => "Slovene", +'so' => "Somali", +'nr' => "Southern Ndebele", +'st' => "Southern Sotho", +'es' => "Spanish", +'su' => "Sundanese", +'sw' => "Swahili", +'ss' => "Swati", +'sv' => "Swedish", +'tl' => "Tagalog", +'ty' => "Tahitian", +'tg' => "Tajik", +'ta' => "Tamil", +'tt' => "Tatar", +'te' => "Telugu", +'th' => "Thai", +'bo' => "Tibetan Standard, Tibetan, Central", +'ti' => "Tigrinya", +'to' => "Tonga (Tonga Islands)", +'ts' => "Tsonga", +'tn' => "Tswana", +'tr' => "Turkish", +'tk' => "Turkmen", +'tw' => "Twi", +'uk' => "Ukrainian", +'ur' => "Urdu", +'ug' => "Uyghur", +'uz' => "Uzbek", +'ve' => "Venda", +'vi' => "Vietnamese", +'vo' => "Volapük", +'wa' => "Walloon", +'cy' => "Welsh", +'fy' => "Western Frisian", +'wo' => "Wolof", +'xh' => "Xhosa", +'yi' => "Yiddish", +'yo' => "Yoruba", +'za' => "Zhuang, Chuang", +'zu' => "Zulu", +); +?> diff --git a/AUTOCONFIG/sprintf.js b/AUTOCONFIG/sprintf.js new file mode 100644 index 00000000..be2edbb1 --- /dev/null +++ b/AUTOCONFIG/sprintf.js @@ -0,0 +1,212 @@ +/** + * JavaScript printf/sprintf functions. + * http://hexmen.com/blog/2007/03/printf-sprintf/ + * + * This code is unrestricted: you are free to use it however you like. + * + * The functions should work as expected, performing left or right alignment, + * truncating strings, outputting numbers with a required precision etc. + * + * For complex cases these functions follow the Perl implementations of + * (s)printf, allowing arguments to be passed out-of-order, and to set + * precision and output-length from other argument + * + * See http://perldoc.perl.org/functions/sprintf.html for more information. + * + * Implemented flags: + * + * - zero or space-padding (default: space) + * sprintf("%4d", 3) -> " 3" + * sprintf("%04d", 3) -> "0003" + * + * - left and right-alignment (default: right) + * sprintf("%3s", "a") -> " a" + * sprintf("%-3s", "b") -> "b " + * + * - out of order arguments (good for templates & message formats) + * sprintf("Estimate: %2$d units total: %1$.2f total", total, quantity) + * + * - binary, octal and hex prefixes (default: none) + * sprintf("%b", 13) -> "1101" + * sprintf("%#b", 13) -> "0b1101" + * sprintf("%#06x", 13) -> "0x000d" + * + * - positive number prefix (default: none) + * sprintf("%d", 3) -> "3" + * sprintf("%+d", 3) -> "+3" + * sprintf("% d", 3) -> " 3" + * + * - min/max width (with truncation); e.g. "%9.3s" and "%-9.3s" + * sprintf("%5s", "catfish") -> "catfish" + * sprintf("%.5s", "catfish") -> "catfi" + * sprintf("%5.3s", "catfish") -> " cat" + * sprintf("%-5.3s", "catfish") -> "cat " + * + * - precision (see note below); e.g. "%.2f" + * sprintf("%.3f", 2.1) -> "2.100" + * sprintf("%.3e", 2.1) -> "2.100e+0" + * sprintf("%.3g", 2.1) -> "2.10" + * sprintf("%.3p", 2.1) -> "2.1" + * sprintf("%.3p", '2.100') -> "2.10" + * + * Deviations from perl spec: + * - %n suppresses an argument + * - %p and %P act like %g, but without over-claiming accuracy: + * Compare: + * sprintf("%.3g", "2.1") -> "2.10" + * sprintf("%.3p", "2.1") -> "2.1" + * + * @version 2011.09.23 + * @author Ash Searle + */ +function sprintf() { + function pad(str, len, chr, leftJustify) { + var padding = (str.length >= len) ? '' : Array(1 + len - str.length >>> 0).join(chr); + return leftJustify ? str + padding : padding + str; + + } + + function justify(value, prefix, leftJustify, minWidth, zeroPad) { + var diff = minWidth - value.length; + if (diff > 0) { + if (leftJustify || !zeroPad) { + value = pad(value, minWidth, ' ', leftJustify); + } else { + value = value.slice(0, prefix.length) + pad('', diff, '0', true) + value.slice(prefix.length); + } + } + return value; + } + + var a = arguments, i = 0, format = a[i++]; + return format.replace(sprintf.regex, function(substring, valueIndex, flags, minWidth, _, precision, type) { + if (substring == '%%') return '%'; + + // parse flags + var leftJustify = false, positivePrefix = '', zeroPad = false, prefixBaseX = false; + for (var j = 0; flags && j < flags.length; j++) switch (flags.charAt(j)) { + case ' ': positivePrefix = ' '; break; + case '+': positivePrefix = '+'; break; + case '-': leftJustify = true; break; + case '0': zeroPad = true; break; + case '#': prefixBaseX = true; break; + } + + // parameters may be null, undefined, empty-string or real valued + // we want to ignore null, undefined and empty-string values + + if (!minWidth) { + minWidth = 0; + } else if (minWidth == '*') { + minWidth = +a[i++]; + } else if (minWidth.charAt(0) == '*') { + minWidth = +a[minWidth.slice(1, -1)]; + } else { + minWidth = +minWidth; + } + + // Note: undocumented perl feature: + if (minWidth < 0) { + minWidth = -minWidth; + leftJustify = true; + } + + if (!isFinite(minWidth)) { + throw new Error('sprintf: (minimum-)width must be finite'); + } + + if (precision && precision.charAt(0) == '*') { + precision = +a[(precision == '*') ? i++ : precision.slice(1, -1)]; + if (precision < 0) { + precision = null; + } + } + + if (precision == null) { + precision = 'fFeE'.indexOf(type) > -1 ? 6 : (type == 'd') ? 0 : void(0); + } else { + precision = +precision; + } + + // grab value using valueIndex if required? + var value = valueIndex ? a[valueIndex.slice(0, -1)] : a[i++]; + var prefix, base; + + switch (type) { + case 'c': value = String.fromCharCode(+value); + case 's': { + // If you'd rather treat nulls as empty-strings, uncomment next line: + // if (value == null) return ''; + + value = String(value); + if (precision != null) { + value = value.slice(0, precision); + } + prefix = ''; + break; + } + case 'b': base = 2; break; + case 'o': base = 8; break; + case 'u': base = 10; break; + case 'x': case 'X': base = 16; break; + case 'i': + case 'd': { + var number = parseInt(+value); + if (isNaN(number)) { + return ''; + } + prefix = number < 0 ? '-' : positivePrefix; + value = prefix + pad(String(Math.abs(number)), precision, '0', false); + break; + } + case 'e': case 'E': + case 'f': case 'F': + case 'g': case 'G': + case 'p': case 'P': + { + var number = +value; + if (isNaN(number)) { + return ''; + } + prefix = number < 0 ? '-' : positivePrefix; + var method; + if ('p' != type.toLowerCase()) { + method = ['toExponential', 'toFixed', 'toPrecision']['efg'.indexOf(type.toLowerCase())]; + } else { + // Count significant-figures, taking special-care of zeroes ('0' vs '0.00' etc.) + var sf = String(value).replace(/[eE].*|[^\d]/g, ''); + sf = (number ? sf.replace(/^0+/,'') : sf).length; + precision = precision ? Math.min(precision, sf) : precision; + method = (!precision || precision <= sf) ? 'toPrecision' : 'toExponential'; + } + var number_str = Math.abs(number)[method](precision); + // number_str = thousandSeparation ? thousand_separate(number_str): number_str; + value = prefix + number_str; + break; + } + case 'n': return ''; + default: return substring; + } + + if (base) { + // cast to non-negative integer: + var number = value >>> 0; + prefix = prefixBaseX && base != 10 && number && ['0b', '0', '0x'][base >> 3] || ''; + value = prefix + pad(number.toString(base), precision || 0, '0', false); + } + var justified = justify(value, prefix, leftJustify, minWidth, zeroPad); + return ('EFGPX'.indexOf(type) > -1) ? justified.toUpperCase() : justified; + }); +} +sprintf.regex = /%%|%(\d+\$)?([-+#0 ]*)(\*\d+\$|\*|\d+)?(\.(\*\d+\$|\*|\d+))?([scboxXuidfegpEGP])/g; + +/** + * Trival printf implementation, probably only useful during page-load. + * Note: you may as well use "document.write(sprintf(....))" directly + */ +function printf() { + // delegate the work to sprintf in an IE5 friendly manner: + var i = 0, a = arguments, args = Array(arguments.length); + while (i < args.length) args[i] = 'a[' + (i++) + ']'; + document.write(eval('sprintf(' + args + ')')); +} diff --git a/config.inc.php b/config.inc.php index 949fe96e..8c008695 100644 --- a/config.inc.php +++ b/config.inc.php @@ -692,6 +692,17 @@ $CONF['xmlrpc_enabled'] = false; //More details in README.password_expiration $CONF['password_expiration'] = 'YES'; +// Autodiscovery configuration (autoconfig) +// You can leave it blank to not sign the Mac Mail mobileconfig system setting, or +// You can provide default values here and override them with each configuration specific values +// You can set this to true or false to simply activate or deactivate signing the mobileconfig file +// This will apply to all, including per configuration settings +$CONF['autoconfig'] = 'YES'; +$CONF['autoconfig_sign'] = true; +$CONF['autoconfig_cert'] = null; +$CONF['autoconfig_privkey'] = null; +$CONF['autoconfig_chain'] = null; + // If you want to keep most settings at default values and/or want to ensure // that future updates work without problems, you can use a separate config // file (config.local.php) instead of editing this file and override some diff --git a/functions.inc.php b/functions.inc.php index 5d2d2398..a07f29a4 100644 --- a/functions.inc.php +++ b/functions.inc.php @@ -2027,6 +2027,21 @@ function db_where_clause(array $condition, array $struct, $additional_raw_where return $query; } +function db_begin() +{ + return( db_query( "BEGIN" ) ); +} + +function db_commit() +{ + return( db_query( "COMMIT" ) ); +} + +function db_rollback() +{ + return( db_query( "ROLLBACK" ) ); +} + /** * Convert a programmatic db table name into what may be the actual name. * diff --git a/languages/en.lang b/languages/en.lang index addbd846..cfa682a1 100644 --- a/languages/en.lang +++ b/languages/en.lang @@ -412,6 +412,129 @@ $PALANG['pFetchmail_desc_mda'] = 'Mail Delivery Agent'; $PALANG['pFetchmail_desc_date'] = 'Date of last polling/configuration change'; $PALANG['pFetchmail_desc_returned_text'] = 'Text message from last polling'; +// Autoconfig +$PALANG['pAutoconfig_page_title'] = 'Mail Autodiscovery Configuration'; +$PALANG['pEdit_autoconfig_set'] = 'Save Configuration'; +$PALANG['pEdit_autoconfig_remove'] = 'Remove Configuration'; +$PALANG['pAutoconfig_encoding'] = 'Encoding'; +$PALANG['pAutoconfig_provider_id'] = 'Provider ID'; +$PALANG['pAutoconfig_provider_domain'] = 'Domain names'; +$PALANG['pAutoconfig_provider_name'] = 'Provider name'; +$PALANG['pAutoconfig_provider_short'] = 'Provider short name'; +$PALANG['pAutoconfig_incoming_server'] = 'Incoming servers'; +$PALANG['pAutoconfig_outgoing_server'] = 'Outgoing servers'; +$PALANG['pAutoconfig_type'] = 'Type'; +$PALANG['pAutoconfig_hostname'] = 'Host name'; +$PALANG['pAutoconfig_port'] = 'Port'; +$PALANG['pAutoconfig_socket_type'] = 'Socket type'; +$PALANG['pAutoconfig_auth'] = 'Authentication scheme'; +$PALANG['pAutoconfig_username'] = 'Username'; +$PALANG['pAutoconfig_leave_messages_on_server']= 'Leave message on server?'; +$PALANG['pAutoconfig_download_on_biff'] = 'Download on biff?'; +$PALANG['pAutoconfig_days_to_leave_messages_on_server']= 'Number of days to leave messages on server'; +$PALANG['pAutoconfig_check_interval'] = 'Mail check interval in minute'; +$PALANG['pAutoconfig_enable'] = 'Login Enable Instruction'; +$PALANG['pAutoconfig_enable_url'] = 'URL to enable login'; +$PALANG['pAutoconfig_enable_instruction'] = 'Enabling instruction'; +$PALANG['pAutoconfig_documentation'] = 'Support documentation'; +$PALANG['pAutoconfig_documentation_url'] = 'Support documentation url'; +$PALANG['pAutoconfig_documentation_desc'] = 'Support documentation description'; +$PALANG['pAutoconfig_webmail_login_page'] = 'Webmail login page'; +$PALANG['pAutoconfig_webmail_login_page_info']= 'Webmail information page'; +$PALANG['pAutoconfig_lp_info_url'] = 'Login page info url'; +$PALANG['pAutoconfig_lp_info_username'] = 'Login page info username'; +$PALANG['pAutoconfig_lp_info_username_field_id']= 'Login page info username field id'; +$PALANG['pAutoconfig_lp_info_username_field_name']= 'Login page info username field name'; +$PALANG['pAutoconfig_lp_info_login_button'] = 'Login page info button'; +$PALANG['pAutoconfig_lp_info_login_button_id']= 'Login page info button id'; +$PALANG['pAutoconfig_lp_info_login_button_name']= 'Login page info button name'; +$PALANG['pAutoconfig_mac_specific_settings']= 'Mac specific settings'; +$PALANG['pAutoconfig_account_name'] = 'Account name (you can leave it blank)'; +$PALANG['pAutoconfig_account_type'] = 'Account type'; +$PALANG['pAutoconfig_email'] = 'e-mail address (you can leave it blank)'; +$PALANG['pAutoconfig_ssl'] = 'Use of SSL?'; +$PALANG['pAutoconfig_password'] = 'Password (you can leave it blank)'; +$PALANG['pAutoconfig_description'] = 'Description'; +$PALANG['pAutoconfig_organisation'] = 'Organisation'; +$PALANG['pAutoconfig_payload_type'] = 'Payload type'; +$PALANG['pAutoconfig_prevent_app_sheet'] = 'Prevent app sheet'; +$PALANG['pAutoconfig_prevent_move'] = 'Prevent move'; +$PALANG['pAutoconfig_smime_enabled'] = 'S/MIME enabled?'; +$PALANG['pAutoconfig_payload_remove_ok'] = 'Payload removal allowed?'; +$PALANG['pAutoconfig_spa'] = 'Microsoft SPA (Secure Password Authentication)'; +$PALANG['pAutoconfig_active'] = 'Status'; +// Error messages +$PALANG['pAutoconfig_invalid_encoding'] = 'Invalid encoding'; +$PALANG['pAutoconfig_empty_provider_id'] = 'No provider id provided'; +$PALANG['pAutoconfig_host_no_type_provided']= 'No type provided'; +$PALANG['pAutoconfig_host_invalid_type_value'] = 'Invalid type value'; +$PALANG['pAutoconfig_host_no_hostname_provided'] = 'No host name provided'; +$PALANG['pAutoconfig_host_no_port_provided']= 'No port provided'; +$PALANG['pAutoconfig_host_port_is_not_an_integer'] = 'Port provided is not an integer'; +// sprintf +$PALANG['pAutoconfig_host_invalid_socket_type'] = 'Invalid connection type \"%s\"'; +// sprintf +$PALANG['pAutoconfig_host_invalid_auth_scheme'] = 'Invalid authentication scheme \"%s\"'; +$PALANG['pAutoconfig_host_days_on_server_is_not_an_integer'] = 'Value provided for days to remain on server is not an integer'; +$PALANG['pAutoconfig_host_check_interval_is_not_an_integer'] = 'Check interval provided is not an integer'; +$PALANG['pAutoconfig_duplicate_host'] = 'You have already defined a host "%1$s" of type %2$s and with port number %3$d'; +$PALANG['pAutoconfig_text_type_not_provided'] = 'No type provided for this text'; +// sprintf +$PALANG['pAutoconfig_text_type_invalid'] = 'Text type is invalid'; +$PALANG['pAutoconfig_text_lang_not_provided'] = 'No language is provided'; +// sprintf +$PALANG['pAutoconfig_text_lang_invalid'] = 'Language code provided is invalid'; +$PALANG['pAutoconfig_text_text_not_provided'] = 'No text was provided'; +$PALANG['pAutoconfig_no_config_found'] = 'No configuration found'; +$PALANG['pAutoconfig_save_no_data_provided']= 'No data was provided to save'; +$PALANG['pAutoconfig_lack_permission_over_config_id'] = 'You lack permission to modify this configuration with id "%s"'; +$PALANG['pAutoconfig_text_id_is_not_an_integer'] = 'The text id provided "%s" is not an integer'; +$PALANG['pAutoconfig_config_id_not_found'] = 'No configuration could be found with id "%s"'; +$PALANG['pAutoconfig_config_id_submitted_is_unauthorised'] = 'Configuration id submitted "%s" is not the one to be used for this configuration'; +$PALANG['pAutoconfig_no_config_id_declared']= 'No configuration id was declared'; +$PALANG['pAutoconfig_no_config_id_provded']= 'No configuration id was provided'; +$PALANG['pAutoconfig_unauthorised_domains'] = 'The following domain names are not authorised to the admin: %s'; +$PALANG['pAutoconfig_data_provided_is_not_array'] = 'Data submitted is not an array'; +$PALANG['pAutoconfig_no_domain_names_have_been_selected'] = 'No domain name have been selected. If you want to remove this configuration, simply click on "Remove"'; +$PALANG['pAutoconfig_domain_data_provided_is_not_an_array'] = 'Domain names data provided is not an array'; +$PALANG['pAutoconfig_no_domain_authorised_for_this_admin'] = 'No domain name have been authorised for this admin'; +$PALANG['pAutoconfig_host_id_is_not_an_integer'] = 'Host id provided "%s" is not an integer'; +$PALANG['pAutoconfig_text_language_already_used'] = 'You have already selected the language "%s". Please choose a different one.'; +$PALANG['pAutoconfig_no_config_yet_to_remove'] = 'No configuration id yet provided to remove'; +$PALANG['pAutoconfig_config_removed'] = 'Configuration has been removed'; +$PALANG['pAutoconfig_config_saved'] = 'Configuration has been saved'; +$PALANG['pAutoconfig_failed_to_add_config'] = 'Failed to add configuration'; +$PALANG['pAutoconfig_failed_to_add_domain_to_config'] = 'Failed to add domain name "%s" to configuration.'; +$PALANG['pAutoconfig_failed_to_add_host_to_config'] = 'Failed to add host configuration for host "%s"'; +$PALANG['pAutoconfig_failed_to_add_text_to_config'] = 'Failed to add the text that starts with: "%s"'; +$PALANG['pAutoconfig_placeholder_provider_name'] = 'Your Organisation, Inc'; +$PALANG['pAutoconfig_password_cleartext'] = 'Plain password'; +$PALANG['pAutoconfig_password_encrypted'] = 'MD5 encrypted password'; +$PALANG['pAutoconfig_client_ip_address'] = 'Client IP address'; +$PALANG['pAutoconfig_tls_client_cert'] = 'TLS client certificate'; +$PALANG['pAutoconfig_smtp_after_pop'] = 'SMTP after POP3'; +$PALANG['pAutoconfig_copy_provider_name'] = 'Copy provider name'; +$PALANG['pAutoconfig_toggle_select_all'] = 'Toggle select all'; +$PALANG['pAutoconfig_username_template'] = 'Use template:'; +$PALANG['pAutoconfig_no_selection'] = 'None'; +$PALANG['pAutoconfig_jump_to'] = 'Jump to configuration:'; +$PALANG['pAutoconfig_new_configuration'] = 'New configuration'; +$PALANG['pAutoconfig_add_new_host'] = 'Add new host'; +$PALANG['pAutoconfig_remove_host'] = 'Remove this host'; +$PALANG['pAutoconfig_move_up_host'] = 'Move this host up in priority'; +$PALANG['pAutoconfig_move_down_host'] = 'Move this host down in priority'; +$PALANG['pAutoconfig_add_new_text'] = 'Add new text'; +$PALANG['pAutoconfig_remove_text'] = 'Remove text'; +$PALANG['pAutoconfig_not_supported'] = 'Not currently supported by Thunderbird'; +$PALANG['pAutoconfig_cert_sign'] = 'Mac mobile config signature'; +$PALANG['pAutoconfig_cert_option'] = 'How to sign'; +$PALANG['pAutoconfig_cert_none'] = 'No signature'; +$PALANG['pAutoconfig_cert_local'] = 'Configuration certificate'; +$PALANG['pAutoconfig_cert_global'] = 'Global certificate'; +$PALANG['pAutoconfig_on'] = 'On'; +$PALANG['pAutoconfig_off'] = 'Off'; +$PALANG['pAutoconfig_server_side_error'] = 'A server side error has occured, please check the web server log for details. %s'; + $PALANG['dateformat_pgsql'] = 'YYYY-mm-dd'; # translators: rearrange to your local date format, but make sure it's a valid PostgreSQL date format $PALANG['dateformat_mysql'] = '%Y-%m-%d'; # translators: rearrange to your local date format, but make sure it's a valid MySQL date format $PALANG['password_expiration'] = 'Pass expires'; diff --git a/languages/fr.lang b/languages/fr.lang index f8a5f728..f09150a7 100644 --- a/languages/fr.lang +++ b/languages/fr.lang @@ -407,6 +407,131 @@ $PALANG['dateformat_mysql'] = '%d-%m-%Y'; $PALANG['password_expiration'] = 'Expiration du mot de passe'; $PALANG['password_expiration_desc'] = 'Date when password will expire'; # XXX +// Autoconfig +$PALANG['pAutoconfig_page_title'] = 'Configuration de l\'auto découverte'; +$PALANG['pEdit_autoconfig_set'] = 'Enregistrer la configuration'; +$PALANG['pEdit_autoconfig_remove'] = 'Supprimer la configuration'; +$PALANG['pAutoconfig_encoding'] = 'Encodage'; +$PALANG['pAutoconfig_provider_id'] = 'Identifiant du fournisseur'; +$PALANG['pAutoconfig_provider_domain'] = 'Noms de domaine'; +$PALANG['pAutoconfig_provider_name'] = 'Nom du fournisseur'; +$PALANG['pAutoconfig_provider_short'] = 'Nom court du fournisseur'; +$PALANG['pAutoconfig_incoming_server'] = 'Serveurs entrants'; +$PALANG['pAutoconfig_outgoing_server'] = 'Serveurs sortants'; +$PALANG['pAutoconfig_type'] = 'Type'; +$PALANG['pAutoconfig_hostname'] = 'No d\'hôte'; +$PALANG['pAutoconfig_port'] = 'Port'; +$PALANG['pAutoconfig_socket_type'] = 'Type de socket'; +$PALANG['pAutoconfig_auth'] = 'Méthode d\'autentification'; +$PALANG['pAutoconfig_username'] = 'Nom d\'utilisateur'; +$PALANG['pAutoconfig_leave_messages_on_server']= 'Laisser les message sur le serveur ?'; +$PALANG['pAutoconfig_download_on_biff'] = 'Télécharger sur biff ?'; +$PALANG['pAutoconfig_days_to_leave_messages_on_server']= 'Nom de jours des messages sur le serveur'; +$PALANG['pAutoconfig_check_interval'] = 'Interval de relevée de courriels en minute'; +$PALANG['pAutoconfig_enable'] = 'Instruction d\'activation du login'; +$PALANG['pAutoconfig_enable_url'] = 'Adresse URL pour activer le login'; +$PALANG['pAutoconfig_enable_instruction'] = 'Tnstruction d\'activation'; +$PALANG['pAutoconfig_documentation'] = 'Documentation de support'; +$PALANG['pAutoconfig_documentation_url'] = 'Adresse url de la documentation support'; +$PALANG['pAutoconfig_documentation_desc'] = 'Description de la documentation support'; +$PALANG['pAutoconfig_webmail_login_page'] = 'Page de login du Webmail'; +$PALANG['pAutoconfig_webmail_login_page_info']= 'Page d\'information du Webmail'; +$PALANG['pAutoconfig_lp_info_url'] = 'Adresse url de la page d\'information login'; +$PALANG['pAutoconfig_lp_info_username'] = 'Nom d\'utilisateur de la page info login'; +$PALANG['pAutoconfig_lp_info_username_field_id']= 'ID du champs login de la page info'; +$PALANG['pAutoconfig_lp_info_username_field_name']= 'Nom du champs login de la page info'; +$PALANG['pAutoconfig_lp_info_login_button'] = 'Bouton de la page info login'; +$PALANG['pAutoconfig_lp_info_login_button_id']= 'ID du bouton de la page info login'; +$PALANG['pAutoconfig_lp_info_login_button_name']= 'Nom du bouton de la page info login'; +$PALANG['pAutoconfig_mac_specific_settings']= 'Réglages spécifiques au Mac'; +$PALANG['pAutoconfig_account_name'] = 'Nom du compte (vous pouvez le laisser vide)'; +$PALANG['pAutoconfig_account_type'] = 'Type de compte'; +$PALANG['pAutoconfig_email'] = 'Adresse e-mail (vous pouvez le laisser vide)'; +$PALANG['pAutoconfig_ssl'] = 'Utilisation de la SSL ?'; +$PALANG['pAutoconfig_password'] = 'Mot de passe (vous pouvez le laisser vide)'; +$PALANG['pAutoconfig_description'] = 'Description'; +$PALANG['pAutoconfig_organisation'] = 'Organisation'; +$PALANG['pAutoconfig_payload_type'] = 'Type de payload'; +// https://stackoverflow.com/questions/19385776/preventappsheet-use-only-in-mail-ios-mail-payload-configuration +// https://developer.apple.com/documentation/devicemanagement/mail +$PALANG['pAutoconfig_prevent_app_sheet'] = 'Empêcher app sheet'; +$PALANG['pAutoconfig_prevent_move'] = 'EMpêcher le déplacement'; +$PALANG['pAutoconfig_smime_enabled'] = 'S/MIME activé ?'; +$PALANG['pAutoconfig_payload_remove_ok'] = 'Autoriser la suppresion du payload ?'; +$PALANG['pAutoconfig_spa'] = 'Microsoft SPA (Secure Password Authentication)'; +$PALANG['pAutoconfig_active'] = 'Etat'; +// Error messages +$PALANG['pAutoconfig_invalid_encoding'] = 'Encodage invalide'; +$PALANG['pAutoconfig_empty_provider_id'] = 'Aucun idenitifant de fournisseurn\'a été fourni'; +$PALANG['pAutoconfig_host_no_type_provided']= 'Aucun type spécifié'; +$PALANG['pAutoconfig_host_invalid_type_value'] = 'Incorrecte valeur de type'; +$PALANG['pAutoconfig_host_no_hostname_provided'] = 'Aucun hôte n\'a été fourni'; +$PALANG['pAutoconfig_host_no_port_provided']= 'Aucun port n\'a été spécifié'; +$PALANG['pAutoconfig_host_port_is_not_an_integer'] = 'Le port fourni n\'est pas un entier'; +// sprintf +$PALANG['pAutoconfig_host_invalid_socket_type'] = 'Type de connection \"%s\" invalide'; +// sprintf +$PALANG['pAutoconfig_host_invalid_auth_scheme'] = 'Méthode d\'autentification \"%s\" invaide'; +$PALANG['pAutoconfig_host_days_on_server_is_not_an_integer'] = 'La valeur fournie pour le nombre de jours à rester sur le serveur n\'est pas un entier'; +$PALANG['pAutoconfig_host_check_interval_is_not_an_integer'] = 'L\'intervale fourni n\'est pas un entier'; +$PALANG['pAutoconfig_duplicate_host'] = 'Vou avez déjà défini un hôte "%1$s" de type %2$s et avec le port numéro %3$d'; +$PALANG['pAutoconfig_text_type_not_provided'] = 'Aucun type n\'a été fourni pour ce texte'; +// sprintf +$PALANG['pAutoconfig_text_type_invalid'] = 'Type de texte est invalide'; +$PALANG['pAutoconfig_text_lang_not_provided'] = 'AUcune langue n\'a été spécifiée'; +// sprintf +$PALANG['pAutoconfig_text_lang_invalid'] = 'Le code langue fourni est invalide'; +$PALANG['pAutoconfig_text_text_not_provided'] = 'Aucun text n\'a été fourni'; +$PALANG['pAutoconfig_no_config_found'] = 'Aucune configuration n\'a été trouvée'; +$PALANG['pAutoconfig_save_no_data_provided']= 'Aucune donnée à enregistrer n\'a été fournie'; +$PALANG['pAutoconfig_lack_permission_over_config_id'] = 'Vous n\'avez pas les permissions de modifier cette configuration avec L\'id "%s"'; +$PALANG['pAutoconfig_text_id_is_not_an_integer'] = 'L\'id fourni du texte "%s" n\'est pas un entier'; +$PALANG['pAutoconfig_config_id_not_found'] = 'Aucune configuration n\'a pu être trouvée avec l\'id "%s"'; +$PALANG['pAutoconfig_config_id_submitted_is_unauthorised'] = 'L\'id de la configuration soumis "%s" n\'est pas celui de cette configuration'; +$PALANG['pAutoconfig_no_config_id_declared']= 'Aucun id de configuration n\'a été déclaré'; +$PALANG['pAutoconfig_no_config_id_provded']= 'Aucun id de configuration id n\'a été fourni'; +$PALANG['pAutoconfig_unauthorised_domains'] = 'Les noms de domaine suivants ne sont pas autorisés pour cet admin : %s'; +$PALANG['pAutoconfig_data_provided_is_not_array'] = 'Les données soumises ne sont pas un array'; +$PALANG['pAutoconfig_no_domain_names_have_been_selected'] = 'Aucun nom de domaine n\'a été selectionné. Si vous voulez supprimer cette configuration, cliquez simplement sur "Supprimer"'; +$PALANG['pAutoconfig_domain_data_provided_is_not_an_array'] = 'Les données des noms de domaine fournis n\'est pas un array'; +$PALANG['pAutoconfig_no_domain_authorised_for_this_admin'] = 'Aucun nom de domaine n\'a été autorisé pour cet admin'; +$PALANG['pAutoconfig_host_id_is_not_an_integer'] = 'L\'id fourni de cet hôte "%s" n\'est pas un entier'; +$PALANG['pAutoconfig_text_language_already_used'] = 'Vous avez selectionné la langue "%s". Veuillez en choisir un autre.'; +$PALANG['pAutoconfig_no_config_yet_to_remove'] = 'Aucune id de configuration à supprimer n\' été fournie'; +$PALANG['pAutoconfig_config_removed'] = 'La configuration a été supprimée'; +$PALANG['pAutoconfig_config_saved'] = 'La configuration a été enregistrée'; +$PALANG['pAutoconfig_failed_to_add_config'] = 'Echec d\'ajout de la configuration'; +$PALANG['pAutoconfig_failed_to_add_domain_to_config'] = 'Echec d\'ajout du nom de domaine "%s" à cette configuration.'; +$PALANG['pAutoconfig_failed_to_add_host_to_config'] = 'Echec de l\'ajout de la configuration pour l\'hôte "%s"'; +$PALANG['pAutoconfig_failed_to_add_text_to_config'] = 'Echec d\'ajout du texte qui commence par : "%s"'; +$PALANG['pAutoconfig_placeholder_provider_name'] = 'Votre organisation S.A.'; +$PALANG['pAutoconfig_password_cleartext'] = 'Mot de passe simple'; +$PALANG['pAutoconfig_password_encrypted'] = 'Mot de passe MD5 encrypté'; +$PALANG['pAutoconfig_client_ip_address'] = 'Adresse IP client'; +$PALANG['pAutoconfig_tls_client_cert'] = 'Certificat client TLS'; +$PALANG['pAutoconfig_smtp_after_pop'] = 'SMTP après POP3'; +$PALANG['pAutoconfig_copy_provider_name'] = 'Copier le nom du fournisseur'; +$PALANG['pAutoconfig_toggle_select_all'] = 'Tout selectionner'; +$PALANG['pAutoconfig_username_template'] = 'Utiliser le modèle:'; +$PALANG['pAutoconfig_no_selection'] = 'Aucun'; +$PALANG['pAutoconfig_jump_to'] = 'Suter à la configuration:'; +$PALANG['pAutoconfig_new_configuration'] = 'Nouvelle configuration'; +$PALANG['pAutoconfig_add_new_host'] = 'Ajouter un nouvel hôte'; +$PALANG['pAutoconfig_remove_host'] = 'Supprimer cet hôte'; +$PALANG['pAutoconfig_move_up_host'] = 'Augmenter la priorité de cet hôte'; +$PALANG['pAutoconfig_move_down_host'] = 'Réduire la priorité de cet hôte'; +$PALANG['pAutoconfig_add_new_text'] = 'Ajouter un nouveau texte'; +$PALANG['pAutoconfig_remove_text'] = 'Supprimer le texte'; +$PALANG['pAutoconfig_not_supported'] = 'Pas reconnu pour l\'instant par Thunderbird'; +$PALANG['pAutoconfig_cert_sign'] = 'Signature de la config mobile Mac'; +$PALANG['pAutoconfig_cert_option'] = 'Comment signer'; +$PALANG['pAutoconfig_cert_none'] = 'Pas de signature'; +$PALANG['pAutoconfig_cert_local'] = 'Certificat de la configuration'; +$PALANG['pAutoconfig_cert_global'] = 'Certificat global'; +$PALANG['pAutoconfig_on'] = 'On'; +$PALANG['pAutoconfig_off'] = 'Off'; +$PALANG['pAutoconfig_server_side_error'] = 'Un erreur serveur s\'est produite. Veuillez voir les logs du serveur web pour plus d\'information. %s'; + $PALANG['please_keep_this_as_last_entry'] = ''; # needed for language-check.sh /* vim: set expandtab ft=php softtabstop=3 tabstop=3 shiftwidth=3: */ ?> diff --git a/languages/ja.lang b/languages/ja.lang index 6412831b..59d60ad3 100644 --- a/languages/ja.lang +++ b/languages/ja.lang @@ -412,6 +412,128 @@ $PALANG['dateformat_pgsql'] = 'YYYY-mm-dd'; # translators: rearrange to your loc $PALANG['dateformat_mysql'] = '%Y-%m-%d'; # translators: rearrange to your local date format, but make sure it's a valid MySQL date format $PALANG['password_expiration'] = 'Pass expires'; # XXX $PALANG['password_expiration_desc'] = 'Date when password will expire'; # XXX +// Autoconfig +$PALANG['pAutoconfig_page_title'] = 'メールオトディスカバリーの設定'; +$PALANG['pEdit_autoconfig_set'] = '設定を保存'; +$PALANG['pEdit_autoconfig_remove'] = '設定を削除'; +$PALANG['pAutoconfig_encoding'] = 'エンコディング'; +$PALANG['pAutoconfig_provider_id'] = 'プロバイダID'; +$PALANG['pAutoconfig_provider_domain'] = 'ドメイン名'; +$PALANG['pAutoconfig_provider_name'] = 'プロバイダ名'; +$PALANG['pAutoconfig_provider_short'] = 'プロバイダ省略'; +$PALANG['pAutoconfig_incoming_server'] = '受信サーバー'; +$PALANG['pAutoconfig_outgoing_server'] = '送信サーバー'; +$PALANG['pAutoconfig_type'] = 'タイプ'; +$PALANG['pAutoconfig_hostname'] = 'ホスト名'; +$PALANG['pAutoconfig_port'] = 'ポート'; +$PALANG['pAutoconfig_socket_type'] = 'ソケットタイプ'; +$PALANG['pAutoconfig_auth'] = '認証方法'; +$PALANG['pAutoconfig_username'] = 'ユーザー名'; +$PALANG['pAutoconfig_leave_messages_on_server']= 'メッセージをサーバーで残す'; +$PALANG['pAutoconfig_download_on_biff'] = 'ビフでダウンロード?'; +$PALANG['pAutoconfig_days_to_leave_messages_on_server']= 'サーバーで残す日間'; +$PALANG['pAutoconfig_check_interval'] = 'メール確認の頻度(分)'; +$PALANG['pAutoconfig_enable'] = 'ログインを有効化の説明'; +$PALANG['pAutoconfig_enable_url'] = 'ログイン有効化のURL'; +$PALANG['pAutoconfig_enable_instruction'] = '有効化の説明'; +$PALANG['pAutoconfig_documentation'] = 'サポート情報'; +$PALANG['pAutoconfig_documentation_url'] = 'サポート情報URL'; +$PALANG['pAutoconfig_documentation_desc'] = 'サポート情報の記述'; +$PALANG['pAutoconfig_webmail_login_page'] = 'Webmailログインページ'; +$PALANG['pAutoconfig_webmail_login_page_info']= 'Webmail情報ページ'; +$PALANG['pAutoconfig_lp_info_url'] = 'ログイン情報ページのログイン情報ページのURL'; +$PALANG['pAutoconfig_lp_info_username'] = 'ログイン情報ページのユーザー名'; +$PALANG['pAutoconfig_lp_info_username_field_id']= 'ログイン情報ページのユーザー名のフィルドID'; +$PALANG['pAutoconfig_lp_info_username_field_name']= 'ログイン情報ページのユーザー名のフィルド名'; +$PALANG['pAutoconfig_lp_info_login_button'] = 'ログイン情報ページのユーザー名'; +$PALANG['pAutoconfig_lp_info_login_button_id']= 'ログイン情報ページのボタンID'; +$PALANG['pAutoconfig_lp_info_login_button_name']= 'ログイン情報ページのボタン名'; +$PALANG['pAutoconfig_mac_specific_settings']= 'Mac専用設定'; +$PALANG['pAutoconfig_account_name'] = 'アカウント名(空欄可能)'; +$PALANG['pAutoconfig_account_type'] = 'アカウントタイプ'; +$PALANG['pAutoconfig_email'] = '電子メールアドレス(空欄可能)'; +$PALANG['pAutoconfig_ssl'] = 'SSLを利用するか?'; +$PALANG['pAutoconfig_password'] = 'パスワード(空欄可能)'; +$PALANG['pAutoconfig_description'] = '記述'; +$PALANG['pAutoconfig_organisation'] = '法人'; +$PALANG['pAutoconfig_payload_type'] = 'ペイロードタイプ'; +$PALANG['pAutoconfig_prevent_app_sheet'] = 'アップシートを防ぐ'; +$PALANG['pAutoconfig_prevent_move'] = '移動を防ぐ'; +$PALANG['pAutoconfig_smime_enabled'] = 'S/MIMEは有効ですか?'; +$PALANG['pAutoconfig_payload_remove_ok'] = 'ペイロード削除は可能?'; +$PALANG['pAutoconfig_spa'] = 'マイクロソフトSPA(Secure Password Authentication)'; +$PALANG['pAutoconfig_active'] = 'ステータス'; +// Error messages +$PALANG['pAutoconfig_invalid_encoding'] = '無効なエンコディング'; +$PALANG['pAutoconfig_empty_provider_id'] = 'プロバイダIDは指定おらず'; +$PALANG['pAutoconfig_host_no_type_provided']= 'タイプは指定おらず'; +$PALANG['pAutoconfig_host_invalid_type_value'] = '無効タイプの価値'; +$PALANG['pAutoconfig_host_no_hostname_provided'] = 'ホスト名は指定おらず'; +$PALANG['pAutoconfig_host_no_port_provided']= 'ポートは未設定'; +$PALANG['pAutoconfig_host_port_is_not_an_integer'] = '指定したポートは整数ではあります。'; +// sprintf +$PALANG['pAutoconfig_host_invalid_socket_type'] = '無効な接続タイプ「%s」'; +// sprintf +$PALANG['pAutoconfig_host_invalid_auth_scheme'] = '無効な認証方法「%s」'; +$PALANG['pAutoconfig_host_days_on_server_is_not_an_integer'] = 'サーバーでメッセージを残す日間の価値は整数ではありません。'; +$PALANG['pAutoconfig_host_check_interval_is_not_an_integer'] = '確認頻度の価値は整数ではありません。'; +$PALANG['pAutoconfig_duplicate_host'] = 'すでにポート「%3$d」のある%2$sタイプのホスト「%1$s」を定義しました。'; +$PALANG['pAutoconfig_text_type_not_provided'] = 'このテキストのためのタイプは指定おらず'; +// sprintf +$PALANG['pAutoconfig_text_type_invalid'] = '無効なテキストタイプ'; +$PALANG['pAutoconfig_text_lang_not_provided'] = '言語は指定おらず'; +// sprintf +$PALANG['pAutoconfig_text_lang_invalid'] = '指定した言語コードは無効です。'; +$PALANG['pAutoconfig_text_text_not_provided'] = 'テキストは入力されていません。'; +$PALANG['pAutoconfig_no_config_found'] = '設定を見つかりません。'; +$PALANG['pAutoconfig_save_no_data_provided']= '保存データはありません。'; +$PALANG['pAutoconfig_lack_permission_over_config_id'] = 'ID「%s」のある設定を変更する権限は足りません。'; +$PALANG['pAutoconfig_text_id_is_not_an_integer'] = '指定したテキストID「$s」は整数ではありません。'; +$PALANG['pAutoconfig_config_id_not_found'] = 'ID「%s」のある設定は見つかりません。'; +$PALANG['pAutoconfig_config_id_submitted_is_unauthorised'] = '送信した設定ID「%s」とこの設定IDと異なっています。'; +$PALANG['pAutoconfig_no_config_id_declared']= '設定IDは指定されていません。'; +$PALANG['pAutoconfig_no_config_id_provded']= '設定IDは指定されていません。'; +$PALANG['pAutoconfig_unauthorised_domains'] = 'この管理者はこのドメイン名へのアクセス許可はありません:%s'; +$PALANG['pAutoconfig_data_provided_is_not_array'] = '送信したデータはarrayではありません。'; +$PALANG['pAutoconfig_no_domain_names_have_been_selected'] = 'ドメイン名を選定していません。もしこの設定の削除をご希望であれば単純に「削除」ボタンをクリックください。'; +$PALANG['pAutoconfig_domain_data_provided_is_not_an_array'] = 'ドメイン名のデータはarrayではありません。'; +$PALANG['pAutoconfig_no_domain_authorised_for_this_admin'] = 'この管理者は一つのドメイン名でもアクセスがありません。'; +$PALANG['pAutoconfig_host_id_is_not_an_integer'] = '指定のホストID「%s」は整数ではありません。'; +$PALANG['pAutoconfig_text_language_already_used'] = 'すでに、「言語」を選定しました。他の言語を選んでください。'; +$PALANG['pAutoconfig_no_config_yet_to_remove'] = '削除すべきの設定のIDは送信されませんでした。'; +$PALANG['pAutoconfig_config_removed'] = '設定は削除されました。'; +$PALANG['pAutoconfig_config_saved'] = '設定は保存されました。'; +$PALANG['pAutoconfig_failed_to_add_config'] = '設定の追加を失敗しました。'; +$PALANG['pAutoconfig_failed_to_add_domain_to_config'] = '設定にドメイン名「%s」の追加を失敗しました。'; +$PALANG['pAutoconfig_failed_to_add_host_to_config'] = 'ホスト「%s」の設定を追加できませんでした。'; +$PALANG['pAutoconfig_failed_to_add_text_to_config'] = 'この部分で始まるテキストを追加できませんでした。'; +$PALANG['pAutoconfig_placeholder_provider_name'] = '太郎法人株式会社'; +$PALANG['pAutoconfig_password_cleartext'] = '未暗号化パスワード'; +$PALANG['pAutoconfig_password_encrypted'] = 'MD5で暗号化パスワード'; +$PALANG['pAutoconfig_client_ip_address'] = 'クライエントIPアドレス'; +$PALANG['pAutoconfig_tls_client_cert'] = 'TLSクライエント証明書'; +$PALANG['pAutoconfig_smtp_after_pop'] = 'POP3後のSMTP'; +$PALANG['pAutoconfig_copy_provider_name'] = 'プロバイダ名をコピー'; +$PALANG['pAutoconfig_toggle_select_all'] = '全て選択を切り替える'; +$PALANG['pAutoconfig_username_template'] = 'テンプレートを適用'; +$PALANG['pAutoconfig_no_selection'] = 'なし'; +$PALANG['pAutoconfig_jump_to'] = '設定へジャンプ:'; +$PALANG['pAutoconfig_new_configuration'] = '新規設定'; +$PALANG['pAutoconfig_add_new_host'] = '新ホストを追加'; +$PALANG['pAutoconfig_remove_host'] = 'このホストを削除'; +$PALANG['pAutoconfig_move_up_host'] = 'このホストの優先順位を上がる'; +$PALANG['pAutoconfig_move_down_host'] = 'このホストの優先順位を下がる'; +$PALANG['pAutoconfig_add_new_text'] = '新テキストを追加'; +$PALANG['pAutoconfig_remove_text'] = 'テキストを削除'; +$PALANG['pAutoconfig_not_supported'] = '現在Thunderbirdではサポートなし'; +$PALANG['pAutoconfig_cert_sign'] = 'Macのmobileconfigの認証'; +$PALANG['pAutoconfig_cert_option'] = '認証方法'; +$PALANG['pAutoconfig_cert_none'] = '認証なし'; +$PALANG['pAutoconfig_cert_local'] = 'この設定の認証'; +$PALANG['pAutoconfig_cert_global'] = 'グロバール設定による認証'; +$PALANG['pAutoconfig_on'] = 'オン'; +$PALANG['pAutoconfig_off'] = 'オフ'; +$PALANG['pAutoconfig_server_side_error'] = 'サーバー側、予期せぬエラーが発生してしまいました。詳細情報のためサーバーログをご参照ください。%s'; $PALANG['please_keep_this_as_last_entry'] = ''; # needed for language-check.sh /* vim: set expandtab ft=php softtabstop=3 tabstop=3 shiftwidth=3: */ From 0d10af6b5da4f064462c97cdfb749b208bd392a6 Mon Sep 17 00:00:00 2001 From: Jacques Deguest Date: Wed, 25 Mar 2020 20:42:47 +0900 Subject: [PATCH 02/12] Removing a backup file autoconfig-v2.js --- AUTOCONFIG/autoconfig-v2.js | 908 ------------------------------------ 1 file changed, 908 deletions(-) delete mode 100644 AUTOCONFIG/autoconfig-v2.js diff --git a/AUTOCONFIG/autoconfig-v2.js b/AUTOCONFIG/autoconfig-v2.js deleted file mode 100644 index 2034a46c..00000000 --- a/AUTOCONFIG/autoconfig-v2.js +++ /dev/null @@ -1,908 +0,0 @@ -/* -Created on 2020-03-12 -Copyright 2020 Jacques Deguest -Distributed under the same licence as Postfix Admin -*/ -$(document).ready(function() -{ - const DEBUG = true; - - // Credits to: https://tdanemar.wordpress.com/2010/08/24/jquery-serialize-method-and-checkboxes/ - // Modified by Jacques Deguest to include other form elements: - // http://www.w3schools.com/tags/tag_input.asp - (function($) - { - $.fn.serializeAll = function(options) - { - return $.param(this.serializeArrayAll(options)); - }; - - $.fn.serializeArrayAll = function (options) - { - var o = $.extend({ - checkboxesAsBools: false - }, options || {}); - - var rselectTextarea = /select|textarea/i; - var rinput = /text|hidden|password|search|date|time|number|color|datetime|email|file|image|month|range|tel|url|week/i; - - return this.map(function () - { - return this.elements ? $.makeArray(this.elements) : this; - }) - .filter(function () - { - return this.name && !this.disabled && - (this.checked - || (o.checkboxesAsBools && this.type === 'checkbox') - || rselectTextarea.test(this.nodeName) - || rinput.test(this.type)); - }) - .map(function (i, elem) - { - var val = $(this).val(); - return val == null ? - null : - $.isArray(val) ? - $.map(val, function (val, i) - { - return { name: elem.name, value: val }; - }) : - { - name: elem.name, - value: (o.checkboxesAsBools && this.type === 'checkbox') ? - (this.checked ? 1 : 0) : - val - }; - }).get(); - }; - })(jQuery); - - window.makeMessage = function( type, mesg ) - { - return( sprintf( '
    %s
    ', type, mesg ) ); - }; - - window.showMessage = function() - { - if( DEBUG ) console.log( "Called from " + ( arguments.callee.caller === null ? 'void' : arguments.callee.caller.name ) ); - var opts = { - div: $('#message'), - append: false, - dom: null, - timeout: null, - scroll: false, - timeoutCallback: null, - }; - var msgDiv = $('#message'); - if( arguments.length == 3 && typeof( arguments[2] ) === 'object' ) - { - var param = arguments[2]; - opts.type = arguments[0]; - opts.message = arguments[1]; - if( typeof( param.append ) !== 'undefined' ) opts.append = param.append; - if( typeof( param.dom ) !== 'undefined' ) opts.dom = param.dom; - if( typeof( param.timeout ) !== 'undefined' ) opts.timeout = param.timeout; - if( typeof( param.timeoutCallback ) !== 'undefined' ) opts.timeoutCallback = param.timeoutCallback; - if( typeof( param.scroll ) !== 'undefined' ) opts.scroll = param.scroll; - if( typeof( param.speak ) !== 'undefined' ) opts.speak = param.speak; - } - // Backward compatibility - else if( arguments.length >= 2 ) - { - opts.type = arguments[0]; - opts.message = arguments[1]; - if( arguments.length > 2 ) opts.append = arguments[2]; - if( arguments.length > 3 ) opts.dom = arguments[3]; - if( arguments.length > 4 ) opts.timeout = arguments[4]; - } - else - { - msgDiv.append( makeMessage( 'warning', "showMessage called with only " + arguments.length + " parameters while 2 at minimum are required. Usage: showMessage( type, message, append, domObject, timeout ) or showMessage( type, message, options )" ) ); - return( false ); - } - if( typeof( opts.dom ) === 'object' && opts.dom != null ) - { - msgDiv = opts.dom; - } - // Check if message is an array of messages - // https://stackoverflow.com/a/4775741/4814971 - if( Array.isArray ) - { - if( Array.isArray( opts.message ) ) - { - opts.messages = opts.message; - } - } - else if( opts.message instanceof( Array ) ) - { - opts.messages = opts.message; - } - else if( $.isArray( opts.message ) ) - { - opts.messages = opts.message; - } - - // messages has been set with previous check - // Make this array a list of errors - if( opts.hasOwnProperty( 'messages' ) ) - { - opts.message = sprintf( "
      \n%s\n
    ", opts.messages.map(function(e){ return('
  • ' + e + '
  • '); }).join( "\n" ) ); - } - - if( opts.append ) - { - msgDiv.append(makeMessage(opts.type, opts.message)); - } - else - { - msgDiv.html(makeMessage(opts.type, opts.message)); - } - - if( opts.type == 'error' ) - { - msgDiv.addClass( 'error-shake' ); - setTimeout(function() - { - msgDiv.removeClass( 'error-shake' ); - }, 70000); - } - - if( parseInt( opts.timeout ) > 0 ) - { - var thisTimeout = parseInt( opts.timeout ); - setTimeout(function() - { - msgDiv.html( '' ); - if( typeof( opts.timeoutCallback ) === 'function' ) - { - opts.timeoutCallback(); - } - },thisTimeout); - } - else - { - setTimeout(function() - { - msgDiv.html( '' ); - },15000); - } - if( opts.scroll ) - { - if( DEBUG ) console.log( "Scrolling to the top of the page..." ); - $('html, body').animate( { scrollTop: 0 }, 500 ); - } - else - { - if( DEBUG ) console.log( "No scrolling..." ); - } - }; - - window.postfixAdminProgressBar = function() - { - var xhr = new window.XMLHttpRequest(); - if( DEBUG ) console.log( "Initiating the progress bar." ); - if( DEBUG ) console.log( "Called from:\n" + (new Error).stack ); - $('#postfixadmin-progress').show().removeClass('done'); - xhr.upload.addEventListener('progress', function(evt) - { - if( evt.lengthComputable ) - { - var percentComplete = evt.loaded / evt.total; - if( DEBUG ) console.log(percentComplete); - $('#postfixadmin-progress').css({ - width: percentComplete * 100 + '%' }); - if( DEBUG ) console.log( "upload.addEventListener: " + percentComplete ); - if( percentComplete === 1 ) - { - $('#postfixadmin-progress').addClass('done').hide(); - } - } - }, false); - xhr.addEventListener('progress', function(evt) - { - if( evt.lengthComputable ) - { - var percentComplete = evt.loaded / evt.total; - if( DEBUG ) console.log("addEventListener: " + percentComplete); - $('#postfixadmin-progress').css({ - width: percentComplete * 100 + '%' }); - if( percentComplete === 1 ) - { - $('#postfixadmin-progress').addClass('done').hide(); - } - } - }, false); - return( xhr ); - }; - - window.postfixAdminProgressBarStart = function() - { -// $('#postfixadmin-progress').show().addClass('done'); - $('#postfixadmin-progress').show(); - $({property: 0}).animate({property: 85}, - { - // Arbitrary time, which should well cover the time it takes to get response from server - // Otherwise, well our progress bar will hang at 85% until we get a call to kill it - duration: 4000, - step: function() - { - var _percent = Math.round( this.property ); - $('#postfixadmin-progress').css( 'width', _percent + '%' ); - } - }); - }; - - window.postfixAdminProgressBarStop = function() - { - $({property: 85}).animate({property: 105}, - { - duration: 1000, - step: function() - { - var _percent = Math.round( this.property ); - $('#postfixadmin-progress').css( 'width', _percent + '%' ); - if( _percent == 105 ) - { - $('#postfixadmin-progress').addClass('done'); - } - }, - complete: function() - { - $('#postfixadmin-progress').hide(); - $('#postfixadmin-progress').removeClass('done'); - $('#postfixadmin-progress').css( 'width', '0%' ); - } - }); - }; - - window.autoconfigShowHideArrow = function() - { - // Reset. Show them all - $('table.server .autoconfig-command').show(); - // Hide first and last ones - // This requires jQUery up to v3.3. Version 3.4 onward do not support :first and :last anymore - $('.autoconfig-incoming:first .autoconfig-move-up').hide(); - $('.autoconfig-incoming:last .autoconfig-move-down').hide(); - $('.autoconfig-outgoing:first .autoconfig-move-up').hide(); - $('.autoconfig-outgoing:last .autoconfig-move-down').hide(); - }; - - window.autoconfig_ajax_call = function( postData ) - { - var prom = $.Deferred(); - $this = $(this); - $.ajax({ - xhr: postfixAdminProgressBar(), - type: "POST", - url: "autoconfig.php", - dataType: "json", - data: postData, - beforeSend: function(xhr) - { - xhr.overrideMimeType( "application/json; charset=utf-8" ); - postfixAdminProgressBarStart(); - }, - error: function(xhr, errType, ExceptionObject) - { - postfixAdminProgressBarStop(); - if( DEBUG ) console.log( "Returned error " + xhr.status + " with error type " + errType + " and exception object " + JSON.stringify( ExceptionObject ) ); - if( DEBUG ) console.log( "Current url is: " + xhr.responseURL ); - if( DEBUG ) console.log( "Http headers are: " + xhr.getAllResponseHeaders() ); - if( DEBUG ) console.log( "Response raw data is: \n" + xhr.responseText ); - // There was a redirect most likely due to some timeout -// if( xhr.getResponseHeader( 'Content-Type' ).toLowerCase().indexOf( 'text/html' ) >= 0 ) -// { -// window.location.reload(); -// return( true ); -// } - var msg = 'An unexpected error has occurred'; - showMessage( 'error', msg, { scroll: true }); - $this.addClass( 'error-shake' ); - prom.reject(); - }, - success: function(data, status, xhr) - { - postfixAdminProgressBarStop(); - if( data.error ) - { - showMessage( 'error', data.error, { scroll: true }); - $this.addClass( 'error-shake' ); - setTimeout(function() - { - $this.removeClass( 'error-shake' ); - },5000); - prom.reject(); - } - else - { - if( data.success ) - { - prom.resolve(data); - showMessage( 'success', data.success, { scroll: true }); - if( DEBUG ) console.log( "save(): " + data.success ); - } - else if( data.info ) - { - showMessage( 'info', data.info, { scroll: true } ); - prom.resolve(); - } - else - { - showMessage( 'info', data.msg, { scroll: true } ); - prom.resolve(); - } - } - } - }); - return( prom.promise() ); - }; - - $(document).on('click','#autoconfig_save', function(e) - { - e.preventDefault(); - $this = $(this); - var data = {handler: 'autoconfig_save'}; - $('#autoconfig_form').serializeArrayAll().map(function(item) - { - if( data[ item.name ] !== undefined ) - { - if( !data[ item.name ].push ) - { - data[ item.name ] = [ data[item.name] ]; - } - data[ item.name ].push( item.value ); - } - else - { - data[ item.name ] = item.value; - } - }); - if( DEBUG ) console.log( "serialized data is: " + JSON.stringify( data ) ); - autoconfig_ajax_call( data ).done(function(data) - { - // Since this shared function is used for both saving (adding and updating) as well as deleting - // we check if those data properties are returned by the server. - // Those here are only returned if this is the result of an addition - if( data.hasOwnProperty('config_id') ) - { - if( $('#autoconfig_form input[name="config_id"]').length == 0 ) - { - showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); - throw( "Unable to find the field \"config_id\" !" ); - } - if( typeof( data.config_id ) !== 'undefined' && data.config_id.length > 0 ) - { - // We force trigger change, because this is a hidden field and it hidden field do not trigger change - $('#autoconfig_form input[name="config_id"]').val( data.config_id ).trigger('change'); - } - } - // Do the hosts - var hostTypes = [ 'incoming', 'outgoing' ]; - for( var j = 0; j < hostTypes.length; j++ ) - { - var hostType = hostTypes[j]; - if( DEBUG ) console.log( "Checking host of type " + hostType ); - if( data.hasOwnProperty(hostType + '_server') && Array.isArray( data[hostType + '_server'] ) ) - { - var thoseHosts = $('.autoconfig-' + hostType); - var dataHosts = data[hostType + '_server']; - if( thoseHosts.length == 0 ) - { - showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); - throw( "Unable to find any hosts block in our form!" ); - } - else if( dataHosts.length != thoseHosts.length ) - { - - showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); - throw( "Total of " + hostType + " servers returned from the server (" + dataHosts.length + ") do not match the total hosts we have in our form (" + thoseHosts.length + ")." ); - } - dataHosts.forEach(function(def, index) - { - if( DEBUG ) console.log( "def contains: " + JSON.stringify( def ) ); - if( !def.hasOwnProperty('config_id') || - !def.hasOwnProperty('hostname') || - !def.hasOwnProperty('port') ) - { - if( DEBUG ) console.error( "Something is wrong. Data received is missing fields config_id, or hostname or port" ); - return( false ); - } - thoseHosts.each(function(offset, obj) - { - var dom = $(obj); - var hostId = dom.find('input[name="host_id[]"]'); - var hostName = dom.find('input[name="hostname[]"]'); - var hostPort = dom.find('input[name="port[]"]'); - if( !hostId.length ) - { - showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); - throw( "Unable to find host id field for host type \"" + hostType + "\" at offset " + i ); - } - if( !hostName.length ) - { - showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); - throw( "Unable to find host name field for host type \"" + hostType + "\" at offset " + i ); - } - if( !hostPort.length ) - { - showMessage( 'error', 'An unexpected error has occurred (check web console for details)', { scroll: true }); - throw( "Unable to find host port field for host type \"" + hostType + "\" at offset " + i ); - } - // We found our match: no id, hostname and port match - if( hostId.val().length == 0 && hostName.val() == def.hostname && hostPort.val() == def.port ) - { - if( DEBUG ) console.log( "Setting host id " + def.host_id + " to host name " + hostName.val() + " with port " + hostPort.val() ); - hostId.val( def.host_id ); - // exit the loop - return( false ); - } - }); - }); - } - else - { - if( DEBUG ) console.error( "Something is wrong. Data received for hosts of type " + hostType + " does not exist or is not an array." ); - } - } - - // Now, do the texts - var textTypes = ['instruction', 'documentation']; - for( var j = 0; j < textTypes.length; j++ ) - { - var textType = textTypes[j]; - if( DEBUG ) console.log( "Checking text of type " + textType ); - if( data.hasOwnProperty(textType) && Array.isArray( data[textType] ) ) - { - var dataTexts = data[textType]; - var thoseTextIds = $('input[name="' + textType + '_id[]"]'); - var thoseTextLang = $('select[name="' + textType + '_lang[]"]'); - var thoseTextData = $('textarea[name="' + textType + '_text[]"]'); - // The array could very well be empty - dataTexts.forEach(function(def, index) - { - if( !def.hasOwnProperty('id') || - !def.hasOwnProperty('type') || - !def.hasOwnProperty('lang') ) - { - if( DEBUG ) console.error( "Something is wrong. Data received is missing fields id, or type or lang" ); - return( false ); - } - for( var k = 0; k < thoseTextIds.length; k++ ) - { - var textId = thoseTextIds.eq(k); - var textLang = thoseTextLang.eq(k); - // Found a match - if( textId.val().length == 0 && textLang.val() == def.lang ) - { - if( DEBUG ) console.log( "Setting text id " + def.id + " to text with type " + textType + " and language " + def.lang ); - textId.val( def.id ); - return( false ); - } - } - }); - } - else - { - if( DEBUG ) console.error( "Something is wrong. Data received for " + textType + " text does not exist or is not an array." ); - } - } - }).fail(function() - { - // Nothing for now - }); - }); - - $(document).on('click','#autoconfig_remove', function(e) - { - e.preventDefault(); - var data = - { - handler: 'autoconfig_remove', - config_id: $('input[name="config_id"]').val(), - token: $('input[name="token"]').val(), - }; - if( DEBUG ) console.log( "Data to be sent is: " + JSON.stringify( data ) ); - autoconfig_ajax_call( data ).done(function(data) - { - // Reload the page, but without the query string at the end - window.location.href = window.location.pathname; - }).fail(function() - { - // Nothing for now - }); - }); - - $(document).on('click', '#autoconfig_cancel', function(e) - { - e.preventDefault(); - window.location.href = 'list.php?table=domain'; - return( true ); - }); - - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random - window.getRandomInt = function(min, max) - { - return( Math.floor( Math.random() * Math.floor((max - min) + min) ) ); - } - /* - I could have just created one function and called it - */ - // Add and remove hosts - $(document).on('click','.autoconfig-server-add',function(e) - { - e.preventDefault(); - var row = $(this).closest('table.server').closest('tr'); - if( !row.length ) - { - throw( "Unable to find the current enclosing row." ); - } - var clone = row.clone(); - clone.find('select,input[type!="hidden"],textarea').each(function(i,item) - { - $(item).val( '' ); - }); - // We need to remove the host_id so it can be treated as a new host and not an update of an existing one - clone.find('input[name="host_id[]"]').val( '' ); - // Set default value and trigger change, which will call an event handler that will hide/show the section on pop3 - clone.find('.host_type').val('imap').trigger('change'); - var optionLabels = ['leave_messages_on_server', 'download_on_biff', 'days_to_leave_messages_on_server', 'check_interval']; - optionLabels.forEach(function(fieldName, index) - { - if( DEBUG ) console.log( "Checking field name " + fieldName ); - var thisField = clone.find('input[name="' + fieldName + '[]"]'); - if( thisField.length > 0 ) - { - var forThisField = $('label[for="' + thisField.attr('id') + '"]', clone); - if( forThisField.length > 0 ) - { - if( DEBUG ) console.log( "Field is " + thisField.html() + "\nLabel field is: " + forThisField.html() ); - thisField.attr('id', 'autoconfig_' + fieldName + '_' + getRandomInt(100,1000)); - maxAttempt = 10; - if( DEBUG ) console.log( "Checking if generated id exists: " + thisField.attr('id') ); - while( $('#' + thisField.attr('id'), clone).length > 0 && ++maxAttempt < 10 ) - { - thisField.attr('id', 'autoconfig_' + fieldName + '_' + getRandomInt(100,1000)); - } - forThisField.attr('for', thisField.attr('id')); - if( DEBUG ) console.log( "Final generated id is: " + thisField.attr('id') ); - } - else - { - if( DEBUG ) console.error( "Unable to find label element for field name." ); - } - } - }); - - clone.insertAfter( row ); - autoconfigShowHideArrow(); - $('html, body').animate( { scrollTop: clone.offset().top }, 500 ); - return( true ); - }); - - $(document).on('click','.autoconfig-server-remove',function(e) - { - e.preventDefault(); - var row = $(this).closest('table.server').closest('tr'); - if( !row.length ) - { - throw( "Unable to find the current enclosing row." ); - } - // Check if there are at least 2 elements, so that after at least one remain - var re = new RegExp('(autoconfig-(?:incoming|outgoing))-server'); - var res = $(this).attr('class').match( re ); - console.log( res ); - if( res == null ) - { - throw( "Cannot find class \"autoconfig-incoming-server\" or class \"autoconfig-outgoing-server\" in our clicked element." ); - } - // Now find how many elements we have with this class - var total = $('tr.' + res[1]).length; - if( total < 2 ) - { - row.addClass('autoconfig-error-shake'); - setTimeout(function() - { - row.removeClass('autoconfig-error-shake'); - },1000); - return( false ); - } - row.remove(); - autoconfigShowHideArrow(); - }); - - // Add and remove account enable instructions or support documentation - $(document).on('click','.autoconfig-locale-text-add',function(e) - { - e.preventDefault(); - var row = $(this).closest('tr'); - if( !row.length ) - { - throw( "Unable to find the current enclosing row." ); - } - var clone = row.clone(); - clone.find('select,input[type!="hidden"],textarea').each(function(i,item) - { - $(item).val( '' ); - }); - // We need to remove the host_id so it can be treated as a new text and not an update of an existing one - clone.find('input[name$="_id[]"]').val( '' ); - clone.insertAfter( row ); - $('html, body').animate( { scrollTop: clone.offset().top }, 500 ); - }); - - $(document).on('click','.autoconfig-locale-text-remove',function(e) - { - e.preventDefault(); - var row = $(this).closest('tr'); - if( !row.length ) - { - throw( "Unable to find the current enclosing row." ); - } - var re = new RegExp('(autoconfig-(instruction|documentation))'); - var res = $(this).attr('class').match( re ); - if( res == null ) - { - throw( "Cannot find class \"autoconfig-instruction\" or class \"autoconfig-documentation\" in our clicked element." ); - } - var textType = res[2]; - var total = $('tr.' + res[1]).length; - if( DEBUG ) console.log( total + " rows found for text type " + textType ); - if( total < 2 ) - { - if( DEBUG ) console.log( "text remove: one left" ); - if( DEBUG ) console.log( "Getting lang object with " + '[name="' + textType + '_lang[]"]' ); - var textLang = row.find('[name="' + textType + '_lang[]"]'); - if( DEBUG ) console.log( "Getting text object with " + '[name="' + textType + '_text[]"]' ); - var textData = row.find('[name="' + textType + '_text[]"]'); - if( DEBUG ) console.log( "text remove: found lang and text field? " + ( ( textLang.length > 0 && textData.length > 0 ) ? "Yes" : "No" ) ); - // This is remaining default tet row and there is no more data - if( ( textLang.val() === null || ( textLang.val() !== null && textLang.val() == '' ) ) && $.trim(textData.val()) == '' ) - { - if( DEBUG ) console.log( "text remove: lang and text fields are empty already, error shake it" ); - row.addClass('autoconfig-error-shake'); - setTimeout(function() - { - row.removeClass('autoconfig-error-shake'); - },1000); - } - else - { - if( DEBUG ) console.log( "text remove: empty fields" ); - textLang.val( '' ); - textData.val( '' ); - } - return( false ); - } - row.remove(); - }); - - $(document).on('click', '#copy_provider_value', function(e) - { - e.preventDefault(); - var orgField = $('input[name="organisation"]'); - var providerNameField = $('input[name="provider_name"]'); - if( !orgField.length || !providerNameField.length ) - { - throw( "Unable to find either the provider name field or the organisation field!" ); - } - if( providerNameField.val().length == 0 ) - { - return( false ); - } - orgField.val( providerNameField.val() ); - return( true ); - }); - - $(document).on('click', '#autoconfig_toggle_select_all_domains', function(e) - { - e.preventDefault(); - // if( DEBUG ) console.log( "provider_domain options length: " + $('#autoconfig_provider_domain option').length + " and disabled options are: " + $('#autoconfig_provider_domain option:disabled').length + " and selected options are: " + $('#autoconfig_provider_domain option:selected').length ); - if( $('#autoconfig_provider_domain option:selected').length > 0 ) - { - $('#autoconfig_provider_domain option').prop('selected', false); - } - else - { - if( $('#autoconfig_provider_domain option:disabled').length == $('#autoconfig_provider_domain option').length ) - { - var row = $(this).closest('tr'); - row.addClass('error-shake'); - setTimeout(function() - { - row.removeClass('error-shake'); - },500); - return( false ); - } - else - { - $('#autoconfig_provider_domain option:not(:disabled)').prop('selected', true); - } - } - }); - - $(document).on('change', '#autoconfig_form .host_type', function(e) - { - var typeValue = $(this).val(); - // We get the enclosing table object to set some limiting context to the host_pop3 selector below - var tbl = $(this).closest('table'); - if( typeValue == 'imap' ) - { - $('.host_pop3', tbl).hide(); - } - else - { - $('.host_pop3', tbl).show(); - } - }); - - $(document).on('change', '#autoconfig_form .username_template', function(e) - { - var usernameField = $(this).closest('.server').find('input[name="username[]"]'); - if( usernameField.length == 0 ) - { - throw( "Unable to find the username field!" ); - } - usernameField.val( $(this).val() ); - }); - - $(document).on('change', '#autoconfig_form select[name="jump_to"]', function(e) - { - var id = $(this).val(); - var re = new RegExp( '([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})' ); - // This will display an empty form - if( id.length == 0 ) - { - window.location.href = 'autoconfig.php'; - } - else if( id.match( re ) ) - { - window.location.href = 'autoconfig.php?config_id=' + encodeURIComponent( id ); - } - }); - - // Upon change in the content of the hidden config_id, enable or disable the "Remove" buttton - // The Remove button can only be used if this is for an existing configuration obviously - $(document).on('change', '#autoconfig_form input[name="config_id"]', function(e) - { - if( DEBUG ) console.log( "Config id field contains \"" + $(this).val() + "\"." ); - if( $(this).val().length == 0 || $(this).val().match( /^[[:blank:]\t]*$/ ) ) - { - $('#autoconfig_remove').attr( 'disabled', true ); - } - else - { - $('#autoconfig_remove').attr( 'disabled', false ); - } - }); - - $(document).on('click', '.autoconfig-move-up', function(e) - { - e.preventDefault(); - var row = $(this).closest('.server').closest('tr'); - if( row.prev().length == 0 || ( !row.prev().hasClass('autoconfig-incoming') && !row.prev().hasClass('autoconfig-outgoing') ) ) - { - return( false ); - } - row.insertBefore( row.prev() ); - $('html, body').animate( { scrollTop: row.offset().top }, 500 ); - autoconfigShowHideArrow(); - }); - - $(document).on('click', '.autoconfig-move-down', function(e) - { - e.preventDefault(); - var row = $(this).closest('.server').closest('tr'); - if( row.next().length == 0 || ( !row.next().hasClass('autoconfig-incoming') && !row.next().hasClass('autoconfig-outgoing') ) ) - { - return( false ); - } - row.insertAfter( row.next() ); - $('html, body').animate( { scrollTop: row.offset().top }, 500 ); - autoconfigShowHideArrow(); - }); - - window.checkExistingTextLanguage = function(opts = {}) - { - if( typeof( opts ) !== 'object' ) - { - throw( "Parameters provided is not an object. Call checkExistingTextLanguage like this: checkExistingTextLanguage({ type: 'instruction', lang: 'fr', caller: $(this) })" ); - } - var textType = opts.type; - var callerMenu = opts.caller; - var langToCheck = opts.lang; - if( DEBUG ) console.log( "checkExistingTextLanguage() checking text type " + textType + " for language " + langToCheck ); - if( typeof( textType ) === 'undefined' ) - { - throw( "No text type was provided." ); - } - var langMenu = $('select[name="' + textType + '_lang[]"]'); - if( DEBUG ) console.log( "checkExistingTextLanguage() Found " + langMenu.length + " language menu(s)." ); - if( langMenu.length == 0 ) - { - throw( "Could not find any language menu for this text type " + textType ); - } - if( langMenu.length == 1 ) - { - return( true ); - } - var alreadySelected = false; - if( DEBUG ) console.log( "checkExistingTextLanguage() checking each existing language menu." ); - langMenu.each(function(offset, menuObject) - { - if( $(menuObject).is( callerMenu ) ) - { - if( DEBUG ) console.log( "checkExistingTextLanguage() skipping because this is our caller menu." ); - return( true ); - } - // Found a match, stop there - else if( $(menuObject).val() == langToCheck ) - { - if( DEBUG ) console.log( "checkExistingTextLanguage() Found match with this menu value (" + $(menuObject).val() + ") matching the language to check \"" + langToCheck + "\"." ); - alreadySelected = true; - return( false ); - } - }); - if( DEBUG ) console.log( "checkExistingTextLanguage() returning: " + !alreadySelected ); - return( !alreadySelected ); - }; - - // Upon selection or change of a language, we check it has not been selected already - $(document).on('change', 'select[name="instruction_lang[]"]', function(e) - { - if( !checkExistingTextLanguage({ type: 'instruction', lang: $(this).val(), caller: $(this) }) ) - { - $(this).addClass('error-shake'); - // - var warning = $('', - { - class: 'fas fa-exclamation-triangle fa-2x', - style: 'color: red; font-size: 20px;', - }); - warning.insertAfter( $(this) ); - var that = $(this); - setTimeout(function() - { - that.removeClass('error-shake'); - warning.remove(); - },5000); - $(this).val(''); - return( false ); - } - return( true ); - }); - - window.toggleCertFiles = function(option) - { - if( typeof( option ) === 'undefined' ) - { - return( false ); - } - if( option == 'local' ) - { - $('.cert_files').show(); - } - else - { - $('.cert_files').hide(); - } - }; - - $(document).on('change', 'select[name="sign_option"]', function(e) - { - toggleCertFiles( $(this).val() ); - }); - - // Need to trigger the change for the host type menu - if( $('#autoconfig_form .host_type').length > 0 ) - { - $('#autoconfig_form .host_type').trigger('change'); - } - if( $('#autoconfig_form input[name="config_id"]').length > 0 ) - { - $('#autoconfig_form input[name="config_id"]').trigger('change'); - } - // Hide useless up/down arrows - autoconfigShowHideArrow(); - toggleCertFiles( $('select[name="sign_option"]').val() ); -}); From 7f62fe4220dd4fe74976b0abc4965314bc9e8080 Mon Sep 17 00:00:00 2001 From: David Goodwin Date: Wed, 25 Mar 2020 19:21:55 +0000 Subject: [PATCH 03/12] run: composer format --- AUTOCONFIG/AutoconfigHandler.php | 2446 ++++++++++++--------------- AUTOCONFIG/autoconfig.php | 398 +++-- AUTOCONFIG/autoconfig_languages.php | 1 - functions.inc.php | 15 +- psalm.xml | 2 +- 5 files changed, 1321 insertions(+), 1541 deletions(-) diff --git a/AUTOCONFIG/AutoconfigHandler.php b/AUTOCONFIG/AutoconfigHandler.php index b4448981..02e5a914 100644 --- a/AUTOCONFIG/AutoconfigHandler.php +++ b/AUTOCONFIG/AutoconfigHandler.php @@ -4,32 +4,29 @@ Created on 2020-03-05 Copyright 2020 Jacques Deguest Distributed under the same licence as Postfix Admin */ -class AutoconfigHandler extends PFAHandler -{ - protected $db_table_auto = 'autoconfig'; - protected $db_table_host = 'autoconfig_hosts'; - protected $db_table_text = 'autoconfig_text'; - protected $username; - protected $is_admin = false; - // All the domain names an admin is allwoed to manage - public $all_domains = []; - private $allowed_config_ids = []; - // The domain names for this autoconfig - protected $domains = []; - protected $config_id; - protected $db_data; - public $error; - public $debug = false; - - public function init( $id ) - { +class AutoconfigHandler extends PFAHandler { + protected $db_table_auto = 'autoconfig'; + protected $db_table_host = 'autoconfig_hosts'; + protected $db_table_text = 'autoconfig_text'; + protected $username; + protected $is_admin = false; + // All the domain names an admin is allwoed to manage + public $all_domains = []; + private $allowed_config_ids = []; + // The domain names for this autoconfig + protected $domains = []; + protected $config_id; + protected $db_data; + public $error; + public $debug = false; + + public function init($id) { } /** * @return void */ - protected function initStruct() - { + protected function initStruct() { $this->struct = array( # field name allow display in... type $PALANG label $PALANG description default / options / ... # editing? form list @@ -93,28 +90,24 @@ class AutoconfigHandler extends PFAHandler /** * @param string $username */ - public function __construct( $username ) - { - $this->username = $username; - if( authentication_has_role('admin') ) - { - $this->is_admin = true; - $this->all_domains = list_domains_for_admin( $username ); - // Get the list of configuration ids, if any, this admin is allowed to access - $this->allowed_config_ids = $this->get_config_ids_for_user( $username ); + public function __construct($username) { + $this->username = $username; + if ( authentication_has_role('admin') ) { + $this->is_admin = true; + $this->all_domains = list_domains_for_admin( $username ); + // Get the list of configuration ids, if any, this admin is allowed to access + $this->allowed_config_ids = $this->get_config_ids_for_user( $username ); } } - protected function initMsg() - { - // Need to develop this part + protected function initMsg() { + // Need to develop this part } /** * @return array */ - public function webformConfig() - { + public function webformConfig() { return array( # $PALANG labels 'formtitle_create' => 'pAutoconfig_page_title', @@ -128,20 +121,15 @@ class AutoconfigHandler extends PFAHandler ); } - protected function validate_new_id() - { + protected function validate_new_id() { # autoconfig can only be enabled if a domain name exists - if( $this->is_admin ) - { - if( count( $this->all_domains ) > 0 ) - { + if ( $this->is_admin ) { + if ( count( $this->all_domains ) > 0 ) { return( true ); } - } - else - { - // Need to develop this part - return( true ); + } else { + // Need to develop this part + return( true ); } # still here? This means the mailbox doesn't exist or the admin/user doesn't have permissions to view it @@ -149,1346 +137,1150 @@ class AutoconfigHandler extends PFAHandler return( false ); } - public function get_config_ids_for_user( $user ) - { - $table_autoconfig = table_by_key('autoconfig'); - $table_autoconfig_domains = table_by_key('autoconfig_domains'); - $table_domain_admins = table_by_key('domain_admins'); - $table_domain = table_by_key('domain'); - // This is a super admin, so he/she has access to all configs - if( authentication_has_role( 'global-admin' ) ) - { - // $sql = "SELECT DISTINCT ad.config_id FROM $table_autoconfig_domains ad LEFT JOIN $table_domain d ON ad.domain = d.domain WHERE d.domain != 'ALL AND d.active IS TRUE'"; - // global admin has access to all config - $sql = "SELECT c.config_id FROM $table_autoconfig c"; - } - // This is a per-domain admin, so we use the table domain_admis to cross check which configuration he/she has access - elseif( authentication_has_role( 'admin' ) ) - { - $E_username = escape_string( $user ); - $sql = "SELECT DISTINCT ad.config_id FROM $table_domain d LEFT JOIN $table_autoconfig_domains ad ON ad.domain = d.domain WHERE d.active IS TUE AND d.username='$E_username'"; - } - $res = db_query( $sql ); - if( !empty( $res['error'] ) ) - { - $this->error = $res['error']; - return( false ); - } - $sth = $res['result']; - if( DEBUG ) error_log( "get_config_ids_for_user() \$sth = " . print_r( $sth, true ) ); - $all = $this->db_fetchall( $sth ); - $list = []; - foreach( $all as $row ) - { - $list[] = $row['config_id']; - } - return( $list ); + public function get_config_ids_for_user($user) { + $table_autoconfig = table_by_key('autoconfig'); + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $table_domain_admins = table_by_key('domain_admins'); + $table_domain = table_by_key('domain'); + // This is a super admin, so he/she has access to all configs + if ( authentication_has_role( 'global-admin' ) ) { + // $sql = "SELECT DISTINCT ad.config_id FROM $table_autoconfig_domains ad LEFT JOIN $table_domain d ON ad.domain = d.domain WHERE d.domain != 'ALL AND d.active IS TRUE'"; + // global admin has access to all config + $sql = "SELECT c.config_id FROM $table_autoconfig c"; + } + // This is a per-domain admin, so we use the table domain_admis to cross check which configuration he/she has access + elseif ( authentication_has_role( 'admin' ) ) { + $E_username = escape_string( $user ); + $sql = "SELECT DISTINCT ad.config_id FROM $table_domain d LEFT JOIN $table_autoconfig_domains ad ON ad.domain = d.domain WHERE d.active IS TUE AND d.username='$E_username'"; + } + $res = db_query( $sql ); + if ( !empty( $res['error'] ) ) { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + if ( DEBUG ) { + error_log( "get_config_ids_for_user() \$sth = " . print_r( $sth, true ) ); + } + $all = $this->db_fetchall( $sth ); + $list = []; + foreach ( $all as $row ) { + $list[] = $row['config_id']; + } + return( $list ); } - public function has_permission_over_config_id( $user, $this_config_id ) - { - if( empty( $user ) || empty( $this_config_id ) ) - { - if( $this->debug ) error_log( "has_permission_over_config_id() user is empty, or no config was provided." ); - return( false ); - } - $table_admin = table_by_key('admin'); - $table_autoconfig_domains = table_by_key('autoconfig_domains'); - $table_domain_admins = table_by_key('domain_admins'); - $E_username = escape_string( $user ); - $E_config_id = escape_string( $this_config_id ); - $sql_admin = "SELECT a.* FROM $table_admin a WHERE a.username = '$E_username'"; - $res = db_query( $sql_admin ); - if( !empty( $res['error'] ) ) - { - $this->error = $res['error']; - return( false ); - } - $sth = $res['result']; - $row = $this->db_assoc( $sth ); - if( !empty( $row ) ) - { - // Admin status is not active - if( !$row['active'] ) - { - if( $this->debug ) error_log( "has_permission_over_config_id() config $this_config_id is not active." ); - return( false ); - } - // Admin is a super admin, so he has access to everything - elseif( $row['superadmin'] ) - { - if( $this->debug ) error_log( "has_permission_over_config_id() user $user is a super admin, returning true." ); - return( true ); - } - else - { - $true = db_get_boolean( true ); - $sql = "SELECT c.config_id FROM $table_autoconfig_domains c LEFT JOIN $table_domain_admins da ON da.domain = c.domain WHERE da.username = '$E_username' AND da.active = '$true' AND c.config_id = '$E_config_id'"; - if( $this->debug ) error_log( "has_permission_over_config_id() checking user '$user' permission over config '$this_config_id' with sql query: $sql" ); - $res = db_query( $sql ); - if( !empty( $res['error'] ) ) - { - $this->error = $res['error']; - return( false ); - } - $sth = $res['result']; - $row = $this->db_assoc( $sth ); - return( !empty( $row ) ); - } - } - // This is a regular user - else - { - return( false ); - } + public function has_permission_over_config_id($user, $this_config_id) { + if ( empty( $user ) || empty( $this_config_id ) ) { + if ( $this->debug ) { + error_log( "has_permission_over_config_id() user is empty, or no config was provided." ); + } + return( false ); + } + $table_admin = table_by_key('admin'); + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $table_domain_admins = table_by_key('domain_admins'); + $E_username = escape_string( $user ); + $E_config_id = escape_string( $this_config_id ); + $sql_admin = "SELECT a.* FROM $table_admin a WHERE a.username = '$E_username'"; + $res = db_query( $sql_admin ); + if ( !empty( $res['error'] ) ) { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $row = $this->db_assoc( $sth ); + if ( !empty( $row ) ) { + // Admin status is not active + if ( !$row['active'] ) { + if ( $this->debug ) { + error_log( "has_permission_over_config_id() config $this_config_id is not active." ); + } + return( false ); + } + // Admin is a super admin, so he has access to everything + elseif ( $row['superadmin'] ) { + if ( $this->debug ) { + error_log( "has_permission_over_config_id() user $user is a super admin, returning true." ); + } + return( true ); + } else { + $true = db_get_boolean( true ); + $sql = "SELECT c.config_id FROM $table_autoconfig_domains c LEFT JOIN $table_domain_admins da ON da.domain = c.domain WHERE da.username = '$E_username' AND da.active = '$true' AND c.config_id = '$E_config_id'"; + if ( $this->debug ) { + error_log( "has_permission_over_config_id() checking user '$user' permission over config '$this_config_id' with sql query: $sql" ); + } + $res = db_query( $sql ); + if ( !empty( $res['error'] ) ) { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $row = $this->db_assoc( $sth ); + return( !empty( $row ) ); + } + } + // This is a regular user + else { + return( false ); + } } - public function allowed_ids() - { - return( $this->allowed_config_ids ); + public function allowed_ids() { + return( $this->allowed_config_ids ); } - public function config_id( $id ) - { - if( isset( $id ) ) - { - if( $this->debug ) error_log( "config_id() checking config id \"$id\"." ); - $table_autoconfig = table_by_key('autoconfig'); - $E_id = escape_string( $id ); - $sql = "SELECT config_id FROM $table_autoconfig WHERE config_id = '$E_id'"; - $res = db_query( $sql ); - if( !empty( $res['error'] ) ) - { - $this->error = $res['error']; - return( false ); - } - $sth = $res['result']; - $row = $this->db_assoc( $sth ); - if( empty( $row ) ) - { - if( $this->debug ) error_log( "config_id() could not find config id \"$id\"." ); - return( false ); - } - if( $this->debug ) error_log( "config_id() config id \"$id\" found." ); - $this->config_id = $row['config_id']; - } - return( $this->config_id ); + public function config_id($id) { + if ( isset( $id ) ) { + if ( $this->debug ) { + error_log( "config_id() checking config id \"$id\"." ); + } + $table_autoconfig = table_by_key('autoconfig'); + $E_id = escape_string( $id ); + $sql = "SELECT config_id FROM $table_autoconfig WHERE config_id = '$E_id'"; + $res = db_query( $sql ); + if ( !empty( $res['error'] ) ) { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $row = $this->db_assoc( $sth ); + if ( empty( $row ) ) { + if ( $this->debug ) { + error_log( "config_id() could not find config id \"$id\"." ); + } + return( false ); + } + if ( $this->debug ) { + error_log( "config_id() config id \"$id\" found." ); + } + $this->config_id = $row['config_id']; + } + return( $this->config_id ); } - public function db_assoc( $sth ) - { - if( empty( $sth ) ) throw( "No statement handler was provided." ); - try - { - return( $sth->fetch( PDO::FETCH_ASSOC ) ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - return( false ); - } + public function db_assoc($sth) { + if ( empty( $sth ) ) { + throw( "No statement handler was provided." ); + } + try { + return( $sth->fetch( PDO::FETCH_ASSOC ) ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + return( false ); + } } - public function db_fetchall( $sth ) - { - if( empty( $sth ) ) throw( "No statement handler was provided." ); - // if( DEBUG ) error_log( "db_fetchall() \$sth = " . print_r( $sth, true ) ); - try - { - return( $sth->fetchAll(PDO::FETCH_ASSOC) ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - return( false ); - } + public function db_fetchall($sth) { + if ( empty( $sth ) ) { + throw( "No statement handler was provided." ); + } + // if( DEBUG ) error_log( "db_fetchall() \$sth = " . print_r( $sth, true ) ); + try { + return( $sth->fetchAll(PDO::FETCH_ASSOC) ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + return( false ); + } } - public function db_rows( $sth ) - { - if( empty( $sth ) ) throw( "No statement handler was provided." ); - try - { - return( $sth->rowCount() ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - return( false ); - } + public function db_rows($sth) { + if ( empty( $sth ) ) { + throw( "No statement handler was provided." ); + } + try { + return( $sth->rowCount() ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + return( false ); + } } - public function error_as_string() - { - if( is_array( $this->error ) ) - { - return( implode( ', ', $this->error ) ); - } - else - { - return( $this->error ); - } + public function error_as_string() { + if ( is_array( $this->error ) ) { + return( implode( ', ', $this->error ) ); + } else { + return( $this->error ); + } } - private function get_config( $id ) - { - if( !isset( $id ) ) $id = $this->config_id; - $table_autoconfig = table_by_key('autoconfig'); - $E_config_id = escape_string( $id ); - $sql = "SELECT * FROM $table_autoconfig WHERE config_id = '$E_config_id'"; - $res = db_query( $sql ); - if( !empty( $res['error'] ) ) - { - $this->error = $res['error']; - return( false ); - } - $sth = $res['result']; - $row = $this->db_assoc( $sth ); - if( empty( $row ) ) - { - return( false ); - } - return( $row ); + private function get_config($id) { + if ( !isset( $id ) ) { + $id = $this->config_id; + } + $table_autoconfig = table_by_key('autoconfig'); + $E_config_id = escape_string( $id ); + $sql = "SELECT * FROM $table_autoconfig WHERE config_id = '$E_config_id'"; + $res = db_query( $sql ); + if ( !empty( $res['error'] ) ) { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $row = $this->db_assoc( $sth ); + if ( empty( $row ) ) { + return( false ); + } + return( $row ); } // Get the list of config id that this user has access - public function get_config_ids() - { - $table_autoconfig = table_by_key('autoconfig'); - $table_autoconfig_domains = table_by_key('autoconfig_domains'); - $table_domain_admins = table_by_key('domain_admins'); - $true = db_get_boolean( true ); + public function get_config_ids() { + $table_autoconfig = table_by_key('autoconfig'); + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $table_domain_admins = table_by_key('domain_admins'); + $true = db_get_boolean( true ); $E_username = escape_string( $this->username ); - $sql = "SELECT distinct c.config_id, c.provider_id FROM $table_autoconfig_domains d LEFT JOIN $table_autoconfig c ON c.config_id = d.config_id LEFT JOIN $table_domain_admins da ON da.username = '$E_username' AND da.active = '$true' AND (da.domain = 'ALL' OR da.domain = d.domain)"; - $res = db_query( $sql ); - if( !empty( $res['error'] ) ) - { - $this->error = $res['error']; - return( false ); - } - $sth = $res['result']; - $all = $this->db_fetchall( $sth ); - $list = []; - foreach( $all as $row ) - { - $list[$row['config_id']] = $row['provider_id']; - } - if( $this->debug ) error_log( "get_config_ids() returning '" . print_r( $list, true ) . "'" ); - return( $list ); + $sql = "SELECT distinct c.config_id, c.provider_id FROM $table_autoconfig_domains d LEFT JOIN $table_autoconfig c ON c.config_id = d.config_id LEFT JOIN $table_domain_admins da ON da.username = '$E_username' AND da.active = '$true' AND (da.domain = 'ALL' OR da.domain = d.domain)"; + $res = db_query( $sql ); + if ( !empty( $res['error'] ) ) { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $all = $this->db_fetchall( $sth ); + $list = []; + foreach ( $all as $row ) { + $list[$row['config_id']] = $row['provider_id']; + } + if ( $this->debug ) { + error_log( "get_config_ids() returning '" . print_r( $list, true ) . "'" ); + } + return( $list ); } - public function get_id_by_domain( $thisDomain ) - { - if( !isset( $thisDomain ) ) - { - // Are the domain names for this autoconfig set ? - if( !isset( $this->domains ) ) - { - return( false ); - } - // Pick one - $thisDomain = $this->domains[0]; - } - $E_domain = escape_string( $thisDomain ); - $table_autoconfig_domains = table_by_key('autoconfig_domains'); - $sql = "SELECT config_id FROM $table_autoconfig_domains WHERE domain = '$E_domain'"; - $res = db_query( $sql ); - if( !empty( $res['error'] ) ) - { - $this->error = $res['error']; - return( false ); - } - $sth = $res['result']; - $row = $this->db_assoc( $sth ); - if( empty( $row ) ) - { - return( false ); - } - return( $row['config_id'] ); + public function get_id_by_domain($thisDomain) { + if ( !isset( $thisDomain ) ) { + // Are the domain names for this autoconfig set ? + if ( !isset( $this->domains ) ) { + return( false ); + } + // Pick one + $thisDomain = $this->domains[0]; + } + $E_domain = escape_string( $thisDomain ); + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $sql = "SELECT config_id FROM $table_autoconfig_domains WHERE domain = '$E_domain'"; + $res = db_query( $sql ); + if ( !empty( $res['error'] ) ) { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $row = $this->db_assoc( $sth ); + if ( empty( $row ) ) { + return( false ); + } + return( $row['config_id'] ); } - private function get_domains( $id ) - { - if( count( $this->domains ) ) - { - return( $this->domains ); - } - elseif( !isset( $id ) ) - { - if( !empty( $this->config_id ) ) - { - $id = $this->config_id; - } - else - { - return( false ); - } - } - $table_autoconfig_domains = table_by_key('autoconfig_domains'); - $table_domain_admins = table_by_key('domain_admins'); + private function get_domains($id) { + if ( count( $this->domains ) ) { + return( $this->domains ); + } elseif ( !isset( $id ) ) { + if ( !empty( $this->config_id ) ) { + $id = $this->config_id; + } else { + return( false ); + } + } + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $table_domain_admins = table_by_key('domain_admins'); $E_config_id = escape_string( $id ); $E_username = escape_string( $this->username ); // Make sure the admin can only get the list of domain names he is in charge of - $sql = "SELECT d.domain FROM $table_autoconfig_domains AS d LEFT JOIN $table_domain_admins AS da ON da.username = '$E_username' AND (da.domain = 'ALL' OR da.domain = d.domain) WHERE d.config_id = '$E_config_id'"; - if( $this->debug ) error_log( "get_domains() executing following query to get the list of authorised domains: $sql" ); - $res = db_query( $sql ); - if( !empty( $res['error'] ) ) - { - $this->error = $res['error']; - return( false ); - } - $sth = $res['result']; - $all = $this->db_fetchall( $sth ); - $list = []; - foreach( $all as $row ) - { - $list[] = $row['domain']; - } - if( $this->debug ) error_log( "get_domains() returning '" . print_r( $list, true ) . "'" ); - return( $list ); + $sql = "SELECT d.domain FROM $table_autoconfig_domains AS d LEFT JOIN $table_domain_admins AS da ON da.username = '$E_username' AND (da.domain = 'ALL' OR da.domain = d.domain) WHERE d.config_id = '$E_config_id'"; + if ( $this->debug ) { + error_log( "get_domains() executing following query to get the list of authorised domains: $sql" ); + } + $res = db_query( $sql ); + if ( !empty( $res['error'] ) ) { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $all = $this->db_fetchall( $sth ); + $list = []; + foreach ( $all as $row ) { + $list[] = $row['domain']; + } + if ( $this->debug ) { + error_log( "get_domains() returning '" . print_r( $list, true ) . "'" ); + } + return( $list ); } - public function get_other_config_domains( $id ) - { - if( is_null( $id ) ) - { - $id = $this->config_id; - } - $table_autoconfig_domains = table_by_key('autoconfig_domains'); - $table_domain_admins = table_by_key('domain_admins'); - $true = db_get_boolean( true ); + public function get_other_config_domains($id) { + if ( is_null( $id ) ) { + $id = $this->config_id; + } + $table_autoconfig_domains = table_by_key('autoconfig_domains'); + $table_domain_admins = table_by_key('domain_admins'); + $true = db_get_boolean( true ); $E_username = escape_string( $this->username ); - if( !empty( $id ) ) - { - $E_config_id = escape_string( $id ); - $sql = "SELECT d.domain FROM autoconfig_domains d LEFT JOIN domain_admins da ON da.username = '$E_username' AND da.active = '$true' AND (da.domain = 'ALL' OR da.domain = d.domain) WHERE d.config_id != '$E_config_id'"; - } - else - { - $sql = "SELECT d.domain FROM autoconfig_domains d LEFT JOIN domain_admins da ON da.username = '$E_username' AND da.active = '$true' AND (da.domain = 'ALL' OR da.domain = d.domain)"; - } - $res = db_query( $sql ); - if( !empty( $res['error'] ) ) - { - $this->error = $res['error']; - return( false ); - } - $sth = $res['result']; - $all = $this->db_fetchall( $sth ); - $list = []; - foreach( $all as $row ) - { - $list[] = $row['domain']; - } - return( $list ); + if ( !empty( $id ) ) { + $E_config_id = escape_string( $id ); + $sql = "SELECT d.domain FROM autoconfig_domains d LEFT JOIN domain_admins da ON da.username = '$E_username' AND da.active = '$true' AND (da.domain = 'ALL' OR da.domain = d.domain) WHERE d.config_id != '$E_config_id'"; + } else { + $sql = "SELECT d.domain FROM autoconfig_domains d LEFT JOIN domain_admins da ON da.username = '$E_username' AND da.active = '$true' AND (da.domain = 'ALL' OR da.domain = d.domain)"; + } + $res = db_query( $sql ); + if ( !empty( $res['error'] ) ) { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $all = $this->db_fetchall( $sth ); + $list = []; + foreach ( $all as $row ) { + $list[] = $row['domain']; + } + return( $list ); } - private function get_hosts( $type, $id ) - { - if( empty( $type ) ) - { - return( false ); - } - elseif( $type != 'in' && $type != 'out' ) - { - return( false ); - } - - if( !isset( $id ) ) - { - if( !empty( $this->config_id ) ) - { - $id = $this->config_id; - } - else - { - return( false ); - } - } - - $table_autoconfig_hosts = table_by_key('autoconfig_hosts'); + private function get_hosts($type, $id) { + if ( empty( $type ) ) { + return( false ); + } elseif ( $type != 'in' && $type != 'out' ) { + return( false ); + } + + if ( !isset( $id ) ) { + if ( !empty( $this->config_id ) ) { + $id = $this->config_id; + } else { + return( false ); + } + } + + $table_autoconfig_hosts = table_by_key('autoconfig_hosts'); $E_config_id = escape_string( $id ); - if( $type == 'in' ) - { - $sql = "SELECT *, id AS \"host_id\" FROM $table_autoconfig_hosts WHERE (type = 'imap' OR type = 'pop3') AND config_id = '$E_config_id' ORDER BY priority"; - } - else - { - $sql = "SELECT *, id AS \"host_id\" FROM $table_autoconfig_hosts WHERE type = 'smtp' AND config_id = '$E_config_id' ORDER BY priority"; - } - $res = db_query( $sql ); - if( !empty( $res['error'] ) ) - { - $this->error = $res['error']; - return( false ); - } - $sth = $res['result']; - $all = $this->db_fetchall( $sth ); - $list = []; - foreach( $all as $row ) - { - $list[] = $row; - } - return( $list ); + if ( $type == 'in' ) { + $sql = "SELECT *, id AS \"host_id\" FROM $table_autoconfig_hosts WHERE (type = 'imap' OR type = 'pop3') AND config_id = '$E_config_id' ORDER BY priority"; + } else { + $sql = "SELECT *, id AS \"host_id\" FROM $table_autoconfig_hosts WHERE type = 'smtp' AND config_id = '$E_config_id' ORDER BY priority"; + } + $res = db_query( $sql ); + if ( !empty( $res['error'] ) ) { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $all = $this->db_fetchall( $sth ); + $list = []; + foreach ( $all as $row ) { + $list[] = $row; + } + return( $list ); } - private function get_text( $type, $id ) - { - // Must provide explicitely the type - if( empty( $type ) ) - { - return( false ); - } - elseif( $type != 'instruction' && $type != 'documentation' ) - { - return( false ); - } - if( !isset( $id ) ) - { - if( !empty( $this->config_id ) ) - { - $id = $this->config_id; - } - else - { - return( false ); - } - } - $table_autoconfig_text = table_by_key('autoconfig_text'); + private function get_text($type, $id) { + // Must provide explicitely the type + if ( empty( $type ) ) { + return( false ); + } elseif ( $type != 'instruction' && $type != 'documentation' ) { + return( false ); + } + if ( !isset( $id ) ) { + if ( !empty( $this->config_id ) ) { + $id = $this->config_id; + } else { + return( false ); + } + } + $table_autoconfig_text = table_by_key('autoconfig_text'); $E_type = escape_string( $type ); $E_config_id = escape_string( $id ); - $sql = "SELECT * FROM $table_autoconfig_text WHERE type = '$E_type' AND config_id = '$E_config_id'"; - $res = db_query( $sql ); - if( !empty( $res['error'] ) ) - { - $this->error = $res['error']; - return( false ); - } - $sth = $res['result']; - $all = $this->db_fetchall( $sth ); - $list = []; - foreach( $all as $row ) - { - $list[] = $row; - } - return( $list ); + $sql = "SELECT * FROM $table_autoconfig_text WHERE type = '$E_type' AND config_id = '$E_config_id'"; + $res = db_query( $sql ); + if ( !empty( $res['error'] ) ) { + $this->error = $res['error']; + return( false ); + } + $sth = $res['result']; + $all = $this->db_fetchall( $sth ); + $list = []; + foreach ( $all as $row ) { + $list[] = $row; + } + return( $list ); } - public function get_details( $id ) - { - if( !$this->is_admin ) return( false ); - if( empty( $id ) ) - { - if( $this->debug ) error_log( "No id provided, returning false." ); - return( false ); - } - // Some security: do not get the details for this configuration if the user does not have permission - if( !$this->has_permission_over_config_id( $this->username, $id ) ) - { - if( $this->debug ) error_log( sprintf( "Admin %s has no permission over this config %s, returning false.", $this->username, $id ) ); - return( false ); - } - $config = []; - $conf_domains = []; - $conf_hosts_in = []; - $conf_hosts_out = []; - $conf_texts_inst = []; - $conf_texts_doc = []; - if( ( $config = $this->get_config( $id ) ) === false ) - { - if( $this->debug ) error_log( "Unable to get basic config data, returning false." ); - return( false ); - } - // If active is null, set it to true by default - if( is_null( $config['active'] ) ) $config['active'] = true; - $booleanProperties = array( 'enable_status', 'documentation_status', 'ssl_enabled', 'prevent_app_sheet', 'prevent_move', 'smime_enabled', 'payload_remove_ok', 'active', 'spa' ); - $this->encode_boolean( $config, $booleanProperties ); - if( ( $conf_domains = $this->get_domains( $id ) ) === false ) - { - if( $this->debug ) error_log( "Unable to get config domains, returning false." ); - return( false ); - } - if( $this->debug ) error_log( sprintf( "Found %d domains for config $id", count( $conf_domains ) ) ); - $config['provider_domain'] = $conf_domains; - // To build the domain names html menu - $config['provider_domain_options'] = $this->all_domains; - if( $this->debug ) error_log( sprintf( "Found %d total domains for config $id", count( $conf_domains ) ) ); - - // Get incoming servers - if( ( $conf_hosts_in = $this->get_hosts( 'in', $id ) ) === false ) - { - if( $this->debug ) error_log( "Unable to get config incoming hosts, returning false." ); - return( false ); - } - if( $this->debug ) error_log( sprintf( "Found %d incoming servers for config $id", count( $conf_hosts_in ) ) ); - $booleanProperties = array( 'leave_messages_on_server', 'download_on_biff' ); - // foreach( $conf_hosts_in as $conf_hosts_ref ) - for( $j = 0; $j < count( $conf_hosts_in ); $j++ ) - { - $conf_hosts_in[$j] = $this->encode_boolean( $conf_hosts_in[$j], $booleanProperties ); - if( $this->debug ) error_log( "get_details(): incoming host data is now: " . print_r( $conf_hosts_in[$j], true ) ); - } - $config['incoming_server'] = $conf_hosts_in; - - // Get outgoing servers - if( ( $conf_hosts_out = $this->get_hosts( 'out', $id ) ) === false ) - { - if( $this->debug ) error_log( "Unable to get config outgoing hosts, returning false." ); - return( false ); - } - if( $this->debug ) error_log( sprintf( "Found %d outgoing servers for config $id", count( $conf_hosts_out ) ) ); - // foreach( $conf_hosts_out as $conf_hosts_ref ) - for( $j = 0; $j < count( $conf_hosts_out ); $j++ ) - { - $conf_hosts_out[$j] = $this->encode_boolean( $conf_hosts_out[$j], $booleanProperties ); - } - $config['outgoing_server'] = $conf_hosts_out; - - // Get enabling instructions, if any - if( ( $conf_texts_inst = $this->get_text( 'instruction', $id ) ) === false ) - { - if( $this->debug ) error_log( "Unable to get enabling instrucctions, returning false." ); - return( false ); - } - if( $this->debug ) error_log( sprintf( "Found %d enable instruction(s) for config $id", count( $conf_texts_inst ) ) ); + public function get_details($id) { + if ( !$this->is_admin ) { + return( false ); + } + if ( empty( $id ) ) { + if ( $this->debug ) { + error_log( "No id provided, returning false." ); + } + return( false ); + } + // Some security: do not get the details for this configuration if the user does not have permission + if ( !$this->has_permission_over_config_id( $this->username, $id ) ) { + if ( $this->debug ) { + error_log( sprintf( "Admin %s has no permission over this config %s, returning false.", $this->username, $id ) ); + } + return( false ); + } + $config = []; + $conf_domains = []; + $conf_hosts_in = []; + $conf_hosts_out = []; + $conf_texts_inst = []; + $conf_texts_doc = []; + if ( ( $config = $this->get_config( $id ) ) === false ) { + if ( $this->debug ) { + error_log( "Unable to get basic config data, returning false." ); + } + return( false ); + } + // If active is null, set it to true by default + if ( is_null( $config['active'] ) ) { + $config['active'] = true; + } + $booleanProperties = array( 'enable_status', 'documentation_status', 'ssl_enabled', 'prevent_app_sheet', 'prevent_move', 'smime_enabled', 'payload_remove_ok', 'active', 'spa' ); + $this->encode_boolean( $config, $booleanProperties ); + if ( ( $conf_domains = $this->get_domains( $id ) ) === false ) { + if ( $this->debug ) { + error_log( "Unable to get config domains, returning false." ); + } + return( false ); + } + if ( $this->debug ) { + error_log( sprintf( "Found %d domains for config $id", count( $conf_domains ) ) ); + } + $config['provider_domain'] = $conf_domains; + // To build the domain names html menu + $config['provider_domain_options'] = $this->all_domains; + if ( $this->debug ) { + error_log( sprintf( "Found %d total domains for config $id", count( $conf_domains ) ) ); + } + + // Get incoming servers + if ( ( $conf_hosts_in = $this->get_hosts( 'in', $id ) ) === false ) { + if ( $this->debug ) { + error_log( "Unable to get config incoming hosts, returning false." ); + } + return( false ); + } + if ( $this->debug ) { + error_log( sprintf( "Found %d incoming servers for config $id", count( $conf_hosts_in ) ) ); + } + $booleanProperties = array( 'leave_messages_on_server', 'download_on_biff' ); + // foreach( $conf_hosts_in as $conf_hosts_ref ) + for ( $j = 0; $j < count( $conf_hosts_in ); $j++ ) { + $conf_hosts_in[$j] = $this->encode_boolean( $conf_hosts_in[$j], $booleanProperties ); + if ( $this->debug ) { + error_log( "get_details(): incoming host data is now: " . print_r( $conf_hosts_in[$j], true ) ); + } + } + $config['incoming_server'] = $conf_hosts_in; + + // Get outgoing servers + if ( ( $conf_hosts_out = $this->get_hosts( 'out', $id ) ) === false ) { + if ( $this->debug ) { + error_log( "Unable to get config outgoing hosts, returning false." ); + } + return( false ); + } + if ( $this->debug ) { + error_log( sprintf( "Found %d outgoing servers for config $id", count( $conf_hosts_out ) ) ); + } + // foreach( $conf_hosts_out as $conf_hosts_ref ) + for ( $j = 0; $j < count( $conf_hosts_out ); $j++ ) { + $conf_hosts_out[$j] = $this->encode_boolean( $conf_hosts_out[$j], $booleanProperties ); + } + $config['outgoing_server'] = $conf_hosts_out; + + // Get enabling instructions, if any + if ( ( $conf_texts_inst = $this->get_text( 'instruction', $id ) ) === false ) { + if ( $this->debug ) { + error_log( "Unable to get enabling instrucctions, returning false." ); + } + return( false ); + } + if ( $this->debug ) { + error_log( sprintf( "Found %d enable instruction(s) for config $id", count( $conf_texts_inst ) ) ); + } // $langs = array(); // foreach( $conf_texts_inst as $ref ) // { // $langs[ $ref['lang'] ] = $ref['phrase']; // } - $config['enable'] = array( - 'url' => $config['enable_url'], - 'instruction' => $conf_texts_inst, - ); - - // Configuration support documentation - if( ( $conf_texts_doc = $this->get_text( 'documentation', $id ) ) === false ) - { - if( $this->debug ) error_log( "Unable to get documentation description, returning false." ); - return( false ); - } - if( $this->debug ) error_log( sprintf( "Found %d documentatoin description(s) for config $id", count( $conf_texts_doc ) ) ); + $config['enable'] = array( + 'url' => $config['enable_url'], + 'instruction' => $conf_texts_inst, + ); + + // Configuration support documentation + if ( ( $conf_texts_doc = $this->get_text( 'documentation', $id ) ) === false ) { + if ( $this->debug ) { + error_log( "Unable to get documentation description, returning false." ); + } + return( false ); + } + if ( $this->debug ) { + error_log( sprintf( "Found %d documentatoin description(s) for config $id", count( $conf_texts_doc ) ) ); + } // $langs = array(); // foreach( $conf_texts_doc as $ref ) // { // $langs[ $ref['lang'] ] = $ref['phrase']; // } - $config['documentation'] = array( - 'url' => $config['documentation_url'], - 'description' => $conf_texts_doc, - ); - if( $this->debug ) error_log( sprintf( "Returning hash ref with %d keys", count( array_keys( $config ) ) ) ); - return( $config ); + $config['documentation'] = array( + 'url' => $config['documentation_url'], + 'description' => $conf_texts_doc, + ); + if ( $this->debug ) { + error_log( sprintf( "Returning hash ref with %d keys", count( array_keys( $config ) ) ) ); + } + return( $config ); } - public function remove_config( $id ) - { - global $CONF, $PALANG; - $conf_data = array(); - if( empty( $id ) ) - { - $this->error = $PALANG['pAutoconfig_no_config_id_provded']; - return( false ); - } - elseif( ( $conf_data = $this->get_config( $id ) ) === false ) - { - $this->error = sprintf( $PALANG['pAutoconfig_config_id_not_found'], $id ); - return( false ); - } - elseif( !$this->has_permission_over_config_id( $this->username, $id ) ) - { - $this->error = sprintf( $PALANG['pAutoconfig_lack_permission_over_config_id'], $id ); - return( false ); - } - $table_autoconfig = table_by_key('autoconfig'); - $ok = 0; - try - { - // $ok = db_delete( $table_autoconfig, 'config_id', $id ); - // I need this to throw an exception so I can report the issue - $ok = db_execute( "DELETE FROM $table_autoconfig WHERE config_id = ?", array($id), true ); - } - catch( Exception $e ) - { - if( DEBUG ) error_log( "remove_config(): An error occurred while trying to remove config id $id: " . $e->getMessage() ); - $this->error = $e->getMessage(); - return( false ); - } - return( $ok ); + public function remove_config($id) { + global $CONF, $PALANG; + $conf_data = array(); + if ( empty( $id ) ) { + $this->error = $PALANG['pAutoconfig_no_config_id_provded']; + return( false ); + } elseif ( ( $conf_data = $this->get_config( $id ) ) === false ) { + $this->error = sprintf( $PALANG['pAutoconfig_config_id_not_found'], $id ); + return( false ); + } elseif ( !$this->has_permission_over_config_id( $this->username, $id ) ) { + $this->error = sprintf( $PALANG['pAutoconfig_lack_permission_over_config_id'], $id ); + return( false ); + } + $table_autoconfig = table_by_key('autoconfig'); + $ok = 0; + try { + // $ok = db_delete( $table_autoconfig, 'config_id', $id ); + // I need this to throw an exception so I can report the issue + $ok = db_execute( "DELETE FROM $table_autoconfig WHERE config_id = ?", array($id), true ); + } catch ( Exception $e ) { + if ( DEBUG ) { + error_log( "remove_config(): An error occurred while trying to remove config id $id: " . $e->getMessage() ); + } + $this->error = $e->getMessage(); + return( false ); + } + return( $ok ); } - public function save_config( &$data ) - { - global $CONF, $PALANG; - // Number of rows changed - $ok = 0; - if( !is_array( $data ) ) - { - $this->error = $PALANG['pAutoconfig_save_no_data_provided']; - return( false ); - } - elseif( !isset( $data['provider_domain'] ) ) - { - $this->error = $PALANG['pAutoconfig_no_domain_names_have_been_selected']; - return( false ); - } - // Should not happen - elseif( !is_array( $data['provider_domain'] ) ) - { - $this->error = $PALANG['pAutoconfig_domain_data_provided_is_not_an_array']; - return( false ); - } - elseif( count( $data['provider_domain'] ) == 0 ) - { - $this->error = $PALANG['pAutoconfig_no_domain_names_have_been_selected']; - return( false ); - } - $config_data = array( - 'encoding' => @$data['encoding'], - 'provider_id' => @$data['provider_id'], - 'provider_name' => @$data['provider_name'], - 'provider_short' => @$data['provider_short'], - 'enable_status' => @$data['enable_status'], - 'enable_url' => @$data['enable_url'], - 'documentation_status' => @$data['documentation_status'], - 'documentation_url' => @$data['documentation_url'], - 'webmail_login_page' => @$data['webmail_login_page'], - 'lp_info_url' => @$data['lp_info_url'], - 'lp_info_username_field_id' => @$data['lp_info_username_field_id'], - 'lp_info_username_field_name' => @$data['lp_info_username_field_name'], - 'lp_info_login_button_id' => @$data['lp_info_login_button_id'], - 'lp_info_login_button_name' => @$data['lp_info_login_button_name'], - 'account_name' => @$data['account_name'], - 'account_type' => @$data['account_type'], - 'email' => @$data['email'], - 'ssl_enabled' => @$data['ssl_enabled'], - 'description' => @$data['description'], - 'organisation' => @$data['organisation'], - 'payload_type' => @$data['payload_type'], - 'prevent_app_sheet' => @$data['prevent_app_sheet'], - 'prevent_move' => @$data['prevent_move'], - 'smime_enabled' => @$data['smime_enabled'], - 'payload_remove_ok' => @$data['payload_remove_ok'], - 'spa' => @$data['spa'], - 'active' => @$data['active'], - 'sign_option' => @$data['sign_option'], - 'cert_filepath' => @$data['cert_filepath'], - 'privkey_filepath' => @$data['privkey_filepath'], - 'chain_filepath' => @$data['chain_filepath'], - ); - $dataError = null; - if( ( $dataError = $this->check_autoconfig_data( $config_data ) ) != null ) - { - $this->error = $dataError; - return( false ); - } - // In case of update - elseif( !empty( $data['config_id'] ) ) - { - // Should not be happening, but let's not assume anything - if( empty( $this->config_id ) ) - { - $this->error = $PALANG['pAutoconfig_no_config_id_declared']; - return( false ); - } - elseif( $data['config_id'] != $this->config_id ) - { - if( $this->debug ) error_log( sprintf( "save_config() config id submitted \"%s\" is not the same as our current id \"%s\"", $data['config_id'], $this->config_id ) ); - $this->error = sprintf( $PALANG['pAutoconfig_config_id_submitted_is_unauthorised'], $data['config_id'] ); - return( false ); - } - } - // For the rest, there could be no imap, pop3 or smtp declared. That's up to the user who is always right - // Likewise, there could be no login enable instruction or support documentation, so we don't make them mandatory - if( DEBUG ) error_log( "Base config data are: " . print_r( $config_data, true ) ); - + public function save_config(&$data) { + global $CONF, $PALANG; + // Number of rows changed + $ok = 0; + if ( !is_array( $data ) ) { + $this->error = $PALANG['pAutoconfig_save_no_data_provided']; + return( false ); + } elseif ( !isset( $data['provider_domain'] ) ) { + $this->error = $PALANG['pAutoconfig_no_domain_names_have_been_selected']; + return( false ); + } + // Should not happen + elseif ( !is_array( $data['provider_domain'] ) ) { + $this->error = $PALANG['pAutoconfig_domain_data_provided_is_not_an_array']; + return( false ); + } elseif ( count( $data['provider_domain'] ) == 0 ) { + $this->error = $PALANG['pAutoconfig_no_domain_names_have_been_selected']; + return( false ); + } + $config_data = array( + 'encoding' => @$data['encoding'], + 'provider_id' => @$data['provider_id'], + 'provider_name' => @$data['provider_name'], + 'provider_short' => @$data['provider_short'], + 'enable_status' => @$data['enable_status'], + 'enable_url' => @$data['enable_url'], + 'documentation_status' => @$data['documentation_status'], + 'documentation_url' => @$data['documentation_url'], + 'webmail_login_page' => @$data['webmail_login_page'], + 'lp_info_url' => @$data['lp_info_url'], + 'lp_info_username_field_id' => @$data['lp_info_username_field_id'], + 'lp_info_username_field_name' => @$data['lp_info_username_field_name'], + 'lp_info_login_button_id' => @$data['lp_info_login_button_id'], + 'lp_info_login_button_name' => @$data['lp_info_login_button_name'], + 'account_name' => @$data['account_name'], + 'account_type' => @$data['account_type'], + 'email' => @$data['email'], + 'ssl_enabled' => @$data['ssl_enabled'], + 'description' => @$data['description'], + 'organisation' => @$data['organisation'], + 'payload_type' => @$data['payload_type'], + 'prevent_app_sheet' => @$data['prevent_app_sheet'], + 'prevent_move' => @$data['prevent_move'], + 'smime_enabled' => @$data['smime_enabled'], + 'payload_remove_ok' => @$data['payload_remove_ok'], + 'spa' => @$data['spa'], + 'active' => @$data['active'], + 'sign_option' => @$data['sign_option'], + 'cert_filepath' => @$data['cert_filepath'], + 'privkey_filepath' => @$data['privkey_filepath'], + 'chain_filepath' => @$data['chain_filepath'], + ); + $dataError = null; + if ( ( $dataError = $this->check_autoconfig_data( $config_data ) ) != null ) { + $this->error = $dataError; + return( false ); + } + // In case of update + elseif ( !empty( $data['config_id'] ) ) { + // Should not be happening, but let's not assume anything + if ( empty( $this->config_id ) ) { + $this->error = $PALANG['pAutoconfig_no_config_id_declared']; + return( false ); + } elseif ( $data['config_id'] != $this->config_id ) { + if ( $this->debug ) { + error_log( sprintf( "save_config() config id submitted \"%s\" is not the same as our current id \"%s\"", $data['config_id'], $this->config_id ) ); + } + $this->error = sprintf( $PALANG['pAutoconfig_config_id_submitted_is_unauthorised'], $data['config_id'] ); + return( false ); + } + } + // For the rest, there could be no imap, pop3 or smtp declared. That's up to the user who is always right + // Likewise, there could be no login enable instruction or support documentation, so we don't make them mandatory + if ( DEBUG ) { + error_log( "Base config data are: " . print_r( $config_data, true ) ); + } + $table_autoconfig = table_by_key('autoconfig'); $table_autoconfig_domains = table_by_key('autoconfig_domains'); $table_autoconfig_hosts = table_by_key('autoconfig_hosts'); $table_autoconfig_text = table_by_key('autoconfig_text'); - - // Start sql transaction - db_begin(); - try - { - $is_new = empty( $this->config_id ); - if( !$is_new ) - { - try - { - $ok = db_update( 'autoconfig', 'config_id', $this->config_id, $config_data, array('modified'), true ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - db_rollback(); - return( false ); - } - $config_data['config_id'] = $this->config_id; - } - // New entry - else - { - $this->config_id = $config_data['config_id'] = $data['config_id'] = $this->generate_uuid_v4(); - try - { - $ok = db_insert( 'autoconfig', $config_data, array('created', 'modified'), true ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - db_rollback(); - return( false ); - } - if( $ok == 0 ) - { - $this->error = $PALANG['pAutoconfig_failed_to_add_config']; - db_rollback(); - return( false ); - } - } - - // Process domain names. We get the current list, first remove the ones that have been removed and add the new ones - $selected_domains = $data['provider_domain']; - if( !$is_new ) - { - if( $this->debug ) error_log( "save_config() get current domain names for this update." ); - $current_domains = array(); - if( ( $current_domains = $this->get_domains( $this->config_id ) ) === false ) - { - $this->error = $PALANG['pAutoconfig_no_domain_authorised_for_this_admin']; - db_rollback(); - return( false ); - } - // First remove the ones that are not anymore in our selection - // $E_config_id = escape_string( $this->config_id ); - foreach( $current_domains as $domain ) - { - if( !in_array( $domain, $selected_domains ) ) - { - try - { - // $ok += db_delete( $table_autoconfig_domains, 'domain', $domain, "AND config_id = '$E_config_id'" ); - $ok += db_execute( "DELETE FROM $table_autoconfig_domains WHERE domain = ? AND config_id = ?", array( $domain, $this->config_id ), true ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - db_rollback(); - return( false ); - } - } - } - } - // Now, add the ones selected that are not in the current domains - $current_domains = array(); - if( !empty( $this->config_id ) ) - { - if( $this->debug ) error_log( "save_config() get current domain names for this config id \"" . $this->config_id . "\"." ); - if( ( $current_domains = $this->get_domains( $this->config_id ) ) === false ) - { - if( $this->debug ) error_log( "save_config() get_domains returned: '" . print_r( $current_domains, true ) . "'." ); - $this->error = $PALANG['pAutoconfig_no_domain_authorised_for_this_admin']; - db_rollback(); - return( false ); - } - } - - foreach( $selected_domains as $domain ) - { - if( !in_array( $domain, $current_domains ) ) - { - $this_data = array( - 'config_id' => $this->config_id, - 'domain' => $domain, - ); - try - { - $added = db_insert( 'autoconfig_domains', $this_data, [], true ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - db_rollback(); - return( false ); - } - if( $added == 0 ) - { - $this->error = sprintf( $PALANG['pAutoconfig_failed_to_add_domain_to_config'], $domain ); - db_rollback(); - return( false ); - } - else - { - $ok += $added; - } - } - } - $config_data['provider_domain'] = $selected_domains; - $config_data['provider_domain_options'] = $this->all_domains; - - // Process hosts, if any - // First, get current host, and remove the ones that have been removed - if( !$is_new ) - { - $host_types = ['in','out']; - foreach( $host_types as $this_type ) - { - $current_servers = $this->get_hosts( $this_type, $this->config_id ); - // There must be at least one host for each type, even if blank - if( !array_key_exists( 'host_id', $data ) ) - { - error_log( "No host id could be found at all from the web data submitted for config id \"" . $this->config_id . "\" which is" . ( $is_new ? '' : ' not' ) . " new." ); - $this->error = "No host id could be found at all from the web data submitted for config id \"" . $this->config_id . "\" which is" . ( $is_new ? '' : ' not' ) . " new."; - db_rollback(); - return( false ); - } - foreach( $current_servers as $ref ) - { - if( !in_array( $ref['host_id'], $data['host_id'] ) ) - { - try - { - // $deleted = db_delete( $table_autoconfig_hosts, 'id', $ref['host_id'] ); - $deleted = db_execute( "DELETE FROM $table_autoconfig_hosts WHERE id = ?", array( $ref['host_id'] ), true ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - db_rollback(); - return( false ); - } - } - } - } - } - - if( count( $data['hostname'] ) > 0 ) - { - // counter by type - $counter = []; - // To check for duplicates - $processed = []; - for( $i = 0; $i < count( $data['hostname'] ); $i++ ) - { - $host_data = array( - 'host_id' => @$data['host_id'][$i], - 'type' => @$data['type'][$i], - 'hostname' => @$data['hostname'][$i], - 'port' => @$data['port'][$i], - 'socket_type' => @$data['socket_type'][$i], - 'auth' => @$data['auth'][$i], - 'username' => @$data['username'][$i], - 'leave_messages_on_server' => @$data['leave_messages_on_server'][$i], - 'download_on_biff' => @$data['download_on_biff'][$i], - 'days_to_leave_messages_on_server' => @$data['days_to_leave_messages_on_server'][$i], - 'check_interval' => @$data['check_interval'][$i], - 'priority' => ++$counter[$data['type'][$i]], - ); - if( ( $dataError = $this->check_autoconfig_host_data( $host_data ) ) != null ) - { - $this->error = $dataError; - db_rollback(); - return( false ); - } - elseif( array_key_exists( $host_data['hostname'], $processed ) && - $processed[ $host_data['hostname'] ]['type'] == $host_data['type'] && - $processed[ $host_data['hostname'] ]['port'] == $host_data['port'] ) - { - $this->error = sprintf( $PALANG['pAutoconfig_duplicate_host'], $host_data['hostname'], $host_data['type'], $host_data['port'] ); - db_rollback(); - return( false ); - } - $processed[ $host_data['hostname'] ] = array( 'type' => $host_data['type'], 'port' => $host_data['port'] ); - - if( !empty( $host_data['host_id'] ) ) - { - // This was just temporary for checking. There is no host_id field - $this_id = $host_data['host_id']; - unset( $host_data['host_id'] ); - try - { - $ok += db_update( 'autoconfig_hosts', 'id', $this_id, $host_data, [], true ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - db_rollback(); - return( false ); - } - } - else - { - unset( $host_data['host_id'] ); - $host_data['config_id'] = $config_data['config_id']; - try - { - $added = db_insert( 'autoconfig_hosts', $host_data, [], true ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - db_rollback(); - return( false ); - } - if( $added == 0 ) - { - $this->error = sprintf( $PALANG['pAutoconfig_failed_to_add_host_to_config'], $host_data['hostname'] ); - db_rollback(); - return( false ); - } - else - { - $ok += $added; - } - } - } - } - $host_types = ['in','out']; - foreach( $host_types as $this_type ) - { - $current_servers = $this->get_hosts( $this_type, $this->config_id ); - if( $this_type == 'in' ) - { - $config_data['incoming_server'] = $current_servers; - } - else - { - $config_data['outgoing_server'] = $current_servers; - } - } - - // First remove instructions or documentation that have been removed from the interface - $textTypes = array( 'instruction', 'documentation' ); - if( !$is_new ) - { - foreach( $textTypes as $textType ) - { - $all_text = []; - // No need to bother checking one by one, if there are no text at all - if( !array_key_exists( "${textType}_id", $data ) || - ( is_array( $data["${textType}_id"] ) && count( $data["${textType}_id"] ) == 0 ) ) - { - try - { - // $ok += db_delete( $table_autoconfig_text, 'config_id', $this->config_id ); - $ok += db_execute( "DELETE FROM $table_autoconfig_text WHERE config_id = ?", array( $this->config_id ), true ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - db_rollback(); - return( false ); - } - continue; - } - // An error occurred. Need to report it: TODO - if( ( $all_text = $this->get_text( $textType, $this->config_id ) ) === false ) - { - error_log( "An error occurred. Could not get all the text type ${textType} for config id \"" . $this->config_id . "\"." ); - db_rollback(); - $this->error = "An error occurred. Could not get all the text type ${textType} for config id \"" . $this->config_id . "\"."; - return( false ); - } - - // One by one. If our existing text id for this type is not in the array of ids, then remove it - foreach( $all_text as $ref ) - { - if( empty( $ref['id'] ) ) - { - error_log( "Somehow, I got an empty text id from function get_text() for config \"" . $this->config_id . "\"." ); - db_rollback(); - $this->error = "Somehow, I got an empty text id from function get_text() for config \"" . $this->config_id . "\"."; - return( false ); - } - if( !in_array( $ref['id'], $data["${textType}_id"] ) ) - { - try - { - // $ok += db_delete( $table_autoconfig_text, 'id', $ref['id'] ); - $ok += db_execute( "DELETE FROM $table_autoconfig_text WHERE id = ?", array( $ref['id'] ), true ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - db_rollback(); - return( false ); - } - } - } - } - } - - // Now, do the additions - // Process login enable instruction and support documentation - foreach( $textTypes as $textType ) - { - $existing_langs = []; - if( array_key_exists( "${textType}_lang", $data ) && - is_array( $data["${textType}_lang"] ) && - count( $data["${textType}_lang"] ) ) - { - for( $i = 0; $i < count( $data["${textType}_lang"] ); $i++ ) - { - $text_data = array( - 'type' => $textType, - 'id' => @$data["${textType}_id"][$i], - 'lang' => @$data["${textType}_lang"][$i], - 'phrase' => @$data["${textType}_text"][$i], - ); - // The text is empty: no need to go further - if( preg_match( '/^[[:blank:]\r\n]*$/', $text_data['phrase'] ) ) - { - continue; - } - // Found a language duplicate - elseif( in_array( $text_data['lang'], $existing_langs ) ) - { - $this->error = sprintf( $PALANG['pAutoconfig_text_language_already_used'], $text_data['lang'] ); - db_rollback(); - return( false ); - } - $existing_langs[] = $text_data['lang']; - - if( ( $dataError = $this->check_autoconfig_text_data( $text_data ) ) != null ) - { - $this->error = $dataError; - db_rollback(); - return( false ); - } - if( empty( $text_data['id'] ) ) - { - $text_data['config_id'] = $config_data['config_id']; - unset( $text_data['id'] ); - try - { - $added = db_insert( 'autoconfig_text', $text_data, [], true ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - db_rollback(); - return( false ); - } - if( $added == 0 ) - { - $this->error = sprintf( $PALANG['pAutoconfig_failed_to_add_text_to_config'], mb_substr( $text_data['phrase'], 0, 12 ) ); - db_rollback(); - return( false ); - } - else - { - $ok += $added; - } - } - else - { - $textId = $text_data['id']; - unset( $text_data['id'] ); - try - { - $ok += db_update( 'autoconfig_text', 'id', $textId, $text_data, [], true ); - } - catch( Exception $e ) - { - $this->error = $e->getMessage(); - db_rollback(); - return( false ); - } - } - } - } - else - { - if( $this->debug ) error_log( "save_config() No lang found for text $textType" ); - } - - $all_text = []; - if( ( $all_text = $this->get_text( $textType, $this->config_id ) ) === false ) - { - error_log( "An error occurred. Could not get all the text type ${textType} for config id \"" . $this->config_id . "\"." ); - db_rollback(); - $this->error = "An error occurred. Could not get all the text type ${textType} for config id \"" . $this->config_id . "\"."; - return( false ); - } - - if( $textType == 'instruction' ) - { - $config_data['enable'] = array( - 'url' => $config_data['enable_url'], - 'instruction' => $all_text, - ); - } - // Otherwise this is the suppport documentation - else - { - $config_data['documentation'] = array( - 'url' => $config_data['documentation_url'], - 'description' => $all_text, - ); - } - } - // All clear, we commit the changes - db_commit(); - return( $config_data ); - } - catch( Exception $e ) - { - db_rollback(); - $this->error = $e->getMessage(); - return( false ); - } + + // Start sql transaction + db_begin(); + try { + $is_new = empty( $this->config_id ); + if ( !$is_new ) { + try { + $ok = db_update( 'autoconfig', 'config_id', $this->config_id, $config_data, array('modified'), true ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + $config_data['config_id'] = $this->config_id; + } + // New entry + else { + $this->config_id = $config_data['config_id'] = $data['config_id'] = $this->generate_uuid_v4(); + try { + $ok = db_insert( 'autoconfig', $config_data, array('created', 'modified'), true ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + if ( $ok == 0 ) { + $this->error = $PALANG['pAutoconfig_failed_to_add_config']; + db_rollback(); + return( false ); + } + } + + // Process domain names. We get the current list, first remove the ones that have been removed and add the new ones + $selected_domains = $data['provider_domain']; + if ( !$is_new ) { + if ( $this->debug ) { + error_log( "save_config() get current domain names for this update." ); + } + $current_domains = array(); + if ( ( $current_domains = $this->get_domains( $this->config_id ) ) === false ) { + $this->error = $PALANG['pAutoconfig_no_domain_authorised_for_this_admin']; + db_rollback(); + return( false ); + } + // First remove the ones that are not anymore in our selection + // $E_config_id = escape_string( $this->config_id ); + foreach ( $current_domains as $domain ) { + if ( !in_array( $domain, $selected_domains ) ) { + try { + // $ok += db_delete( $table_autoconfig_domains, 'domain', $domain, "AND config_id = '$E_config_id'" ); + $ok += db_execute( "DELETE FROM $table_autoconfig_domains WHERE domain = ? AND config_id = ?", array( $domain, $this->config_id ), true ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + } + } + } + // Now, add the ones selected that are not in the current domains + $current_domains = array(); + if ( !empty( $this->config_id ) ) { + if ( $this->debug ) { + error_log( "save_config() get current domain names for this config id \"" . $this->config_id . "\"." ); + } + if ( ( $current_domains = $this->get_domains( $this->config_id ) ) === false ) { + if ( $this->debug ) { + error_log( "save_config() get_domains returned: '" . print_r( $current_domains, true ) . "'." ); + } + $this->error = $PALANG['pAutoconfig_no_domain_authorised_for_this_admin']; + db_rollback(); + return( false ); + } + } + + foreach ( $selected_domains as $domain ) { + if ( !in_array( $domain, $current_domains ) ) { + $this_data = array( + 'config_id' => $this->config_id, + 'domain' => $domain, + ); + try { + $added = db_insert( 'autoconfig_domains', $this_data, [], true ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + if ( $added == 0 ) { + $this->error = sprintf( $PALANG['pAutoconfig_failed_to_add_domain_to_config'], $domain ); + db_rollback(); + return( false ); + } else { + $ok += $added; + } + } + } + $config_data['provider_domain'] = $selected_domains; + $config_data['provider_domain_options'] = $this->all_domains; + + // Process hosts, if any + // First, get current host, and remove the ones that have been removed + if ( !$is_new ) { + $host_types = ['in','out']; + foreach ( $host_types as $this_type ) { + $current_servers = $this->get_hosts( $this_type, $this->config_id ); + // There must be at least one host for each type, even if blank + if ( !array_key_exists( 'host_id', $data ) ) { + error_log( "No host id could be found at all from the web data submitted for config id \"" . $this->config_id . "\" which is" . ( $is_new ? '' : ' not' ) . " new." ); + $this->error = "No host id could be found at all from the web data submitted for config id \"" . $this->config_id . "\" which is" . ( $is_new ? '' : ' not' ) . " new."; + db_rollback(); + return( false ); + } + foreach ( $current_servers as $ref ) { + if ( !in_array( $ref['host_id'], $data['host_id'] ) ) { + try { + // $deleted = db_delete( $table_autoconfig_hosts, 'id', $ref['host_id'] ); + $deleted = db_execute( "DELETE FROM $table_autoconfig_hosts WHERE id = ?", array( $ref['host_id'] ), true ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + } + } + } + } + + if ( count( $data['hostname'] ) > 0 ) { + // counter by type + $counter = []; + // To check for duplicates + $processed = []; + for ( $i = 0; $i < count( $data['hostname'] ); $i++ ) { + $host_data = array( + 'host_id' => @$data['host_id'][$i], + 'type' => @$data['type'][$i], + 'hostname' => @$data['hostname'][$i], + 'port' => @$data['port'][$i], + 'socket_type' => @$data['socket_type'][$i], + 'auth' => @$data['auth'][$i], + 'username' => @$data['username'][$i], + 'leave_messages_on_server' => @$data['leave_messages_on_server'][$i], + 'download_on_biff' => @$data['download_on_biff'][$i], + 'days_to_leave_messages_on_server' => @$data['days_to_leave_messages_on_server'][$i], + 'check_interval' => @$data['check_interval'][$i], + 'priority' => ++$counter[$data['type'][$i]], + ); + if ( ( $dataError = $this->check_autoconfig_host_data( $host_data ) ) != null ) { + $this->error = $dataError; + db_rollback(); + return( false ); + } elseif ( array_key_exists( $host_data['hostname'], $processed ) && + $processed[ $host_data['hostname'] ]['type'] == $host_data['type'] && + $processed[ $host_data['hostname'] ]['port'] == $host_data['port'] ) { + $this->error = sprintf( $PALANG['pAutoconfig_duplicate_host'], $host_data['hostname'], $host_data['type'], $host_data['port'] ); + db_rollback(); + return( false ); + } + $processed[ $host_data['hostname'] ] = array( 'type' => $host_data['type'], 'port' => $host_data['port'] ); + + if ( !empty( $host_data['host_id'] ) ) { + // This was just temporary for checking. There is no host_id field + $this_id = $host_data['host_id']; + unset( $host_data['host_id'] ); + try { + $ok += db_update( 'autoconfig_hosts', 'id', $this_id, $host_data, [], true ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + } else { + unset( $host_data['host_id'] ); + $host_data['config_id'] = $config_data['config_id']; + try { + $added = db_insert( 'autoconfig_hosts', $host_data, [], true ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + if ( $added == 0 ) { + $this->error = sprintf( $PALANG['pAutoconfig_failed_to_add_host_to_config'], $host_data['hostname'] ); + db_rollback(); + return( false ); + } else { + $ok += $added; + } + } + } + } + $host_types = ['in','out']; + foreach ( $host_types as $this_type ) { + $current_servers = $this->get_hosts( $this_type, $this->config_id ); + if ( $this_type == 'in' ) { + $config_data['incoming_server'] = $current_servers; + } else { + $config_data['outgoing_server'] = $current_servers; + } + } + + // First remove instructions or documentation that have been removed from the interface + $textTypes = array( 'instruction', 'documentation' ); + if ( !$is_new ) { + foreach ( $textTypes as $textType ) { + $all_text = []; + // No need to bother checking one by one, if there are no text at all + if ( !array_key_exists( "${textType}_id", $data ) || + ( is_array( $data["${textType}_id"] ) && count( $data["${textType}_id"] ) == 0 ) ) { + try { + // $ok += db_delete( $table_autoconfig_text, 'config_id', $this->config_id ); + $ok += db_execute( "DELETE FROM $table_autoconfig_text WHERE config_id = ?", array( $this->config_id ), true ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + continue; + } + // An error occurred. Need to report it: TODO + if ( ( $all_text = $this->get_text( $textType, $this->config_id ) ) === false ) { + error_log( "An error occurred. Could not get all the text type ${textType} for config id \"" . $this->config_id . "\"." ); + db_rollback(); + $this->error = "An error occurred. Could not get all the text type ${textType} for config id \"" . $this->config_id . "\"."; + return( false ); + } + + // One by one. If our existing text id for this type is not in the array of ids, then remove it + foreach ( $all_text as $ref ) { + if ( empty( $ref['id'] ) ) { + error_log( "Somehow, I got an empty text id from function get_text() for config \"" . $this->config_id . "\"." ); + db_rollback(); + $this->error = "Somehow, I got an empty text id from function get_text() for config \"" . $this->config_id . "\"."; + return( false ); + } + if ( !in_array( $ref['id'], $data["${textType}_id"] ) ) { + try { + // $ok += db_delete( $table_autoconfig_text, 'id', $ref['id'] ); + $ok += db_execute( "DELETE FROM $table_autoconfig_text WHERE id = ?", array( $ref['id'] ), true ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + } + } + } + } + + // Now, do the additions + // Process login enable instruction and support documentation + foreach ( $textTypes as $textType ) { + $existing_langs = []; + if ( array_key_exists( "${textType}_lang", $data ) && + is_array( $data["${textType}_lang"] ) && + count( $data["${textType}_lang"] ) ) { + for ( $i = 0; $i < count( $data["${textType}_lang"] ); $i++ ) { + $text_data = array( + 'type' => $textType, + 'id' => @$data["${textType}_id"][$i], + 'lang' => @$data["${textType}_lang"][$i], + 'phrase' => @$data["${textType}_text"][$i], + ); + // The text is empty: no need to go further + if ( preg_match( '/^[[:blank:]\r\n]*$/', $text_data['phrase'] ) ) { + continue; + } + // Found a language duplicate + elseif ( in_array( $text_data['lang'], $existing_langs ) ) { + $this->error = sprintf( $PALANG['pAutoconfig_text_language_already_used'], $text_data['lang'] ); + db_rollback(); + return( false ); + } + $existing_langs[] = $text_data['lang']; + + if ( ( $dataError = $this->check_autoconfig_text_data( $text_data ) ) != null ) { + $this->error = $dataError; + db_rollback(); + return( false ); + } + if ( empty( $text_data['id'] ) ) { + $text_data['config_id'] = $config_data['config_id']; + unset( $text_data['id'] ); + try { + $added = db_insert( 'autoconfig_text', $text_data, [], true ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + if ( $added == 0 ) { + $this->error = sprintf( $PALANG['pAutoconfig_failed_to_add_text_to_config'], mb_substr( $text_data['phrase'], 0, 12 ) ); + db_rollback(); + return( false ); + } else { + $ok += $added; + } + } else { + $textId = $text_data['id']; + unset( $text_data['id'] ); + try { + $ok += db_update( 'autoconfig_text', 'id', $textId, $text_data, [], true ); + } catch ( Exception $e ) { + $this->error = $e->getMessage(); + db_rollback(); + return( false ); + } + } + } + } else { + if ( $this->debug ) { + error_log( "save_config() No lang found for text $textType" ); + } + } + + $all_text = []; + if ( ( $all_text = $this->get_text( $textType, $this->config_id ) ) === false ) { + error_log( "An error occurred. Could not get all the text type ${textType} for config id \"" . $this->config_id . "\"." ); + db_rollback(); + $this->error = "An error occurred. Could not get all the text type ${textType} for config id \"" . $this->config_id . "\"."; + return( false ); + } + + if ( $textType == 'instruction' ) { + $config_data['enable'] = array( + 'url' => $config_data['enable_url'], + 'instruction' => $all_text, + ); + } + // Otherwise this is the suppport documentation + else { + $config_data['documentation'] = array( + 'url' => $config_data['documentation_url'], + 'description' => $all_text, + ); + } + } + // All clear, we commit the changes + db_commit(); + return( $config_data ); + } catch ( Exception $e ) { + db_rollback(); + $this->error = $e->getMessage(); + return( false ); + } } - - // Taken from StackOverflow: https://stackoverflow.com/a/44504979/4814971 - private function generate_uuid_v4() - { - if (function_exists('com_create_guid') === true) - return trim(com_create_guid(), '{}'); + + // Taken from StackOverflow: https://stackoverflow.com/a/44504979/4814971 + private function generate_uuid_v4() { + if (function_exists('com_create_guid') === true) { + return trim(com_create_guid(), '{}'); + } - $data = PHP_MAJOR_VERSION < 7 ? openssl_random_pseudo_bytes(16) : random_bytes(16); - $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100 - $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10 - return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); - } - - public function check_autoconfig_data( &$data ) - { - global $CONF, $PALANG; - $errorList = []; - if( !empty( $data['config_id'] ) ) - { - if( !$this->get_config( $data['config_id'] ) ) - { - $errorList[] = sprintf( $PALANG['pAutoconfig_config_id_not_found'], $data['config_id'] ); - } - elseif( !$this->has_permission_over_config_id( $this->username, $data['config_id'] ) ) - { - $errorList[] = sprintf( $PALANG['pAutoconfig_lack_permission_over_config_id'], $data['config_id'] ); - } - } - if( !empty( $data['encoding'] ) && !preg_match( '/^[a-zA-Z][\w\-]+$/', $data['encoding'] ) ) - { - $errorList[] = sprintf( $PALANG['pAutoconfig_invalid_encoding'], $data['encoding'] ); - } - if( empty( $data['provider_id'] ) ) - { - $errorList[] = $PALANG['pAutoconfig_empty_provider_id']; - } - // I do not check on purpose the file path of the cert, private key and chain (if any), because the user may have provided the information before setting up those files, and I do not want to annoy the user - $booleanProperties = array( 'enable_status', 'documentation_status', 'ssl_enabled', 'prevent_app_sheet', 'prevent_move', 'smime_enabled', 'payload_remove_ok', 'active', 'spa' ); - $this->decode_boolean( $data, $booleanProperties ); - if( count( $errorList ) == 0 ) - { - $this->empty2null( $data ); - return( null ); - } - else - { - return( $errorList ); - } - } - - public function check_autoconfig_domains( $domain_list ) - { - global $CONF, $PALANG; - $errorList = []; - if( count( $this->all_domains ) == 0 ) - { - $errorList[] = $PALANG['pAutoconfig_no_domain_allocated_to_admin']; - return( $errorList ); - } - elseif( !is_array( $domain_list ) ) - { - $errorList[] = $PALANG['pAutoconfig_data_provided_is_not_array']; - return( $errorList ); - } - // Nothing to check - elseif( !count( $domain_list ) ) - { - return( null ); - } - - $bad_domains = []; - foreach( $domain_list as $domain ) - { - if( !in_array( $domain, $this->all_domains ) ) - { - $bad_domains[] = $domain; - } - } - if( count( $bad_domains ) > 0 ) - { - $errorList[] = sprintf( $PALANG['pAutoconfig_unauthorised_domains'], join( ', ', $bad_domains ) ); - } - - if( count( $errorList ) == 0 ) - { - return( null ); - } - else - { - return( $errorList ); - } - } - - public function check_autoconfig_host_data( &$data ) - { - global $PALANG; - $errorList = []; - if( empty( $data['type'] ) ) - { - $errorList[] = $PALANG['pAutoconfig_host_no_type_provided']; - } - elseif( !preg_match( '/^imap|pop3|smtp$/i', $data['type'] ) ) - { - $errorList[] = $PALANG['pAutoconfig_host_invalid_type_value']; - } - else - { - $data['type'] = strtolower( $data['type'] ); - } - - if( !empty( $data['host_id'] ) ) - { - if( !preg_match( '/^\d+$/', $data['host_id'] ) ) - { - $errorList[] = sprintf( $PALANG['pAutoconfig_host_id_is_not_an_integer'], $data['host_id'] ); - } - } - if( empty( $data['hostname'] ) ) - { - $errorList[] = $PALANG['pAutoconfig_host_no_hostname_provided']; - } - if( strlen( $data['port'] ) == 0 ) - { - $errorList[] = $PALANG['pAutoconfig_host_no_port_provided']; - } - elseif( !preg_match( '/^\d+$/', $data['port'] ) ) - { - $errorList[] = $PALANG['pAutoconfig_host_port_is_not_an_integer']; - } - if( !empty( $data['socket_type'] ) && !preg_match( '/^SSL|STARTTLS|TLS$/', $data['socket_type'] ) ) - { - $errorList[] = sprintf( $PALANG['pAutoconfig_host_invalid_socket_type'], $data['socket_type'] ); - } - if( empty( $data['auth'] ) ) - { - $data['auth'] = 'none'; - } - // password-cleartext, password-encrypted (CRAM-MD5 or DIGEST-MD5), NTLM (Windows), GSSAPI (Kerberos), client-IP-address, TLS-client-cert, none, smtp-after-pop (for smtp), OAuth2 - elseif( !preg_match( '/^(password-cleartext|password-encrypted|NTLM|GSSAPI|client-IP-address|TLS-client-cert|smtp-after-pop|oauth2|none)$/i', $data['auth'] ) ) - { - $errorList[] = sprintf( $PALANG['pAutoconfig_host_invalid_auth_scheme'], $data['auth'] ); - } - // username may be blank in the case of authentication by ip for example - $booleanProperties = array( 'leave_messages_on_server', 'download_on_biff' ); - $this->decode_boolean( $data, $booleanProperties ); - if( strlen( $data['days_to_leave_messages_on_server'] ) > 0 && !preg_match( '/^\d+$/', $data['days_to_leave_messages_on_server'] ) ) - { - $errorList[] = $PALANG['pAutoconfig_host_days_on_server_is_not_an_integer']; - } - if( strlen( $data['check_interval'] ) > 0 && !preg_match( '/^\d+$/', $data['check_interval'] ) ) - { - $errorList[] = $PALANG['pAutoconfig_host_check_interval_is_not_an_integer']; - } - - if( count( $errorList ) == 0 ) - { - $this->empty2null( $data ); - return( null ); - } - else - { - return( $errorList ); - } - } + $data = PHP_MAJOR_VERSION < 7 ? openssl_random_pseudo_bytes(16) : random_bytes(16); + $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100 + $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10 + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); + } + + public function check_autoconfig_data(&$data) { + global $CONF, $PALANG; + $errorList = []; + if ( !empty( $data['config_id'] ) ) { + if ( !$this->get_config( $data['config_id'] ) ) { + $errorList[] = sprintf( $PALANG['pAutoconfig_config_id_not_found'], $data['config_id'] ); + } elseif ( !$this->has_permission_over_config_id( $this->username, $data['config_id'] ) ) { + $errorList[] = sprintf( $PALANG['pAutoconfig_lack_permission_over_config_id'], $data['config_id'] ); + } + } + if ( !empty( $data['encoding'] ) && !preg_match( '/^[a-zA-Z][\w\-]+$/', $data['encoding'] ) ) { + $errorList[] = sprintf( $PALANG['pAutoconfig_invalid_encoding'], $data['encoding'] ); + } + if ( empty( $data['provider_id'] ) ) { + $errorList[] = $PALANG['pAutoconfig_empty_provider_id']; + } + // I do not check on purpose the file path of the cert, private key and chain (if any), because the user may have provided the information before setting up those files, and I do not want to annoy the user + $booleanProperties = array( 'enable_status', 'documentation_status', 'ssl_enabled', 'prevent_app_sheet', 'prevent_move', 'smime_enabled', 'payload_remove_ok', 'active', 'spa' ); + $this->decode_boolean( $data, $booleanProperties ); + if ( count( $errorList ) == 0 ) { + $this->empty2null( $data ); + return( null ); + } else { + return( $errorList ); + } + } + + public function check_autoconfig_domains($domain_list) { + global $CONF, $PALANG; + $errorList = []; + if ( count( $this->all_domains ) == 0 ) { + $errorList[] = $PALANG['pAutoconfig_no_domain_allocated_to_admin']; + return( $errorList ); + } elseif ( !is_array( $domain_list ) ) { + $errorList[] = $PALANG['pAutoconfig_data_provided_is_not_array']; + return( $errorList ); + } + // Nothing to check + elseif ( !count( $domain_list ) ) { + return( null ); + } + + $bad_domains = []; + foreach ( $domain_list as $domain ) { + if ( !in_array( $domain, $this->all_domains ) ) { + $bad_domains[] = $domain; + } + } + if ( count( $bad_domains ) > 0 ) { + $errorList[] = sprintf( $PALANG['pAutoconfig_unauthorised_domains'], join( ', ', $bad_domains ) ); + } + + if ( count( $errorList ) == 0 ) { + return( null ); + } else { + return( $errorList ); + } + } + + public function check_autoconfig_host_data(&$data) { + global $PALANG; + $errorList = []; + if ( empty( $data['type'] ) ) { + $errorList[] = $PALANG['pAutoconfig_host_no_type_provided']; + } elseif ( !preg_match( '/^imap|pop3|smtp$/i', $data['type'] ) ) { + $errorList[] = $PALANG['pAutoconfig_host_invalid_type_value']; + } else { + $data['type'] = strtolower( $data['type'] ); + } + + if ( !empty( $data['host_id'] ) ) { + if ( !preg_match( '/^\d+$/', $data['host_id'] ) ) { + $errorList[] = sprintf( $PALANG['pAutoconfig_host_id_is_not_an_integer'], $data['host_id'] ); + } + } + if ( empty( $data['hostname'] ) ) { + $errorList[] = $PALANG['pAutoconfig_host_no_hostname_provided']; + } + if ( strlen( $data['port'] ) == 0 ) { + $errorList[] = $PALANG['pAutoconfig_host_no_port_provided']; + } elseif ( !preg_match( '/^\d+$/', $data['port'] ) ) { + $errorList[] = $PALANG['pAutoconfig_host_port_is_not_an_integer']; + } + if ( !empty( $data['socket_type'] ) && !preg_match( '/^SSL|STARTTLS|TLS$/', $data['socket_type'] ) ) { + $errorList[] = sprintf( $PALANG['pAutoconfig_host_invalid_socket_type'], $data['socket_type'] ); + } + if ( empty( $data['auth'] ) ) { + $data['auth'] = 'none'; + } + // password-cleartext, password-encrypted (CRAM-MD5 or DIGEST-MD5), NTLM (Windows), GSSAPI (Kerberos), client-IP-address, TLS-client-cert, none, smtp-after-pop (for smtp), OAuth2 + elseif ( !preg_match( '/^(password-cleartext|password-encrypted|NTLM|GSSAPI|client-IP-address|TLS-client-cert|smtp-after-pop|oauth2|none)$/i', $data['auth'] ) ) { + $errorList[] = sprintf( $PALANG['pAutoconfig_host_invalid_auth_scheme'], $data['auth'] ); + } + // username may be blank in the case of authentication by ip for example + $booleanProperties = array( 'leave_messages_on_server', 'download_on_biff' ); + $this->decode_boolean( $data, $booleanProperties ); + if ( strlen( $data['days_to_leave_messages_on_server'] ) > 0 && !preg_match( '/^\d+$/', $data['days_to_leave_messages_on_server'] ) ) { + $errorList[] = $PALANG['pAutoconfig_host_days_on_server_is_not_an_integer']; + } + if ( strlen( $data['check_interval'] ) > 0 && !preg_match( '/^\d+$/', $data['check_interval'] ) ) { + $errorList[] = $PALANG['pAutoconfig_host_check_interval_is_not_an_integer']; + } + + if ( count( $errorList ) == 0 ) { + $this->empty2null( $data ); + return( null ); + } else { + return( $errorList ); + } + } - public function check_autoconfig_text_data( &$data ) - { - global $PALANG; - $errorList = []; - if( !empty( $data['id'] ) && !preg_match( '//', $data['id'] ) ) - { - $errorList[] = sprintf( $PALANG['pAutoconfig_text_id_is_not_an_integer'], $data['id'] ); - } - if( empty( $data['type'] ) ) - { - $errorList[] = $PALANG['pAutoconfig_text_type_not_provided']; - } - elseif( !preg_match( '/^(instruction|documentation)$/', $data['type'] ) ) - { - $errorList[] = sprintf( $PALANG['pAutoconfig_text_type_invalid'], $data['type'] ); - } - if( empty( $data['lang'] ) ) - { - $errorList[] = $PALANG['pAutoconfig_text_lang_not_provided']; - } - elseif( !preg_match( '/^[a-zA-Z]{2}$/', $data['lang'] ) ) - { - $errorList[] = sprintf( $PALANG['pAutoconfig_text_lang_invalid'], $data['lang'] ); - } - if( empty( $data['phrase'] ) ) - { - $errorList[] = $PALANG['pAutoconfig_text_text_not_provided']; - } - - if( count( $errorList ) == 0 ) - { - $this->empty2null( $data ); - return( null ); - } - else - { - return( $errorList ); - } - } - - // Set the boolean value for web - private function encode_boolean( &$data, $booleanProperties ) - { - foreach( $booleanProperties as $prop ) - { - if( strlen( $data[ $prop ] ) > 0 ) - { - $data[ $prop ] = ( $data[ $prop ] == true ? 1 : 0 ); - } - } - return( $data ); - } + public function check_autoconfig_text_data(&$data) { + global $PALANG; + $errorList = []; + if ( !empty( $data['id'] ) && !preg_match( '//', $data['id'] ) ) { + $errorList[] = sprintf( $PALANG['pAutoconfig_text_id_is_not_an_integer'], $data['id'] ); + } + if ( empty( $data['type'] ) ) { + $errorList[] = $PALANG['pAutoconfig_text_type_not_provided']; + } elseif ( !preg_match( '/^(instruction|documentation)$/', $data['type'] ) ) { + $errorList[] = sprintf( $PALANG['pAutoconfig_text_type_invalid'], $data['type'] ); + } + if ( empty( $data['lang'] ) ) { + $errorList[] = $PALANG['pAutoconfig_text_lang_not_provided']; + } elseif ( !preg_match( '/^[a-zA-Z]{2}$/', $data['lang'] ) ) { + $errorList[] = sprintf( $PALANG['pAutoconfig_text_lang_invalid'], $data['lang'] ); + } + if ( empty( $data['phrase'] ) ) { + $errorList[] = $PALANG['pAutoconfig_text_text_not_provided']; + } + + if ( count( $errorList ) == 0 ) { + $this->empty2null( $data ); + return( null ); + } else { + return( $errorList ); + } + } + + // Set the boolean value for web + private function encode_boolean(&$data, $booleanProperties) { + foreach ( $booleanProperties as $prop ) { + if ( strlen( $data[ $prop ] ) > 0 ) { + $data[ $prop ] = ( $data[ $prop ] == true ? 1 : 0 ); + } + } + return( $data ); + } - private function decode_boolean( &$data, $booleanProperties ) - { - foreach( $booleanProperties as $prop ) - { - // if( DEBUG ) error_log( "decode_boolean() checking property '$prop' with value \"" . $data[ $prop ] . "\"." ); - if( strlen( $data[ $prop ] ) > 0 ) - { - // if( DEBUG ) error_log( "decode_boolean() is property '$prop' value equal to 1 ? " . ( $data[ $prop ] == 1 ? true : false ) ); - $data[ $prop ] = db_get_boolean( $data[ $prop ] == 1 ? true : false ); - // if( DEBUG ) error_log( "decode_boolean() property '$prop' now has value \"" . $data[ $prop ] . "\"." ); - } - // Remove the boolean field since it is null. Null is different from false - else - { - // unset( $data[ $prop ] ); - $data[ $prop ] = db_get_boolean( false ); - } - } - // Since we are dealing with data reference we should not need to return anything, but just in case. - return( $data ); - } - - // Set to null empty strings, so they can be stored as NULL in sql - private function empty2null( &$data ) - { - foreach( $data as $key => $val ) - { - if( empty( $val ) && $val !== 0 ) - { - $data[ $key ] = null; - } - } - } + private function decode_boolean(&$data, $booleanProperties) { + foreach ( $booleanProperties as $prop ) { + // if( DEBUG ) error_log( "decode_boolean() checking property '$prop' with value \"" . $data[ $prop ] . "\"." ); + if ( strlen( $data[ $prop ] ) > 0 ) { + // if( DEBUG ) error_log( "decode_boolean() is property '$prop' value equal to 1 ? " . ( $data[ $prop ] == 1 ? true : false ) ); + $data[ $prop ] = db_get_boolean( $data[ $prop ] == 1 ? true : false ); + // if( DEBUG ) error_log( "decode_boolean() property '$prop' now has value \"" . $data[ $prop ] . "\"." ); + } + // Remove the boolean field since it is null. Null is different from false + else { + // unset( $data[ $prop ] ); + $data[ $prop ] = db_get_boolean( false ); + } + } + // Since we are dealing with data reference we should not need to return anything, but just in case. + return( $data ); + } + + // Set to null empty strings, so they can be stored as NULL in sql + private function empty2null(&$data) { + foreach ( $data as $key => $val ) { + if ( empty( $val ) && $val !== 0 ) { + $data[ $key ] = null; + } + } + } }; -?> diff --git a/AUTOCONFIG/autoconfig.php b/AUTOCONFIG/autoconfig.php index 1c01613c..42b8cfef 100644 --- a/AUTOCONFIG/autoconfig.php +++ b/AUTOCONFIG/autoconfig.php @@ -13,7 +13,7 @@ * * File: autoconfig.php * - * Allows admin to configure Autodiscovery settings for mail domain names and + * Allows admin to configure Autodiscovery settings for mail domain names and * for Mac users or admins to generate the .mobile configuration file for Mac Mail. * * Template File: autoconfig.tpl, autoconfig-host-settings.tpl @@ -33,7 +33,7 @@ $Return_url = "list.php?table=domain"; mb_internal_encoding( 'UTF-8' ); // $smarty->error_reporting = E_ALL & ~E_NOTICE; /* -if( authentication_has_role('admin') ) +if( authentication_has_role('admin') ) { $Admin_role = 1 ; $fDomain = safeget('domain'); @@ -41,12 +41,12 @@ if( authentication_has_role('admin') ) // list(null $fDomain) = explode('@', $fUsername); $Return_url = "list-virtual.php?domain=" . urlencode( $fDomain ); - if( $fDomain == '' || !check_owner( authentication_get_username(), $fDomain ) ) + if( $fDomain == '' || !check_owner( authentication_get_username(), $fDomain ) ) { die( "Invalid username!" ); # TODO: better error message } -} -else +} +else { $Admin_role = 0 ; $Return_url = "main.php"; @@ -55,8 +55,7 @@ else */ // is autoconfig support enabled in $CONF ? -if( $CONF['autoconfig'] == 'NO' || !array_key_exists( 'autoconfig', $CONF ) ) -{ +if ( $CONF['autoconfig'] == 'NO' || !array_key_exists( 'autoconfig', $CONF ) ) { header( "Location: $Return_url" ); exit( 0 ); } @@ -69,9 +68,8 @@ $fDomain = safeget('domain'); $ah = new AutoconfigHandler( $fUsername ); $ah->debug = DEBUG; $config_id = safeget('config_id'); -if( !empty( $fDomain ) && empty( $config_id ) ) -{ - $config_id = $ah->get_id_by_domain( $fDomain ); +if ( !empty( $fDomain ) && empty( $config_id ) ) { + $config_id = $ah->get_id_by_domain( $fDomain ); } // if( !$config_id ) @@ -80,228 +78,222 @@ if( !empty( $fDomain ) && empty( $config_id ) ) // $error = 1; // } $form = array(); -if( count( $ah->all_domains ) == 0 ) -{ - if( authentication_has_role( 'global-admin' ) ) - { +if ( count( $ah->all_domains ) == 0 ) { + if ( authentication_has_role( 'global-admin' ) ) { flash_error( $PALANG['no_domains_exist'] ); - } - else - { + } else { flash_error( $PALANG['no_domains_for_this_admin'] ); } header( "Location: list.php?table=domain" ); # no domains (for this admin at least) - redirect to domain list exit; -} -else -{ - $form['provider_domain_options'] = $ah->all_domains; +} else { + $form['provider_domain_options'] = $ah->all_domains; } -if( $_SERVER['REQUEST_METHOD'] == "GET" || empty( $_SERVER['REQUEST_METHOD'] ) ) -{ - if( DEBUG ) error_log( "config id submitted is: '$config_id'." ); - if( !empty( $config_id ) ) - { - if( DEBUG ) error_log( "Getting configuration details with get_details()" ); - $form = $ah->get_details( $config_id ); - if( DEBUG ) error_log( "get_details() returned: " . print_r( $form, true ) ); +if ( $_SERVER['REQUEST_METHOD'] == "GET" || empty( $_SERVER['REQUEST_METHOD'] ) ) { + if ( DEBUG ) { + error_log( "config id submitted is: '$config_id'." ); } - if( empty( $form['account_type'] ) ) - { - $form['account_type'] = 'imap'; + if ( !empty( $config_id ) ) { + if ( DEBUG ) { + error_log( "Getting configuration details with get_details()" ); + } + $form = $ah->get_details( $config_id ); + if ( DEBUG ) { + error_log( "get_details() returned: " . print_r( $form, true ) ); + } } - if( empty( $form['ssl_enabled'] ) ) - { - $form['ssl_enabled'] = 1; + if ( empty( $form['account_type'] ) ) { + $form['account_type'] = 'imap'; } - if( empty( $form['active'] ) ) - { - $form['active'] = 1; + if ( empty( $form['ssl_enabled'] ) ) { + $form['ssl_enabled'] = 1; + } + if ( empty( $form['active'] ) ) { + $form['active'] = 1; } $form['placeholder'] = array( - 'provider_id' => $ah->all_domains[0], - 'provider_name' => $PALANG['pAutoconfig_placeholder_provider_name'], + 'provider_id' => $ah->all_domains[0], + 'provider_name' => $PALANG['pAutoconfig_placeholder_provider_name'], ); $form['config_options'] = $ah->get_config_ids(); - if( DEBUG ) error_log( "config_options is: " . print_r( $form['config_options'], true ) ); + if ( DEBUG ) { + error_log( "config_options is: " . print_r( $form['config_options'], true ) ); + } // $config_id could be null - $form['provider_domain_disabled'] = $ah->get_other_config_domains( $config_id ); - if( DEBUG ) error_log( "provider_domain_disabled is: " . print_r( $form['provider_domain_disabled'], true ) ); - // Get defaults - if( count( $form['enable']['instruction'] ) == 0 ) - { - $form['enable']['instruction'] = array( - array( 'lang' => 'en', 'phrase' => '' ) - ); - if( strlen( $form['enable_status'] ) == 0 ) $form['enable_status'] = 0; - } - else - { - if( strlen( $form['enable_status'] ) == 0 ) $form['enable_status'] = 1; - } - if( count( $form['documentation']['description'] ) == 0 ) - { - $form['documentation']['description'] = array( - array( 'lang' => 'en', 'phrase' => '' ) - ); - if( strlen( $form['documentation_status'] ) == 0 ) $form['documentation_status'] = 0; - } - else - { - if( strlen( $form['documentation_status'] ) == 0 ) $form['documentation_status'] = 1; - } + $form['provider_domain_disabled'] = $ah->get_other_config_domains( $config_id ); + if ( DEBUG ) { + error_log( "provider_domain_disabled is: " . print_r( $form['provider_domain_disabled'], true ) ); + } + // Get defaults + if ( count( $form['enable']['instruction'] ) == 0 ) { + $form['enable']['instruction'] = array( + array( 'lang' => 'en', 'phrase' => '' ) + ); + if ( strlen( $form['enable_status'] ) == 0 ) { + $form['enable_status'] = 0; + } + } else { + if ( strlen( $form['enable_status'] ) == 0 ) { + $form['enable_status'] = 1; + } + } + if ( count( $form['documentation']['description'] ) == 0 ) { + $form['documentation']['description'] = array( + array( 'lang' => 'en', 'phrase' => '' ) + ); + if ( strlen( $form['documentation_status'] ) == 0 ) { + $form['documentation_status'] = 0; + } + } else { + if ( strlen( $form['documentation_status'] ) == 0 ) { + $form['documentation_status'] = 1; + } + } showAutoconfigForm( $form ); exit( 0 ); -} -elseif( $_SERVER['REQUEST_METHOD'] == "POST" ) -{ - if( safepost('token') != $_SESSION['PFA_token'] ) - { +} elseif ( $_SERVER['REQUEST_METHOD'] == "POST" ) { + if ( safepost('token') != $_SESSION['PFA_token'] ) { die('Invalid token!'); } - if( !isset( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) || - strtolower( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) != 'xmlhttprequest' ) - { - if( DEBUG ) error_log( "This request is not using Ajax." ); - flash_error( "Request is not using Ajax" ); - showAutoconfigForm( $_POST ); - exit( 0 ); - } - if( isset( $_POST['config_id'] ) && !empty( $_POST['config_id'] ) ) - { - if( DEBUG ) error_log( "Got config_id: " . $_POST['config_id'] ); - if( !$ah->config_id( $_POST['config_id'] ) ) - { - json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_config_id_not_found'], $_POST['config_id'] ) ) ); - exit( 0 ); - } + if ( !isset( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) || + strtolower( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) != 'xmlhttprequest' ) { + if ( DEBUG ) { + error_log( "This request is not using Ajax." ); + } + flash_error( "Request is not using Ajax" ); + showAutoconfigForm( $_POST ); + exit( 0 ); + } + if ( isset( $_POST['config_id'] ) && !empty( $_POST['config_id'] ) ) { + if ( DEBUG ) { + error_log( "Got config_id: " . $_POST['config_id'] ); + } + if ( !$ah->config_id( $_POST['config_id'] ) ) { + json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_config_id_not_found'], $_POST['config_id'] ) ) ); + exit( 0 ); + } } $handler = null; - if( isset( $_POST['handler'] ) ) - { - if( preg_match( '/^[a-z][a-z_]+$/', $_POST['handler'] ) ) - { - $handler = $_POST['handler']; - } - else - { - if( DEBUG ) error_log( "Illegal character provided in handler \"" . $_POST['handler'] . "\"." ); - json_reply( array( 'error' => "Bad handler provided." ) ); - exit( 0 ); - } + if ( isset( $_POST['handler'] ) ) { + if ( preg_match( '/^[a-z][a-z_]+$/', $_POST['handler'] ) ) { + $handler = $_POST['handler']; + } else { + if ( DEBUG ) { + error_log( "Illegal character provided in handler \"" . $_POST['handler'] . "\"." ); + } + json_reply( array( 'error' => "Bad handler provided." ) ); + exit( 0 ); + } } - if( DEBUG ) error_log( "handler is \"$handler\"." ); + if ( DEBUG ) { + error_log( "handler is \"$handler\"." ); + } - if( $handler == 'autoconfig_save' ) - { - if( DEBUG ) error_log( "Got here saving configuration." ); - if( !( $form = $ah->save_config( $_POST ) ) ) - { - if( DEBUG ) error_log( "Failed to save config: " . $ah->error_as_string() ); - json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_server_side_error'], $ah->error_as_string() ) ) ); - } - else - { - if( DEBUG ) error_log( "Ok, config saved." ); - // We return the newly created ids so the user can perform a follow-on update - // The Ajax script will take care of setting those values in the hidden fields - json_reply( array( - 'success' => $PALANG['pAutoconfig_config_saved'], - 'config_id' => $form['config_id'], - 'incoming_server' => $form['incoming_server'], - 'outgoing_server' => $form['outgoing_server'], - 'instruction' => $form['enable']['instruction'], - 'documentation' => $form['documentation']['description'], - ) ); - } - } - elseif( $handler == 'autoconfig_remove' ) - { - if( DEBUG ) error_log( "Got here removing configuration id " . $_POST['config_id'] ); - if( empty( $_POST['config_id'] ) ) - { - json_reply( array( 'error' => $PALANG['pAutoconfig_no_config_yet_to_remove'] ) ); - exit( 0 ); - } - if( !$ah->remove_config( $_POST['config_id'] ) ) - { - if( DEBUG ) error_log( "Failed to remove config: " . $ah->error_as_string() ); - json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_server_side_error'], $ah->error_as_string() ) ) ); - } - else - { - if( DEBUG ) error_log( "Ok, config removed." ); - json_reply( array( 'success' => $PALANG['pAutoconfig_config_removed'] ) ); - } - exit( 0 ); - } - else - { - json_reply( array( 'error' => 'Unknown handler provided "' . $handler . '".' ) ); - } - exit( 0 ); + if ( $handler == 'autoconfig_save' ) { + if ( DEBUG ) { + error_log( "Got here saving configuration." ); + } + if ( !( $form = $ah->save_config( $_POST ) ) ) { + if ( DEBUG ) { + error_log( "Failed to save config: " . $ah->error_as_string() ); + } + json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_server_side_error'], $ah->error_as_string() ) ) ); + } else { + if ( DEBUG ) { + error_log( "Ok, config saved." ); + } + // We return the newly created ids so the user can perform a follow-on update + // The Ajax script will take care of setting those values in the hidden fields + json_reply( array( + 'success' => $PALANG['pAutoconfig_config_saved'], + 'config_id' => $form['config_id'], + 'incoming_server' => $form['incoming_server'], + 'outgoing_server' => $form['outgoing_server'], + 'instruction' => $form['enable']['instruction'], + 'documentation' => $form['documentation']['description'], + ) ); + } + } elseif ( $handler == 'autoconfig_remove' ) { + if ( DEBUG ) { + error_log( "Got here removing configuration id " . $_POST['config_id'] ); + } + if ( empty( $_POST['config_id'] ) ) { + json_reply( array( 'error' => $PALANG['pAutoconfig_no_config_yet_to_remove'] ) ); + exit( 0 ); + } + if ( !$ah->remove_config( $_POST['config_id'] ) ) { + if ( DEBUG ) { + error_log( "Failed to remove config: " . $ah->error_as_string() ); + } + json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_server_side_error'], $ah->error_as_string() ) ) ); + } else { + if ( DEBUG ) { + error_log( "Ok, config removed." ); + } + json_reply( array( 'success' => $PALANG['pAutoconfig_config_removed'] ) ); + } + exit( 0 ); + } else { + json_reply( array( 'error' => 'Unknown handler provided "' . $handler . '".' ) ); + } + exit( 0 ); } -function json_reply( $data ) -{ - if( !array_key_exists( 'error', $data ) && - !array_key_exists( 'info', $data ) && - !array_key_exists( 'success', $data ) ) - { - error_log( "json_reply() missing message type: error, info or success" ); - return( false ); - } - $allowed_domain = 'http' - . ( ( array_key_exists( 'HTTPS', $_SERVER ) - && $_SERVER[ 'HTTPS' ] - && strtolower( $_SERVER[ 'HTTPS' ] ) !== 'off' ) - ? 's' - : null ) - . '://' . $_SERVER[ 'HTTP_HOST' ]; - header( "Access-Control-Allow-Origin: $allowed_domain" ); - header( 'Content-Type: application/json;charset=utf-8' ); - if( DEBUG ) error_log( "Returning to client the payload: " . json_encode( $data ) ); - echo json_encode( $data ); - return( true ); +function json_reply($data) { + if ( !array_key_exists( 'error', $data ) && + !array_key_exists( 'info', $data ) && + !array_key_exists( 'success', $data ) ) { + error_log( "json_reply() missing message type: error, info or success" ); + return( false ); + } + $allowed_domain = 'http' + . ( ( array_key_exists( 'HTTPS', $_SERVER ) + && $_SERVER[ 'HTTPS' ] + && strtolower( $_SERVER[ 'HTTPS' ] ) !== 'off' ) + ? 's' + : null ) + . '://' . $_SERVER[ 'HTTP_HOST' ]; + header( "Access-Control-Allow-Origin: $allowed_domain" ); + header( 'Content-Type: application/json;charset=utf-8' ); + if ( DEBUG ) { + error_log( "Returning to client the payload: " . json_encode( $data ) ); + } + echo json_encode( $data ); + return( true ); } -function showAutoconfigForm( &$form ) -{ - global $PALANG, $CONF, $languages, $smarty; - if( DEBUG ) error_log( "showAutoconfigForm() received form data: " + print_r( $form, true ) ); - if( $form == null ) $form = array(); - if( array_key_exists( 'enable', $form ) ) - { - if( array_key_exists( 'instruction', $form['enable'] ) ) - { - if( count( $form['enable']['instruction'] ) == 0 ) - { - $form['enable']['instruction'][] = array( 'lang' => 'en' ); - } - } - } - - if( array_key_exists( 'documentation', $form ) ) - { - if( array_key_exists( 'description', $form['documentation'] ) ) - { - if( count( $form['documentation']['description'] ) == 0 ) - { - $form['documentation']['description'][] = array( 'lang' => 'en' ); - } - } - } - $smarty->assign( 'form', $form ); - $smarty->assign( 'language_options', $languages ); - $smarty->assign( 'default_lang', 'en' ); - $smarty->assign( 'smarty_template', 'autoconfig' ); - $smarty->display( 'index.tpl'); - exit( 0 ); +function showAutoconfigForm(&$form) { + global $PALANG, $CONF, $languages, $smarty; + if ( DEBUG ) { + error_log( "showAutoconfigForm() received form data: " + print_r( $form, true ) ); + } + if ( $form == null ) { + $form = array(); + } + if ( array_key_exists( 'enable', $form ) ) { + if ( array_key_exists( 'instruction', $form['enable'] ) ) { + if ( count( $form['enable']['instruction'] ) == 0 ) { + $form['enable']['instruction'][] = array( 'lang' => 'en' ); + } + } + } + + if ( array_key_exists( 'documentation', $form ) ) { + if ( array_key_exists( 'description', $form['documentation'] ) ) { + if ( count( $form['documentation']['description'] ) == 0 ) { + $form['documentation']['description'][] = array( 'lang' => 'en' ); + } + } + } + $smarty->assign( 'form', $form ); + $smarty->assign( 'language_options', $languages ); + $smarty->assign( 'default_lang', 'en' ); + $smarty->assign( 'smarty_template', 'autoconfig' ); + $smarty->display( 'index.tpl'); + exit( 0 ); } /* vim: set expandtab softtabstop=3 tabstop=3 shiftwidth=3: */ - -?> diff --git a/AUTOCONFIG/autoconfig_languages.php b/AUTOCONFIG/autoconfig_languages.php index 09b70b7f..e062437e 100644 --- a/AUTOCONFIG/autoconfig_languages.php +++ b/AUTOCONFIG/autoconfig_languages.php @@ -190,4 +190,3 @@ $languages = array( 'za' => "Zhuang, Chuang", 'zu' => "Zulu", ); -?> diff --git a/functions.inc.php b/functions.inc.php index a07f29a4..36a36092 100644 --- a/functions.inc.php +++ b/functions.inc.php @@ -2027,19 +2027,16 @@ function db_where_clause(array $condition, array $struct, $additional_raw_where return $query; } -function db_begin() -{ - return( db_query( "BEGIN" ) ); +function db_begin() { + return( db_query( "BEGIN" ) ); } -function db_commit() -{ - return( db_query( "COMMIT" ) ); +function db_commit() { + return( db_query( "COMMIT" ) ); } -function db_rollback() -{ - return( db_query( "ROLLBACK" ) ); +function db_rollback() { + return( db_query( "ROLLBACK" ) ); } /** diff --git a/psalm.xml b/psalm.xml index 08df1553..fda6b084 100644 --- a/psalm.xml +++ b/psalm.xml @@ -3,7 +3,7 @@ totallyTyped="false" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" - xsi:schemaLocation="https://getpsalm.org/schema/config file:///home/david/src/postfixadmin-stuff/postfixadmin.trunk/vendor/vimeo/psalm/config.xsd" + xsi:schemaLocation="https://getpsalm.org/schema/config" > From 2367f6024beb8a41cd14fdf8ba21af35a8b012af Mon Sep 17 00:00:00 2001 From: David Goodwin Date: Wed, 25 Mar 2020 19:32:17 +0000 Subject: [PATCH 04/12] replace DEBUG with this->debug --- AUTOCONFIG/AutoconfigHandler.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/AUTOCONFIG/AutoconfigHandler.php b/AUTOCONFIG/AutoconfigHandler.php index 02e5a914..af3b4ce9 100644 --- a/AUTOCONFIG/AutoconfigHandler.php +++ b/AUTOCONFIG/AutoconfigHandler.php @@ -159,7 +159,7 @@ class AutoconfigHandler extends PFAHandler { return( false ); } $sth = $res['result']; - if ( DEBUG ) { + if ( $this->debug ) { error_log( "get_config_ids_for_user() \$sth = " . print_r( $sth, true ) ); } $all = $this->db_fetchall( $sth ); @@ -275,7 +275,7 @@ class AutoconfigHandler extends PFAHandler { if ( empty( $sth ) ) { throw( "No statement handler was provided." ); } - // if( DEBUG ) error_log( "db_fetchall() \$sth = " . print_r( $sth, true ) ); + try { return( $sth->fetchAll(PDO::FETCH_ASSOC) ); } catch ( Exception $e ) { @@ -658,7 +658,7 @@ class AutoconfigHandler extends PFAHandler { // I need this to throw an exception so I can report the issue $ok = db_execute( "DELETE FROM $table_autoconfig WHERE config_id = ?", array($id), true ); } catch ( Exception $e ) { - if ( DEBUG ) { + if ( $this->debug ) { error_log( "remove_config(): An error occurred while trying to remove config id $id: " . $e->getMessage() ); } $this->error = $e->getMessage(); @@ -740,7 +740,7 @@ class AutoconfigHandler extends PFAHandler { } // For the rest, there could be no imap, pop3 or smtp declared. That's up to the user who is always right // Likewise, there could be no login enable instruction or support documentation, so we don't make them mandatory - if ( DEBUG ) { + if ( $this->debug ) { error_log( "Base config data are: " . print_r( $config_data, true ) ); } @@ -1259,11 +1259,9 @@ class AutoconfigHandler extends PFAHandler { private function decode_boolean(&$data, $booleanProperties) { foreach ( $booleanProperties as $prop ) { - // if( DEBUG ) error_log( "decode_boolean() checking property '$prop' with value \"" . $data[ $prop ] . "\"." ); + if ( strlen( $data[ $prop ] ) > 0 ) { - // if( DEBUG ) error_log( "decode_boolean() is property '$prop' value equal to 1 ? " . ( $data[ $prop ] == 1 ? true : false ) ); $data[ $prop ] = db_get_boolean( $data[ $prop ] == 1 ? true : false ); - // if( DEBUG ) error_log( "decode_boolean() property '$prop' now has value \"" . $data[ $prop ] . "\"." ); } // Remove the boolean field since it is null. Null is different from false else { From e4abfd3ede56a0abbdfa1b27da03e450a0ffc350 Mon Sep 17 00:00:00 2001 From: David Goodwin Date: Wed, 25 Mar 2020 19:37:10 +0000 Subject: [PATCH 05/12] fix exception throwing --- AUTOCONFIG/AutoconfigHandler.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AUTOCONFIG/AutoconfigHandler.php b/AUTOCONFIG/AutoconfigHandler.php index af3b4ce9..20c29fde 100644 --- a/AUTOCONFIG/AutoconfigHandler.php +++ b/AUTOCONFIG/AutoconfigHandler.php @@ -261,7 +261,7 @@ class AutoconfigHandler extends PFAHandler { public function db_assoc($sth) { if ( empty( $sth ) ) { - throw( "No statement handler was provided." ); + throw new Exception( "No statement handler was provided." ); } try { return( $sth->fetch( PDO::FETCH_ASSOC ) ); @@ -273,7 +273,7 @@ class AutoconfigHandler extends PFAHandler { public function db_fetchall($sth) { if ( empty( $sth ) ) { - throw( "No statement handler was provided." ); + throw new Exception( "No statement handler was provided." ); } try { @@ -286,7 +286,7 @@ class AutoconfigHandler extends PFAHandler { public function db_rows($sth) { if ( empty( $sth ) ) { - throw( "No statement handler was provided." ); + throw new Exception( "No statement handler was provided." ); } try { return( $sth->rowCount() ); From 4af8acb36975e3de11c1b8a02352ad02f06eb8c1 Mon Sep 17 00:00:00 2001 From: David Goodwin Date: Wed, 25 Mar 2020 19:50:34 +0000 Subject: [PATCH 06/12] typo fixes --- AUTOCONFIG/INSTALL.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/AUTOCONFIG/INSTALL.md b/AUTOCONFIG/INSTALL.md index 7aca1c58..787f166f 100644 --- a/AUTOCONFIG/INSTALL.md +++ b/AUTOCONFIG/INSTALL.md @@ -6,7 +6,7 @@ Installation procedure for Autodiscovery configuration tool ## Quick background & overview -Autodiscovery is a somewhat standardised features that makes it possible for mail client to find out the proper configuration for a mail account, and to prevent the every day user from guessing the right parameters. +Autodiscovery is a somewhat standardised feature that makes it possible for mail client to find out the proper configuration for a mail account, and to prevent the every day user from guessing the right parameters. Let's take the example of joe@example.com. @@ -14,7 +14,7 @@ When creating an account on Thurderbird and other who use the same configuration See this page from Mozilla for more information: -If a dns record exist +If a DNS record exist For Outlook, the mail client will attempt a POST request to: and submit a xml-based request @@ -84,7 +84,7 @@ Load the sql script `autoconfig.sql` into your Postfix Admin database. For exapl * SQLite : sqlite3 /path/to/database.sqlite < autoconfig.sql -This will create 4 separate tables. Rest assured that `autoconfig` does not alter in any way other areas of Postfix Admin database. +This will create 4 new standalone tables, and does not alter other areas of the PostfixAdmin database. ### PHP, perl and other web files @@ -101,9 +101,9 @@ mv ./AUTOCONFIG/*.tpl ./templates/ `autoconfig.js` is a small file containing event handlers to make the use of the admin interface smooth, and also makes use of Ajax with jQuery 3.3.1. jQuery 3.3.1 is used, and not the latest 3.3.2, because the pseudo selector `:first` and `:last` have been deprecated and are needed here, at least until I can find an alternative solution. If you have one, please let me know! -The general use of javascript is light and only to support workflow, nothing more. Pure css is used whenever possible (such as the switch button). No other framework is used to keep things light. +The general use of Javascript is light and only to support workflow, nothing more. Pure CSS is used whenever possible (such as the switch button). No other framework is used to keep things light. -FontAwesome version 5.12.1 is loaded as import in the css file +FontAwesome version 5.12.1 is loaded as import in the CSS file `autoconfig.pl` will guess the location of the `config.inc.php` based on the file path. You can change that, such as by specifiying `config.local.php` instead by editing the perl script and change the line `our $POSTFIXADMIN_CONF_FILE = File::Basename::dirname( __FILE__ ) . '/../config.inc.php';` for example into `our $POSTFIXADMIN_CONF_FILE = '/var/www/postfix/config.inc.php';` @@ -111,7 +111,7 @@ FontAwesome version 5.12.1 is loaded as import in the css file ### DNS -Not required, but to take full advaantage of the idea of auto discovery, you should set up the following dns record in your domain name zones: +Not required, but to take full advaantage of the idea of auto discovery, you should set up the following DNS records in your domain name zones: ```bind _submission._tcp IN SRV 0 1 587 mail.example.com. @@ -120,7 +120,7 @@ _imaps._tcp IN SRV 0 0 993 mail.example.com. _pop3._tcp IN SRV 0 0 110 mail.example.com. ``` -If you want to use a dedicated autodisvoer sub domain, you could set up yoru dns zone with the following record: +If you want to use a dedicated autodiscover sub domain, you could set up your DNS zone with the following record: ```bind _autodiscover._tcp IN SRV 0 10 443 autoconfig.example.com. @@ -128,7 +128,7 @@ _autodiscover._tcp IN SRV 0 10 443 autoconfig.example.com. ### Apache -Add the following tp the general config file or to the relevant Vitual Hosts. You can also add it as a conf file under `/etc/apache2/conf-available` if it exists and then issue `a2enconf autoconfig.conf` to activate it (assuming the file name was `autoconfig.conf`) +Add the following to the general config file or to the relevant Vitual Hosts. You can also add it as a conf file under `/etc/apache2/conf-available` if it exists and then issue `a2enconf autoconfig.conf` to activate it (assuming the file name was `autoconfig.conf`) (Here I presumed Postfix Admin is installed under /var/www/postfixadmin) @@ -150,7 +150,7 @@ RewriteRule "^/.well-known/autoconfig/mail/config-v1.1.xml" "/autoconfig/autocon # For Outlook; POST request RewriteRule "^/autodiscover/autodiscover.xml" "/autoconfig/autoconfig.pl?outlook=1" [PT,L] -# For autodiscovery settings in dns +# For autodiscovery settings in DNS RewriteRule "^/mail/config-v1\.1\.xml(.*)$" "/autoconfig/autoconfig.pl" [PT,L] ``` From bf4864e87a6f857206656cb84f26d842724ae267 Mon Sep 17 00:00:00 2001 From: David Goodwin Date: Wed, 25 Mar 2020 19:52:17 +0000 Subject: [PATCH 07/12] more formatting etc --- AUTOCONFIG/INSTALL.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/AUTOCONFIG/INSTALL.md b/AUTOCONFIG/INSTALL.md index 787f166f..45542057 100644 --- a/AUTOCONFIG/INSTALL.md +++ b/AUTOCONFIG/INSTALL.md @@ -16,9 +16,9 @@ See this page from Mozilla for more information: and submit a xml-based request +For Outlook, the mail client will attempt a POST request to: and submit a XML-based request -For Mac mail and iOS, the user needs to download a `mobileconfig` file, which is basically an xml file, that can be signed. +For Mac mail and iOS, the user needs to download a `mobileconfig` file, which is basically an XML file, that can be signed. Unfortunately, there is no auto discovery system for Mac/iOS mail, so you need to redirect your users to the `autoconfig.pl` cgi script under the Postfix Admin web root. You need to pass a `emailaddress` parameter for example @@ -30,7 +30,7 @@ Unfortunately, there is no auto discovery system for Mac/iOS mail, so you need t You need to activate the `uuid-ossp` PostgreSQL extension to use the UUID_V4. You can do that, as an admin logged on PostgreSQL, with `CREATE EXTENSION IF NOT EXISTS "uuid-ossp";` -If you cannot or do not want to do that, edit the sql script for PostgreSQL and comment line 9 and uncomment line 11, comment line 72 and uncoment line 74, comment line 84 and uncomment line 86, comment line 107 and uncomment line 109 +If you cannot or do not want to do that, edit the SQL script for PostgreSQL and comment line 9 and uncomment line 11, comment line 72 and uncoment line 74, comment line 84 and uncomment line 86, comment line 107 and uncomment line 109 #### Perl @@ -76,13 +76,13 @@ You need to have `openssl` installed. I used version 1.0.2g. You would also need ### SQL -Load the sql script `autoconfig.sql` into your Postfix Admin database. For exaple: +Load the SQL script `autoconfig.sql` into your Postfix Admin database. For exaple: -* PostgreSQL : psql -U postfixadmin postfixadmin < autoconfig.sql +* PostgreSQL : `psql -U postfixadmin postfixadmin < autoconfig.sql` -* MySQL : mysql -u postfixadmin postfixadmin < autoconfig.sql +* MySQL : `mysql -u postfixadmin postfixadmin < autoconfig.sql` -* SQLite : sqlite3 /path/to/database.sqlite < autoconfig.sql +* SQLite : `sqlite3 /path/to/database.sqlite < autoconfig.sql` This will create 4 new standalone tables, and does not alter other areas of the PostfixAdmin database. From 36f449b814990d9e3dac904be4e8bf725687439c Mon Sep 17 00:00:00 2001 From: David Goodwin Date: Thu, 26 Mar 2020 09:26:11 +0000 Subject: [PATCH 08/12] remove duplicate type column --- AUTOCONFIG/AutoconfigHandler.php | 1 - 1 file changed, 1 deletion(-) diff --git a/AUTOCONFIG/AutoconfigHandler.php b/AUTOCONFIG/AutoconfigHandler.php index 20c29fde..62718c72 100644 --- a/AUTOCONFIG/AutoconfigHandler.php +++ b/AUTOCONFIG/AutoconfigHandler.php @@ -37,7 +37,6 @@ class AutoconfigHandler extends PFAHandler { 'provider_short' => pacol( 1, 1, 1, 'text', 'Autoconfig_provider_short' , '' , '' ), 'incoming_server' => pacol( 1, 1, 1, 'text', 'Autoconfig_incoming_server' , '' , '' ), 'outgoing_server' => pacol( 1, 1, 1, 'text', 'Autoconfig_outgoing_server' , '' , '' ), - 'type' => pacol( 1, 1, 1, 'text', 'Autoconfig_type' , '' , '' ), 'hostname' => pacol( 1, 1, 1, 'text', 'Autoconfig_hostname' , '' , '' ), 'port' => pacol( 1, 1, 1, 'integer', 'Autoconfig_port' , '' , '' ), 'socket_type' => pacol( 1, 1, 1, 'text', 'Autoconfig_socket_type' , '' , '' ), From 2cee56c8ed57d3c48e8f41a4616a3b0c7a9e04e9 Mon Sep 17 00:00:00 2001 From: David Goodwin Date: Thu, 26 Mar 2020 09:26:22 +0000 Subject: [PATCH 09/12] Ensure $sql is defined; change to an INNER JOIN; fix typo in TUE (TRUE). --- AUTOCONFIG/AutoconfigHandler.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/AUTOCONFIG/AutoconfigHandler.php b/AUTOCONFIG/AutoconfigHandler.php index 62718c72..07304822 100644 --- a/AUTOCONFIG/AutoconfigHandler.php +++ b/AUTOCONFIG/AutoconfigHandler.php @@ -141,17 +141,17 @@ class AutoconfigHandler extends PFAHandler { $table_autoconfig_domains = table_by_key('autoconfig_domains'); $table_domain_admins = table_by_key('domain_admins'); $table_domain = table_by_key('domain'); + + // This is a per-domain admin, so we use the table domain_admis to cross check which configuration he/she has access + $E_username = escape_string( $user ); + $sql = "SELECT DISTINCT ad.config_id FROM $table_domain d INNER JOIN $table_autoconfig_domains ad ON ad.domain = d.domain WHERE d.active IS TRUE AND d.username='$E_username'"; + // This is a super admin, so he/she has access to all configs if ( authentication_has_role( 'global-admin' ) ) { // $sql = "SELECT DISTINCT ad.config_id FROM $table_autoconfig_domains ad LEFT JOIN $table_domain d ON ad.domain = d.domain WHERE d.domain != 'ALL AND d.active IS TRUE'"; // global admin has access to all config $sql = "SELECT c.config_id FROM $table_autoconfig c"; } - // This is a per-domain admin, so we use the table domain_admis to cross check which configuration he/she has access - elseif ( authentication_has_role( 'admin' ) ) { - $E_username = escape_string( $user ); - $sql = "SELECT DISTINCT ad.config_id FROM $table_domain d LEFT JOIN $table_autoconfig_domains ad ON ad.domain = d.domain WHERE d.active IS TUE AND d.username='$E_username'"; - } $res = db_query( $sql ); if ( !empty( $res['error'] ) ) { $this->error = $res['error']; From 6a7d033ff462a3512cbc37c3e6ff0bd350531357 Mon Sep 17 00:00:00 2001 From: David Goodwin Date: Thu, 26 Mar 2020 19:31:23 +0000 Subject: [PATCH 10/12] drop print_r, use json_encode instead; fix string concatenation --- AUTOCONFIG/autoconfig.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AUTOCONFIG/autoconfig.php b/AUTOCONFIG/autoconfig.php index 42b8cfef..2827c6b4 100644 --- a/AUTOCONFIG/autoconfig.php +++ b/AUTOCONFIG/autoconfig.php @@ -100,7 +100,7 @@ if ( $_SERVER['REQUEST_METHOD'] == "GET" || empty( $_SERVER['REQUEST_METHOD'] ) } $form = $ah->get_details( $config_id ); if ( DEBUG ) { - error_log( "get_details() returned: " . print_r( $form, true ) ); + error_log( "get_details() returned: " . json_encode( $form ) ); } } if ( empty( $form['account_type'] ) ) { @@ -118,12 +118,12 @@ if ( $_SERVER['REQUEST_METHOD'] == "GET" || empty( $_SERVER['REQUEST_METHOD'] ) ); $form['config_options'] = $ah->get_config_ids(); if ( DEBUG ) { - error_log( "config_options is: " . print_r( $form['config_options'], true ) ); + error_log( "config_options is: " . json_encode( $form['config_options'] ) ); } // $config_id could be null $form['provider_domain_disabled'] = $ah->get_other_config_domains( $config_id ); if ( DEBUG ) { - error_log( "provider_domain_disabled is: " . print_r( $form['provider_domain_disabled'], true ) ); + error_log( "provider_domain_disabled is: " . json_encode( $form )); } // Get defaults if ( count( $form['enable']['instruction'] ) == 0 ) { @@ -268,7 +268,7 @@ function json_reply($data) { function showAutoconfigForm(&$form) { global $PALANG, $CONF, $languages, $smarty; if ( DEBUG ) { - error_log( "showAutoconfigForm() received form data: " + print_r( $form, true ) ); + error_log( "showAutoconfigForm() received form data: " . json_encode( $form ) ); } if ( $form == null ) { $form = array(); From 76d5c996893083238ba3bc4a7997495e81c22ed7 Mon Sep 17 00:00:00 2001 From: David Goodwin Date: Thu, 26 Mar 2020 19:53:40 +0000 Subject: [PATCH 11/12] simplify debug logging --- AUTOCONFIG/autoconfig.php | 90 ++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 54 deletions(-) diff --git a/AUTOCONFIG/autoconfig.php b/AUTOCONFIG/autoconfig.php index 2827c6b4..b68cae4e 100644 --- a/AUTOCONFIG/autoconfig.php +++ b/AUTOCONFIG/autoconfig.php @@ -25,8 +25,17 @@ require_once( 'common.php' ); require_once( 'autoconfig_languages.php' ); -const DEBUG = false; +$DEBUG = false; +if (isset($_GET['debug'])) { + $DEBUG=true; +} +function debug($msg) { + global $DEBUG; + if ($DEBUG) { + error_log($msg); + } +} authentication_require_role('admin'); $fUsername = authentication_get_username(); # enforce login $Return_url = "list.php?table=domain"; @@ -66,7 +75,7 @@ $error = 0; $fDomain = safeget('domain'); $ah = new AutoconfigHandler( $fUsername ); -$ah->debug = DEBUG; +$ah->debug = $DEBUG; $config_id = safeget('config_id'); if ( !empty( $fDomain ) && empty( $config_id ) ) { $config_id = $ah->get_id_by_domain( $fDomain ); @@ -91,17 +100,12 @@ if ( count( $ah->all_domains ) == 0 ) { } if ( $_SERVER['REQUEST_METHOD'] == "GET" || empty( $_SERVER['REQUEST_METHOD'] ) ) { - if ( DEBUG ) { - error_log( "config id submitted is: '$config_id'." ); - } + debug( "config id submitted is: " . json_encode($config_id) ); + if ( !empty( $config_id ) ) { - if ( DEBUG ) { - error_log( "Getting configuration details with get_details()" ); - } + debug( "Getting configuration details with get_details()" ); $form = $ah->get_details( $config_id ); - if ( DEBUG ) { - error_log( "get_details() returned: " . json_encode( $form ) ); - } + debug( "get_details() returned: " . json_encode( $form ) ); } if ( empty( $form['account_type'] ) ) { $form['account_type'] = 'imap'; @@ -117,14 +121,10 @@ if ( $_SERVER['REQUEST_METHOD'] == "GET" || empty( $_SERVER['REQUEST_METHOD'] ) 'provider_name' => $PALANG['pAutoconfig_placeholder_provider_name'], ); $form['config_options'] = $ah->get_config_ids(); - if ( DEBUG ) { - error_log( "config_options is: " . json_encode( $form['config_options'] ) ); - } + debug( "config_options is: " . json_encode( $form['config_options'] ) ); // $config_id could be null $form['provider_domain_disabled'] = $ah->get_other_config_domains( $config_id ); - if ( DEBUG ) { - error_log( "provider_domain_disabled is: " . json_encode( $form )); - } + debug( "provider_domain_disabled is: " . json_encode( $form )); // Get defaults if ( count( $form['enable']['instruction'] ) == 0 ) { $form['enable']['instruction'] = array( @@ -158,17 +158,14 @@ if ( $_SERVER['REQUEST_METHOD'] == "GET" || empty( $_SERVER['REQUEST_METHOD'] ) } if ( !isset( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) || strtolower( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) != 'xmlhttprequest' ) { - if ( DEBUG ) { - error_log( "This request is not using Ajax." ); - } + debug( "This request is not using Ajax." ); flash_error( "Request is not using Ajax" ); showAutoconfigForm( $_POST ); exit( 0 ); } if ( isset( $_POST['config_id'] ) && !empty( $_POST['config_id'] ) ) { - if ( DEBUG ) { - error_log( "Got config_id: " . $_POST['config_id'] ); - } + debug( "Got config_id: " . $_POST['config_id'] ); + if ( !$ah->config_id( $_POST['config_id'] ) ) { json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_config_id_not_found'], $_POST['config_id'] ) ) ); exit( 0 ); @@ -180,31 +177,22 @@ if ( $_SERVER['REQUEST_METHOD'] == "GET" || empty( $_SERVER['REQUEST_METHOD'] ) if ( preg_match( '/^[a-z][a-z_]+$/', $_POST['handler'] ) ) { $handler = $_POST['handler']; } else { - if ( DEBUG ) { - error_log( "Illegal character provided in handler \"" . $_POST['handler'] . "\"." ); - } + debug( "Illegal character provided in handler \"" . $_POST['handler'] . "\"." ); json_reply( array( 'error' => "Bad handler provided." ) ); exit( 0 ); } } - if ( DEBUG ) { - error_log( "handler is \"$handler\"." ); - } + debug( "handler is '$handler'" ); if ( $handler == 'autoconfig_save' ) { - if ( DEBUG ) { - error_log( "Got here saving configuration." ); - } + debug( "Got here saving configuration." . json_encode($_POST)); if ( !( $form = $ah->save_config( $_POST ) ) ) { - if ( DEBUG ) { - error_log( "Failed to save config: " . $ah->error_as_string() ); - } + debug( "Failed to save config: " . $ah->error_as_string() ); json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_server_side_error'], $ah->error_as_string() ) ) ); } else { - if ( DEBUG ) { - error_log( "Ok, config saved." ); - } + debug( "Ok, config saved." ); + // We return the newly created ids so the user can perform a follow-on update // The Ajax script will take care of setting those values in the hidden fields json_reply( array( @@ -217,22 +205,17 @@ if ( $_SERVER['REQUEST_METHOD'] == "GET" || empty( $_SERVER['REQUEST_METHOD'] ) ) ); } } elseif ( $handler == 'autoconfig_remove' ) { - if ( DEBUG ) { - error_log( "Got here removing configuration id " . $_POST['config_id'] ); - } + debug( "Got here removing configuration id " . $_POST['config_id'] ); + if ( empty( $_POST['config_id'] ) ) { json_reply( array( 'error' => $PALANG['pAutoconfig_no_config_yet_to_remove'] ) ); exit( 0 ); } if ( !$ah->remove_config( $_POST['config_id'] ) ) { - if ( DEBUG ) { - error_log( "Failed to remove config: " . $ah->error_as_string() ); - } + debug( "Failed to remove config: " . $ah->error_as_string() ); json_reply( array( 'error' => sprintf( $PALANG['pAutoconfig_server_side_error'], $ah->error_as_string() ) ) ); } else { - if ( DEBUG ) { - error_log( "Ok, config removed." ); - } + debug( "Ok, config removed." ); json_reply( array( 'success' => $PALANG['pAutoconfig_config_removed'] ) ); } exit( 0 ); @@ -258,18 +241,17 @@ function json_reply($data) { . '://' . $_SERVER[ 'HTTP_HOST' ]; header( "Access-Control-Allow-Origin: $allowed_domain" ); header( 'Content-Type: application/json;charset=utf-8' ); - if ( DEBUG ) { - error_log( "Returning to client the payload: " . json_encode( $data ) ); - } + + debug( "Returning to client the payload: " . json_encode( $data ) ); + echo json_encode( $data ); return( true ); } function showAutoconfigForm(&$form) { - global $PALANG, $CONF, $languages, $smarty; - if ( DEBUG ) { - error_log( "showAutoconfigForm() received form data: " . json_encode( $form ) ); - } + global $PALANG, $CONF, $languages, $smarty, $DEBUG; + debug( "showAutoconfigForm() received form data: " . json_encode( $form ) ); + if ( $form == null ) { $form = array(); } From ad00d935e7cfa195ffbc96cf547970e2ecbc5d86 Mon Sep 17 00:00:00 2001 From: David Goodwin Date: Thu, 26 Mar 2020 19:53:58 +0000 Subject: [PATCH 12/12] psalm fixes --- AUTOCONFIG/AutoconfigHandler.php | 38 +++++++++++++++++++------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/AUTOCONFIG/AutoconfigHandler.php b/AUTOCONFIG/AutoconfigHandler.php index 07304822..d1dab276 100644 --- a/AUTOCONFIG/AutoconfigHandler.php +++ b/AUTOCONFIG/AutoconfigHandler.php @@ -855,8 +855,10 @@ class AutoconfigHandler extends PFAHandler { $current_servers = $this->get_hosts( $this_type, $this->config_id ); // There must be at least one host for each type, even if blank if ( !array_key_exists( 'host_id', $data ) ) { - error_log( "No host id could be found at all from the web data submitted for config id \"" . $this->config_id . "\" which is" . ( $is_new ? '' : ' not' ) . " new." ); - $this->error = "No host id could be found at all from the web data submitted for config id \"" . $this->config_id . "\" which is" . ( $is_new ? '' : ' not' ) . " new."; + $msg = "No host id could be found at all from the web data submitted for config id '{$this->config_id}' which is not new."; + + error_log($msg); + $this->error = $msg; db_rollback(); return( false ); } @@ -881,19 +883,21 @@ class AutoconfigHandler extends PFAHandler { // To check for duplicates $processed = []; for ( $i = 0; $i < count( $data['hostname'] ); $i++ ) { + $counter[$data['type']] = $counter[$data['type']] ?? 0; + $host_data = array( - 'host_id' => @$data['host_id'][$i], - 'type' => @$data['type'][$i], - 'hostname' => @$data['hostname'][$i], - 'port' => @$data['port'][$i], - 'socket_type' => @$data['socket_type'][$i], - 'auth' => @$data['auth'][$i], - 'username' => @$data['username'][$i], - 'leave_messages_on_server' => @$data['leave_messages_on_server'][$i], - 'download_on_biff' => @$data['download_on_biff'][$i], - 'days_to_leave_messages_on_server' => @$data['days_to_leave_messages_on_server'][$i], - 'check_interval' => @$data['check_interval'][$i], - 'priority' => ++$counter[$data['type'][$i]], + 'host_id' => $data['host_id'][$i], + 'type' => $data['type'][$i], + 'hostname' => $data['hostname'][$i], + 'port' => $data['port'][$i], + 'socket_type' => $data['socket_type'][$i], + 'auth' => $data['auth'][$i], + 'username' => $data['username'][$i], + 'leave_messages_on_server' => $data['leave_messages_on_server'][$i], + 'download_on_biff' => $data['download_on_biff'][$i], + 'days_to_leave_messages_on_server' => $data['days_to_leave_messages_on_server'][$i], + 'check_interval' => $data['check_interval'][$i], + 'priority' => ++$counter[$data['type'][$i]], ); if ( ( $dataError = $this->check_autoconfig_host_data( $host_data ) ) != null ) { $this->error = $dataError; @@ -1102,6 +1106,11 @@ class AutoconfigHandler extends PFAHandler { } $data = PHP_MAJOR_VERSION < 7 ? openssl_random_pseudo_bytes(16) : random_bytes(16); + + + if (!is_string($data) || strlen($data) < 9) { + throw new \Exception("random bytes too short?"); + } $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100 $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10 return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); @@ -1258,7 +1267,6 @@ class AutoconfigHandler extends PFAHandler { private function decode_boolean(&$data, $booleanProperties) { foreach ( $booleanProperties as $prop ) { - if ( strlen( $data[ $prop ] ) > 0 ) { $data[ $prop ] = db_get_boolean( $data[ $prop ] == 1 ? true : false ); }