From 87c2574acde4408b2b9cd3b9fa857f56c9f16f70 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 12 Jul 2017 21:03:51 +0300 Subject: [PATCH] TB-70: Keep dashboard state as base64 url parameter. --- ui/package.json | 1 + ui/src/app/common/utf8-support.js | 138 ++++++++++++++++++ ui/src/app/common/utils.service.js | 27 +++- .../components/widget/widget.controller.js | 2 +- .../states/default-state-controller.js | 8 +- .../states/entity-state-controller.js | 8 +- ...d-widget-to-dashboard-dialog.controller.js | 4 +- 7 files changed, 173 insertions(+), 15 deletions(-) create mode 100644 ui/src/app/common/utf8-support.js diff --git a/ui/package.json b/ui/package.json index 45f2f4b250..b4485dcb7d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -45,6 +45,7 @@ "angular-ui-ace": "^0.2.3", "angular-ui-router": "^0.3.1", "angular-websocket": "^2.0.1", + "base64-js": "^1.2.1", "brace": "^0.8.0", "canvas-gauges": "^2.0.9", "clipboard": "^1.5.15", diff --git a/ui/src/app/common/utf8-support.js b/ui/src/app/common/utf8-support.js new file mode 100644 index 0000000000..464fa4071e --- /dev/null +++ b/ui/src/app/common/utf8-support.js @@ -0,0 +1,138 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function utf8Encode(str) { + var result; + + if (angular.isUndefined(Uint8Array)) { // eslint-disable-line no-undef + result = utf8ToBytes(str); + } else { + result = new Uint8Array(utf8ToBytes(str)); // eslint-disable-line no-undef + } + + return result; +} + +export function utf8Decode(bytes) { + return utf8Slice(bytes, 0, bytes.length); +} + +function utf8Slice (buf, start, end) { + var res = '' + var tmp = '' + end = Math.min(buf.length, end || Infinity) + start = start || 0; + + for (var i = start; i < end; i++) { + if (buf[i] <= 0x7F) { + res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i]) + tmp = '' + } else { + tmp += '%' + buf[i].toString(16) + } + } + + return res + decodeUtf8Char(tmp) +} + +function decodeUtf8Char (str) { + try { + return decodeURIComponent(str) + } catch (err) { + return String.fromCharCode(0xFFFD) // UTF 8 invalid char + } +} + +function utf8ToBytes (string, units) { + units = units || Infinity + var codePoint + var length = string.length + var leadSurrogate = null + var bytes = [] + var i = 0 + + for (; i < length; i++) { + codePoint = string.charCodeAt(i) + + // is surrogate component + if (codePoint > 0xD7FF && codePoint < 0xE000) { + // last char was a lead + if (leadSurrogate) { + // 2 leads in a row + if (codePoint < 0xDC00) { + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) + leadSurrogate = codePoint + continue + } else { + // valid surrogate pair + codePoint = leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00 | 0x10000 + leadSurrogate = null + } + } else { + // no lead yet + + if (codePoint > 0xDBFF) { + // unexpected trail + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) + continue + } else if (i + 1 === length) { + // unpaired lead + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) + continue + } else { + // valid lead + leadSurrogate = codePoint + continue + } + } + } else if (leadSurrogate) { + // valid bmp char, but last char was a lead + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) + leadSurrogate = null + } + + // encode utf8 + if (codePoint < 0x80) { + if ((units -= 1) < 0) break + bytes.push(codePoint) + } else if (codePoint < 0x800) { + if ((units -= 2) < 0) break + bytes.push( + codePoint >> 0x6 | 0xC0, + codePoint & 0x3F | 0x80 + ) + } else if (codePoint < 0x10000) { + if ((units -= 3) < 0) break + bytes.push( + codePoint >> 0xC | 0xE0, + codePoint >> 0x6 & 0x3F | 0x80, + codePoint & 0x3F | 0x80 + ) + } else if (codePoint < 0x200000) { + if ((units -= 4) < 0) break + bytes.push( + codePoint >> 0x12 | 0xF0, + codePoint >> 0xC & 0x3F | 0x80, + codePoint >> 0x6 & 0x3F | 0x80, + codePoint & 0x3F | 0x80 + ) + } else { + throw new Error('Invalid code point') + } + } + + return bytes +} \ No newline at end of file diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js index 7c9f97d51a..61d4d81f05 100644 --- a/ui/src/app/common/utils.service.js +++ b/ui/src/app/common/utils.service.js @@ -20,9 +20,12 @@ import materialIconsCodepoints from 'raw-loader!material-design-icons/iconfont/c /* eslint-enable import/no-unresolved, import/default */ -import tinycolor from "tinycolor2"; -import jsonSchemaDefaults from "json-schema-defaults"; -import thingsboardTypes from "./types.constant"; +import tinycolor from 'tinycolor2'; +import jsonSchemaDefaults from 'json-schema-defaults'; +import base64js from 'base64-js'; +import {utf8Encode, utf8Decode} from './utf8-support'; + +import thingsboardTypes from './types.constant'; export default angular.module('thingsboard.utils', [thingsboardTypes]) .factory('utils', Utils) @@ -153,7 +156,9 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t createKey: createKey, createLabelFromDatasource: createLabelFromDatasource, insertVariable: insertVariable, - customTranslation: customTranslation + customTranslation: customTranslation, + objToBase64: objToBase64, + base64toObj: base64toObj } return service; @@ -486,4 +491,18 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t return result; } + function objToBase64(obj) { + var json = angular.toJson(obj); + var encoded = utf8Encode(json); + var b64Encoded = base64js.fromByteArray(encoded); + return b64Encoded; + } + + function base64toObj(b64Encoded) { + var encoded = base64js.toByteArray(b64Encoded); + var json = utf8Decode(encoded); + var obj = angular.fromJson(json); + return obj; + } + } diff --git a/ui/src/app/components/widget/widget.controller.js b/ui/src/app/components/widget/widget.controller.js index 4e85718257..7ed79aece9 100644 --- a/ui/src/app/components/widget/widget.controller.js +++ b/ui/src/app/components/widget/widget.controller.js @@ -473,7 +473,7 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele } var stateParams = { dashboardId: targetDashboardId, - state: angular.toJson([ stateObject ]) + state: utils.objToBase64([ stateObject ]) } $state.go('home.dashboards.dashboard', stateParams); break; diff --git a/ui/src/app/dashboard/states/default-state-controller.js b/ui/src/app/dashboard/states/default-state-controller.js index ae92d37518..7d810ca3f9 100644 --- a/ui/src/app/dashboard/states/default-state-controller.js +++ b/ui/src/app/dashboard/states/default-state-controller.js @@ -121,11 +121,11 @@ export default function DefaultStateController($scope, $location, $state, $state return utils.customTranslation(state.name, id); } - function parseState(stateJson) { + function parseState(stateBase64) { var result; - if (stateJson) { + if (stateBase64) { try { - result = angular.fromJson(stateJson); + result = utils.base64toObj(stateBase64); } catch (e) { result = [ { id: null, params: {} } ]; } @@ -205,7 +205,7 @@ export default function DefaultStateController($scope, $location, $state, $state function updateLocation() { if (vm.stateObject[0].id) { - $location.search({state : angular.toJson(vm.stateObject)}); + $location.search({state : utils.objToBase64(vm.stateObject)}); } } } diff --git a/ui/src/app/dashboard/states/entity-state-controller.js b/ui/src/app/dashboard/states/entity-state-controller.js index cbe91faaee..a554adfdae 100644 --- a/ui/src/app/dashboard/states/entity-state-controller.js +++ b/ui/src/app/dashboard/states/entity-state-controller.js @@ -168,11 +168,11 @@ export default function EntityStateController($scope, $location, $state, $stateP return deferred.promise; } - function parseState(stateJson) { + function parseState(stateBase64) { var result; - if (stateJson) { + if (stateBase64) { try { - result = angular.fromJson(stateJson); + result = utils.base64toObj(stateBase64); } catch (e) { result = [ { id: null, params: {} } ]; } @@ -278,7 +278,7 @@ export default function EntityStateController($scope, $location, $state, $stateP function updateLocation() { if (vm.stateObject[vm.stateObject.length-1].id) { - $location.search({state : angular.toJson(vm.stateObject)}); + $location.search({state : utils.objToBase64(vm.stateObject)}); } } diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js index a133829086..64726dbaf7 100644 --- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js +++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js @@ -23,7 +23,7 @@ import selectTargetLayoutTemplate from '../../dashboard/layouts/select-target-la /*@ngInject*/ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, $q, $document, dashboardUtils, - types, itembuffer, dashboardService, entityId, entityType, entityName, widget) { + utils, types, itembuffer, dashboardService, entityId, entityType, entityName, widget) { var vm = this; @@ -143,7 +143,7 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, var stateIds = Object.keys(dashboard.configuration.states); var stateIndex = stateIds.indexOf(targetState); if (stateIndex > 0) { - stateParams.state = angular.toJson([ {id: targetState, params: {}} ]); + stateParams.state = utils.objToBase64([ {id: targetState, params: {}} ]); } $state.go('home.dashboards.dashboard', stateParams); }