Fix handling of Web calendars

- handle Web calendars that require authentication;
- properly save the "reload on login" option;
- reload Web calendars when clicking on the reload button on top of the
  events/tasks list;
- properly activate the checkbox of new calendars (as they are enabled
  by default).

Fixes #3326
pull/208/head
Francis Lachapelle 2016-05-13 15:50:24 -04:00
parent 199a5f5215
commit e396e29430
11 changed files with 188 additions and 28 deletions

2
NEWS
View File

@ -20,6 +20,7 @@ Enhancements
- [web] style events depending on the user participation state - [web] style events depending on the user participation state
- [web] style transparent events (show time as free) (#3192) - [web] style transparent events (show time as free) (#3192)
- [web] improved input parsing of time picker (#3659) - [web] improved input parsing of time picker (#3659)
- [web] restored support for Web calendars that require authentication
Bug fixes Bug fixes
- [core] properly escape wide characters (#3616) - [core] properly escape wide characters (#3616)
@ -53,6 +54,7 @@ Bug fixes
- [web] adapted time picker to match changes of md calendar picker - [web] adapted time picker to match changes of md calendar picker
- [web] fixed sender addresses of draft when using multiple IMAP accounts (#3577) - [web] fixed sender addresses of draft when using multiple IMAP accounts (#3577)
- [web] create a new message when clicking on a "mailto" link (#3588) - [web] create a new message when clicking on a "mailto" link (#3588)
- [web] fixed handling of Web calendars option "reload on login"
- [dav] we now handle the default classifications for tasks (#3541) - [dav] we now handle the default classifications for tasks (#3541)
- [eas] properly unfold long mail headers (#3152) - [eas] properly unfold long mail headers (#3152)
- [eas] correctly set EAS message class for S/MIME messages (#3576) - [eas] correctly set EAS message class for S/MIME messages (#3576)

View File

@ -206,8 +206,7 @@ size_t curl_body_function(void *ptr, size_t size, size_t nmemb, void *buffer)
- (BOOL) reloadOnLogin - (BOOL) reloadOnLogin
{ {
return [[self folderPropertyValueInCategory: @"AutoReloadedWebCalendars"] return [[self folderPropertyValueInCategory: @"AutoReloadedWebCalendars"] boolValue];
boolValue];
} }
- (NSException *) delete - (NSException *) delete

View File

@ -41,6 +41,7 @@
= "Potentially anyone on the Internet will be able to access your address book \"%{0}\", even if they do not have an account on this system. Is this information suitable for the public Internet?"; = "Potentially anyone on the Internet will be able to access your address book \"%{0}\", even if they do not have an account on this system. Is this information suitable for the public Internet?";
"Give Access" = "Give Access"; "Give Access" = "Give Access";
"Keep Private" = "Keep Private"; "Keep Private" = "Keep Private";
/* generic.js */ /* generic.js */
"Unable to subscribe to that folder!" "Unable to subscribe to that folder!"
= "Unable to subscribe to that folder!"; = "Unable to subscribe to that folder!";
@ -76,16 +77,19 @@
"45 minutes" = "45 minutes"; "45 minutes" = "45 minutes";
"1 hour" = "1 hour"; "1 hour" = "1 hour";
"1 day" = "1 day"; "1 day" = "1 day";
/* common buttons */ /* common buttons */
"OK" = "OK"; "OK" = "OK";
"Cancel" = "Cancel"; "Cancel" = "Cancel";
"Yes" = "Yes"; "Yes" = "Yes";
"No" = "No"; "No" = "No";
/* alarms */ /* alarms */
"Reminder" = "Reminder"; "Reminder" = "Reminder";
"Start" = "Start"; "Start" = "Start";
"Due Date" = "Due Date"; "Due Date" = "Due Date";
"Location" = "Location"; "Location" = "Location";
/* mail labels */ /* mail labels */
"Important" = "Important"; "Important" = "Important";
"Work" = "Work"; "Work" = "Work";
@ -105,5 +109,14 @@
"No such user." = "No such user."; "No such user." = "No such user.";
"You cannot (un)subscribe to a folder that you own!" = "You cannot (un)subscribe to a folder that you own!"; "You cannot (un)subscribe to a folder that you own!" = "You cannot (un)subscribe to a folder that you own!";
/* Authentication username */
"Username" = "Username";
/* Authentication password */
"Password" = "Password";
/* Authentication failed */
"Wrong username or password." = "Wrong username or password.";
/* Error message display bellow search field when the search string has less than the required number of characters */ /* Error message display bellow search field when the search string has less than the required number of characters */
"Enter at least %{minimumSearchLength} characters" = "Enter at least %{minimumSearchLength} characters"; "Enter at least %{minimumSearchLength} characters" = "Enter at least %{minimumSearchLength} characters";

View File

@ -193,8 +193,7 @@
NSString *folderName; NSString *folderName;
[self _setupContext]; [self _setupContext];
folderSubscription folderSubscription = [moduleSettings objectForKey: @"InactiveFolders"];
= [moduleSettings objectForKey: @"InactiveFolders"];
if (!folderSubscription) if (!folderSubscription)
{ {
folderSubscription = [NSMutableArray array]; folderSubscription = [NSMutableArray array];

View File

@ -1,6 +1,5 @@
/* /*
Copyright (C) 2006-2015 Inverse inc. Copyright (C) 2006-2016 Inverse inc.
Copyright (C) 2004-2005 SKYRIX Software AG
This file is part of SOGo This file is part of SOGo
@ -15,7 +14,7 @@
License for more details. License for more details.
You should have received a copy of the GNU Lesser General Public You should have received a copy of the GNU Lesser General Public
License along with OGo; see the file COPYING. If not, write to the License along with SOGo; see the file COPYING. If not, write to the
Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
02111-1307, USA. 02111-1307, USA.
*/ */
@ -34,6 +33,7 @@
#import <NGCards/iCalCalendar.h> #import <NGCards/iCalCalendar.h>
#import <SOGo/NSDictionary+Utilities.h> #import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/NSString+Utilities.h>
#import <Appointments/SOGoWebAppointmentFolder.h> #import <Appointments/SOGoWebAppointmentFolder.h>
#import <Appointments/SOGoAppointmentFolderICS.h> #import <Appointments/SOGoAppointmentFolderICS.h>
@ -142,22 +142,24 @@
httpCode = 200; httpCode = 200;
results = [[self clientObject] loadWebCalendar]; results = [[self clientObject] loadWebCalendar];
if ([results objectForKey: @"error"]) if ([results objectForKey: @"status"])
httpCode = 500; httpCode = [[results objectForKey: @"status"] intValue];
return [self responseWithStatus: httpCode andJSONRepresentation: results]; return [self responseWithStatus: httpCode andJSONRepresentation: results];
} }
- (WOResponse *) setCredentialsAction - (WOResponse *) setCredentialsAction
{ {
NSDictionary *params;
NSString *username, *password;
WORequest *request; WORequest *request;
WOResponse *response; WOResponse *response;
NSString *username, *password;
request = [context request]; request = [context request];
params = [[request contentAsString] objectFromJSONString];
username = [[request formValueForKey: @"username"] stringByTrimmingSpaces]; username = [[params objectForKey: @"username"] stringByTrimmingSpaces];
password = [[request formValueForKey: @"password"] stringByTrimmingSpaces]; password = [[params objectForKey: @"password"] stringByTrimmingSpaces];
if ([username length] > 0 && [password length] > 0) if ([username length] > 0 && [password length] > 0)
{ {
[[self clientObject] setUsername: username [[self clientObject] setUsername: username
@ -165,10 +167,10 @@
response = [self responseWith204]; response = [self responseWith204];
} }
else else
response response = [self responseWithStatus: 400
= (WOResponse *) [NSException exceptionWithHTTPStatus: 400 andJSONRepresentation: [NSDictionary dictionaryWithObject: @"missing 'username' and/or"
reason: @"missing 'username' and/or" @" 'password' parameters"
@" 'password' parameters"]; forKey: @"message"]];
return response; return response;
} }

View File

@ -107,6 +107,10 @@
if ([o isKindOfClass: [NSNumber class]]) if ([o isKindOfClass: [NSNumber class]])
[calendar setSynchronize: [o boolValue]]; [calendar setSynchronize: [o boolValue]];
o = [params objectForKey: @"reloadOnLogin"];
if ([o isKindOfClass: [NSNumber class]] && [calendar isKindOfClass: [SOGoWebAppointmentFolder class]])
[(SOGoWebAppointmentFolder *) calendar setReloadOnLogin: [o boolValue]];
values = [params objectForKey: @"notifications"]; values = [params objectForKey: @"notifications"];
if ([values isKindOfClass: [NSDictionary class]]) if ([values isKindOfClass: [NSDictionary class]])
{ {

View File

@ -195,6 +195,11 @@
<var:string label:value="Unsubscribe Calendar"/> <var:string label:value="Unsubscribe Calendar"/>
</md-button> </md-button>
</md-menu-item> </md-menu-item>
<md-menu-item>
<md-button ng-click="app.exportCalendar(calendar)">
<var:string label:value="Export"/>
</md-button>
</md-menu-item>
<md-menu-divider><!-- divider --></md-menu-divider> <md-menu-divider><!-- divider --></md-menu-divider>
<md-menu-item> <md-menu-item>
<md-button ng-click="app.showLinks(calendar)"> <md-button ng-click="app.showLinks(calendar)">
@ -227,7 +232,13 @@
ng-true-value="1" ng-true-value="1"
ng-false-value="0" ng-false-value="0"
label:aria-label="Enable"><!-- enable --></md-checkbox> label:aria-label="Enable"><!-- enable --></md-checkbox>
<p class="sg-item-name" ng-show="app.editMode != calendar.id">{{calendar.name}}</p> <p class="sg-item-name" ng-show="app.editMode != calendar.id">
<span class="ng-hide" ng-show="calendar.$error">
<md-icon class="md-warn">warning</md-icon>
<md-tooltip>{{ calendar.$error }}</md-tooltip>
</span>
{{calendar.name}}
</p>
<md-input-container class="md-flex" <md-input-container class="md-flex"
ng-show="app.editMode == calendar.id"> ng-show="app.editMode == calendar.id">
<input class="sg-item-name" type="text" <input class="sg-item-name" type="text"
@ -645,6 +656,7 @@
</section> </section>
</script> </script>
<!-- modal inner content for acl editor -->
<script type="text/ng-template" id="UIxUserRightsEditor"> <script type="text/ng-template" id="UIxUserRightsEditor">
<var:component className="UIxCalUserRightsEditor" /> <var:component className="UIxCalUserRightsEditor" />
</script> </script>
@ -724,4 +736,37 @@
</md-dialog> </md-dialog>
</script> </script>
<!-- modal for Web calendar authentication -->
<script type="text/ng-template" id="UIxWebCalendarAuthDialog">
<md-dialog flex="50" flex-xs="90">
<form name="authForm" ng-submit="$WebCalendarAuthDialogController.authenticate(authForm)">
<md-dialog-content class="md-dialog-content" layout="column">
<h2 class="md-title" ng-bind="$WebCalendarAuthDialogController.title"><!-- title --></h2>
<md-input-container>
<label>{{ ::'Username' | loc }}</label>
<input type="text" name="username"
ng-model="$WebCalendarAuthDialogController.username" md-autofocus="true" required="required" />
</md-input-container>
<md-input-container>
<label>{{ ::'Password' | loc }}</label>
<input type="password" name="password"
ng-model="$WebCalendarAuthDialogController.password" required="required" />
<div ng-messages="authForm.password.$error">
<div ng-message="credentials">{{ ::'Wrong username or password.' | loc }}</div>
</div>
</md-input-container>
</md-dialog-content>
<md-dialog-actions>
<md-button ng-click="$WebCalendarAuthDialogController.cancel()"
aria-label="::'Cancel' | loc"
ng-bind="::'Cancel' | loc"><!-- Cancel --></md-button>
<md-button class="md-primary" type="submit"
ng-disabled="authForm.$error.required"
aria-label="::'OK' | loc"
ng-bind="::'OK' | loc"><!-- Submit --></md-button>
</md-dialog-actions>
</form>
</md-dialog>
</script>
</var:component> </var:component>

View File

@ -77,10 +77,7 @@
}) })
.then(function(response) { .then(function(response) {
return deferred.resolve(response.data); return deferred.resolve(response.data);
}, function(response) { }, deferred.reject);
if (response.status == 404)
return deferred.reject();
});
return deferred.promise; return deferred.promise;
}; };

View File

@ -231,20 +231,48 @@
urls: { webCalendarURL: url } urls: { webCalendarURL: url }
}); });
var calendar = new Calendar(calendarData); var calendar = new Calendar(calendarData);
Calendar.$add(calendar);
Calendar.$$resource.fetch(calendar.id, 'reload').then(function(data) { Calendar.$$resource.fetch(calendar.id, 'reload').then(function(data) {
// TODO: show a toast of the reload status // TODO: show a toast of the reload status
Calendar.$log.debug(JSON.stringify(data, undefined, 2)); Calendar.$log.debug(JSON.stringify(data, undefined, 2));
Calendar.$add(calendar);
d.resolve();
}, function(response) {
if (response.status == 401) {
// Web calendar requires authentication
d.resolve(calendar);
}
else {
d.reject();
}
}); });
d.resolve(); }, d.reject);
}, function() {
d.reject();
});
} }
return d.promise; return d.promise;
}; };
/**
* @function reloadWebCalendars
* @memberof Calendar
* @desc Reload all Web calendars
* @return a promise combining the results of all HTTP operations
*/
Calendar.reloadWebCalendars = function() {
var promises = [];
_.forEach(this.$webcalendars, function(calendar) {
var promise = Calendar.$$resource.fetch(calendar.id, 'reload');
promise.then(function(data) {
calendar.$error = false;
}, function(response) {
calendar.$error = l(response.statusText);
});
promises.push(promise);
});
return Calendar.$q.all(promises);
};
/** /**
* @function $deleteComponents * @function $deleteComponents
* @memberof Calendar * @memberof Calendar
@ -292,6 +320,7 @@
*/ */
Calendar.prototype.init = function(data) { Calendar.prototype.init = function(data) {
this.color = this.color || '#AAAAAA'; this.color = this.color || '#AAAAAA';
this.active = 1;
angular.extend(this, data); angular.extend(this, data);
if (this.id) { if (this.id) {
this.$acl = new Calendar.$$Acl('Calendar/' + this.id); this.$acl = new Calendar.$$Acl('Calendar/' + this.id);
@ -438,6 +467,34 @@
}); });
}; };
/**
* @function setCredentials
* @memberof Calendar.prototype
* @desc Set the credentials for a Web calendar that requires authentication
* @returns a promise of the HTTP operation
*/
Calendar.prototype.setCredentials = function(username, password) {
var _this = this,
d = Calendar.$q.defer();
Calendar.$$resource.post(this.id, 'set-credentials', { username: username, password: password }).then(function() {
Calendar.$$resource.fetch(_this.id, 'reload').then(function(data) {
Calendar.$add(_this);
d.resolve();
}, function(response) {
if (response.status == 401) {
// Authentication failed
d.reject(l('Wrong username or password'));
}
else {
d.reject(response.statusText);
}
});
}, d.reject);
return d.promise;
};
/** /**
* @function export * @function export
* @memberof Calendar.prototype * @memberof Calendar.prototype

View File

@ -309,7 +309,9 @@
} }
function reload() { function reload() {
$rootScope.$emit('calendars:list'); Calendar.reloadWebCalendars().finally(function() {
$rootScope.$emit('calendars:list');
});
} }
function cancelSearch() { function cancelSearch() {

View File

@ -127,8 +127,48 @@
function addWebCalendar() { function addWebCalendar() {
Dialog.prompt(l('Subscribe to a web calendar...'), l('URL of the Calendar'), {inputType: 'url'}) Dialog.prompt(l('Subscribe to a web calendar...'), l('URL of the Calendar'), {inputType: 'url'})
.then(function(url) { .then(function(url) {
Calendar.$addWebCalendar(url); Calendar.$addWebCalendar(url).then(function(calendar) {
if (angular.isObject(calendar)) {
// Web calendar requires HTTP authentication
$mdDialog.show({
parent: angular.element(document.body),
clickOutsideToClose: true,
escapeToClose: true,
templateUrl: 'UIxWebCalendarAuthDialog',
controller: WebCalendarAuthDialogController,
controllerAs: '$WebCalendarAuthDialogController',
locals: {
url: url,
calendar: calendar
}
});
}
});
}); });
/**
* @ngInject
*/
WebCalendarAuthDialogController.$inject = ['scope', '$mdDialog', 'url', 'calendar'];
function WebCalendarAuthDialogController(scope, $mdDialog, url, calendar) {
var vm = this,
parts = url.split("/"),
hostname = parts[2];
vm.title = l("Please identify yourself to %{0}").formatted(hostname);
vm.authenticate = function(form) {
if (form.$valid || !form.$error.required) {
calendar.setCredentials(vm.username, vm.password).then(function(message) {
$mdDialog.hide();
}, function(reason) {
form.password.$setValidity('credentials', false);
});
}
};
vm.cancel = function() {
$mdDialog.cancel();
};
}
} }
function confirmDelete(folder) { function confirmDelete(folder) {