diff --git a/Makefile b/Makefile index d44affe16..4de27e3de 100644 --- a/Makefile +++ b/Makefile @@ -137,6 +137,7 @@ appstore: $(copy_command) --parents -r \ "appinfo" \ "controller" \ + "http" \ "img" \ "l10n" \ "templates" \ diff --git a/appinfo/application.php b/appinfo/application.php index 4e8a21867..629572fb8 100644 --- a/appinfo/application.php +++ b/appinfo/application.php @@ -55,6 +55,12 @@ public function __construct($params=[]) { return new Controller\ViewController($c->getAppName(), $request, $userSession, $config); }); + $container->registerService('ProxyController', function(IAppContainer $c) { + $request = $c->query('Request'); + $client = $c->getServer()->getHTTPClientService(); + + return new Controller\ProxyController($c->getAppName(), $request, $client); + }); } /** diff --git a/appinfo/routes.php b/appinfo/routes.php index 67c34e463..ef0f47131 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -35,5 +35,7 @@ //Autocompletion ['name' => 'contact#searchAttendee', 'url' => '/v1/autocompletion/attendee', 'verb' => 'GET'], ['name' => 'contact#searchLocation', 'url' => '/v1/autocompletion/location', 'verb' => 'GET'], + + ['name' => 'proxy#proxy', 'url' => '/v1/proxy', 'verb' => 'GET'], ] ]; diff --git a/controller/proxycontroller.php b/controller/proxycontroller.php new file mode 100644 index 000000000..4a1fcd0fc --- /dev/null +++ b/controller/proxycontroller.php @@ -0,0 +1,73 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see . + * + */ +namespace OCA\Calendar\Controller; + +use GuzzleHttp\Exception\ClientException; +use OCA\Calendar\Http\StreamResponse; +use OCP\AppFramework\Http\JSONResponse; + +use OCP\AppFramework\Controller; +use OCP\Http\Client\IClientService; +use OCP\IRequest; + +class ProxyController extends Controller { + + /** + * @var IClientService + */ + protected $client; + + /** + * @param string $appName + * @param IRequest $request an instance of the request + * @param IClientService $client + */ + public function __construct($appName, IRequest $request, + IClientService $client) { + parent::__construct($appName, $request); + $this->client = $client; + } + + /** + * @NoAdminRequired + * + * @param $url + * @return StreamResponse|JSONResponse + */ + public function proxy($url) { + $client = $this->client->newClient(); + try { + $clientResponse = $client->get($url, [ + 'stream' => true, + ]); + $response = new StreamResponse($clientResponse->getBody()); + $response->setHeaders([ + 'Content-Type' => 'text/calendar', + ]); + } catch (ClientException $e) { + $error_code = $e->getResponse()->getStatusCode(); + $response = new JSONResponse(); + $response->setStatus($error_code); + } + return $response; + } +} diff --git a/controller/viewcontroller.php b/controller/viewcontroller.php index 80382c9c3..019c5e621 100644 --- a/controller/viewcontroller.php +++ b/controller/viewcontroller.php @@ -47,8 +47,8 @@ class ViewController extends Controller { /** * @param string $appName * @param IRequest $request an instance of the request - * @param IConfig $config * @param IUserSession $userSession + * @param IConfig $config */ public function __construct($appName, IRequest $request, IUserSession $userSession, IConfig $config) { diff --git a/css/app/calendarlist.css b/css/app/calendarlist.css index ab0a7f128..763d08d13 100644 --- a/css/app/calendarlist.css +++ b/css/app/calendarlist.css @@ -387,3 +387,7 @@ ul.dropdown-menu li > a:hover { line-height: initial; padding-left: 3px; } + +.app-navigation-entry-menu .icon-link { + background-size: 16px; +} diff --git a/http/streamresponse.php b/http/streamresponse.php new file mode 100644 index 000000000..ec63c8237 --- /dev/null +++ b/http/streamresponse.php @@ -0,0 +1,51 @@ + + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Calendar\Http; + +use OCP\AppFramework\Http\ICallbackResponse; +use OCP\AppFramework\Http\IOutput; +use OCP\AppFramework\Http\Response; + +class StreamResponse extends Response implements ICallbackResponse { + + /** + * @var resource + */ + protected $stream; + + /** + * @param resource $stream + */ + public function __construct ($stream) { + $this->stream = $stream; + } + + + /** + * @param IOutput $output a small wrapper that handles output + */ + public function callback (IOutput $output) { + rewind($this->stream); + fpassthru($this->stream); + } + +} diff --git a/js/app/controllers/calendarlistcontroller.js b/js/app/controllers/calendarlistcontroller.js index 26a757eaf..8969a8764 100644 --- a/js/app/controllers/calendarlistcontroller.js +++ b/js/app/controllers/calendarlistcontroller.js @@ -26,8 +26,8 @@ * Description: Takes care of CalendarList in App Navigation. */ -app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'CalendarService', 'is', 'CalendarListItem', 'Calendar', - function ($scope, $rootScope, $window, CalendarService, is, CalendarListItem, Calendar) { +app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'CalendarService', 'WebCalService', 'is', 'CalendarListItem', 'Calendar', 'ColorUtility', + function ($scope, $rootScope, $window, CalendarService, WebCalService, is, CalendarListItem, Calendar, ColorUtility) { 'use strict'; $scope.calendarListItems = []; @@ -35,7 +35,8 @@ app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'Ca $scope.newCalendarInputVal = ''; $scope.newCalendarColorVal = ''; - window.scope = $scope; + $scope.newSubscriptionUrl = ''; + $scope.newSubscriptionLocked = false; $scope.$watchCollection('calendars', function(newCalendars, oldCalendars) { newCalendars = newCalendars || []; @@ -62,10 +63,6 @@ app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'Ca return itemToCheck.calendar !== calendar; }); }); - - if (!$scope.$$phase) { - $scope.$apply(); - } }); $scope.create = function (name, color) { @@ -80,15 +77,32 @@ app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'Ca angular.element('#new-calendar-button').click(); }; - $scope.download = function (item) { - var url = item.calendar.url; - // cut off last slash to have a fancy name for the ics - if (url.slice(url.length - 1) === '/') { - url = url.slice(0, url.length - 1); - } - url += '?export'; + $scope.createSubscription = function(url) { + $scope.newSubscriptionLocked = true; + WebCalService.get(url, true).then(function(splittedICal) { + const color = splittedICal.color || ColorUtility.randomColor(); + const name = splittedICal.name || url; + CalendarService.createWebCal(name, color, url) + .then(function(calendar) { + $scope.newSubscriptionUrl = ''; + angular.element('#new-subscription-button').click(); + $scope.calendars.push(calendar); + $scope.$digest(); + $scope.$parent.$digest(); + $scope.newSubscriptionLocked = false; + }) + .catch(function() { + OC.Notification.showTemporary(t('calendar', 'Error saving WebCal-calendar')); + $scope.newSubscriptionLocked = false; + }); + }).catch(function(error) { + OC.Notification.showTemporary(error); + $scope.newSubscriptionLocked = false; + }); + }; - $window.open(url); + $scope.download = function (item) { + $window.open(item.calendar.downloadUrl); }; $scope.toggleSharesEditor = function (calendar) { diff --git a/js/app/controllers/editorcontroller.js b/js/app/controllers/editorcontroller.js index 90dcb020e..f08db54ba 100644 --- a/js/app/controllers/editorcontroller.js +++ b/js/app/controllers/editorcontroller.js @@ -35,6 +35,7 @@ app.controller('EditorController', ['$scope', 'TimezoneService', 'AutoCompletion $scope.calendar = calendar; $scope.oldCalendar = isNew ? calendar : vevent.calendar; $scope.readOnly = isNew ? false : !vevent.calendar.isWritable(); + $scope.accessibleViaCalDAV = vevent.calendar.eventsAccessibleViaCalDAV(); $scope.selected = 1; $scope.timezones = []; $scope.emailAddress = emailAddress; diff --git a/js/app/models/calendarListItemModel.js b/js/app/models/calendarListItemModel.js index 85851a3ac..a22e8992a 100644 --- a/js/app/models/calendarListItemModel.js +++ b/js/app/models/calendarListItemModel.js @@ -21,7 +21,7 @@ * */ -app.factory('CalendarListItem', function(Calendar) { +app.factory('CalendarListItem', function(Calendar, WebCal) { 'use strict'; function CalendarListItem(calendar) { @@ -29,7 +29,8 @@ app.factory('CalendarListItem', function(Calendar) { calendar: calendar, isEditingShares: false, isEditingProperties: false, - isDisplayingCalDAVUrl: false + isDisplayingCalDAVUrl: false, + isDisplayingWebCalUrl: false }; const iface = { _isACalendarListItemObject: true @@ -55,10 +56,22 @@ app.factory('CalendarListItem', function(Calendar) { context.isDisplayingCalDAVUrl = true; }; + iface.displayWebCalUrl = function() { + return context.isDisplayingWebCalUrl; + }; + iface.hideCalDAVUrl = function() { context.isDisplayingCalDAVUrl = false; }; + iface.showWebCalUrl = function() { + context.isDisplayingWebCalUrl = true; + }; + + iface.hideWebCalUrl = function() { + context.isDisplayingWebCalUrl = false; + }; + iface.isEditingShares = function() { return context.isEditingShares; }; @@ -107,6 +120,10 @@ app.factory('CalendarListItem', function(Calendar) { context.isEditingProperties = false; }; + iface.isWebCal = function() { + return WebCal.isWebCal(context.calendar); + }; + //Properties for ng-model of calendar editor iface.color = ''; iface.displayname = ''; diff --git a/js/app/models/calendarmodel.js b/js/app/models/calendarmodel.js index 43465a95f..cdea963bd 100644 --- a/js/app/models/calendarmodel.js +++ b/js/app/models/calendarmodel.js @@ -30,32 +30,35 @@ app.factory('Calendar', function($window, Hook, VEventService, TimezoneService, context.fcEventSource.events = function (start, end, timezone, callback) { const fcAPI = this; + context.fcEventSource.isRendering = true; + iface.emit(Calendar.hookFinishedRendering); - TimezoneService.get(timezone).then(function (tz) { - context.fcEventSource.isRendering = true; - iface.emit(Calendar.hookFinishedRendering); + const TimezoneServicePromise = TimezoneService.get(timezone); + const VEventServicePromise = VEventService.getAll(iface, start, end); + Promise.all([TimezoneServicePromise, VEventServicePromise]).then(function(results) { + const [tz, events] = results; + let vevents = []; - VEventService.getAll(iface, start, end).then(function (events) { - var vevents = []; - for (var i = 0; i < events.length; i++) { - var vevent; - try { - vevent = events[i].getFcEvent(start, end, tz); - } catch (err) { - iface.addWarning(err.toString()); - console.log(err); - console.log(events[i]); - continue; - } - vevents = vevents.concat(vevent); + for (var i = 0; i < events.length; i++) { + var vevent; + try { + vevent = events[i].getFcEvent(start, end, tz); + } catch (err) { + iface.addWarning(err.toString()); + console.log(err); + console.log(events[i]); + continue; } + vevents = vevents.concat(vevent); + } - callback(vevents); - fcAPI.reportEvents(fcAPI.clientEvents()); - context.fcEventSource.isRendering = false; + callback(vevents); + fcAPI.reportEvents(fcAPI.clientEvents()); + context.fcEventSource.isRendering = false; - iface.emit(Calendar.hookFinishedRendering); - }); + iface.emit(Calendar.hookFinishedRendering); + }).catch(function(reason) { + console.log(reason); }); }; context.fcEventSource.editable = context.writable; @@ -142,6 +145,19 @@ app.factory('Calendar', function($window, Hook, VEventService, TimezoneService, return context.url; } }, + downloadUrl: { + get: function() { + let url = context.url; + // cut off last slash to have a fancy name for the ics + if (url.slice(url.length - 1) === '/') { + url = url.slice(0, url.length - 1); + } + url += '?export'; + + return url; + }, + configurable: true + }, caldav: { get: function() { return $window.location.origin + context.url; @@ -233,6 +249,10 @@ app.factory('Calendar', function($window, Hook, VEventService, TimezoneService, return context.writableProperties; }; + iface.eventsAccessibleViaCalDAV = function() { + return true; + }; + Object.assign( iface, Hook(context) diff --git a/js/app/models/veventmodel.js b/js/app/models/veventmodel.js index dc4829606..d80bd182d 100644 --- a/js/app/models/veventmodel.js +++ b/js/app/models/veventmodel.js @@ -162,6 +162,12 @@ app.factory('VEvent', function(FcEvent, SimpleEvent, ICalFactory, RandomStringSe registerTimezones(this.comp); + etag = etag || ''; + if (typeof uri === 'undefined') { + const vevent = this.comp.getFirstSubcomponent('vevent'); + uri = vevent.getFirstPropertyValue('uid'); + } + angular.extend(this, { calendar: calendar, etag: etag, diff --git a/js/app/models/webcalModel.js b/js/app/models/webcalModel.js new file mode 100644 index 000000000..dd96e90f4 --- /dev/null +++ b/js/app/models/webcalModel.js @@ -0,0 +1,97 @@ +/** + * ownCloud - Calendar App + * + * @author Georg Ehrke + * @copyright 2016 Georg Ehrke + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see . + * + */ + +app.factory('WebCal', function($http, Calendar, VEvent, TimezoneService, WebCalService, WebCalUtility) { + 'use strict'; + + function WebCal(url, props) { + const context = { + storedUrl: props.href, //URL stored in CalDAV + url: WebCalUtility.fixURL(props.href) + }; + + const iface = Calendar(url, props); + iface._isAWebCalObject = true; + + Object.defineProperties(iface, { + downloadUrl: { + get: function() { + return context.url; + } + }, + storedUrl: { + get: function () { + return context.storedUrl; + } + } + }); + + iface.fcEventSource.events = function (start, end, timezone, callback) { + var fcAPI = this; + iface.fcEventSource.isRendering = true; + iface.emit(Calendar.hookFinishedRendering); + + const allowDowngradeToHttp = !context.storedUrl.startsWith('https://'); + + const TimezoneServicePromise = TimezoneService.get(timezone); + const WebCalServicePromise = WebCalService.get(context.url, allowDowngradeToHttp); + Promise.all([TimezoneServicePromise, WebCalServicePromise]).then(function(results) { + const [tz, response] = results; + let vevents = []; + + response.vevents.forEach(function(ical) { + try { + const vevent = new VEvent(iface, ical); + const events = vevent.getFcEvent(start, end, tz); + vevents = vevents.concat(events); + } catch (err) { + iface.addWarning(err.toString()); + console.log(err); + console.log(event); + } + }); + + callback(vevents); + fcAPI.reportEvents(fcAPI.clientEvents()); + + iface.fcEventSource.isRendering = false; + iface.emit(Calendar.hookFinishedRendering); + }).catch(function(reason) { + iface.addWarning(reason); + console.log(reason); + iface.fcEventSource.isRendering = false; + iface.emit(Calendar.hookFinishedRendering); + }); + }; + + iface.eventsAccessibleViaCalDAV = function() { + return false; + }; + + return iface; + } + + WebCal.isWebCal = function(obj) { + return (typeof obj === 'object' && obj !== null && obj._isAWebCalObject === true); + }; + + return WebCal; +}); diff --git a/js/app/service/calendarservice.js b/js/app/service/calendarservice.js index 7123db709..79b817c52 100644 --- a/js/app/service/calendarservice.js +++ b/js/app/service/calendarservice.js @@ -21,7 +21,7 @@ * */ -app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Calendar){ +app.service('CalendarService', function(DavClient, StringUtility, XMLUtility, Calendar, WebCal){ 'use strict'; var self = this; @@ -34,6 +34,7 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal this._PROPERTIES = [ '{' + DavClient.NS_DAV + '}displayname', + '{' + DavClient.NS_DAV + '}resourcetype', '{' + DavClient.NS_IETF + '}calendar-description', '{' + DavClient.NS_IETF + '}calendar-timezone', '{' + DavClient.NS_APPLE + '}calendar-order', @@ -42,7 +43,8 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal '{' + DavClient.NS_OWNCLOUD + '}calendar-enabled', '{' + DavClient.NS_DAV + '}acl', '{' + DavClient.NS_DAV + '}owner', - '{' + DavClient.NS_OWNCLOUD + '}invite' + '{' + DavClient.NS_OWNCLOUD + '}invite', + '{' + DavClient.NS_CALENDARSERVER + '}source' ]; this._xmls = new XMLSerializer(); @@ -111,8 +113,23 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal continue; } - var calendar = Calendar(body.href, props); - calendars.push(calendar); + const resourceTypes = body.propStat[0].properties['{' + DavClient.NS_DAV + '}resourcetype']; + if (!resourceTypes) { + continue; + } + + for (var j = 0; j < resourceTypes.length; j++) { + var name = DavClient.getNodesFullName(resourceTypes[j]); + + if (name === '{' + DavClient.NS_IETF + '}calendar') { + const calendar = Calendar(body.href, props); + calendars.push(calendar); + } + if (name === '{' + DavClient.NS_CALENDARSERVER + '}subscribed') { + const webcal = WebCal(body.href, props); + calendars.push(webcal); + } + } } return calendars; @@ -144,7 +161,21 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal return; } - return Calendar(body.href, props); + const resourceTypes = body.propStat[0].properties['{' + DavClient.NS_DAV + '}resourcetype']; + if (!resourceTypes) { + return; + } + + for (var j = 0; j < resourceTypes.length; j++) { + var name = DavClient.getNodesFullName(resourceTypes[j]); + + if (name === '{' + DavClient.NS_IETF + '}calendar') { + return Calendar(body.href, props); + } + if (name === '{' + DavClient.NS_CALENDARSERVER + '}subscribed') { + return WebCal(body.href, props); + } + } }); }; @@ -189,7 +220,7 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal var body = this._xmls.serializeToString(cMkcalendar); - var uri = this._suggestUri(name); + var uri = StringUtility.uri(name, (suggestedUri) => self._takenUrls.indexOf(self._CALENDAR_HOME + suggestedUri + '/') === -1); var url = this._CALENDAR_HOME + uri + '/'; var headers = { 'Content-Type' : 'application/xml; charset=utf-8', @@ -207,6 +238,58 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal }); }; + this.createWebCal = function(name, color, source) { + if (this._CALENDAR_HOME === null) { + return discoverHome(function() { + return self.createWebCal(name, color, source); + }); + } + + const [skeleton, dPropChildren] = XMLUtility.getRootSkeleton('d:mkcol', 'd:set', 'd:prop'); + dPropChildren.push({ + name: 'd:resourcetype', + children: [{ + name: 'd:collection' + }, { + name: 'cs:subscribed' + }] + }); + dPropChildren.push({ + name: 'd:displayname', + value: name + }); + dPropChildren.push({ + name: 'a:calendar-color', + value: color + }); + dPropChildren.push({ + name: 'o:calendar-enabled', + value: '1' + }); + dPropChildren.push({ + name: 'cs:source', + children: [{ + name: 'd:href', + value: source + }] + }); + + const uri = StringUtility.uri(name, (suggestedUri) => self._takenUrls.indexOf(self._CALENDAR_HOME + suggestedUri + '/') === -1); + const url = this._CALENDAR_HOME + uri + '/'; + const headers = { + 'Content-Type' : 'application/xml; charset=utf-8', + 'requesttoken' : OC.requestToken + }; + const xml = XMLUtility.serialize(skeleton); + + return DavClient.request('MKCOL', url, headers, xml).then(function(response) { + if (response.status === 201) { + self._takenUrls.push(url); + return self.get(url); + } + }); + }; + this.update = function(calendar) { var xmlDoc = document.implementation.createDocument('', '', null); var dPropUpdate = xmlDoc.createElement('d:propertyupdate'); @@ -250,6 +333,9 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal }; this.delete = function(calendar) { + if (WebCal.isWebCal(calendar)) { + localStorage.removeItem(calendar.storedUrl); + } return DavClient.request('DELETE', calendar.url, {'requesttoken': OC.requestToken}, '').then(function(response) { if (response.status === 204) { return true; @@ -488,6 +574,17 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal simple.writableProperties = (oc_current_user === simple.owner) && simple.writable; + var source = props['{' + DavClient.NS_CALENDARSERVER + '}source']; + if (source) { + for (var k=0; k < source.length; k++) { + if (DavClient.getNodesFullName(source[k]) === '{' + DavClient.NS_DAV + '}href') { + simple.href = source[k].textContent; + simple.writable = false; //this is a webcal calendar + simple.writableProperties = (oc_current_user === simple.owner); + } + } + } + return simple; }; @@ -512,48 +609,4 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal } props.canWrite = canWrite; }; - - this._isUriAlreadyTaken = function(uri) { - return (this._takenUrls.indexOf(this._CALENDAR_HOME + uri + '/') !== -1); - }; - - this._suggestUri = function(displayname) { - var uri = displayname.toString().toLowerCase() - .replace(/\s+/g, '-') // Replace spaces with - - .replace(/[^\w\-]+/g, '') // Remove all non-word chars - .replace(/\-\-+/g, '-') // Replace multiple - with single - - .replace(/^-+/, '') // Trim - from start of text - .replace(/-+$/, ''); // Trim - from end of text - - if (!this._isUriAlreadyTaken(uri)) { - return uri; - } - - if (uri.indexOf('-') === -1) { - uri = uri + '-1'; - if (!this._isUriAlreadyTaken(uri)) { - return uri; - } - } - - while (this._isUriAlreadyTaken(uri)) { - var positionLastDash = uri.lastIndexOf('-'); - var firstPart = uri.substr(0, positionLastDash); - var lastPart = uri.substr(positionLastDash + 1); - - if (lastPart.match(/^\d+$/)) { - lastPart = parseInt(lastPart); - lastPart++; - - uri = firstPart + '-' + lastPart; - } else if (lastPart === '') { - uri = uri + '1'; - } else { - uri = uri = '-1'; - } - } - - return uri; - }; - -}]); +}); \ No newline at end of file diff --git a/js/app/service/davclient.js b/js/app/service/davclient.js index 821849dfb..bc932615c 100644 --- a/js/app/service/davclient.js +++ b/js/app/service/davclient.js @@ -47,6 +47,12 @@ app.service('DavClient', function() { }, wasRequestSuccessful: function(status) { return (status >= 200 && status <= 299); + }, + getResponseCodeFromHTTPResponse: function(t) { + return parseInt(t.split(' ')[1]); + }, + getNodesFullName: function(node) { + return '{' + node.namespaceURI + '}' + node.localName; } }); diff --git a/js/app/service/webcalService.js b/js/app/service/webcalService.js new file mode 100644 index 000000000..0eb97cee2 --- /dev/null +++ b/js/app/service/webcalService.js @@ -0,0 +1,74 @@ +/** + * ownCloud - Calendar App + * + * @author Raghu Nayyar + * @author Georg Ehrke + * @copyright 2016 Raghu Nayyar + * @copyright 2016 Georg Ehrke + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see . + * + */ + +app.service('WebCalService', function ($http, ICalSplitterUtility, WebCalUtility, SplittedICal) { + 'use strict'; + + const self = this; + const context = { + cachedSplittedICals: {} + }; + + this.get = function(webcalUrl, allowDowngradeToHttp) { + if (context.cachedSplittedICals.hasOwnProperty(webcalUrl)) { + return Promise.resolve(context.cachedSplittedICals[webcalUrl]); + } + + webcalUrl = WebCalUtility.fixURL(webcalUrl); + const url = WebCalUtility.buildProxyURL(webcalUrl); + + let localWebcal = JSON.parse(localStorage.getItem(webcalUrl)); + if (localWebcal && localWebcal.timestamp > new Date().getTime()) { + return Promise.resolve(ICalSplitterUtility.split(localWebcal.value)); + } + + return $http.get(url).then(function(response) { + const splitted = ICalSplitterUtility.split(response.data); + + if (!SplittedICal.isSplittedICal(splitted)) { + return Promise.reject(t('calendar', 'Please enter a valid WebCal-URL')); + } + + context.cachedSplittedICals[webcalUrl] = splitted; + localStorage.setItem(webcalUrl, JSON.stringify({value: response.data, timestamp: new Date().getTime() + 7200000})); // That would be two hours in milliseconds + + return splitted; + }).catch(function(e) { + if (WebCalUtility.downgradePossible(webcalUrl, allowDowngradeToHttp)) { + const httpUrl = WebCalUtility.downgradeURL(webcalUrl); + + return self.get(httpUrl, false).then(function(splitted) { + context.cachedSplittedICals[webcalUrl] = splitted; + return splitted; + }); + } + if (e.status < 200 || e.status > 299) { + return Promise.reject(t('calendar', 'The remote server did not give us access to the calendar (HTTP {code} error)', + {code: e.status} + )); + } + + return Promise.reject(t('calendar', 'Please enter a valid WebCal-URL')); + }); + }; +}); diff --git a/js/app/utility/webcalUtility.js b/js/app/utility/webcalUtility.js new file mode 100644 index 000000000..ec74cad1e --- /dev/null +++ b/js/app/utility/webcalUtility.js @@ -0,0 +1,48 @@ +/** + * ownCloud - Calendar App + * + * @author Georg Ehrke + * @copyright 2016 Georg Ehrke + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see . + * + */ + +app.service('WebCalUtility', function($rootScope) { + 'use strict'; + + this.downgradeURL = function(url) { + if (url.startsWith('https://')) { + return 'http://' + url.substr(8); + } + }; + + this.downgradePossible = function(url, allowDowngradeToHttp) { + return url.startsWith('https://') && allowDowngradeToHttp; + }; + + this.buildProxyURL = function(url) { + return $rootScope.baseUrl + 'proxy?url=' + encodeURIComponent(url); + }; + + this.fixURL = function(url) { + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } else if (url.startsWith('webcal://')) { + return 'https://' + url.substr(9); + } else { + return 'https://' + url; + } + }; +}); diff --git a/js/app/utility/xmlUtility.js b/js/app/utility/xmlUtility.js index 7966ea32f..db10a2a16 100644 --- a/js/app/utility/xmlUtility.js +++ b/js/app/utility/xmlUtility.js @@ -47,23 +47,24 @@ app.service('XMLUtility', function() { const serializer = new XMLSerializer(); - this.getRootSceleton = function() { + this.getRootSkeleton = function() { if (arguments.length === 0) { return [{}, null]; } - const sceleton = { + const skeleton = { name: arguments[0], attributes: { 'xmlns:c': 'urn:ietf:params:xml:ns:caldav', 'xmlns:d': 'DAV:', 'xmlns:a': 'http://apple.com/ns/ical/', - 'xmlns:o': 'http://owncloud.org/ns' + 'xmlns:o': 'http://owncloud.org/ns', + 'xmlns:cs': 'http://calendarserver.org/ns/' }, children: [] }; - let childrenWrapper = sceleton.children; + let childrenWrapper = skeleton.children; const args = Array.prototype.slice.call(arguments, 1); args.forEach(function(argument) { @@ -75,7 +76,7 @@ app.service('XMLUtility', function() { childrenWrapper = level.children; }); - return [sceleton, childrenWrapper]; + return [skeleton, childrenWrapper]; }; this.serialize = function(json) { diff --git a/templates/editor.sidebar.php b/templates/editor.sidebar.php index d3eb99b38..41a6ce04e 100644 --- a/templates/editor.sidebar.php +++ b/templates/editor.sidebar.php @@ -138,6 +138,7 @@ class="evens--button button btn primary btn-full" diff --git a/templates/part.calendarlist.item.php b/templates/part.calendarlist.item.php index 67556af7b..06c53724f 100644 --- a/templates/part.calendarlist.item.php +++ b/templates/part.calendarlist.item.php @@ -80,12 +80,18 @@ class="app-navigation-entry-menu hidden"> t('Edit')); ?> -
  • +
  • +
  • + +
  • +
    + + +
    - +