You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
postfixadmin/AUTOCONFIG/AutoconfigHandler.php

1285 lines
61 KiB
PHP

<?php
/*
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) {
}
/**
* @return void
*/
protected function initStruct() {
$this->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 ( $this->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." );
}
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 ( $this->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 ( $this->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 ( strlen( $data[ $prop ] ) > 0 ) {
$data[ $prop ] = db_get_boolean( $data[ $prop ] == 1 ? true : false );
}
// 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;
}
}
}
};