WIP: Multiple emails for principles

This commit is contained in:
Andrew Ruthven 2021-01-23 15:38:14 +13:00 committed by Florian Schlichting
parent f476675b10
commit ee8a751add
8 changed files with 271 additions and 24 deletions

36
dba/patches/1.3.4.sql Normal file
View File

@ -0,0 +1,36 @@
-- Notable enhancement: allow users to have multiple email addresses
BEGIN;
SELECT check_db_revision(1,3,3);
-- We now enforce uniqueness of email addresses.
-- This may want to be relaxed to allow duplication with inactive
-- accounts, but I don't want to think about how to enforce that in
-- the database.
CREATE TABLE usr_emails (
user_no INTEGER NOT NULL REFERENCES usr(user_no) ON UPDATE CASCADE ON DELETE CASCADE,
email VARCHAR,
main boolean DEFAULT true,
UNIQUE (email)
);
-- Ensure that a principal only has one primary email address.
CREATE UNIQUE INDEX usr_emails_primary
ON usr_emails
USING btree
(user_no)
WHERE main = true;
-- Move email addresses over, by default they'll be the primary email address.
INSERT INTO usr_emails (user_no, email)
SELECT user_no, email FROM usr WHERE email IS NOT NULL AND email <> '';
ALTER TABLE usr
DROP COLUMN email;
-- http://blogs.transparent.com/polish/names-of-the-months-and-their-meaning/
SELECT new_db_revision(1,3,4, 'Kwiecień' );
COMMIT;
ROLLBACK;

View File

@ -1,11 +1,11 @@
-- Define an updateable view for dav_principal which conbines the AWL usr
-- Define an updateable view for dav_principal which combines the AWL usr
-- record 1:1 with the principal table
DROP VIEW IF EXISTS dav_principal CASCADE;
CREATE OR REPLACE VIEW dav_principal AS
SELECT user_no, usr.active AS user_active, joined AS created, updated AS modified,
username, password, fullname, email,
username, password, fullname,
email_ok, date_format_type, locale,
principal_id, type_id, displayname, default_privileges,
TRUE AS is_principal,
@ -19,7 +19,7 @@ CREATE OR REPLACE VIEW dav_principal AS
CREATE or REPLACE RULE dav_principal_insert AS ON INSERT TO dav_principal
DO INSTEAD
(
INSERT INTO usr ( user_no, active, joined, updated, username, password, fullname, email, email_ok, date_format_type, locale )
INSERT INTO usr ( user_no, active, joined, updated, username, password, fullname, email_ok, date_format_type, locale )
VALUES(
COALESCE( NEW.user_no, nextval('usr_user_no_seq')),
COALESCE( NEW.user_active, TRUE),
@ -27,7 +27,7 @@ DO INSTEAD
COALESCE( NEW.modified, current_timestamp),
NEW.username, NEW.password,
COALESCE( NEW.fullname, NEW.displayname ),
NEW.email, NEW.email_ok,
NEW.email_ok,
COALESCE( NEW.date_format_type, 'E'),
NEW.locale
);
@ -53,7 +53,6 @@ DO INSTEAD
username=NEW.username,
password=NEW.password,
fullname=NEW.fullname,
email=NEW.email,
email_ok=NEW.email_ok,
date_format_type=NEW.date_format_type,
locale=NEW.locale

View File

@ -202,7 +202,7 @@ class Principal {
$sql .= '0::BIT(24) AS privileges ';
$params = array( );
}
$sql .= 'FROM dav_principal WHERE ';
$sql .= 'FROM dav_principal LEFT JOIN usr_emails USING (user_no) WHERE ';
switch ( $type ) {
case 'username':
$sql .= 'lower(username)=lower(text(:param))';
@ -215,7 +215,7 @@ class Principal {
break;
case 'email':
$this->by_email = true;
$sql .= 'lower(email)=lower(:param)';
$sql .= 'lower(usr_emails.email)=lower(:param)';
break;
}
$params[':param'] = $value;
@ -529,11 +529,22 @@ class Principal {
$update_list = array();
}
$sql_params = array();
$email = "";
foreach( self::updateableFields() AS $k ) {
if ( !isset($field_values->{$k}) && !isset($this->{$k}) ) continue;
$param_name = ':'.$k;
// special case email, it now goes in a different table, this is taken
// as being the primary email address for the principal.
if ( $k == 'email' ) {
$email = (isset($field_values->{$k}) ? $field_values->{$k} : $this->{$k});
continue;
}
$sql_params[$param_name] = (isset($field_values->{$k}) ? $field_values->{$k} : $this->{$k});
if ( $k == 'default_privileges' ) {
$sql_params[$param_name] = sprintf('%024s',$sql_params[$param_name]);
$param_name = 'cast('.$param_name.' as text)::BIT(24)';
@ -580,11 +591,29 @@ class Principal {
$qry = new AwlQuery($sql, $sql_params);
if ( $qry->Exec('Principal',__FILE__,__LINE__) ) {
$this->unCache();
$new_principal = new Principal('username', $sql_params[':username']);
foreach( $new_principal AS $k => $v ) {
$this->{$k} = $v;
}
}
// email
if ($email != "") {
$sql_params[':email'] = $email;
$sql_params[':user_no'] = $this->user_no;
if ( $inserting ) {
$sql = 'INSERT INTO usr_emails (user_no, email) VALUES (:user_no, :email)';
} else {
$sql = 'UPDATE usr_emails SET email = :email WHERE user_no = :user_no AND main = true';
}
$qry = new AwlQuery($sql, $sql_params);
if ( $qry->Exec('Principal',__FILE__,__LINE__) ) {
$this->{email} = $email;
}
}
}
@ -618,4 +647,17 @@ class Principal {
}
$cache->delete('principal-'.$value, null);
}
/**
* Find if an email address is associated with this principle.
* @return boolean True if found.
*/
protected function searchEmails( $email ) {
$qry = new AwlQuery('SELECT * FROM usr_emails WHERE user_no = :user_no, email = :email',
array(':user_no' => $this->user_no(), ':email' => $email) );
if ( $qry->Exec('Principal') ) {
return $qry->Rows;
}
}
}

View File

@ -64,7 +64,39 @@ function handle_freebusy_request( $ic ) {
}
if ($localname) {
dbg_error_log( 'POST', 'try to resolve local attendee %s', $localname);
$qry = new AwlQuery('SELECT fullname, email FROM usr WHERE user_no = (SELECT user_no FROM principal WHERE type_id = 1 AND user_no = (SELECT user_no FROM usr WHERE lower(username) = (text(:username)))) UNION SELECT fullname, email FROM usr WHERE user_no IN (SELECT user_no FROM principal WHERE principal_id IN (SELECT member_id FROM group_member WHERE group_id = (SELECT principal_id FROM principal WHERE type_id = 3 AND user_no = (SELECT user_no FROM usr WHERE lower(username) = (text(:username))))))', array(':username' => strtolower($localname)));
$qry = new AwlQuery(' SELECT fullname, email
FROM usr
WHERE user_no = (
SELECT user_no
FROM principal
WHERE type_id = 1
AND user_no = (
SELECT user_no
FROM usr
WHERE lower(username) = (text(:username))
)
)
UNION
SELECT fullname, email
FROM usr
WHERE user_no IN (
SELECT user_no
FROM principal
WHERE principal_id IN (
SELECT member_id
FROM group_member
WHERE group_id = (
SELECT principal_id
FROM principal
WHERE type_id = 3
AND user_no = (
SELECT user_no
FROM usr
WHERE lower(username) = (text(:username))
)
)
)
)', array(':username' => strtolower($localname)));
if ( $qry->Exec('POST',__LINE__,__FILE__) && $qry->rows() >= 1 ) {
dbg_error_log( 'POST', 'resolved local name %s to %d individual attendees', $localname, $qry->rows());
while ($row = $qry->Fetch()) {

View File

@ -28,6 +28,7 @@ foreach( $searches AS $k => $search ) {
dbg_log_array( "principal", "MATCH", $match, true );
$match = $match[0]->GetContent();
$subwhere = "";
$from_extra = "";
foreach( $qry_props AS $k1 => $v1 ) {
if ( $subwhere != "" ) $subwhere .= " OR ";
switch( $v1->GetNSTag() ) {
@ -39,7 +40,8 @@ foreach( $searches AS $k => $search ) {
case 'urn:ietf:params:xml:ns:caldav:calendar-user-address-set':
$match = preg_replace('{^.*/caldav.php/([^/]+)(/.*)?$}', '\\1', $match);
$match = preg_replace('{^mailto:}', '', $match);
$subwhere .= ' (email ILIKE :user_address_match OR username ILIKE :user_address_match) ';
$subwhere .= ' (usr_emails.email ILIKE :user_address_match OR username ILIKE :user_address_match) ';
$from_extra = " LEFT JOIN usr_emails USING (user_no) ";
$params[':user_address_match'] = '%'.$match.'%';
break;
@ -62,7 +64,7 @@ foreach( $searches AS $k => $search ) {
}
}
if ( $where != "" ) $where = "WHERE $where";
$sql = "SELECT * FROM dav_principal $where ORDER BY principal_id LIMIT 100";
$sql = "SELECT * FROM dav_principal $from_extra $where ORDER BY principal_id LIMIT 100";
$qry = new AwlQuery($sql, $params);

View File

@ -99,7 +99,7 @@ function doItipAttendeeReply( vCalendar $resource, $partstat ) {
$attendees = $vcal->GetAttendees();
foreach( $attendees AS $v ) {
$email = preg_replace( '/^mailto:/i', '', $v->Value() );
if ( $email == $request->principal->email() ) {
if ( $request->principal->searchEmails($email) ) {
$attendee = $v;
break;
}

View File

@ -24,7 +24,7 @@ if ( !isset($principal_type) || $principal_type == 3 ) {
}
$browser->SetOrdering( 'username', 'A' );
$browser->SetJoins( "dav_principal " );
$browser->SetJoins( "dav_principal LEFT JOIN usr_emails ON (dav_principal.user_no = usr_emails.user_no AND usr_emails.main = true)" );
if ( isset($principal_active) && $principal_active == 'f' )
$browser->SetWhere( 'NOT user_active' );

View File

@ -41,6 +41,7 @@ $delete_principal_confirmation_required = null;
$delete_ticket_confirmation_required = null;
$delete_bind_in_confirmation_required = null;
$delete_binding_confirmation_required = null;
$delete_email_confirmation_required = null;
function handle_subaction( $subaction ) {
global $session, $c, $id, $editor;
@ -49,6 +50,7 @@ function handle_subaction( $subaction ) {
global $delete_ticket_confirmation_required;
global $delete_bind_in_confirmation_required;
global $delete_binding_confirmation_required;
global $delete_email_confirmation_required;
global $can_write_principal;
dbg_error_log('admin-principal-edit',':handle_action: Action %s', $subaction );
@ -164,6 +166,33 @@ function handle_subaction( $subaction ) {
}
break;
# This isn't used because the classEditor handles deleting, but there is no confirmation. Which is better?
# case 'delete_email':
# dbg_error_log('admin-principal-edit',':handle_action: Deleting email "%s" for principal %d', $_GET['email'], $id );
# if ($can_write_principal) {
# if ( $session->CheckConfirmationHash('GET', 'confirm') ) {
# dbg_error_log('admin-principal-edit',':handle_action: Allowed to delete email "%s" for principal %d', $_GET['email'], $id );
# $qry = new AwlQuery('DELETE FROM usr_emails WHERE user_no=? AND email=?;', $id, $_GET['email'] );
# if ( $qry->Exec() ) {
# $c->messages[] = i18n('Email deleted.');
# return true;
# }
# else {
# $c->messages[] = i18n('There was an error writing to the database.');
# return false;
# }
# }
# else {
# $c->messages[] = i18n('Please confirm deletion of email - see below');
# $delete_email_confirmation_required = $session->BuildConfirmationHash('GET', 'confirm');
# return false;
# }
# }
# else {
# $c->messages[] = i18n('You are not allowed to delete emails for this principal.');
# }
# break;
default:
return false;
}
@ -179,7 +208,6 @@ function principal_editor() {
$editor->SetLookup( 'locale', 'SELECT \'\', \''.translate("*** Default Locale ***").'\' UNION SELECT locale, locale_name_locale FROM supported_locales ORDER BY 1 ASC' );
$editor->AddAttribute( 'locale', 'title', translate("The preferred language for this person.") );
$editor->AddAttribute( 'fullname', 'title', translate("The full name for this person, group or other type of principal.") );
$editor->AddAttribute( 'email', 'title', translate("The email address identifies principals when processing invitations and freebusy lookups. It should be set to a unique value.") );
$editor->SetWhere( 'principal_id='.$id );
if($_SERVER['REQUEST_METHOD'] === "POST" && !verifyCsrfPost()) {
@ -302,7 +330,6 @@ function principal_editor() {
$prompt_password_2 = translate('Confirm Password');
$prompt_fullname = translate('Fullname');
$prompt_displayname = translate('Display Name');
$prompt_email = translate('Email Address');
$prompt_date_format = translate('Date Format Style');
$prompt_admin = translate('Administrator');
$prompt_active = translate('Active');
@ -327,14 +354,6 @@ function principal_editor() {
$delete_principal_button = '<a href="'.$c->base_url . '/admin.php?action=edit&t=principal&subaction=delete_principal&id='.$id.'" class="submit">' . translate("Delete Principal") . '</a>';
}
$email_unique = '';
$qry = new AwlQuery('SELECT user_no FROM usr WHERE lower(usr.email) = lower(:email)',
array( ':email' => $editor->Value('email') ));
$qry->Exec('principal-edit', __LINE__, __FILE__);
if ($qry->rows() > 1 ) {
$email_unique = ' <b style="color:red;">' . translate('Attention: email address not unique, scheduling may not work!') . '</b>';
}
$id = $editor->Value('principal_id');
$template = <<<EOTEMPLATE
##form##
@ -396,7 +415,6 @@ label.privilege {
<tr> <th class="right">$prompt_password_1:</th> <td class="left">##newpass1.password.$pwstars##</td> </tr>
<tr> <th class="right">$prompt_password_2:</th> <td class="left">##newpass2.password.$pwstars##</td> </tr>
<tr> <th class="right">$prompt_fullname:</th> <td class="left">##fullname.input.50##</td> </tr>
<tr> <th class="right">$prompt_email:</th> <td class="left">##email.input.50##$email_unique</td> </tr>
<tr> <th class="right">$prompt_locale:</th> <td class="left">##locale.select##</td> </tr>
<tr> <th class="right">$prompt_date_format:</th> <td class="left">##date_format_type.select##</td> </tr>
<tr> <th class="right">$prompt_type:</th> <td class="left">##type_id.select##</td> </tr>
@ -487,6 +505,121 @@ function confirm_delete_principal($confirmation_hash, $displayname ) {
return $html;
}
function email_row_editor() {
global $c, $id, $editor, $can_write_principal, $email;
$emailrow = new Editor("Email Addresses", "usr_emails");
$emailrow->SetSubmitName( 'saveemailrow' );
$edit_email_clause = '';
if ( $can_write_principal ) {
if ( $emailrow->IsSubmit() ) {
$_POST['user_no'] = $id;
$email = $_POST['email'];
$orig_email = $_POST['orig_email'];
$emailrow->SetWhere( 'user_no='.$id.' AND email=\''.$orig_email.'\'');
$emailrow->Assign('email', $email);
$emailrow->Assign('main', $_POST['main']);
$emailrow->Write( );
unset($_GET['email']);
}
elseif ( isset($_GET['delete_email']) ) {
$qry = new AwlQuery("DELETE FROM usr_emails WHERE user_no=:user_no AND email = :email",
array( ':user_no' => $id, ':email' => $_GET['delete_email'] ));
$qry->Exec('email-delete');
$c->messages[] = translate('Deleted an email from this Principal');
}
}
return $emailrow;
}
function edit_email_row_email( $row_data ) {
global $id, $emailrow;
$email = $row_data->email;
if ( ! empty($email) ) {
$emailrow->SetRecord( $row_data );
}
else {
$emailrow->Initialise( $row_data );
}
$form_id = $emailrow->Id();
$form_url = preg_replace( '#&(edit|delete)_email=\d+#', '', $_SERVER['REQUEST_URI'] );
$csrf_field = getCsrfField();
$template = <<<EOTEMPLATE
<form method="POST" enctype="multipart/form-data" id="form_$form_id" action="$form_url">
$csrf_field
<td class="left"><input type="hidden" name="id" value="$id"><input type="hidden" name="orig_email" value="$email">
<input type="text" name="email" value="$row_data->email"></d>
<td class="center">##main.checkbox##</td>
<td class="center">##submit##</td>
</form>
EOTEMPLATE;
$emailrow->SetTemplate( $template );
$emailrow->Title("");
return $emailrow->Render();
}
function format_boolean($col_val, $field, $row) {
if ($col_val == 0 || $col_val == '') {
return Translate('No');
} else if ($col_val == '1') {
return Translate('Yes');
}
return $col_val;
}
function email_browser() {
global $c, $id, $editor, $can_write_principal;
$browser = new Browser(translate('Email Addresses'));
$browser->AddColumn( 'email', translate('Email'), '', '##email_link##' );
$rowurl = $c->base_url . '/admin.php?action=edit&t=principal&id='.$id.'&edit_email=';
$browser->AddHidden( 'email_link', "'<a href=\"$rowurl' || email || '\">' || email || '</a>'" );
$browser->AddColumn( 'main', translate('Primary'), 'center', '', '', '', '', 'format_boolean');
if ( $can_write_principal ) {
$del_link = '<a href="'.$c->base_url.'/admin.php?action=edit&t=principal&id='.$id.'&delete_email=##email##" class="submit" title="">'.translate('Delete').'</a>';
$edit_link = '<a href="'.$c->base_url.'/admin.php?action=edit&t=principal&id='.$id.'&edit_email=##email##" class="submit" title="">'.translate('Edit').'</a>';
$browser->AddColumn( 'action', translate('Action'), 'center', '', "'$edit_link&nbsp;$del_link'" );
}
$browser->SetOrdering( 'email', 'A' );
$browser->SetJoins( "usr_emails " );
$browser->SetWhere( 'user_no = '.$id );
if ( $c->enable_row_linking ) {
$browser->RowFormat( '<tr onMouseover="LinkHref(this,1);" title="'.translate('Click to edit email').'" class="r%d">', '</tr>', '#even' );
}
else {
$browser->RowFormat( '<tr class="r%d">', '</tr>', '#even' );
}
$browser->DoQuery();
if ( $can_write_principal ) {
if ( isset($_GET['edit_email']) ) {
$browser->MatchedRow('email', $_GET['edit_email'], 'edit_email_row_email');
}
else if ( isset($id ) ) {
$browser->ExtraRowFormat( '<tr class="r%d">', '</tr>', '#even' );
$extra_row = array( 'email' => "" );
$browser->MatchedRow('email', "", 'edit_email_row_email');
$extra_row = (object) $extra_row;
$browser->AddRow($extra_row);
}
}
return $browser;
}
function group_memberships_browser() {
@ -737,7 +870,6 @@ function principal_grants_browser() {
return $browser;
}
function ticket_row_editor() {
global $c, $id, $editor, $can_write_principal, $privilege_names;
@ -1151,6 +1283,10 @@ if ( isset($id) && $id > 0 ) {
$page_elements[] = confirm_delete_principal($delete_principal_confirmation_required, $editor->Value('displayname'));
$emailrow = email_row_editor();
$page_elements[] = email_browser();
if ( isset($delete_email_confirmation_required) ) $page_elements[] = confirm_delete_email($delete_email_confirmation_required);
$page_elements[] = group_memberships_browser();
if ( $editor->Value('type_id') == 3 ) {
$grouprow = group_row_editor();