diff --git a/.babelrc.js b/.babelrc.js new file mode 100644 index 000000000..e54da4c9a --- /dev/null +++ b/.babelrc.js @@ -0,0 +1,11 @@ +module.exports = { + plugins: [ + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-proposal-object-rest-spread' + ], + presets: [ + [ + '@babel/preset-env' + ] + ] +} \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..220348c2b --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,27 @@ +module.exports = { + root: true, + env: { + node: true + }, + extends: [ + "plugin:vue/recommended", + "eslint:recommended", + "prettier/vue", + "plugin:prettier/recommended" + ], + rules: { + "vue/component-name-in-template-casing": ["error", "PascalCase"], + "no-console": process.env.NODE_ENV === "production" ? "error" : "off", + "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" + }, + globals: { + OC: false, + OCA: false, + t: false, + n: false, + $: false // TODO: remove once jQuery has been removed + }, + parserOptions: { + parser: "babel-eslint" + } +}; diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 000000000..40db42c66 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "stylelint-config-standard" +} diff --git a/appinfo/application.php b/appinfo/application.php index 8ffc8e0c1..8b081be00 100644 --- a/appinfo/application.php +++ b/appinfo/application.php @@ -12,14 +12,13 @@ namespace OCA\Maps\AppInfo; -use OC\AppFramework\Utility\SimpleContainer; -use OCA\Maps\Service\AddressService; +use OCA\Maps\Controller\PublicFavoritesApiController; use \OCP\AppFramework\App; -use OCA\Maps\Controller\PageController; use OCA\Maps\Controller\UtilsController; use OCA\Maps\Controller\FavoritesController; use OCA\Maps\Controller\FavoritesApiController; use OCA\Maps\Controller\DevicesController; +use OCA\Maps\Controller\PublicPageController; use OCA\Maps\Controller\DevicesApiController; use OCA\Maps\Controller\RoutingController; use OCA\Maps\Controller\TracksController; @@ -37,7 +36,7 @@ public function __construct (array $urlParams=array()) { $container = $this->getContainer(); - $this->getContainer()->registerService('FileHooks', function($c) { + $this->getContainer()->registerService('FileHooks', function ($c) { return new FileHooks( $c->query('ServerContainer')->getRootFolder(), \OC::$server->query(PhotofilesService::class), @@ -52,136 +51,164 @@ public function __construct (array $urlParams=array()) { $container->registerService( 'FavoritesController', function ($c) { - return new FavoritesController( - $c->query('AppName'), - $c->query('Request'), - $c->query('UserId'), - $c->query('ServerContainer')->getUserFolder($c->query('UserId')), - $c->query('ServerContainer')->getConfig(), - $c->getServer()->getShareManager(), - $c->getServer()->getAppManager(), - $c->getServer()->getUserManager(), - $c->getServer()->getGroupManager(), - $c->query('ServerContainer')->getL10N($c->query('AppName')), + return new FavoritesController( + $c->query('AppName'), + $c->query('Request'), + $c->query('UserId'), + $c->query('ServerContainer')->getUserFolder($c->query('UserId')), + $c->query('ServerContainer')->getConfig(), + $c->getServer()->getShareManager(), + $c->getServer()->getAppManager(), + $c->getServer()->getUserManager(), + $c->getServer()->getGroupManager(), + $c->query('ServerContainer')->getL10N($c->query('AppName')), + $c->query('ServerContainer')->getLogger(), + new FavoritesService( $c->query('ServerContainer')->getLogger(), - new FavoritesService( - $c->query('ServerContainer')->getLogger(), - $c->query('ServerContainer')->getL10N($c->query('AppName')) - ), - $c->query('ServerContainer')->getDateTimeZone() - ); - } - ); + $c->query('ServerContainer')->getL10N($c->query('AppName')), + $c->query('ServerContainer')->getSecureRandom() + ), + $c->query('ServerContainer')->getDateTimeZone() + ); + }); $container->registerService( 'FavoritesApiController', function ($c) { - return new FavoritesApiController( - $c->query('AppName'), - $c->query('Request'), - $c->query('UserId'), - $c->query('ServerContainer')->getUserFolder($c->query('UserId')), - $c->query('ServerContainer')->getConfig(), - $c->getServer()->getShareManager(), - $c->getServer()->getAppManager(), - $c->getServer()->getUserManager(), - $c->getServer()->getGroupManager(), + return new FavoritesApiController( + $c->query('AppName'), + $c->query('Request'), + $c->query('UserId'), + $c->query('ServerContainer')->getUserFolder($c->query('UserId')), + $c->query('ServerContainer')->getConfig(), + $c->getServer()->getShareManager(), + $c->getServer()->getAppManager(), + $c->getServer()->getUserManager(), + $c->getServer()->getGroupManager(), + $c->query('ServerContainer')->getL10N($c->query('AppName')), + $c->query('ServerContainer')->getLogger(), + new FavoritesService( + $c->query('ServerContainer')->getLogger(), $c->query('ServerContainer')->getL10N($c->query('AppName')), + $c->query('ServerContainer')->getSecureRandom() + ) + ); + }); + + $container->registerService( + 'PublicFavoritesAPIController', function ($c) { + return new PublicFavoritesApiController( + $c->query('AppName'), + $c->query('Request'), + $c->query('UserId'), + $c->query('Session'), + $c->query('ServerContainer')->getConfig(), + new FavoritesService( $c->query('ServerContainer')->getLogger(), - new FavoritesService( - $c->query('ServerContainer')->getLogger(), - $c->query('ServerContainer')->getL10N($c->query('AppName')) - ) - ); - } - ); + $c->query('ServerContainer')->getL10N($c->query('AppName')), + $c->query('ServerContainer')->getSecureRandom() + ) + ); + }); $container->registerService( - 'DevicesController', function ($c) { - return new DevicesController( - $c->query('AppName'), - $c->query('Request'), - $c->query('UserId'), - $c->query('ServerContainer')->getUserFolder($c->query('UserId')), - $c->query('ServerContainer')->getConfig(), - $c->getServer()->getShareManager(), - $c->getServer()->getAppManager(), - $c->getServer()->getUserManager(), - $c->getServer()->getGroupManager(), + 'PublicPageController', function ($c) { + return new PublicPageController( + $c->query('AppName'), + $c->query('Request'), + $c->query('Session'), + $c->query('ServerContainer')->getConfig(), + $c->query('Logger'), + new FavoritesService( + $c->query('ServerContainer')->getLogger(), $c->query('ServerContainer')->getL10N($c->query('AppName')), + $c->query('ServerContainer')->getSecureRandom() + ) + ); + }); + + $container->registerService( + 'DevicesController', function ($c) { + return new DevicesController( + $c->query('AppName'), + $c->query('Request'), + $c->query('UserId'), + $c->query('ServerContainer')->getUserFolder($c->query('UserId')), + $c->query('ServerContainer')->getConfig(), + $c->getServer()->getShareManager(), + $c->getServer()->getAppManager(), + $c->getServer()->getUserManager(), + $c->getServer()->getGroupManager(), + $c->query('ServerContainer')->getL10N($c->query('AppName')), + $c->query('ServerContainer')->getLogger(), + new DevicesService( $c->query('ServerContainer')->getLogger(), - new DevicesService( - $c->query('ServerContainer')->getLogger(), - $c->query('ServerContainer')->getL10N($c->query('AppName')) - ), - $c->query('ServerContainer')->getDateTimeZone() - ); - } - ); + $c->query('ServerContainer')->getL10N($c->query('AppName')) + ), + $c->query('ServerContainer')->getDateTimeZone() + ); + }); $container->registerService( 'DevicesApiController', function ($c) { - return new DevicesApiController( - $c->query('AppName'), - $c->query('Request'), - $c->query('UserId'), - $c->query('ServerContainer')->getUserFolder($c->query('UserId')), - $c->query('ServerContainer')->getConfig(), - $c->getServer()->getShareManager(), - $c->getServer()->getAppManager(), - $c->getServer()->getUserManager(), - $c->getServer()->getGroupManager(), - $c->query('ServerContainer')->getL10N($c->query('AppName')), + return new DevicesApiController( + $c->query('AppName'), + $c->query('Request'), + $c->query('UserId'), + $c->query('ServerContainer')->getUserFolder($c->query('UserId')), + $c->query('ServerContainer')->getConfig(), + $c->getServer()->getShareManager(), + $c->getServer()->getAppManager(), + $c->getServer()->getUserManager(), + $c->getServer()->getGroupManager(), + $c->query('ServerContainer')->getL10N($c->query('AppName')), + $c->query('ServerContainer')->getLogger(), + new DevicesService( $c->query('ServerContainer')->getLogger(), - new DevicesService( - $c->query('ServerContainer')->getLogger(), - $c->query('ServerContainer')->getL10N($c->query('AppName')) - ) - ); - } - ); + $c->query('ServerContainer')->getL10N($c->query('AppName')) + ) + ); + }); $container->registerService( 'RoutingController', function ($c) { - return new RoutingController( - $c->query('AppName'), - $c->query('Request'), - $c->query('UserId'), - $c->query('ServerContainer')->getUserFolder($c->query('UserId')), - $c->query('ServerContainer')->getConfig(), - $c->getServer()->getShareManager(), - $c->getServer()->getAppManager(), - $c->getServer()->getUserManager(), - $c->getServer()->getGroupManager(), - $c->query('ServerContainer')->getL10N($c->query('AppName')), - $c->query('ServerContainer')->getLogger(), - $c->query('ServerContainer')->getDateTimeZone() - ); - } - ); + return new RoutingController( + $c->query('AppName'), + $c->query('Request'), + $c->query('UserId'), + $c->query('ServerContainer')->getUserFolder($c->query('UserId')), + $c->query('ServerContainer')->getConfig(), + $c->getServer()->getShareManager(), + $c->getServer()->getAppManager(), + $c->getServer()->getUserManager(), + $c->getServer()->getGroupManager(), + $c->query('ServerContainer')->getL10N($c->query('AppName')), + $c->query('ServerContainer')->getLogger(), + $c->query('ServerContainer')->getDateTimeZone() + ); + }); $container->registerService( 'TracksController', function ($c) { - return new TracksController( - $c->query('AppName'), - $c->query('Request'), - $c->query('UserId'), - $c->query('ServerContainer')->getUserFolder($c->query('UserId')), - $c->query('ServerContainer')->getConfig(), - $c->getServer()->getShareManager(), - $c->getServer()->getAppManager(), - $c->getServer()->getUserManager(), - $c->getServer()->getGroupManager(), - $c->query('ServerContainer')->getL10N($c->query('AppName')), + return new TracksController( + $c->query('AppName'), + $c->query('Request'), + $c->query('UserId'), + $c->query('ServerContainer')->getUserFolder($c->query('UserId')), + $c->query('ServerContainer')->getConfig(), + $c->getServer()->getShareManager(), + $c->getServer()->getAppManager(), + $c->getServer()->getUserManager(), + $c->getServer()->getGroupManager(), + $c->query('ServerContainer')->getL10N($c->query('AppName')), + $c->query('ServerContainer')->getLogger(), + new TracksService( $c->query('ServerContainer')->getLogger(), - new TracksService( - $c->query('ServerContainer')->getLogger(), - $c->query('ServerContainer')->getL10N($c->query('AppName')), - $c->query('ServerContainer')->getRootFolder(), - $c->getServer()->getShareManager() - ) - ); - } - ); + $c->query('ServerContainer')->getL10N($c->query('AppName')), + $c->query('ServerContainer')->getRootFolder(), + $c->getServer()->getShareManager() + ) + ); + }); $container->registerService( 'UtilsController', function ($c) { diff --git a/appinfo/routes.php b/appinfo/routes.php index 2186abd14..1e28dff28 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -12,6 +12,7 @@ ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], ['name' => 'page#do_echo', 'url' => '/echo', 'verb' => 'POST'], ['name' => 'page#openGeoLink', 'url' => '/openGeoLink/{url}', 'verb' => 'GET'], + ['name' => 'public_page#sharedFavoritesCategory', 'url' => '/s/favorites/{token}', 'verb' => 'GET'], // utils @@ -47,6 +48,18 @@ ['name' => 'favorites_api#editFavorite', 'url' => '/api/{apiversion}/favorites/{id}', 'verb' => 'PUT'], ['name' => 'favorites_api#deleteFavorite', 'url' => '/api/{apiversion}/favorites/{id}', 'verb' => 'DELETE'], + // public favorites API + [ + 'name' => 'favorites_api#preflighted_cors', + 'url' => '/api/1.0/public/favorites{path}', + 'verb' => 'OPTIONS', + 'requirements' => ['path' => '.+'] + ], + ['name' => 'public_favorites_api#getFavorites', 'url' => '/api/1.0/public/{token}/favorites', 'verb' => 'GET'], + ['name' => 'public_favorites_api#addFavorite', 'url' => '/api/1.0/public/{token}/favorites', 'verb' => 'POST'], + ['name' => 'public_favorites_api#editFavorite', 'url' => '/api/1.0/public/{token}/favorites/{id}', 'verb' => 'PUT'], + ['name' => 'public_favorites_api#deleteFavorite', 'url' => '/api/1.0/public/{token}/favorites/{id}', 'verb' => 'DELETE'], + // favorites ['name' => 'favorites#getFavorites', 'url' => '/favorites', 'verb' => 'GET'], ['name' => 'favorites#addFavorite', 'url' => '/favorites', 'verb' => 'POST'], @@ -54,6 +67,9 @@ ['name' => 'favorites#deleteFavorite', 'url' => '/favorites/{id}', 'verb' => 'DELETE'], ['name' => 'favorites#deleteFavorites', 'url' => '/favorites', 'verb' => 'DELETE'], ['name' => 'favorites#renameCategories', 'url' => '/favorites-category', 'verb' => 'PUT'], + ['name' => 'favorites#getSharedCategories', 'url' => '/favorites-category/shared-categories', 'verb' => 'GET'], + ['name' => 'favorites#shareCategory', 'url' => '/favorites-category/{category}/share', 'verb' => 'POST'], + ['name' => 'favorites#unShareCategory', 'url' => '/favorites-category/{category}/un-share', 'verb' => 'POST'], ['name' => 'favorites#exportFavorites', 'url' => '/export/favorites', 'verb' => 'POST'], ['name' => 'favorites#importFavorites', 'url' => '/import/favorites', 'verb' => 'POST'], @@ -73,7 +89,7 @@ ['name' => 'devices_api#getDevices', 'url' => '/api/{apiversion}/devices', 'verb' => 'GET'], ['name' => 'devices_api#getDevicePoints', 'url' => '/api/{apiversion}/devices/{id}', 'verb' => 'GET'], ['name' => 'devices_api#addDevicePoint', 'url' => '/api/{apiversion}/devices', 'verb' => 'POST'], - ['name' => 'devices_api#editDevice', 'url' => '/api/{apiversion}/devices/{id}', 'verb' => 'PUT'], + ['name' => 'devices_api#editDevice', 'url' => '/api/{apiversion}/devices/{id}', 'verbGET' => 'PUT'], ['name' => 'devices_api#deleteDevice', 'url' => '/api/{apiversion}/devices/{id}', 'verb' => 'DELETE'], // devices diff --git a/css/style.scss b/css/style.scss index bf4576cd6..529dea664 100644 --- a/css/style.scss +++ b/css/style.scss @@ -808,3 +808,33 @@ tr.selected td { .ui-autocomplete { z-index: 9999; } + +.category-line .category-sharing-dialogue { + width: 100%; + background: rgba(#000, .1); + padding: 0.5em; + display: none; + + .category-sharing-checkbox-container { + display: flex; + align-items: center; + + .category-sharing-checkbox { + margin-right: 1em; + cursor: pointer; + } + } + + .category-sharing-link { + width: 100%; + visibility: hidden; + + &.visible { + visibility: visible; + } + } +} + +.category-line.sharingDialogueVisible .category-sharing-dialogue { + display: block; +} diff --git a/js/favoritesController.js b/js/favoritesController.js index 41e3ec9f4..55f19f272 100644 --- a/js/favoritesController.js +++ b/js/favoritesController.js @@ -24,6 +24,7 @@ function FavoritesController(optionsController, timeFilterController) { this.lastUsedCategory = null; this.movingFavoriteId = null; + this.sharingFavoriteId = null; // used by optionsController to know if favorite loading // was done before or after option restoration @@ -110,6 +111,59 @@ FavoritesController.prototype = { } that.enterAddFavoriteMode(cat); }); + // Open or close sharing dialogue for favorites category + $('body').on('click', '.categoryShareButton', function(e) { + var category = $(this).parent().parent().parent().attr('category'); + var openCategory = that.sharingFavoriteId; + + if (openCategory !== null) { + that.leaveSharingFavoriteMode(); + } + + if (openCategory !== category) { + that.enterSharingFavoriteMode(category); + } + }); + + $('body').on('change', '.category-sharing-checkbox', function(e) { + var category = $(this).parent().parent().parent().attr('category'); + + var shareDialogue = $(this).parent().parent(); + + if (!this.checked) { + $.ajax({ + type: 'POST', + url: OC.generateUrl('/apps/maps/favorites-category/' + category + '/un-share'), + data: {}, + async: true + }).done(function (response) { + const linkEl = shareDialogue.children('.category-sharing-link'); + + linkEl.val(''); + linkEl.removeClass('visible'); + }).always(function () { + + }).fail(function() { + OC.Notification.showTemporary(t('maps', 'Failed to share favorites category')); + }); + } else { + $.ajax({ + type: 'POST', + url: OC.generateUrl('/apps/maps/favorites-category/' + category + '/share'), + data: {}, + async: true + }).done(function (response) { + const linkEl = shareDialogue.children('.category-sharing-link'); + + linkEl.val(OC.generateUrl('/apps/maps/s/favorites/' + response.token)); + linkEl.addClass('visible'); + }).always(function () { + + }).fail(function() { + OC.Notification.showTemporary(t('maps', 'Failed to share favorites category')); + }); + } + }); // cancel favorite edition $('body').on('click', '.canceleditfavorite', function(e) { that.map.closePopup(); @@ -409,34 +463,56 @@ FavoritesController.prototype = { getFavorites: function() { var that = this; $('#navigation-favorites').addClass('icon-loading-small'); - var req = {}; - var url = OC.generateUrl('/apps/maps/favorites'); - $.ajax({ + + var favorites = []; + var sharedCategories = []; + + $.when( + $.ajax({ + url: OC.generateUrl('/apps/maps/favorites'), + data: {}, type: 'GET', - url: url, - data: req, - async: true - }).done(function (response) { - var fav, marker, cat, color; - for (var i=0; i < response.length; i++) { - fav = response[i]; - that.addFavoriteMap(fav); + async: true, + success: function(response) { + favorites = response; + }, + fail: function(response) { + OC.Notification.showTemporary(t('maps', 'Failed to load favorites')); + } + }), $.ajax({ + url: OC.generateUrl('/apps/maps/favorites-category/shared-categories'), + data: {}, + type: 'GET', + async: true, + success: function(response) { + for (var i = 0; i < response.length; i++) { + sharedCategories[response[i].category] = response[i].token; + } + }, + fail: function(response) { + OC.Notification.showTemporary(t('maps', 'Failed to load favorite share token')); } + }) + ).then(function() { + for (var i=0; i < favorites.length; i++) { + var token = sharedCategories[favorites[i].category] || null; + + that.addFavoriteMap(favorites[i], true, false, token); + } + that.updateCategoryCounters(); that.favoritesLoaded = true; that.updateTimeFilterRange(); that.timeFilterController.setSliderToMaxInterval(); - }).always(function (response) { + $('#navigation-favorites').removeClass('icon-loading-small'); - }).fail(function() { - OC.Notification.showTemporary(t('maps', 'Failed to load favorites')); }); }, // add category in side menu // add layer // set color and icon - addCategory: function(rawName, enable=false) { + addCategory: function(rawName, enable=false, shareToken = null) { var name = rawName.replace(' ', '-'); // color @@ -472,61 +548,71 @@ FavoritesController.prototype = { // side menu entry var imgurl = OC.generateUrl('/svg/core/actions/star?color='+color); var li = '
  • ' + - ' '+rawName+'' + - '
    ' + - ' ' + - '
    ' + - '
    ' + - ' ' + - '
    ' + - '
    ' + - '
    '+t('maps', 'Category deleted')+'
    ' + - ' ' + - '
    ' + - '
    ' + - '
    ' + - ' ' + - ' ' + - ' ' + - '
    ' + - '
    ' + - '
  • '; + ' '+rawName+'' + + '
    ' + + ' ' + + '
    ' + + '
    ' + + ' ' + + '
    ' + + '
    ' + + '
    '+t('maps', 'Category deleted')+'
    ' + + ' ' + + '
    ' + + '
    ' + + '
    ' + + ' ' + + ' ' + + ' ' + + '
    ' + + '
    ' + + '
    ' + + ' ' + + ' ' + + '
    ' + + ''; var beforeThis = null; var rawLower = rawName.toLowerCase(); @@ -670,6 +756,16 @@ FavoritesController.prototype = { this.addFavoriteCategory = null; }, + enterSharingFavoriteMode: function(categoryName) { + $('#' + categoryName.replace(' ', '-') + '-category').addClass('sharingDialogueVisible'); + this.sharingFavoriteId = categoryName; + }, + + leaveSharingFavoriteMode: function() { + $('#' + this.sharingFavoriteId.replace(' ', '-') + '-category').removeClass('sharingDialogueVisible'); + this.sharingFavoriteId = null; + }, + addFavoriteClickMap: function(e) { var categoryName = this.favoritesController.addFavoriteCategory; if (categoryName === this.favoritesController.defaultCategory && this.favoritesController.lastUsedCategory !== null) { @@ -721,11 +817,11 @@ FavoritesController.prototype = { }, // add a marker to the corresponding layer - addFavoriteMap: function(fav, enableCategory=false, fromUserAction=false) { + addFavoriteMap: function(fav, enableCategory=false, fromUserAction=false, shareToken = null) { // manage category first cat = fav.category; if (!this.categoryLayers.hasOwnProperty(cat)) { - this.addCategory(cat, enableCategory); + this.addCategory(cat, enableCategory, shareToken); if (enableCategory) { this.saveEnabledCategories(); } diff --git a/js/utils.js b/js/utils.js index 8f214a945..ebe2bf9cc 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,323 +1,340 @@ function basename(str) { - var base = new String(str).substring(str.lastIndexOf('/') + 1); - return base; + var base = new String(str).substring(str.lastIndexOf("/") + 1); + return base; } function dirname(path) { - return path.replace(/\\/g,'/').replace(/\/[^\/]*$/, '');; + return path.replace(/\\/g, "/").replace(/\/[^\/]*$/, ""); } function Timer(callback, mydelay) { - var timerId, start, remaining = mydelay; + var timerId, + start, + remaining = mydelay; - this.pause = function() { - window.clearTimeout(timerId); - remaining -= new Date() - start; - }; + this.pause = function() { + window.clearTimeout(timerId); + remaining -= new Date() - start; + }; - this.resume = function() { - start = new Date(); - window.clearTimeout(timerId); - timerId = window.setTimeout(callback, remaining); - }; + this.resume = function() { + start = new Date(); + window.clearTimeout(timerId); + timerId = window.setTimeout(callback, remaining); + }; - this.resume(); + this.resume(); } function getLetterColor(letter1, letter2) { - var letter1Index = letter1.toLowerCase().charCodeAt(0); - var letter2Index = letter2.toLowerCase().charCodeAt(0); - var letterCoef = (letter1Index * letter2Index) % 100 / 100; - var h = letterCoef * 360; - var s = 75 + letterCoef * 10; - var l = 50 + letterCoef * 10; - return {h: Math.round(h), s: Math.round(s), l: Math.round(l)}; + var letter1Index = letter1.toLowerCase().charCodeAt(0); + var letter2Index = letter2.toLowerCase().charCodeAt(0); + var letterCoef = ((letter1Index * letter2Index) % 100) / 100; + var h = letterCoef * 360; + var s = 75 + letterCoef * 10; + var l = 50 + letterCoef * 10; + return { h: Math.round(h), s: Math.round(s), l: Math.round(l) }; } function hslToRgb(h, s, l) { - var r, g, b; + var r, g, b; - if(s == 0){ - r = g = b = l; // achromatic - }else{ - var hue2rgb = function hue2rgb(p, q, t){ - if(t < 0) t += 1; - if(t > 1) t -= 1; - if(t < 1/6) return p + (q - p) * 6 * t; - if(t < 1/2) return q; - if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; - return p; - } + if (s == 0) { + r = g = b = l; // achromatic + } else { + var hue2rgb = function hue2rgb(p, q, t) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; - var q = l < 0.5 ? l * (1 + s) : l + s - l * s; - var p = 2 * l - q; - r = hue2rgb(p, q, h + 1/3); - g = hue2rgb(p, q, h); - b = hue2rgb(p, q, h - 1/3); - } - var rgb = {r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255)}; - var hexStringR = rgb.r.toString(16); - if (hexStringR.length % 2) { - hexStringR = '0' + hexStringR; - } - var hexStringG = rgb.g.toString(16); - if (hexStringG.length % 2) { - hexStringG = '0' + hexStringG; - } - var hexStringB = rgb.b.toString(16); - if (hexStringB.length % 2) { - hexStringB = '0' + hexStringB; - } - return hexStringR+hexStringG+hexStringB; + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + var rgb = { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; + var hexStringR = rgb.r.toString(16); + if (hexStringR.length % 2) { + hexStringR = "0" + hexStringR; + } + var hexStringG = rgb.g.toString(16); + if (hexStringG.length % 2) { + hexStringG = "0" + hexStringG; + } + var hexStringB = rgb.b.toString(16); + if (hexStringB.length % 2) { + hexStringB = "0" + hexStringB; + } + return hexStringR + hexStringG + hexStringB; } function hexToRgb(hex) { - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) - } : null; + } + : null; } function pad(num, size) { - var s = num + ''; - while (s.length < size) s = '0' + s; - return s; + var s = num + ""; + while (s.length < size) s = "0" + s; + return s; } Date.prototype.toIsoString = function() { - var tzo = -this.getTimezoneOffset(), - dif = tzo >= 0 ? '+' : '-', - pad = function(num) { - var norm = Math.floor(Math.abs(num)); - return (norm < 10 ? '0' : '') + norm; - }; - return this.getFullYear() + - '-' + pad(this.getMonth() + 1) + - '-' + pad(this.getDate()) + - ' ' + pad(this.getHours()) + - ':' + pad(this.getMinutes()) + - ':' + pad(this.getSeconds()) + - ' GMT'+dif + pad(tzo / 60) + - ':' + pad(tzo % 60); -} + var tzo = -this.getTimezoneOffset(), + dif = tzo >= 0 ? "+" : "-", + pad = function(num) { + var norm = Math.floor(Math.abs(num)); + return (norm < 10 ? "0" : "") + norm; + }; + return ( + this.getFullYear() + + "-" + + pad(this.getMonth() + 1) + + "-" + + pad(this.getDate()) + + " " + + pad(this.getHours()) + + ":" + + pad(this.getMinutes()) + + ":" + + pad(this.getSeconds()) + + " GMT" + + dif + + pad(tzo / 60) + + ":" + + pad(tzo % 60) + ); +}; function brify(str, linesize) { - var res = ''; - var words = str.split(' '); - var cpt = 0; - var toAdd = ''; - for (var i=0; i'; - toAdd = words[i] + ' '; - cpt = words[i].length + 1; - } + var res = ""; + var words = str.split(" "); + var cpt = 0; + var toAdd = ""; + for (var i = 0; i < words.length; i++) { + if (cpt + words[i].length < linesize) { + toAdd += words[i] + " "; + cpt += words[i].length + 1; + } else { + res += toAdd + "
    "; + toAdd = words[i] + " "; + cpt = words[i].length + 1; } - res += toAdd; - return res; + } + res += toAdd; + return res; } function metersToDistance(m) { - var unit = 'metric'; - var n = parseFloat(m); - if (unit === 'metric') { - if (n > 1000) { - return (n / 1000).toFixed(2) + ' km'; - } - else{ - return n.toFixed(2) + ' m'; - } + var unit = "metric"; + var n = parseFloat(m); + if (unit === "metric") { + if (n > 1000) { + return (n / 1000).toFixed(2) + " km"; + } else { + return n.toFixed(2) + " m"; } - else if (unit === 'english') { - var mi = n * METERSTOMILES; - if (mi < 1) { - return (n * METERSTOFOOT).toFixed(2) + ' ft'; - } - else { - return mi.toFixed(2) + ' mi'; - } - } - else if (unit === 'nautical') { - var nmi = n * METERSTONAUTICALMILES; - return nmi.toFixed(2) + ' nmi'; + } else if (unit === "english") { + var mi = n * METERSTOMILES; + if (mi < 1) { + return (n * METERSTOFOOT).toFixed(2) + " ft"; + } else { + return mi.toFixed(2) + " mi"; } + } else if (unit === "nautical") { + var nmi = n * METERSTONAUTICALMILES; + return nmi.toFixed(2) + " nmi"; + } } function metersToElevation(m) { - var unit = 'metric'; - var n = parseFloat(m); - if (unit === 'metric' || unit === 'nautical') { - return n.toFixed(2) + ' m'; - } - else { - return (n * METERSTOFOOT).toFixed(2) + ' ft'; - } + var unit = "metric"; + var n = parseFloat(m); + if (unit === "metric" || unit === "nautical") { + return n.toFixed(2) + " m"; + } else { + return (n * METERSTOFOOT).toFixed(2) + " ft"; + } } function kmphToSpeed(kmph) { - var unit = 'metric'; - var nkmph = parseFloat(kmph); - if (unit === 'metric') { - return nkmph.toFixed(2) + ' km/h'; - } - else if (unit === 'english') { - return (nkmph * 1000 * METERSTOMILES).toFixed(2) + ' mi/h'; - } - else if (unit === 'nautical') { - return (nkmph * 1000 * METERSTONAUTICALMILES).toFixed(2) + ' kt'; - } + var unit = "metric"; + var nkmph = parseFloat(kmph); + if (unit === "metric") { + return nkmph.toFixed(2) + " km/h"; + } else if (unit === "english") { + return (nkmph * 1000 * METERSTOMILES).toFixed(2) + " mi/h"; + } else if (unit === "nautical") { + return (nkmph * 1000 * METERSTONAUTICALMILES).toFixed(2) + " kt"; + } } function minPerKmToPace(minPerKm) { - var unit = 'metric'; - var nMinPerKm = parseFloat(minPerKm); - if (unit === 'metric') { - return nMinPerKm.toFixed(2) + ' min/km'; - } - else if (unit === 'english') { - return (nMinPerKm / 1000 / METERSTOMILES).toFixed(2) + ' min/mi'; - } - else if (unit === 'nautical') { - return (nMinPerKm / 1000 / METERSTONAUTICALMILES).toFixed(2) + ' min/nmi'; - } + var unit = "metric"; + var nMinPerKm = parseFloat(minPerKm); + if (unit === "metric") { + return nMinPerKm.toFixed(2) + " min/km"; + } else if (unit === "english") { + return (nMinPerKm / 1000 / METERSTOMILES).toFixed(2) + " min/mi"; + } else if (unit === "nautical") { + return (nMinPerKm / 1000 / METERSTONAUTICALMILES).toFixed(2) + " min/nmi"; + } } -function formatTimeSeconds(time_s){ - var minutes = Math.floor(time_s / 60); - var hours = Math.floor(minutes / 60); +function formatTimeSeconds(time_s) { + var minutes = Math.floor(time_s / 60); + var hours = Math.floor(minutes / 60); - var ph = pad(hours, 2); - var pm = pad(minutes % 60, 2); - var ps = pad(time_s % 60, 2); - return `${ph}:${pm}:${ps}`; + var ph = pad(hours, 2); + var pm = pad(minutes % 60, 2); + var ps = pad(time_s % 60, 2); + return `${ph}:${pm}:${ps}`; } function isComputer(name) { - return ( name.match(/windows/i) - || name.match(/gnu\/linux/i) - || name.match(/mac\s?os/i) - || name.match(/chromium\s?os/i) - || name.match(/ubuntu/i) - ); + return ( + name.match(/windows/i) || + name.match(/gnu\/linux/i) || + name.match(/mac\s?os/i) || + name.match(/chromium\s?os/i) || + name.match(/ubuntu/i) + ); } function isPhone(name) { - return (name.match(/blackberry/i) - || name.match(/symbian/i) - || name.match(/phonetrack/i) - || name.match(/firefox\s?os/i) - || name.match(/android/i) - || name.match(/ios/i) - || name.match(/windows\s?mobile/i) - ); + return ( + name.match(/blackberry/i) || + name.match(/symbian/i) || + name.match(/phonetrack/i) || + name.match(/firefox\s?os/i) || + name.match(/android/i) || + name.match(/ios/i) || + name.match(/windows\s?mobile/i) + ); } function getDeviceInfoFromUserAgent2(ua) { - var res = { - os: null, - client: null, - }; - var parser = new UAParser(ua); - var uap = parser.getResult(); - if (uap.os && uap.os.name) { - res.os = uap.os.name.replace('Linux', 'GNU/Linux').replace('windows', 'Windows'); - } - if (uap.browser && uap.browser.name) { - res.client = uap.browser.name.replace('chrome', 'Chrome'); - } - return res; + var res = { + os: null, + client: null + }; + var parser = new UAParser(ua); + var uap = parser.getResult(); + if (uap.os && uap.os.name) { + res.os = uap.os.name + .replace("Linux", "GNU/Linux") + .replace("windows", "Windows"); + } + if (uap.browser && uap.browser.name) { + res.client = uap.browser.name.replace("chrome", "Chrome"); + } + return res; } function getDeviceInfoFromUserAgent(ua) { - var res = { - os: null, - client: null, - clientVersion: null - }; - var m; - // OS - if (ua.match(/x11/i) || ua.match(/linux/i)) { - res.os = 'GNU/Linux'; - } - else if (ua.match(/android/i)) { - res.os = 'Android'; - } - else if (ua.match(/windows/i)) { - res.os = 'Windows'; - } - else if (ua.match(/iphone/i)) { - res.os = 'IOS'; - } - else if (ua.match(/macintosh/i) || ua.match(/darwin/i)) { - res.os = 'MacOS'; - } - // BROWSER - if (ua.match(/firefox\//i) && !ua.match(/seamonkey\//i)) { - res.client = 'Firefox'; - m = ua.match(/firefox\/([0-9.]*)/i); - if (m.length > 1) { - res.clientVersion = m[1]; - } + var res = { + os: null, + client: null, + clientVersion: null + }; + var m; + // OS + if (ua.match(/x11/i) || ua.match(/linux/i)) { + res.os = "GNU/Linux"; + } else if (ua.match(/android/i)) { + res.os = "Android"; + } else if (ua.match(/windows/i)) { + res.os = "Windows"; + } else if (ua.match(/iphone/i)) { + res.os = "IOS"; + } else if (ua.match(/macintosh/i) || ua.match(/darwin/i)) { + res.os = "MacOS"; + } + // BROWSER + if (ua.match(/firefox\//i) && !ua.match(/seamonkey\//i)) { + res.client = "Firefox"; + m = ua.match(/firefox\/([0-9.]*)/i); + if (m.length > 1) { + res.clientVersion = m[1]; } - else if (ua.match(/safari\//i) && !ua.match(/chrome\//i) && !ua.match(/chromium\//i)) { - res.client = 'Safari'; - m = ua.match(/safari\/([0-9.]*)/i); - if (m.length > 1) { - res.clientVersion = m[1]; - } + } else if ( + ua.match(/safari\//i) && + !ua.match(/chrome\//i) && + !ua.match(/chromium\//i) + ) { + res.client = "Safari"; + m = ua.match(/safari\/([0-9.]*)/i); + if (m.length > 1) { + res.clientVersion = m[1]; } - else if (ua.match(/chrome\//i) && !ua.match(/chromium\//i)) { - res.client = 'Chrome'; - m = ua.match(/chrome\/([0-9.]*)/i); - if (m.length > 1) { - res.clientVersion = m[1]; - } + } else if (ua.match(/chrome\//i) && !ua.match(/chromium\//i)) { + res.client = "Chrome"; + m = ua.match(/chrome\/([0-9.]*)/i); + if (m.length > 1) { + res.clientVersion = m[1]; } - else if (ua.match(/chromium\//i)) { - res.client = 'Chromium'; - m = ua.match(/chromium\/([0-9.]*)/i); - if (m.length > 1) { - res.clientVersion = m[1]; - } + } else if (ua.match(/chromium\//i)) { + res.client = "Chromium"; + m = ua.match(/chromium\/([0-9.]*)/i); + if (m.length > 1) { + res.clientVersion = m[1]; } - else if (ua.match(/opr\//i)) { - res.client = 'Opera'; - m = ua.match(/opr\/([0-9.]*)/i); - if (m.length > 1) { - res.clientVersion = m[1]; - } + } else if (ua.match(/opr\//i)) { + res.client = "Opera"; + m = ua.match(/opr\/([0-9.]*)/i); + if (m.length > 1) { + res.clientVersion = m[1]; } - return res; + } + return res; } function getUrlParameter(sParam) { - var sPageURL = window.location.search.substring(1); - var sURLVariables = sPageURL.split('&'); - for (var i = 0; i < sURLVariables.length; i++) { - var sParameterName = sURLVariables[i].split('='); - if (sParameterName[0] === sParam) { - return decodeURIComponent(sParameterName[1]); - } + var sPageURL = window.location.search.substring(1); + var sURLVariables = sPageURL.split("&"); + for (var i = 0; i < sURLVariables.length; i++) { + var sParameterName = sURLVariables[i].split("="); + if (sParameterName[0] === sParam) { + return decodeURIComponent(sParameterName[1]); } + } } function formatAddress(address) { - var strAddress = - (address.attraction || '')+' '+ - (address.house_number || '')+' '+ - (address.road || '')+' '+ - (address.pedestrian || '')+' '+ - (address.suburb || '')+' '+ - (address.city_district || '')+' '+ - (address.postcode || '')+' '+ - (address.village || address.town || address.city || '')+' '+ - (address.state || '')+' '+ - (address.country || ''); - return strAddress.replace(/\s+/g, ' ').trim(); + var strAddress = + (address.attraction || "") + + " " + + (address.house_number || "") + + " " + + (address.road || "") + + " " + + (address.pedestrian || "") + + " " + + (address.suburb || "") + + " " + + (address.city_district || "") + + " " + + (address.postcode || "") + + " " + + (address.village || address.town || address.city || "") + + " " + + (address.state || "") + + " " + + (address.country || ""); + return strAddress.replace(/\s+/g, " ").trim(); } diff --git a/lib/Controller/FavoritesController.php b/lib/Controller/FavoritesController.php index 287f67a43..4e33bd2de 100644 --- a/lib/Controller/FavoritesController.php +++ b/lib/Controller/FavoritesController.php @@ -33,6 +33,7 @@ use OCP\IDateTimeZone; use OCA\Maps\Service\FavoritesService; +use Punic\Data; //use function OCA\Maps\Service\endswith; @@ -89,6 +90,15 @@ public function getFavorites() { return new DataResponse($favorites); } + /** + * @NoAdminRequired + */ + public function getSharedCategories() { + $categories = $this->favoritesService->getSharedCategories($this->userId); + + return new DataResponse($categories); + } + /** * @NoAdminRequired */ @@ -125,6 +135,28 @@ public function editFavorite($id, $name, $lat, $lng, $category, $comment, $exten } } + public function shareCategory($category) { + // TODO: use better way to check if user owns category + if ($this->favoritesService->countFavorites($this->userId, [$category], null, null) === 0) { + return new DataResponse("Category does not exist", 400); + } + + $response = $this->favoritesService->getCategoryShareLink($this->userId, $category); + + return new DataResponse($response); + } + + public function unShareCategory($category) { + // TODO: use better way to check if user owns category + if ($this->favoritesService->countFavorites($this->userId, [$category], null, null) === 0) { + return new DataResponse("Category does not exist", 400); + } + + $response = $this->favoritesService->deleteCategoryShareLink($this->userId, $category); + + return new DataResponse($response); + } + /** * @NoAdminRequired */ diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 8e5c02657..888946c01 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -14,6 +14,7 @@ use OCP\IConfig; use OCP\IRequest; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Http\Template\PublicTemplateResponse; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Controller; use OCP\IInitialStateService; @@ -47,6 +48,44 @@ public function index() { $params = array('user' => $this->userId); $this->initialStateService->provideInitialState($this->appName, 'photos', $this->config->getAppValue('photos', 'enabled', 'no') === 'yes'); $response = new TemplateResponse('maps', 'index', $params); + + $this->addCsp($response); + + return $response; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function openGeoLink($url) { + $params = array('user' => $this->userId); + $params["geourl"] = $url; + $response = new TemplateResponse('maps', 'index', $params); + if (class_exists('OCP\AppFramework\Http\ContentSecurityPolicy')) { + $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); + // map tiles + $csp->addAllowedImageDomain('https://*.tile.openstreetmap.org'); + $csp->addAllowedImageDomain('https://server.arcgisonline.com'); + $csp->addAllowedImageDomain('https://*.cartocdn.com'); + $csp->addAllowedImageDomain('https://*.opentopomap.org'); + $csp->addAllowedImageDomain('https://*.cartocdn.com'); + $csp->addAllowedImageDomain('https://*.ssl.fastly.net'); + $csp->addAllowedImageDomain('https://*.openstreetmap.se'); + // routing engine + $csp->addAllowedConnectDomain('https://*.project-osrm.org'); + // TODO allow connections to router engine + //$csp->addAllowedConnectDomain('http://192.168.0.66:8989'); + // poi images + $csp->addAllowedImageDomain('https://nominatim.openstreetmap.org'); + // search and geocoder + $csp->addAllowedConnectDomain('https://nominatim.openstreetmap.org'); + $response->setContentSecurityPolicy($csp); + } + return $response; + } + + private function addCsp($response) { if (class_exists('OCP\AppFramework\Http\ContentSecurityPolicy')) { $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); // map tiles @@ -91,38 +130,5 @@ public function index() { $csp->addAllowedConnectDomain('https://nominatim.openstreetmap.org'); $response->setContentSecurityPolicy($csp); } - return $response; - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function openGeoLink($url) { - $params = array('user' => $this->userId); - $params["geourl"] = $url; - $response = new TemplateResponse('maps', 'index', $params); - if (class_exists('OCP\AppFramework\Http\ContentSecurityPolicy')) { - $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); - // map tiles - $csp->addAllowedImageDomain('https://*.tile.openstreetmap.org'); - $csp->addAllowedImageDomain('https://server.arcgisonline.com'); - $csp->addAllowedImageDomain('https://*.cartocdn.com'); - $csp->addAllowedImageDomain('https://*.opentopomap.org'); - $csp->addAllowedImageDomain('https://*.cartocdn.com'); - $csp->addAllowedImageDomain('https://*.ssl.fastly.net'); - $csp->addAllowedImageDomain('https://*.openstreetmap.se'); - // routing engine - $csp->addAllowedConnectDomain('https://*.project-osrm.org'); - // TODO allow connections to router engine - //$csp->addAllowedConnectDomain('http://192.168.0.66:8989'); - // poi images - $csp->addAllowedImageDomain('https://nominatim.openstreetmap.org'); - // search and geocoder - $csp->addAllowedConnectDomain('https://nominatim.openstreetmap.org'); - $response->setContentSecurityPolicy($csp); - } - return $response; } - } diff --git a/lib/Controller/PublicFavoritesApiController.php b/lib/Controller/PublicFavoritesApiController.php new file mode 100644 index 000000000..3eebf7c6d --- /dev/null +++ b/lib/Controller/PublicFavoritesApiController.php @@ -0,0 +1,164 @@ +config = $config; + $this->favoritesService = $favoritesService; + $this->userId = $userId; + + $this->qb = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + } + + public function getPasswordHash(): string { + return ""; // TODO: + } + + protected function isPasswordProtected(): bool { + return false; // TODO + } + + public function isValidToken(): bool { + return $this->favoritesService->getFavoritesShare($this->getToken()) !== null; + } + + /** + * @param $token + * @return DataResponse + * + * @PublicPage + * @Cors + */ + public function getFavorites($token) { + if ($token === '') { + return new DataResponse('Invalid token', Http::STATUS_BAD_REQUEST); + } + + $favorites = $this->favoritesService->getFavoritesByShareToken($token); + + if ($favorites === false) { + return new DataResponse('Not found', Http::STATUS_NOT_FOUND); + } + + return new DataResponse($favorites); + } + + public function addFavorite($lat, $lng, $name, $comment, $extensions) { + + $share = $this->favoritesService->getFavoritesShare($this->getToken()); + $category = $share['category']; + + + if (is_numeric($lat) && is_numeric($lng)) { + $favoriteId = $this->favoritesService->addFavoriteToDB($this->userId, $name, $lat, $lng, $category, $comment, $extensions); + $favorite = $this->favoritesService->getFavoriteFromDB($favoriteId); + return new DataResponse($favorite); + } + else { + return new DataResponse('invalid values', 400); + } + } + + public function editFavorite($id, $lat, $lng, $name, $comment, $extensions) { + $share = $this->favoritesService->getFavoritesShare($this->getToken()); + + //TODO: can $share['owner'] and/or $share['category'] be exploited to be null? + + $favorite = $this->favoritesService->getFavoriteFromDB($id, $share['owner'], $share['category']); + + if ($favorite !== null) { + if (($lat === null || is_numeric($lat)) && + ($lng === null || is_numeric($lng)) + ) { + $this->favoritesService->editFavoriteInDB($id, $name, $lat, $lng, $favorite['category'], $comment, $extensions); + $editedFavorite = $this->favoritesService->getFavoriteFromDB($id); + + return new DataResponse($editedFavorite); + } + else { + return new DataResponse('invalid values', 400); + } + } + else { + return new DataResponse('no such favorite', 400); + } + } + + public function deleteFavorite($id) { + $share = $this->favoritesService->getFavoritesShare($this->getToken()); + + //TODO: can $share['owner'] and/or $share['category'] be exploited to be null? + + $favorite = $this->favoritesService->getFavoriteFromDB($id, $share['owner'], $share['category']); + + if ($favorite !== null) { + $this->favoritesService->deleteFavoriteFromDB($id); + return new DataResponse('deleted'); + } + else { + return new DataResponse('no such favorite', 400); + } + } + + /*private function addCsp($response) { + if (class_exists('OCP\AppFramework\Http\ContentSecurityPolicy')) { + $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); + // map tiles + $csp->addAllowedImageDomain('https://*.tile.openstreetmap.org'); + $csp->addAllowedImageDomain('https://server.arcgisonline.com'); + $csp->addAllowedImageDomain('https://*.cartocdn.com'); + $csp->addAllowedImageDomain('https://*.opentopomap.org'); + $csp->addAllowedImageDomain('https://*.cartocdn.com'); + $csp->addAllowedImageDomain('https://*.ssl.fastly.net'); + $csp->addAllowedImageDomain('https://*.openstreetmap.se'); + + // default routing engine + $csp->addAllowedConnectDomain('https://*.project-osrm.org'); + $csp->addAllowedConnectDomain('https://api.mapbox.com'); + $csp->addAllowedConnectDomain('https://graphhopper.com'); + // allow connections to custom routing engines + $urlKeys = [ + 'osrmBikeURL', + 'osrmCarURL', + 'osrmFootURL', + 'graphhopperURL' + ]; + foreach ($urlKeys as $key) { + $url = $this->config->getAppValue('maps', $key); + if ($url !== '') { + $scheme = parse_url($url, PHP_URL_SCHEME); + $host = parse_url($url, PHP_URL_HOST); + $port = parse_url($url, PHP_URL_PORT); + $cleanUrl = $scheme . '://' . $host; + if ($port && $port !== '') { + $cleanUrl .= ':' . $port; + } + $csp->addAllowedConnectDomain($cleanUrl); + } + } + //$csp->addAllowedConnectDomain('http://192.168.0.66:5000'); + + // poi images + $csp->addAllowedImageDomain('https://nominatim.openstreetmap.org'); + // search and geocoder + $csp->addAllowedConnectDomain('https://nominatim.openstreetmap.org'); + $response->setContentSecurityPolicy($csp); + } + }*/ +} \ No newline at end of file diff --git a/lib/Controller/PublicPageController.php b/lib/Controller/PublicPageController.php new file mode 100644 index 000000000..1c7687684 --- /dev/null +++ b/lib/Controller/PublicPageController.php @@ -0,0 +1,148 @@ + + * @copyright Vinzenz Rosenkranz 2017 + */ + +namespace OCA\Maps\Controller; + +use OC\User\Manager; +use OCA\Maps\Service\FavoritesService; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IConfig; +use OCP\ILogger; +use OCP\IRequest; +use OCP\ISession; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Template\PublicTemplateResponse; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\PublicShareController; + +class PublicPageController extends PublicShareController +{ + private $config; + private $logger; + private $favoritesService; + + public function __construct($appName, IRequest $request, ISession $session, IConfig $config, ILogger $logger, FavoritesService $favoritesService) + { + parent::__construct($appName, $request, $session); + $this->config = $config; + $this->logger = $logger; + $this->favoritesService = $favoritesService; + } + + /** + * @param $token + * + * @return DataResponse|PublicTemplateResponse + * + * @PublicPage + * @NoCSRFRequired + */ + public function sharedFavoritesCategory($token) { + if ($token === '') { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + $share = $this->favoritesService->getFavoritesShare($token); + + + $response = new PublicTemplateResponse('maps', 'public/favorites_index', []); + + if ($share !== false) { + $ownerName = \OC::$server->getUserManager()->get($share['owner'])->getDisplayName(); + + $response->setHeaderTitle($share['category']); + $response->setHeaderDetails('shared by ' . $ownerName); + } + + $this->addCsp($response); + + return $response; + } + + /** + * Get a hash of the password for this share + * + * To ensure access is blocked when the password to a share is changed we store + * a hash of the password for this token. + * + * @since 14.0.0 + */ + protected function getPasswordHash(): string { + return ""; // TODO: + } + + /** + * Is the provided token a valid token + * + * This function is already called from the middleware directly after setting the token. + * + * @since 14.0.0 + */ + public function isValidToken(): bool { + return $this->favoritesService->getFavoritesShare($this->getToken()) !== null; + } + + /** + * Is a share with this token password protected + * + * @since 14.0.0 + */ + protected function isPasswordProtected(): bool { + return false; // TODO + } + + private function addCsp($response) + { + if (class_exists('OCP\AppFramework\Http\ContentSecurityPolicy')) { + $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); + // map tiles + $csp->addAllowedImageDomain('https://*.tile.openstreetmap.org'); + $csp->addAllowedImageDomain('https://server.arcgisonline.com'); + $csp->addAllowedImageDomain('https://*.cartocdn.com'); + $csp->addAllowedImageDomain('https://*.opentopomap.org'); + $csp->addAllowedImageDomain('https://*.cartocdn.com'); + $csp->addAllowedImageDomain('https://*.ssl.fastly.net'); + $csp->addAllowedImageDomain('https://*.openstreetmap.se'); + + // default routing engine + $csp->addAllowedConnectDomain('https://*.project-osrm.org'); + $csp->addAllowedConnectDomain('https://api.mapbox.com'); + $csp->addAllowedConnectDomain('https://graphhopper.com'); + // allow connections to custom routing engines + $urlKeys = [ + 'osrmBikeURL', + 'osrmCarURL', + 'osrmFootURL', + 'graphhopperURL' + ]; + foreach ($urlKeys as $key) { + $url = $this->config->getAppValue('maps', $key); + if ($url !== '') { + $scheme = parse_url($url, PHP_URL_SCHEME); + $host = parse_url($url, PHP_URL_HOST); + $port = parse_url($url, PHP_URL_PORT); + $cleanUrl = $scheme . '://' . $host; + if ($port && $port !== '') { + $cleanUrl .= ':' . $port; + } + $csp->addAllowedConnectDomain($cleanUrl); + } + } + //$csp->addAllowedConnectDomain('http://192.168.0.66:5000'); + + // poi images + $csp->addAllowedImageDomain('https://nominatim.openstreetmap.org'); + // search and geocoder + $csp->addAllowedConnectDomain('https://nominatim.openstreetmap.org'); + $response->setContentSecurityPolicy($csp); + } + } +} diff --git a/lib/Migration/Version000106Date20191118221134.php b/lib/Migration/Version000106Date20191118221134.php new file mode 100644 index 000000000..7b4dbd2cb --- /dev/null +++ b/lib/Migration/Version000106Date20191118221134.php @@ -0,0 +1,68 @@ +hasTable('maps_favorite_shares')) { + $table = $schema->createTable('maps_favorite_shares'); + $table->addColumn('id', 'bigint', [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 41, + ]); + $table->addColumn('owner', 'string', [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('category', 'string', [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('token', 'string', [ + 'notnull' => true, + 'length' => 64, + ]); + + $table->setPrimaryKey(['id']); + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) { + } +} diff --git a/lib/Service/FavoritesService.php b/lib/Service/FavoritesService.php index 81dba7137..47429a5a4 100644 --- a/lib/Service/FavoritesService.php +++ b/lib/Service/FavoritesService.php @@ -15,17 +15,20 @@ use OCP\IL10N; use OCP\ILogger; use OCP\DB\QueryBuilder\IQueryBuilder; +use OC\Security\SecureRandom; use OC\Archive\ZIP; //use function \OCA\Maps\Service\endswith; -class FavoritesService { +class FavoritesService +{ private $l10n; private $logger; private $qb; private $dbconnection; + private $secureRandom; private $currentFavorite; private $currentFavoritesList; @@ -35,23 +38,137 @@ class FavoritesService { private $kmlInsidePlacemark; private $kmlCurrentCategory; - public function __construct (ILogger $logger, IL10N $l10n) { + public function __construct(ILogger $logger, IL10N $l10n, SecureRandom $secureRandom) + { $this->l10n = $l10n; $this->logger = $logger; + $this->secureRandom = $secureRandom; $this->qb = \OC::$server->getDatabaseConnection()->getQueryBuilder(); $this->dbconnection = \OC::$server->getDatabaseConnection(); } - private function db_quote_escape_string($str){ + private function db_quote_escape_string($str) + { return $this->dbconnection->quote($str); } + public function getFavoritesShare($token) { + $qb = $this->qb; + + $qb->select("owner", "category") + ->from("maps_favorite_shares") + ->where( + $qb->expr()->eq('token', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR)) + ); + + $req = $qb->execute(); + + $row = $req->fetch(); + + if ($row == false) { + return null; + } + + $response = [ + 'owner' => $row['owner'], + 'category' => $row['category'] + ]; + + $qb->resetQueryParts(); + + return $response; + } + + /** + * @param $token + * @return bool | array + */ + public function getFavoritesByShareToken($token) { + $favorites = []; + $qb = $this->qb; + + $qb->select('id', 'owner', 'category', 'token') + ->from('maps_favorite_shares') + ->where( + $qb->expr()->eq('token', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR)) + ); + + $req = $qb->execute(); + + if ($req->rowCount() === 0) { + return false; + } + + $row = $req->fetch(); + + $id = $row['id']; + $type = $row['type']; + $userId = $row['owner']; + $category = $row['category']; + + $req->closeCursor(); + $qb->resetQueryParts(); + + $qb->select('id', 'name', 'date_created', 'date_modified', 'lat', 'lng', 'category', 'comment', 'extensions') + ->from('maps_favorites', 'f') + ->where( + $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) + )->andWhere( + $qb->expr()->eq('category', $qb->createNamedParameter($category, IQueryBuilder::PARAM_STR)) + ); + + $req = $qb->execute(); + + while ($row = $req->fetch()) { + array_push($favorites, [ + 'id' => intval($row['id']), + 'name' => $row['name'], + 'date_modified' => intval($row['date_modified']), + 'date_created' => intval($row['date_created']), + 'lat' => floatval($row['lat']), + 'lng' => floatval($row['lng']), + 'category' => $row['category'], + 'comment' => $row['comment'], + 'extensions' => $row['extensions'] + ]); + } + $req->closeCursor(); + $qb->resetQueryParts(); + + return $favorites; + } + + public function getSharedCategories($owner) { + $sharedCategories = []; + + $qb = $this->qb; + $qb->select('category', 'token') + ->from('maps_favorite_shares') + ->where( + $qb->expr()->eq('owner', $qb->createNamedParameter($owner, IQueryBuilder::PARAM_STR)) + ); + $req = $qb->execute(); + + while ($row = $req->fetch()) { + array_push($sharedCategories, [ + 'category' => $row['category'], + 'token' => $row['token'] + ]); + } + + $req->closeCursor(); + $qb->resetQueryParts(); + + return $sharedCategories; + } + /** * @param string $userId * @param int $pruneBefore * @return array with favorites */ - public function getFavoritesFromDB($userId, $pruneBefore=0) { + public function getFavoritesFromDB($userId, $pruneBefore = 0) + { $favorites = []; $qb = $this->qb; $qb->select('id', 'name', 'date_created', 'date_modified', 'lat', 'lng', 'category', 'comment', 'extensions') @@ -93,7 +210,8 @@ public function getFavoritesFromDB($userId, $pruneBefore=0) { return $favorites; } - public function getFavoriteFromDB($id, $userId=null) { + public function getFavoriteFromDB($id, $userId = null, $category = null) + { $favorite = null; $qb = $this->qb; $qb->select('id', 'name', 'date_modified', 'date_created', 'lat', 'lng', 'category', 'comment', 'extensions') @@ -106,6 +224,11 @@ public function getFavoriteFromDB($id, $userId=null) { $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) ); } + if ($category !== null) { + $qb->andWhere( + $qb->expr()->eq('category', $qb->createNamedParameter($category, IQueryBuilder::PARAM_STR)) + ); + } $req = $qb->execute(); while ($row = $req->fetch()) { @@ -136,7 +259,8 @@ public function getFavoriteFromDB($id, $userId=null) { return $favorite; } - public function addFavoriteToDB($userId, $name, $lat, $lng, $category, $comment, $extensions) { + public function addFavoriteToDB($userId, $name, $lat, $lng, $category, $comment, $extensions) + { $nowTimeStamp = (new \DateTime())->getTimestamp(); $qb = $this->qb; $qb->insert('maps_favorites') @@ -157,7 +281,8 @@ public function addFavoriteToDB($userId, $name, $lat, $lng, $category, $comment, return $favoriteId; } - public function addMultipleFavoritesToDB($userId, $favoriteList) { + public function addMultipleFavoritesToDB($userId, $favoriteList) + { $nowTimeStamp = (new \DateTime())->getTimestamp(); $values = []; @@ -167,34 +292,34 @@ public function addMultipleFavoritesToDB($userId, $favoriteList) { !array_key_exists('lng', $fav) or !is_numeric($fav['lng']) ) { continue; - } - else { + } else { $lat = floatval($fav['lat']); $lng = floatval($fav['lng']); } - $value = '('. - $this->db_quote_escape_string($userId).', '. - ((!array_key_exists('name', $fav) or !$fav['name']) ? 'NULL' : $this->db_quote_escape_string($fav['name'])).', '. - ((!array_key_exists('date_created', $fav) or !is_numeric($fav['date_created'])) ? $this->db_quote_escape_string($nowTimeStamp) : $this->db_quote_escape_string($fav['date_created'])).', '. - $this->db_quote_escape_string($nowTimeStamp).', '. - $this->db_quote_escape_string($lat).', '. - $this->db_quote_escape_string($lng).', '. - ((!array_key_exists('category', $fav) or !$fav['category']) ? 'NULL' : $this->db_quote_escape_string($fav['category'])).', '. - ((!array_key_exists('comment', $fav) or !$fav['comment']) ? 'NULL' : $this->db_quote_escape_string($fav['comment'])).', '. - ((!array_key_exists('extensions', $fav) or !$fav['extensions']) ? 'NULL' : $this->db_quote_escape_string($fav['extensions'])).')'; + $value = '(' . + $this->db_quote_escape_string($userId) . ', ' . + ((!array_key_exists('name', $fav) or !$fav['name']) ? 'NULL' : $this->db_quote_escape_string($fav['name'])) . ', ' . + ((!array_key_exists('date_created', $fav) or !is_numeric($fav['date_created'])) ? $this->db_quote_escape_string($nowTimeStamp) : $this->db_quote_escape_string($fav['date_created'])) . ', ' . + $this->db_quote_escape_string($nowTimeStamp) . ', ' . + $this->db_quote_escape_string($lat) . ', ' . + $this->db_quote_escape_string($lng) . ', ' . + ((!array_key_exists('category', $fav) or !$fav['category']) ? 'NULL' : $this->db_quote_escape_string($fav['category'])) . ', ' . + ((!array_key_exists('comment', $fav) or !$fav['comment']) ? 'NULL' : $this->db_quote_escape_string($fav['comment'])) . ', ' . + ((!array_key_exists('extensions', $fav) or !$fav['extensions']) ? 'NULL' : $this->db_quote_escape_string($fav['extensions'])) . ')'; array_push($values, $value); } $valuesStr = implode(', ', $values); $sql = ' INSERT INTO *PREFIX*maps_favorites (user_id, name, date_created, date_modified, lat, lng, category, comment, extensions) - VALUES '.$valuesStr.' ;'; + VALUES ' . $valuesStr . ' ;'; $req = $this->dbconnection->prepare($sql); $req->execute(); $req->closeCursor(); } - public function renameCategoryInDB($userId, $cat, $newName) { + public function renameCategoryInDB($userId, $cat, $newName) + { $qb = $this->qb; $qb->update('maps_favorites'); $qb->set('category', $qb->createNamedParameter($newName, IQueryBuilder::PARAM_STR)); @@ -208,7 +333,8 @@ public function renameCategoryInDB($userId, $cat, $newName) { $qb = $qb->resetQueryParts(); } - public function editFavoriteInDB($id, $name, $lat, $lng, $category, $comment, $extensions) { + public function editFavoriteInDB($id, $name, $lat, $lng, $category, $comment, $extensions) + { $nowTimeStamp = (new \DateTime())->getTimestamp(); $qb = $this->qb; $qb->update('maps_favorites'); @@ -238,7 +364,8 @@ public function editFavoriteInDB($id, $name, $lat, $lng, $category, $comment, $e $qb = $qb->resetQueryParts(); } - public function deleteFavoriteFromDB($id) { + public function deleteFavoriteFromDB($id) + { $qb = $this->qb; $qb->delete('maps_favorites') ->where( @@ -248,7 +375,65 @@ public function deleteFavoriteFromDB($id) { $qb = $qb->resetQueryParts(); } - public function deleteFavoritesFromDB($ids, $userId) { + public function getCategoryShareLink($owner, $category) + { + $qb = $this->qb; + + $qb->select('token')->from('maps_favorite_shares') + ->where( + $qb->expr()->eq('category', $qb->createNamedParameter($category, IQueryBuilder::PARAM_STR)) + )->andWhere( + $qb->expr()->eq('owner', $qb->createNamedParameter($owner, IQueryBuilder::PARAM_INT)) + ); + $req = $qb->execute(); + + $row = $req->fetch(); + + if (!$row) { + $token = $this->secureRandom->generate( + \OC\Share\Constants::TOKEN_LENGTH, + \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE + ); + + $qb = $qb->resetQueryParts(); + + $qb->insert('maps_favorite_shares')->values([ + 'category' => $qb->createNamedParameter($category, IQueryBuilder::PARAM_STR), + 'owner' => $qb->createNamedParameter($owner, IQueryBuilder::PARAM_STR), + 'token' => $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR), + ]); + $qb->execute(); + } else { + $token = $row['token']; + } + + $req->closeCursor(); + $qb->resetQueryParts(); + + return [ + 'token' => $token + ]; + } + + public function deleteCategoryShareLink($owner, $category) { + $qb = $this->qb; + + $qb->delete('maps_favorite_shares') + ->where( + $qb->expr()->eq('owner', $qb->createNamedParameter($owner, IQueryBuilder::PARAM_STR)) + )->andWhere( + $qb->expr()->eq('category', $qb->createNamedParameter($category, IQueryBuilder::PARAM_STR)) + ); + $qb->execute(); + $qb->resetQueryParts(); + + return [ + 'deleted' => true + ]; + } + + public function deleteFavoritesFromDB($ids, $userId) + { $qb = $this->qb; $qb->delete('maps_favorites') ->where( @@ -260,15 +445,15 @@ public function deleteFavoritesFromDB($ids, $userId) { $or->add($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); } $qb->andWhere($or); - } - else { + } else { return; } $req = $qb->execute(); $qb = $qb->resetQueryParts(); } - public function countFavorites($userId, $categoryList, $begin, $end) { + public function countFavorites($userId, $categoryList, $begin, $end) + { if ($categoryList === null or (is_array($categoryList) and count($categoryList) === 0) ) { @@ -313,16 +498,17 @@ public function countFavorites($userId, $categoryList, $begin, $end) { return $nbFavorites; } - public function exportFavorites($userId, $fileHandler, $categoryList, $begin, $end, $appVersion) { + public function exportFavorites($userId, $fileHandler, $categoryList, $begin, $end, $appVersion) + { $qb = $this->qb; $nbFavorites = $this->countFavorites($userId, $categoryList, $begin, $end); $gpxHeader = ' - + favourites '; - fwrite($fileHandler, $gpxHeader."\n"); + fwrite($fileHandler, $gpxHeader . "\n"); $chunkSize = 10000; $favIndex = 0; @@ -357,8 +543,8 @@ public function exportFavorites($userId, $fileHandler, $categoryList, $begin, $e $qb->andWhere($or); } $qb->orderBy('date_created', 'ASC') - ->setMaxResults($chunkSize) - ->setFirstResult($favIndex); + ->setMaxResults($chunkSize) + ->setFirstResult($favIndex); $req = $qb->execute(); while ($row = $req->fetch()) { @@ -377,14 +563,13 @@ public function exportFavorites($userId, $fileHandler, $categoryList, $begin, $e $extensions = str_replace('&', '&', $row['extensions']); $gpxExtension = ''; - $gpxText .= ' ' . "\n"; + $gpxText .= ' ' . "\n"; $gpxText .= ' ' . $name . '' . "\n"; $gpxText .= ' ' . "\n"; if ($category !== null && strlen($category) > 0) { $gpxText .= ' ' . $category . '' . "\n"; - } - else { - $gpxText .= ' '.$this->l10n->t('Personal').'' . "\n"; + } else { + $gpxText .= ' ' . $this->l10n->t('Personal') . '' . "\n"; } if ($comment !== null && strlen($comment) > 0) { $gpxText .= ' ' . $comment . '' . "\n"; @@ -393,7 +578,7 @@ public function exportFavorites($userId, $fileHandler, $categoryList, $begin, $e $gpxExtension .= ' ' . $extensions . '' . "\n"; } if ($gpxExtension !== '') { - $gpxText .= ' '. "\n" . $gpxExtension; + $gpxText .= ' ' . "\n" . $gpxExtension; $gpxText .= ' ' . "\n"; } $gpxText .= ' ' . "\n"; @@ -408,22 +593,22 @@ public function exportFavorites($userId, $fileHandler, $categoryList, $begin, $e fwrite($fileHandler, $gpxEnd); } - public function importFavorites($userId, $file) { + public function importFavorites($userId, $file) + { $lowerFileName = strtolower($file->getName()); if ($this->endswith($lowerFileName, '.gpx')) { return $this->importFavoritesFromGpx($userId, $file); - } - elseif ($this->endswith($lowerFileName, '.kml')) { + } elseif ($this->endswith($lowerFileName, '.kml')) { $fp = $file->fopen('r'); $name = $file->getName(); return $this->importFavoritesFromKml($userId, $fp, $name); - } - elseif ($this->endswith($lowerFileName, '.kmz')) { + } elseif ($this->endswith($lowerFileName, '.kmz')) { return $this->importFavoritesFromKmz($userId, $file); } } - public function importFavoritesFromKmz($userId, $file) { + public function importFavoritesFromKmz($userId, $file) + { $path = $file->getStorage()->getLocalFile($file->getInternalPath()); $name = $file->getName(); $zf = new ZIP($path); @@ -432,17 +617,17 @@ public function importFavoritesFromKmz($userId, $file) { $fstream = $zf->getStream($zippedFilePath, 'r'); $result = $this->importFavoritesFromKml($userId, $fstream, $name); - } - else { + } else { $result = [ - 'nbImported'=>0, - 'linesFound'=>false + 'nbImported' => 0, + 'linesFound' => false ]; } return $result; } - public function importFavoritesFromKml($userId, $fp, $name) { + public function importFavoritesFromKml($userId, $fp, $name) + { $this->nbImported = 0; $this->linesFound = false; $this->currentFavoritesList = []; @@ -459,9 +644,9 @@ public function importFavoritesFromKml($userId, $fp, $name) { while ($data = fread($fp, 4096000)) { if (!xml_parse($xml_parser, $data, feof($fp))) { $this->logger->error( - 'Exception in '.$name.' parsing at line '. - xml_get_current_line_number($xml_parser).' : '. - xml_error_string(xml_get_error_code($xml_parser)), + 'Exception in ' . $name . ' parsing at line ' . + xml_get_current_line_number($xml_parser) . ' : ' . + xml_error_string(xml_get_error_code($xml_parser)), array('app' => 'maps') ); return 0; @@ -471,12 +656,13 @@ public function importFavoritesFromKml($userId, $fp, $name) { xml_parser_free($xml_parser); return [ - 'nbImported'=>$this->nbImported, - 'linesFound'=>$this->linesFound + 'nbImported' => $this->nbImported, + 'linesFound' => $this->linesFound ]; } - private function kmlStartElement($parser, $name, $attrs) { + private function kmlStartElement($parser, $name, $attrs) + { $this->currentXmlTag = $name; if ($name === 'PLACEMARK') { $this->currentFavorite = []; @@ -487,15 +673,15 @@ private function kmlStartElement($parser, $name, $attrs) { } } - private function kmlEndElement($parser, $name) { + private function kmlEndElement($parser, $name) + { if ($name === 'KML') { // create last bunch if (count($this->currentFavoritesList) > 0) { $this->addMultipleFavoritesToDB($this->importUserId, $this->currentFavoritesList); } unset($this->currentFavoritesList); - } - else if ($name === 'PLACEMARK') { + } else if ($name === 'PLACEMARK') { $this->kmlInsidePlacemark = false; // store favorite $this->nbImported++; @@ -526,32 +712,30 @@ private function kmlEndElement($parser, $name) { } } - private function kmlDataElement($parser, $data) { + private function kmlDataElement($parser, $data) + { $d = trim($data); if (!empty($d)) { if (!$this->kmlInsidePlacemark) { if ($this->currentXmlTag === 'NAME') { $this->kmlCurrentCategory = $this->kmlCurrentCategory . $d; } - } - else { + } else { if ($this->currentXmlTag === 'NAME') { - $this->currentFavorite['name'] = (array_key_exists('name', $this->currentFavorite)) ? $this->currentFavorite['name'].$d : $d; - } - else if ($this->currentXmlTag === 'WHEN') { - $this->currentFavorite['date_created'] = (array_key_exists('date_created', $this->currentFavorite)) ? $this->currentFavorite['date_created'].$d : $d; - } - else if ($this->currentXmlTag === 'COORDINATES') { - $this->currentFavorite['coordinates'] = (array_key_exists('coordinates', $this->currentFavorite)) ? $this->currentFavorite['coordinates'].$d : $d; - } - else if ($this->currentXmlTag === 'DESCRIPTION') { - $this->currentFavorite['comment'] = (array_key_exists('comment', $this->currentFavorite)) ? $this->currentFavorite['comment'].$d : $d; + $this->currentFavorite['name'] = (array_key_exists('name', $this->currentFavorite)) ? $this->currentFavorite['name'] . $d : $d; + } else if ($this->currentXmlTag === 'WHEN') { + $this->currentFavorite['date_created'] = (array_key_exists('date_created', $this->currentFavorite)) ? $this->currentFavorite['date_created'] . $d : $d; + } else if ($this->currentXmlTag === 'COORDINATES') { + $this->currentFavorite['coordinates'] = (array_key_exists('coordinates', $this->currentFavorite)) ? $this->currentFavorite['coordinates'] . $d : $d; + } else if ($this->currentXmlTag === 'DESCRIPTION') { + $this->currentFavorite['comment'] = (array_key_exists('comment', $this->currentFavorite)) ? $this->currentFavorite['comment'] . $d : $d; } } } } - public function importFavoritesFromGpx($userId, $file) { + public function importFavoritesFromGpx($userId, $file) + { $this->nbImported = 0; $this->linesFound = false; $this->currentFavoritesList = []; @@ -569,9 +753,9 @@ public function importFavoritesFromGpx($userId, $file) { while ($data = fread($fp, 4096000)) { if (!xml_parse($xml_parser, $data, feof($fp))) { $this->logger->error( - 'Exception in '.$file->getName().' parsing at line '. - xml_get_current_line_number($xml_parser).' : '. - xml_error_string(xml_get_error_code($xml_parser)), + 'Exception in ' . $file->getName() . ' parsing at line ' . + xml_get_current_line_number($xml_parser) . ' : ' . + xml_error_string(xml_get_error_code($xml_parser)), array('app' => 'maps') ); return 0; @@ -581,12 +765,13 @@ public function importFavoritesFromGpx($userId, $file) { xml_parser_free($xml_parser); return [ - 'nbImported'=>$this->nbImported, - 'linesFound'=>$this->linesFound + 'nbImported' => $this->nbImported, + 'linesFound' => $this->linesFound ]; } - private function gpxStartElement($parser, $name, $attrs) { + private function gpxStartElement($parser, $name, $attrs) + { $this->currentXmlTag = $name; if ($name === 'WPT') { $this->insideWpt = true; @@ -603,15 +788,15 @@ private function gpxStartElement($parser, $name, $attrs) { } } - private function gpxEndElement($parser, $name) { + private function gpxEndElement($parser, $name) + { if ($name === 'GPX') { // create last bunch if (count($this->currentFavoritesList) > 0) { $this->addMultipleFavoritesToDB($this->importUserId, $this->currentFavoritesList); } unset($this->currentFavoritesList); - } - else if ($name === 'WPT') { + } else if ($name === 'WPT') { $this->insideWpt = false; // store favorite $this->nbImported++; @@ -634,28 +819,26 @@ private function gpxEndElement($parser, $name) { } } - private function gpxDataElement($parser, $data) { + private function gpxDataElement($parser, $data) + { $d = trim($data); if (!empty($d)) { if ($this->insideWpt and $this->currentXmlTag === 'NAME') { - $this->currentFavorite['name'] = (array_key_exists('name', $this->currentFavorite)) ? $this->currentFavorite['name'].$d : $d; - } - else if ($this->insideWpt and $this->currentXmlTag === 'TIME') { - $this->currentFavorite['date_created'] = (array_key_exists('date_created', $this->currentFavorite)) ? $this->currentFavorite['date_created'].$d : $d; - } - else if ($this->insideWpt and $this->currentXmlTag === 'TYPE') { - $this->currentFavorite['category'] = (array_key_exists('category', $this->currentFavorite)) ? $this->currentFavorite['category'].$d : $d; - } - else if ($this->insideWpt and $this->currentXmlTag === 'DESC') { - $this->currentFavorite['comment'] = (array_key_exists('comment', $this->currentFavorite)) ? $this->currentFavorite['comment'].$d : $d; - } - else if ($this->insideWpt and $this->currentXmlTag === 'MAPS-EXTENSIONS') { - $this->currentFavorite['extensions'] = (array_key_exists('extensions', $this->currentFavorite)) ? $this->currentFavorite['extensions'].$d : $d; + $this->currentFavorite['name'] = (array_key_exists('name', $this->currentFavorite)) ? $this->currentFavorite['name'] . $d : $d; + } else if ($this->insideWpt and $this->currentXmlTag === 'TIME') { + $this->currentFavorite['date_created'] = (array_key_exists('date_created', $this->currentFavorite)) ? $this->currentFavorite['date_created'] . $d : $d; + } else if ($this->insideWpt and $this->currentXmlTag === 'TYPE') { + $this->currentFavorite['category'] = (array_key_exists('category', $this->currentFavorite)) ? $this->currentFavorite['category'] . $d : $d; + } else if ($this->insideWpt and $this->currentXmlTag === 'DESC') { + $this->currentFavorite['comment'] = (array_key_exists('comment', $this->currentFavorite)) ? $this->currentFavorite['comment'] . $d : $d; + } else if ($this->insideWpt and $this->currentXmlTag === 'MAPS-EXTENSIONS') { + $this->currentFavorite['extensions'] = (array_key_exists('extensions', $this->currentFavorite)) ? $this->currentFavorite['extensions'] . $d : $d; } } } - private function endswith($string, $test) { + private function endswith($string, $test) + { $strlen = strlen($string); $testlen = strlen($test); if ($testlen > $strlen) return false; diff --git a/package.json b/package.json index 34df7f843..f62ec40bf 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ "scripts": { "test": "node node_modules/gulp-cli/bin/gulp.js karma", "prebuild": "npm install", - "build": "node node_modules/gulp-cli/bin/gulp.js" + "build-old": "node node_modules/gulp-cli/bin/gulp.js", + "build": "NODE_ENV=production webpack --progress --hide-modules --config webpack.prod.js", + "dev": "NODE_ENV=development webpack --config webpack.dev.js", + "watch": "NODE_ENV=development webpack --progress --watch --config webpack.dev.js" }, "repository": { "type": "git", @@ -25,23 +28,76 @@ }, "homepage": "https://github.com/nextcloud/maps", "dependencies": { + "@nextcloud/vue": "^1.1.0", + "d3": "^3.5.17", "gulp": "^4.0.0", "gulp-cli": "^2.1.0", - "leaflet": "^1.4.0", + "hammerjs": "^2.0.8", + "leaflet": "^1.5.1", + "leaflet-contextmenu": "^1.4.0", "leaflet-control-geocoder": "^1.7.0", "leaflet-easybutton": "^2.4.0", "leaflet-routing-machine": "^3.2.12", + "leaflet.elevation": "^0.0.3", "leaflet.featuregroup.subgroup": "^1.0.2", "leaflet.locatecontrol": "^0.67.0", "leaflet.markercluster": "^1.4.0", "leaflet-mouse-position": "^1.0.4", - "leaflet-contextmenu": "^1.4.0", - "leaflet.elevation": "^0.0.3", "mapbox-gl": "^1.4.1", "mapbox-gl-leaflet": "^0.0.11", - "d3": "^3.5.17", "nouislider": "^14.0.2", + "opening_hours": "^3.5.0", "ua-parser-js": "^0.7.20", - "opening_hours": "^3.5.0" + "vue": "^2.6.10", + "vue-types": "^1.6.0", + "vue2-leaflet": "^2.2.1", + "vue2-leaflet-markercluster": "^3.1.0", + "vuex": "^3.1.1" + }, + "devDependencies": { + "@babel/core": "^7.6.4", + "@babel/plugin-proposal-object-rest-spread": "^7.6.2", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/preset-env": "^7.6.3", + "babel-core": "^7.0.0-bridge.0", + "babel-eslint": "^10.0.3", + "babel-jest": "^24.9.0", + "babel-loader": "^8.0.6", + "browserslist-config-nextcloud": "0.1.0", + "css-loader": "^3.2.0", + "eslint": "^6.6.0", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-config-prettier": "^6.5.0", + "eslint-config-standard": "^14.1.0", + "eslint-friendly-formatter": "^4.0.1", + "eslint-loader": "^3.0.2", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-node": "^10.0.0", + "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.1", + "eslint-plugin-vue": "^5.2.3", + "file-loader": "^4.2.0", + "jest": "^24.9.0", + "jest-serializer-vue": "^2.0.2", + "jsdom": "^15.1.1", + "jsdom-global": "^3.0.2", + "node-sass": "^4.13.0", + "prettier": "^1.18.2", + "raw-loader": "^3.1.0", + "sass-loader": "^8.0.0", + "stylelint": "^11.1.1", + "stylelint-config-recommended-scss": "^4.0.0", + "stylelint-config-standard": "^19.0.0", + "stylelint-scss": "^3.12.0", + "stylelint-webpack-plugin": "^1.0.3", + "svg-sprite": "^1.5.0", + "terser-webpack-plugin": "^2.2.1", + "vue-jest": "^3.0.5", + "vue-loader": "^15.7.1", + "vue-template-compiler": "^2.6.10", + "webpack": "^4.41.2", + "webpack-cli": "^3.3.9", + "webpack-merge": "^4.2.2" } } diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 000000000..9c89a92bd --- /dev/null +++ b/src/App.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/src/components/MapContainer.vue b/src/components/MapContainer.vue new file mode 100644 index 000000000..b00bf22c1 --- /dev/null +++ b/src/components/MapContainer.vue @@ -0,0 +1,388 @@ + + + + + diff --git a/src/components/PublicFavoriteShareSideBar.vue b/src/components/PublicFavoriteShareSideBar.vue new file mode 100644 index 000000000..723ffa4bd --- /dev/null +++ b/src/components/PublicFavoriteShareSideBar.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/src/components/map/ClickPopup.vue b/src/components/map/ClickPopup.vue new file mode 100644 index 000000000..516ffe4db --- /dev/null +++ b/src/components/map/ClickPopup.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/src/components/map/FavoritePopup.vue b/src/components/map/FavoritePopup.vue new file mode 100644 index 000000000..ba501c681 --- /dev/null +++ b/src/components/map/FavoritePopup.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/src/components/map/MapPopupLayer.vue b/src/components/map/MapPopupLayer.vue new file mode 100644 index 000000000..134c1dc8a --- /dev/null +++ b/src/components/map/MapPopupLayer.vue @@ -0,0 +1,112 @@ + + + diff --git a/src/components/map/Popup.vue b/src/components/map/Popup.vue new file mode 100644 index 000000000..071431392 --- /dev/null +++ b/src/components/map/Popup.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/components/map/PopupFormItem.vue b/src/components/map/PopupFormItem.vue new file mode 100644 index 000000000..f96d5aafb --- /dev/null +++ b/src/components/map/PopupFormItem.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/components/map/SimpleOSMAddress.vue b/src/components/map/SimpleOSMAddress.vue new file mode 100644 index 000000000..6a9d4f595 --- /dev/null +++ b/src/components/map/SimpleOSMAddress.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/src/data/enum/MapMode.js b/src/data/enum/MapMode.js new file mode 100644 index 000000000..5106d25bd --- /dev/null +++ b/src/data/enum/MapMode.js @@ -0,0 +1,4 @@ +export default { + DEFAULT: "default", + ADDING_FAVORITES: "adding-favorites" +}; diff --git a/src/data/enum/MapPopupState.js b/src/data/enum/MapPopupState.js new file mode 100644 index 000000000..885663aa4 --- /dev/null +++ b/src/data/enum/MapPopupState.js @@ -0,0 +1,6 @@ +export default { + RequestOpen: "request-open", + RequestClose: "request-close", + Closed: "closed", + Opened: "opened" +}; diff --git a/src/main.js b/src/main.js new file mode 100644 index 000000000..012b9b6c5 --- /dev/null +++ b/src/main.js @@ -0,0 +1,29 @@ +import Vue from "vue"; +import App from "./App"; +import { Icon } from "leaflet"; +import "leaflet/dist/leaflet.css"; + +import store from "./store"; + +Vue.prototype.t = window.t; +Vue.prototype.n = window.n; +Vue.prototype.OC = window.OC; +Vue.prototype.OCA = window.OCA; + +if (process && process.env.NODE_ENV === "development") { + Vue.config.devtools = true; +} + +// this part resolve an issue where the markers would not appear +delete Icon.Default.prototype._getIconUrl; + +Icon.Default.mergeOptions({ + iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"), + iconUrl: require("leaflet/dist/images/marker-icon.png"), + shadowUrl: require("leaflet/dist/images/marker-shadow.png") +}); + +new Vue({ + render: h => h(App), + store +}).$mount("#app"); diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 000000000..fdff6371c --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,14 @@ +import Vuex from "vuex"; +import Vue from "vue"; +import publicFavorites from "./modules/publicFavorites"; +import map from "./modules/map"; + +Vuex.install(Vue); + +export default new Vuex.Store({ + modules: { + publicFavorites, + map + }, + strict: process.env.NODE_ENV !== "production" +}); diff --git a/src/store/modules/map.js b/src/store/modules/map.js new file mode 100644 index 000000000..b403bb710 --- /dev/null +++ b/src/store/modules/map.js @@ -0,0 +1,25 @@ +import MapMode from "../../data/enum/MapMode"; + +export const MAP_NAMESPACE = "map"; + +const state = { + mode: MapMode.DEFAULT +}; + +const getters = {}; + +const actions = {}; + +const mutations = { + setMode(state, mode) { + state.mode = mode; + } +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations +}; diff --git a/src/store/modules/publicFavorites.js b/src/store/modules/publicFavorites.js new file mode 100644 index 000000000..764e349f1 --- /dev/null +++ b/src/store/modules/publicFavorites.js @@ -0,0 +1,96 @@ +import { publicApiRequest, showNotification } from "../../utils/common"; +import { getCategoryRawName } from "../../utils/mapUtils"; +import { getPublicShareCategory } from "../../utils/publicShareUtils"; + +export const PUBLIC_FAVORITES_NAMESPACE = "publicFavorites"; + +const state = { + favorites: [], + selectedFavoriteId: null +}; + +const getters = { + mappedByCategory(state) { + if (state.favorites.length === 0) { + return {}; + } + + return { + [getCategoryRawName(state.favorites[0].category)]: state.favorites + }; + } +}; + +const actions = { + selectFavorite({ commit }, favoriteId) { + commit("setSelectedFavoriteId", favoriteId); + }, + getFavorites({ commit }) { + return publicApiRequest("favorites", "GET") + .then(data => { + commit("setFavorites", data); + }) + .catch(() => showNotification(t("maps", "Failed to get favorites"))); + }, + addFavorite({ commit }, { lat, lng, name, comment }) { + return publicApiRequest("favorites", "POST", { + lat, + lng, + name, + category: getPublicShareCategory(), + comment, + extensions: "" // TODO: + }) + .then(data => { + commit("addFavorite", data); + }) + .catch(() => showNotification(t("maps", "Failed to create favorite"))); + }, + updateFavorite({ commit }, { id, name, comment }) { + return publicApiRequest(`favorites/${id}`, "PUT", { + name, + category: getPublicShareCategory(), + comment, + extensions: "" // TODO: + }) + .then(data => { + commit("editFavorite", data); + }) + .catch(() => showNotification(t("maps", "Failed to update favorite"))); + }, + deleteFavorite({ commit }, { id }) { + return publicApiRequest(`favorites/${id}`, "DELETE") + .then(() => { + commit("deleteFavorite", id); + }) + .catch(() => showNotification(t("maps", "Failed to delete favorite"))); + } +}; + +const mutations = { + setFavorites(state, favorites) { + state.favorites = favorites; + }, + addFavorite(state, favorite) { + state.favorites = [...state.favorites, favorite]; + }, + editFavorite(state, favorite) { + state.favorites = state.favorites.map(el => + el.id === favorite.id ? favorite : el + ); + }, + deleteFavorite(state, id) { + state.favorites = state.favorites.filter(el => el.id !== id); + }, + setSelectedFavoriteId(state, favoriteId) { + state.selectedFavoriteId = favoriteId; + } +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations +}; diff --git a/src/utils/common.js b/src/utils/common.js new file mode 100644 index 000000000..1c57bc1b9 --- /dev/null +++ b/src/utils/common.js @@ -0,0 +1,62 @@ +export const isPublicShare = () => { + return document.body.id === "body-public"; +}; + +export const getCurrentPublicShareToken = () => { + // FIXME: there must be a better way to retrieve the token client side + const path = location.pathname.split("/"); + + return path[path.length - 1]; +}; + +export const publicApiRequest = (slug, method, data = null) => { + return request( + OC.generateUrl( + `/apps/maps/api/1.0/public/${getCurrentPublicShareToken()}/${slug}` + ), + method, + data + ); +}; + +export const apiRequest = (slug, method, data = null) => { + return request( + OC.generateUrl(`apps/maps/api/1.0/${getCurrentPublicShareToken()}/${slug}`), + method, + data + ); +}; + +/** + * Perform a network request + * + * TODO: Use axios or similar instead of jQuery ajax + * + * @param url : string + * @param method : string + * @param data : {} | null + * @returns {Promise<{}>} + */ +export const request = (url, method, data = null) => { + return new Promise((resolve, reject) => { + $.ajax({ + url: url, + type: method.toUpperCase(), + data, + async: true + }) + .done(resolve) + .fail(reject); + }); +}; + +/** + * Show temporary notification + * + * TODO: Use non-deprecated function + * + * @param message : string + */ +export const showNotification = message => { + OC.Notification.showTemporary(t("maps", message)); +}; diff --git a/src/utils/mapUtils.js b/src/utils/mapUtils.js new file mode 100644 index 000000000..5d8eee424 --- /dev/null +++ b/src/utils/mapUtils.js @@ -0,0 +1,26 @@ +import { request } from "./common"; + +export const getCategoryRawName = categoryName => + categoryName.replace(" ", "-"); + +export const isGeocodeable = str => { + const pattern = /^\s*-?\d+\.?\d*,\s*-?\d+\.?\d*\s*$/; + + return pattern.test(str); +}; + +export const constructGeoCodeUrl = (lat, lng) => + `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`; + +export const geocode = latLngStr => { + if (!isGeocodeable(latLngStr)) { + return Promise.reject(`${latLngStr} is not geocodable`); + } + + const latLng = latLngStr.split(","); + + const lat = latLng[0].trim(); + const lng = latLng[1].trim(); + + return request(constructGeoCodeUrl(lat, lng), "GET"); +}; diff --git a/src/utils/publicShareUtils.js b/src/utils/publicShareUtils.js new file mode 100644 index 000000000..cfe381000 --- /dev/null +++ b/src/utils/publicShareUtils.js @@ -0,0 +1,9 @@ +export const getPublicShareCategory = () => { + const el = document.querySelector(".header-appname"); + + if (!el) { + throw new Error("Could not get publis share category"); + } + + return el.textContent; +}; diff --git a/templates/index.php b/templates/index.php index d6bb2172c..bd105fc62 100644 --- a/templates/index.php +++ b/templates/index.php @@ -12,14 +12,14 @@ script('maps', 'script'); ?> -
    -
    - inc('navigation/index')); ?> - inc('settings/index')); ?> -
    +
    +
    + inc('navigation/index')); ?> + inc('settings/index')); ?> +
    -
    - inc('content/index')); ?> +
    + inc('content/index')); ?> +
    -
    diff --git a/templates/public/favorites_index.php b/templates/public/favorites_index.php new file mode 100644 index 000000000..9dab85c41 --- /dev/null +++ b/templates/public/favorites_index.php @@ -0,0 +1,6 @@ + + +
    diff --git a/webpack.common.js b/webpack.common.js new file mode 100644 index 000000000..309b3cc42 --- /dev/null +++ b/webpack.common.js @@ -0,0 +1,65 @@ +const path = require("path"); +const webpack = require("webpack"); +const { VueLoaderPlugin } = require("vue-loader"); +//const StyleLintPlugin = require("stylelint-webpack-plugin"); + +module.exports = { + entry: path.join(__dirname, "src", "main.js"), + output: { + path: path.resolve(__dirname, "./js"), + publicPath: "/js/", + filename: "maps.js" + }, + module: { + rules: [ + { + test: /\.css$/, + use: ["vue-style-loader", "css-loader"] + }, + { + test: /\.scss$/, + use: ["vue-style-loader", "css-loader", "sass-loader"] + }, + { + test: /src\/.*\.(js|vue)$/, + use: "eslint-loader", + enforce: "pre" + }, + { + test: /\.vue$/, + loader: "vue-loader" + }, + { + test: /\.js$/, + use: { + loader: "babel-loader", + options: { + plugins: [ + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-proposal-object-rest-spread" + ], + presets: ["@babel/preset-env"] + } + }, + exclude: /node_modules\/(?!(p-limit|p-defer|p-queue|p-try|cdav-library))/ + }, + { + test: /\.(png|jpg|gif|svg)$/, + loader: "file-loader", + options: { + name: "[name].[ext]?[hash]" + } + } + ] + }, + plugins: [ + new VueLoaderPlugin(), + // new StyleLintPlugin(), + new webpack.DefinePlugin({ + $appVersion: JSON.stringify(require("./package.json").version) + }) + ], + resolve: { + extensions: ["*", ".js", ".vue", ".json"] + } +}; diff --git a/webpack.dev.js b/webpack.dev.js new file mode 100644 index 000000000..d5e928436 --- /dev/null +++ b/webpack.dev.js @@ -0,0 +1,12 @@ +const merge = require('webpack-merge') +const common = require('./webpack.common.js') + +module.exports = merge(common, { + mode: 'development', + devServer: { + historyApiFallback: true, + noInfo: true, + overlay: true + }, + devtool: '#cheap-source-map', +}); diff --git a/webpack.prod.js b/webpack.prod.js new file mode 100644 index 000000000..f7e4d9017 --- /dev/null +++ b/webpack.prod.js @@ -0,0 +1,18 @@ +const merge = require('webpack-merge') +const common = require('./webpack.common.js') +const TerserPlugin = require('terser-webpack-plugin') + +module.exports = merge(common, { + mode: 'production', + devtool: '#source-map', + optimization: { + minimizer: [new TerserPlugin({ + terserOptions: { + output: { + comments: false, + } + }, + sourceMap: true, + })], + } +}); \ No newline at end of file