authtoken_view.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. /* global Handlebars, moment */
  2. /**
  3. * @author Christoph Wurst <christoph@owncloud.com>
  4. *
  5. * @copyright Copyright (c) 2016, ownCloud, Inc.
  6. * @license AGPL-3.0
  7. *
  8. * This code is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU Affero General Public License, version 3,
  10. * as published by the Free Software Foundation.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Affero General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public License, version 3,
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>
  19. *
  20. */
  21. (function (OC, _, $, Handlebars, moment) {
  22. 'use strict';
  23. OC.Settings = OC.Settings || {};
  24. var TEMPLATE_TOKEN =
  25. '<tr data-id="{{id}}">'
  26. + '<td class="has-tooltip" title="{{title}}">'
  27. + '<span class="token-name">{{name}}</span>'
  28. + '</td>'
  29. + '<td><span class="last-activity has-tooltip" title="{{lastActivityTime}}">{{lastActivity}}</span></td>'
  30. + '<td class="more">'
  31. + '{{#if showMore}}<a class="icon icon-more"/>{{/if}}'
  32. + '<div class="popovermenu bubble open menu configure">'
  33. + '{{#if canScope}}'
  34. + '<input class="filesystem checkbox" type="checkbox" id="{{id}}_filesystem" {{#if scope.filesystem}}checked{{/if}}/>'
  35. + '<label for="{{id}}_filesystem">' + t('core', 'Allow filesystem access') + '</label><br/>'
  36. + '{{/if}}'
  37. + '{{#if canDelete}}'
  38. + '<a class="icon icon-delete has-tooltip" title="' + t('core', 'Disconnect') + '">' + t('core', 'Revoke') +'</a>'
  39. + '{{/if}}'
  40. + '</div>'
  41. + '</td>'
  42. + '<tr>';
  43. var SubView = OC.Backbone.View.extend({
  44. collection: null,
  45. /**
  46. * token type
  47. * - 0: browser
  48. * - 1: device
  49. *
  50. * @see OC\Authentication\Token\IToken
  51. */
  52. type: 0,
  53. _template: undefined,
  54. template: function (data) {
  55. if (_.isUndefined(this._template)) {
  56. this._template = Handlebars.compile(TEMPLATE_TOKEN);
  57. }
  58. return this._template(data);
  59. },
  60. initialize: function (options) {
  61. this.type = options.type;
  62. this.collection = options.collection;
  63. this.on(this.collection, 'change', this.render);
  64. },
  65. render: function () {
  66. var _this = this;
  67. var list = this.$('.token-list');
  68. var tokens = this.collection.filter(function (token) {
  69. return token.get('type') === _this.type;
  70. });
  71. list.html('');
  72. // Show header only if there are tokens to show
  73. this._toggleHeader(tokens.length > 0);
  74. tokens.forEach(function (token) {
  75. var viewData = this._formatViewData(token);
  76. var html = _this.template(viewData);
  77. var $html = $(html);
  78. $html.find('.has-tooltip').tooltip({container: 'body'});
  79. list.append($html);
  80. }.bind(this));
  81. },
  82. toggleLoading: function (state) {
  83. this.$('table').toggleClass('icon-loading', state);
  84. },
  85. _toggleHeader: function (show) {
  86. this.$('.hidden-when-empty').toggleClass('hidden', !show);
  87. },
  88. _formatViewData: function (token) {
  89. var viewData = token.toJSON();
  90. var ts = viewData.lastActivity * 1000;
  91. viewData.lastActivity = OC.Util.relativeModifiedDate(ts);
  92. viewData.lastActivityTime = OC.Util.formatDate(ts, 'LLL');
  93. viewData.canScope = token.get('type') === 1;
  94. viewData.showMore = viewData.canScope || viewData.canDelete;
  95. // preserve title for cases where we format it further
  96. viewData.title = viewData.name;
  97. // pretty format sync client user agent
  98. var matches = viewData.name.match(/Mozilla\/5\.0 \((\w+)\) (?:mirall|csyncoC)\/(\d+\.\d+\.\d+)/);
  99. var userAgentMap = {
  100. ie: /(?:MSIE|Trident|Trident\/7.0; rv)[ :](\d+)/,
  101. // Microsoft Edge User Agent from https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
  102. edge: /^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (?:Mobile Safari|Safari)\/[0-9.]+ Edge\/[0-9.]+$/,
  103. // Firefox User Agent from https://developer.mozilla.org/en-US/docs/Web/HTTP/Gecko_user_agent_string_reference
  104. firefox: /^Mozilla\/5\.0 \([^)]*(Windows|OS X|Linux)[^)]+\) Gecko\/[0-9.]+ Firefox\/(\d+)(?:\.\d)?$/,
  105. // Chrome User Agent from https://developer.chrome.com/multidevice/user-agent
  106. chrome: /^Mozilla\/5\.0 \([^)]*(Windows|OS X|Linux)[^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/(\d+)[0-9.]+ (?:Mobile Safari|Safari)\/[0-9.]+$/,
  107. // Safari User Agent from http://www.useragentstring.com/pages/Safari/
  108. safari: /^Mozilla\/5\.0 \([^)]*(Windows|OS X)[^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)(?: Version\/([0-9]+)[0-9.]+)? Safari\/[0-9.A-Z]+$/,
  109. // Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
  110. androidChrome: /Android.*(?:; (.*) Build\/).*Chrome\/(\d+)[0-9.]+/,
  111. iphone: / *CPU +iPhone +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
  112. ipad: /\(iPad\; *CPU +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
  113. iosClient: /^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/,
  114. androidClient:/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/,
  115. // DAVdroid/1.2 (2016/07/03; dav4android; okhttp3) Android/6.0.1
  116. davDroid: /DAVdroid\/([0-9.]+)/,
  117. // Mozilla/5.0 (U; Linux; Maemo; Jolla; Sailfish; like Android 4.3) AppleWebKit/538.1 (KHTML, like Gecko) WebPirate/2.0 like Mobile Safari/538.1 (compatible)
  118. webPirate: /(Sailfish).*WebPirate\/(\d+)/,
  119. // Mozilla/5.0 (Maemo; Linux; U; Jolla; Sailfish; Mobile; rv:31.0) Gecko/31.0 Firefox/31.0 SailfishBrowser/1.0
  120. sailfishBrowser: /(Sailfish).*SailfishBrowser\/(\d+)/
  121. };
  122. var nameMap = {
  123. ie: t('setting', 'Internet Explorer'),
  124. edge: t('setting', 'Edge'),
  125. firefox: t('setting', 'Firefox'),
  126. chrome: t('setting', 'Google Chrome'),
  127. safari: t('setting', 'Safari'),
  128. androidChrome: t('setting', 'Google Chrome for Android'),
  129. iphone: t('setting', 'iPhone iOS'),
  130. ipad: t('setting', 'iPad iOS'),
  131. iosClient: t('setting', 'iOS Client'),
  132. androidClient: t('setting', 'Android Client'),
  133. davDroid: 'DAVdroid',
  134. webPirate: 'WebPirate',
  135. sailfishBrowser: 'SailfishBrowser'
  136. };
  137. if (matches) {
  138. viewData.name = t('settings', 'Sync client - {os}', {
  139. os: matches[1],
  140. version: matches[2]
  141. });
  142. }
  143. for (var client in userAgentMap) {
  144. if (matches = viewData.title.match(userAgentMap[client])) {
  145. if (matches[2] && matches[1]) { // version number and os
  146. viewData.name = nameMap[client] + ' ' + matches[2] + ' - ' + matches[1];
  147. }else if (matches[1]) { // only version number
  148. viewData.name = nameMap[client] + ' ' + matches[1];
  149. } else {
  150. viewData.name = nameMap[client];
  151. }
  152. }
  153. }
  154. if (viewData.current) {
  155. viewData.name = t('settings', 'This session');
  156. }
  157. return viewData;
  158. }
  159. });
  160. var AuthTokenView = OC.Backbone.View.extend({
  161. collection: null,
  162. _views: [],
  163. _form: undefined,
  164. _tokenName: undefined,
  165. _addAppPasswordBtn: undefined,
  166. _result: undefined,
  167. _newAppLoginName: undefined,
  168. _newAppPassword: undefined,
  169. _newAppId: undefined,
  170. _hideAppPasswordBtn: undefined,
  171. _addingToken: false,
  172. initialize: function (options) {
  173. this.collection = options.collection;
  174. var tokenTypes = [0, 1];
  175. var _this = this;
  176. _.each(tokenTypes, function (type) {
  177. var el = type === 0 ? '#sessions' : '#apppasswords';
  178. _this._views.push(new SubView({
  179. el: el,
  180. type: type,
  181. collection: _this.collection
  182. }));
  183. var $el = $(el);
  184. $el.on('click', 'a.icon-delete', _.bind(_this._onDeleteToken, _this));
  185. $el.on('click', '.icon-more', _.bind(_this._onConfigureToken, _this));
  186. $el.on('change', 'input.filesystem', _.bind(_this._onSetTokenScope, _this));
  187. });
  188. this._form = $('#app-password-form');
  189. this._tokenName = $('#app-password-name');
  190. this._addAppPasswordBtn = $('#add-app-password');
  191. this._addAppPasswordBtn.click(_.bind(this._addAppPassword, this));
  192. this._result = $('#app-password-result');
  193. this._newAppLoginName = $('#new-app-login-name');
  194. this._newAppLoginName.on('focus', _.bind(this._onNewTokenLoginNameFocus, this));
  195. this._newAppPassword = $('#new-app-password');
  196. this._newAppPassword.on('focus', _.bind(this._onNewTokenFocus, this));
  197. this._hideAppPasswordBtn = $('#app-password-hide');
  198. this._hideAppPasswordBtn.click(_.bind(this._hideToken, this));
  199. this._result.find('.clipboardButton').tooltip({placement: 'bottom', title: t('core', 'Copy'), trigger: 'hover'});
  200. // Clipboard!
  201. var clipboard = new Clipboard('.clipboardButton');
  202. clipboard.on('success', function(e) {
  203. var $input = $(e.trigger);
  204. $input.tooltip('hide')
  205. .attr('data-original-title', t('core', 'Copied!'))
  206. .tooltip('fixTitle')
  207. .tooltip({placement: 'bottom', trigger: 'manual'})
  208. .tooltip('show');
  209. _.delay(function() {
  210. $input.tooltip('hide')
  211. .attr('data-original-title', t('core', 'Copy'))
  212. .tooltip('fixTitle');
  213. }, 3000);
  214. });
  215. clipboard.on('error', function (e) {
  216. var $input = $(e.trigger);
  217. var actionMsg = '';
  218. if (/iPhone|iPad/i.test(navigator.userAgent)) {
  219. actionMsg = t('core', 'Not supported!');
  220. } else if (/Mac/i.test(navigator.userAgent)) {
  221. actionMsg = t('core', 'Press ⌘-C to copy.');
  222. } else {
  223. actionMsg = t('core', 'Press Ctrl-C to copy.');
  224. }
  225. $input.tooltip('hide')
  226. .attr('data-original-title', actionMsg)
  227. .tooltip('fixTitle')
  228. .tooltip({placement: 'bottom', trigger: 'manual'})
  229. .tooltip('show');
  230. _.delay(function () {
  231. $input.tooltip('hide')
  232. .attr('data-original-title', t('core', 'Copy'))
  233. .tooltip('fixTitle');
  234. }, 3000);
  235. });
  236. },
  237. render: function () {
  238. _.each(this._views, function (view) {
  239. view.render();
  240. view.toggleLoading(false);
  241. });
  242. },
  243. reload: function () {
  244. var _this = this;
  245. _.each(this._views, function (view) {
  246. view.toggleLoading(true);
  247. });
  248. var loadingTokens = this.collection.fetch();
  249. $.when(loadingTokens).done(function () {
  250. _this.render();
  251. });
  252. $.when(loadingTokens).fail(function () {
  253. OC.Notification.showTemporary(t('core', 'Error while loading browser sessions and device tokens'));
  254. });
  255. },
  256. _addAppPassword: function () {
  257. if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
  258. OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this._addAppPassword, this));
  259. return;
  260. }
  261. var _this = this;
  262. this._toggleAddingToken(true);
  263. var deviceName = this._tokenName.val() !== '' ? this._tokenName.val() : new Date();
  264. var creatingToken = $.ajax(OC.generateUrl('/settings/personal/authtokens'), {
  265. method: 'POST',
  266. data: {
  267. name: deviceName
  268. }
  269. });
  270. $.when(creatingToken).done(function (resp) {
  271. // We can delete token we add
  272. resp.deviceToken.canDelete = true;
  273. _this.collection.add(resp.deviceToken);
  274. _this.render();
  275. _this._newAppLoginName.val(resp.loginName);
  276. _this._newAppPassword.val(resp.token);
  277. _this._newAppId = resp.deviceToken.id;
  278. _this._toggleFormResult(false);
  279. _this._newAppPassword.select();
  280. _this._tokenName.val('');
  281. });
  282. $.when(creatingToken).fail(function () {
  283. OC.Notification.showTemporary(t('core', 'Error while creating device token'));
  284. });
  285. $.when(creatingToken).always(function () {
  286. _this._toggleAddingToken(false);
  287. });
  288. },
  289. _onNewTokenLoginNameFocus: function () {
  290. this._newAppLoginName.select();
  291. },
  292. _onNewTokenFocus: function () {
  293. this._newAppPassword.select();
  294. },
  295. _hideToken: function () {
  296. this._toggleFormResult(true);
  297. },
  298. _toggleAddingToken: function (state) {
  299. this._addingToken = state;
  300. this._addAppPasswordBtn.toggleClass('icon-loading-small', state);
  301. },
  302. _onConfigureToken: function (event) {
  303. var $target = $(event.target);
  304. var $row = $target.closest('tr');
  305. $row.toggleClass('active');
  306. var id = $row.data('id');
  307. },
  308. _onDeleteToken: function (event) {
  309. var $target = $(event.target);
  310. var $row = $target.closest('tr');
  311. var id = $row.data('id');
  312. if (id === this._newAppId) {
  313. this._toggleFormResult(true);
  314. }
  315. var token = this.collection.get(id);
  316. if (_.isUndefined(token)) {
  317. // Ignore event
  318. return;
  319. }
  320. var destroyingToken = token.destroy();
  321. $row.find('.icon-delete').tooltip('hide');
  322. var _this = this;
  323. $.when(destroyingToken).fail(function () {
  324. OC.Notification.showTemporary(t('core', 'Error while deleting the token'));
  325. });
  326. $.when(destroyingToken).always(function () {
  327. _this.render();
  328. });
  329. },
  330. _onSetTokenScope: function (event) {
  331. var $target = $(event.target);
  332. var $row = $target.closest('tr');
  333. var id = $row.data('id');
  334. var token = this.collection.get(id);
  335. if (_.isUndefined(token)) {
  336. // Ignore event
  337. return;
  338. }
  339. var scope = token.get('scope');
  340. scope.filesystem = $target.is(":checked");
  341. token.set('scope', scope);
  342. token.save();
  343. },
  344. _toggleFormResult: function (showForm) {
  345. if (showForm) {
  346. this._result.slideUp();
  347. this._form.slideDown();
  348. } else {
  349. this._form.slideUp();
  350. this._result.slideDown();
  351. }
  352. }
  353. });
  354. OC.Settings.AuthTokenView = AuthTokenView;
  355. })(OC, _, $, Handlebars, moment);