authtoken_view.js 13 KB

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