Source: js/permission_manager.js

  1. /* global Service, applications, ManifestHelper, Tagged */
  2. 'use strict';
  3. (function(exports) {
  4. /**
  5. * Handle Web API permissions such as geolocation, getUserMedia
  6. * @class PermissionManager
  7. * @requires Applications
  8. */
  9. function PermissionManager() {
  10. }
  11. PermissionManager.prototype = {
  12. currentOrigin: undefined,
  13. permissionType: undefined,
  14. currentPermissions: undefined,
  15. currentChoices: {}, //select choices
  16. isFullscreenRequest: false,
  17. isVideo: false,
  18. isAudio: false,
  19. /**
  20. * special dialog for camera selection while in app mode and
  21. * permission is granted
  22. */
  23. isCamSelector: false,
  24. responseStatus: undefined,
  25. /**
  26. * A queue of pending requests.
  27. */
  28. pending: [],
  29. /**
  30. * The ID of the request currently visible on the screen. This has the value
  31. * "undefined" when there is no request visible on the screen.
  32. */
  33. currentRequestId: undefined,
  34. /**
  35. * start the PermissionManager to init variables and listeners
  36. * @memberof PermissionManager.prototype
  37. */
  38. start: function pm_start() {
  39. // Div over in which the permission UI resides.
  40. this.overlay = document.getElementById('permission-screen');
  41. this.dialog = document.getElementById('permission-dialog');
  42. this.title = document.getElementById('permission-title');
  43. this.message = document.getElementById('permission-message');
  44. this.moreInfo = document.getElementById('permission-more-info');
  45. this.moreInfoLink = document.getElementById('permission-more-info-link');
  46. this.hideInfoLink = document.getElementById('permission-hide-info-link');
  47. this.moreInfoBox = document.getElementById('permission-more-info-box');
  48. // "Yes"/"No" buttons on the permission UI.
  49. this.buttons = document.getElementById('permission-buttons');
  50. this.yes = document.getElementById('permission-yes');
  51. this.no = document.getElementById('permission-no');
  52. // Remember the choice checkbox
  53. this.remember = document.getElementById('permission-remember-checkbox');
  54. this.rememberSection =
  55. document.getElementById('permission-remember-section');
  56. this.deviceSelector =
  57. document.getElementById('permission-device-selector');
  58. this.devices = document.getElementById('permission-devices');
  59. var self = this;
  60. this.rememberSection.addEventListener('click',
  61. function onLabelClick() {
  62. self.remember.checked = !self.remember.checked;
  63. });
  64. window.addEventListener('mozChromeEvent', this);
  65. window.addEventListener('attentionopening', this);
  66. window.addEventListener('attentionopened', this);
  67. window.addEventListener('lockscreen-appopened', this);
  68. window.addEventListener('screenchange', this);
  69. /* On home/holdhome pressed, discard permission request.
  70. * XXX: We should make permission dialog be embededd in appWindow
  71. * Gaia bug is https://bugzilla.mozilla.org/show_bug.cgi?id=853711
  72. * Gecko bug is https://bugzilla.mozilla.org/show_bug.cgi?id=852013
  73. */
  74. this.discardPermissionRequest = this.discardPermissionRequest.bind(this);
  75. window.addEventListener('home', this.discardPermissionRequest);
  76. window.addEventListener('holdhome', this.discardPermissionRequest);
  77. /* If an application that is currently running needs to get killed for
  78. * whatever reason we want to discard it's request for permissions.
  79. */
  80. window.addEventListener('appterminated', (function(evt) {
  81. if (evt.detail.origin == this.currentOrigin) {
  82. this.discardPermissionRequest();
  83. }
  84. }).bind(this));
  85. // Ensure that the focus is not stolen by the permission overlay, as
  86. // it may appears on top of a <select> element, and just cancel it.
  87. this.overlay.addEventListener('mousedown', function onMouseDown(evt) {
  88. evt.preventDefault();
  89. });
  90. },
  91. /**
  92. * Returns the view for each device option.
  93. * @memberof PermissionManager.prototype
  94. */
  95. deviceOptionView: function({id, checked, label}) {
  96. return Tagged.escapeHTML `<label class="device-list deviceEnable">
  97. <input class="input-enable" id="${id}" type="checkbox" ${checked}>
  98. <span></span>
  99. </label>
  100. <span class="device-item" data-l10n-id="${label}"></span>`;
  101. },
  102. /**
  103. * Request all strings to show
  104. * @memberof PermissionManager.prototype
  105. */
  106. getStrings: function getStrings(detail) {
  107. // If we are in fullscreen, the strings are slightly different.
  108. if (this.isFullscreenRequest) {
  109. var fullscreenMessage = {
  110. id: 'fullscreen-request',
  111. args: {
  112. 'origin': detail.fullscreenorigin
  113. }};
  114. return {
  115. message: fullscreenMessage,
  116. moreInfoText: null
  117. };
  118. }
  119. // If it's a regular request (non-fullscreen), we review
  120. // the permission and create the strings accordingly
  121. var permissionID = 'perm-' + this.permissionType.replace(':', '-');
  122. var app = applications.getByManifestURL(detail.manifestURL);
  123. var message = '';
  124. if (detail.isApp) {
  125. var appName = new ManifestHelper(app.manifest).name;
  126. message = {
  127. id: permissionID + '-appRequest',
  128. args: {
  129. 'app': appName
  130. }};
  131. } else {
  132. message = {
  133. id: permissionID + '-webRequest',
  134. args: {
  135. 'site': detail.origin
  136. }};
  137. }
  138. var moreInfoText = permissionID + '-more-info';
  139. return {
  140. message : message,
  141. moreInfoText: moreInfoText
  142. };
  143. },
  144. /**
  145. * stop the PermissionManager to reset variables and listeners
  146. * @memberof PermissionManager.prototype
  147. */
  148. stop: function pm_stop() {
  149. this.currentOrigin = null;
  150. this.permissionType = null;
  151. this.currentPermissions = null;
  152. this.currentChoices = {};
  153. this.fullscreenRequest = null;
  154. this.isVideo = false;
  155. this.isAudio = false;
  156. this.isCamSelector = false;
  157. this.responseStatus = null;
  158. this.pending = [];
  159. this.currentRequestId = null;
  160. this.overlay = null;
  161. this.dialog = null;
  162. this.title = null;
  163. this.message = null;
  164. this.moreInfo = null;
  165. this.moreInfoLink = null;
  166. this.moreInfoBox = null;
  167. this.remember = null;
  168. this.rememberSection = null;
  169. this.deviceSelector = null;
  170. this.devices = null;
  171. this.buttons = null;
  172. this.yes = null;
  173. this.no = null;
  174. window.removeEventListener('mozChromeEvent', this);
  175. window.removeEventListener('attentionopening', this);
  176. window.removeEventListener('attentionopened', this);
  177. window.removeEventListener('lockscreen-appopened', this);
  178. window.removeEventListener('screenchange', this);
  179. window.removeEventListener('home', this.discardPermissionRequest);
  180. window.removeEventListener('holdhome', this.discardPermissionRequest);
  181. },
  182. /**
  183. * Reset current values
  184. * @memberof PermissionManager.prototype
  185. */
  186. cleanDialog: function pm_cleanDialog() {
  187. delete this.overlay.dataset.type;
  188. this.permissionType = undefined;
  189. this.currentPermissions = undefined;
  190. this.currentChoices = {};
  191. this.isVideo = false;
  192. this.isAudio = false;
  193. this.isCamSelector = false;
  194. //handled in showPermissionPrompt
  195. if (this.message.classList.contains('hidden')) {
  196. this.message.classList.remove('hidden');
  197. }
  198. if (!this.moreInfoBox.classList.contains('hidden')) {
  199. this.moreInfoBox.classList.add('hidden');
  200. }
  201. this.devices.innerHTML = '';
  202. if (!this.deviceSelector.classList.contains('hidden')) {
  203. this.deviceSelector.classList.add('hidden');
  204. }
  205. this.buttons.dataset.items = 2;
  206. this.no.style.display = 'inline';
  207. },
  208. /**
  209. * Queue or show the permission prompt
  210. * @memberof PermissionManager.prototype
  211. */
  212. queuePrompt: function(detail) {
  213. this.pending.push(detail);
  214. },
  215. /**
  216. * Event handler interface for mozChromeEvent.
  217. * @memberof PermissionManager.prototype
  218. * @param {DOMEvent} evt The event.
  219. */
  220. handleEvent: function pm_handleEvent(evt) {
  221. var detail = evt.detail;
  222. switch (detail.type) {
  223. case 'permission-prompt':
  224. if (!!this.currentRequestId) {
  225. this.queuePrompt(detail);
  226. return;
  227. }
  228. this.handlePermissionPrompt(detail);
  229. break;
  230. case 'cancel-permission-prompt':
  231. this.discardPermissionRequest();
  232. break;
  233. case 'fullscreenoriginchange':
  234. delete this.overlay.dataset.type;
  235. this.cleanDialog();
  236. this.handleFullscreenOriginChange(detail);
  237. break;
  238. }
  239. switch (evt.type) {
  240. case 'attentionopened':
  241. case 'attentionopening':
  242. if (this.currentOrigin !== evt.detail.origin) {
  243. this.discardPermissionRequest();
  244. }
  245. break;
  246. case 'lockscreen-appopened':
  247. if (this.currentRequestId == 'fullscreen') {
  248. this.discardPermissionRequest();
  249. }
  250. break;
  251. case 'screenchange':
  252. if (Service.query('locked') && !detail.screenEnabled) {
  253. this.discardPermissionRequest();
  254. }
  255. break;
  256. }
  257. },
  258. /**
  259. * Handle getUserMedia device select options
  260. * @memberof PermissionManager.prototype
  261. * @param {DOMEvent} evt The event.
  262. */
  263. optionClickhandler: function pm_optionClickhandler(evt) {
  264. var link = evt.target;
  265. if (!link) {
  266. return;
  267. }
  268. if (link.classList.contains('input-enable')) {
  269. if (link.checked) {
  270. this.currentChoices['video-capture'] = link.id;
  271. }
  272. var items = this.devices.querySelectorAll('input[type="checkbox"]');
  273. // Uncheck unselected option, allow 1 selection at same time
  274. for (var i = 0; i < items.length; i++) {
  275. if (items[i].id !== link.id) {
  276. items[i].checked = false;
  277. items[i].disabled = false; // Not allow to uncheck last option
  278. } else {
  279. link.disabled = true;
  280. }
  281. }
  282. }
  283. },
  284. /**
  285. * Show the request for the new domain
  286. * @memberof PermissionManager.prototype
  287. * @param {Object} detail The event detail object.
  288. */
  289. handleFullscreenOriginChange:
  290. function pm_handleFullscreenOriginChange(detail) {
  291. // If there's already a fullscreen request visible, cancel it,
  292. // we'll show the request for the new domain.
  293. if (this.isFullscreenRequest) {
  294. this.cancelRequest(this.currentRequestId);
  295. this.isFullscreenRequest = false;
  296. }
  297. if (detail.fullscreenorigin !==
  298. Service.query('getTopMostWindow').origin) {
  299. this.isFullscreenRequest = true;
  300. detail.id = 'fullscreen';
  301. this.showPermissionPrompt(
  302. detail,
  303. function foo() {},
  304. function() {
  305. document.mozCancelFullScreen();
  306. }
  307. );
  308. }
  309. },
  310. /**
  311. * Prepare for permission prompt
  312. * @memberof PermissionManager.prototype
  313. * @param {Object} detail The event detail object.
  314. */
  315. handlePermissionPrompt: function pm_handlePermissionPrompt(detail) {
  316. // Clean dialog if was rendered before
  317. this.cleanDialog();
  318. this.isFullscreenRequest = false;
  319. this.currentOrigin = detail.origin;
  320. this.currentRequestId = detail.id;
  321. if (detail.permissions) {
  322. if ('video-capture' in detail.permissions) {
  323. this.isVideo = true;
  324. // video selector is only for app
  325. if (detail.isApp && detail.isGranted &&
  326. detail.permissions['video-capture'].length > 1) {
  327. this.isCamSelector = true;
  328. }
  329. }
  330. if ('audio-capture' in detail.permissions) {
  331. this.isAudio = true;
  332. }
  333. } else { // work in <1.4 compatible mode
  334. if (detail.permission) {
  335. this.permissionType = detail.permission;
  336. if ('video-capture' === detail.permission) {
  337. this.isVideo = true;
  338. }
  339. if ('audio-capture' === detail.permission) {
  340. this.isAudio = true;
  341. }
  342. }
  343. }
  344. // Set default permission
  345. if (this.isVideo && this.isAudio) {
  346. this.permissionType = 'media-capture';
  347. } else {
  348. if (detail.permission) {
  349. this.permissionType = detail.permission;
  350. } else if (detail.permissions) {
  351. this.permissionType = Object.keys(detail.permissions)[0];
  352. }
  353. }
  354. this.overlay.dataset.type = this.permissionType;
  355. if (this.isAudio || this.isVideo) {
  356. if (!detail.isApp) {
  357. // Not show remember my choice option in website
  358. this.rememberSection.style.display = 'none';
  359. } else {
  360. this.rememberSection.style.display = 'block';
  361. }
  362. // Set default options
  363. this.currentPermissions = detail.permissions;
  364. for (var permission2 in detail.permissions) {
  365. if (detail.permissions.hasOwnProperty(permission2)) {
  366. // gecko might not support audio/video option
  367. if (detail.permissions[permission2].length > 0) {
  368. this.currentChoices[permission2] =
  369. detail.permissions[permission2][0];
  370. }
  371. }
  372. }
  373. }
  374. if ((this.isAudio || this.isVideo) && !detail.isApp &&
  375. !this.isCamSelector) {
  376. // gUM always not remember in web mode
  377. this.remember.checked = false;
  378. } else {
  379. this.remember.checked = detail.remember ? true : false;
  380. }
  381. if (detail.isApp) { // App
  382. var app = applications.getByManifestURL(detail.manifestURL);
  383. if (this.isCamSelector) {
  384. this.title.setAttribute('data-l10n-id', 'title-cam');
  385. } else {
  386. this.title.setAttribute('data-l10n-id', 'title-app');
  387. }
  388. navigator.mozL10n.setAttributes(
  389. this.deviceSelector,
  390. 'perm-camera-selector-appRequest',
  391. { 'app': new ManifestHelper(app.manifest).name }
  392. );
  393. } else { // Web content
  394. this.title.setAttribute('data-l10n-id', 'title-web');
  395. navigator.mozL10n.setAttributes(
  396. this.deviceSelector,
  397. 'perm-camera-selector-webRequest',
  398. { 'site': detail.origin }
  399. );
  400. }
  401. var self = this;
  402. this.showPermissionPrompt(
  403. detail,
  404. function pm_permYesCB() {
  405. self.dispatchResponse(
  406. detail.id,
  407. 'permission-allow',
  408. self.remember.checked
  409. );
  410. },
  411. function pm_permNoCB() {
  412. self.dispatchResponse(
  413. detail.id,
  414. 'permission-deny',
  415. self.remember.checked
  416. );
  417. }
  418. );
  419. },
  420. /**
  421. * Send permission choice to gecko
  422. * @memberof PermissionManager.prototype
  423. */
  424. dispatchResponse: function pm_dispatchResponse(id, type, remember) {
  425. if (this.isCamSelector) {
  426. remember = true;
  427. }
  428. this.responseStatus = type;
  429. var response = {
  430. id: id,
  431. type: type,
  432. remember: remember
  433. };
  434. if (this.isVideo || this.isAudio || this.isCamSelector) {
  435. response.choices = this.currentChoices;
  436. }
  437. var event = document.createEvent('CustomEvent');
  438. event.initCustomEvent('mozContentEvent', true, true, response);
  439. window.dispatchEvent(event);
  440. },
  441. /**
  442. * Hide prompt
  443. * @memberof PermissionManager.prototype
  444. */
  445. hidePermissionPrompt: function pm_hidePermissionPrompt() {
  446. this.overlay.classList.remove('visible');
  447. this.devices.removeEventListener('click', this);
  448. this.devices.classList.remove('visible');
  449. this.currentRequestId = undefined;
  450. // Cleanup the event handlers.
  451. this.yes.removeEventListener('click', this.yesHandler);
  452. this.yes.callback = null;
  453. this.no.removeEventListener('click', this.noHandler);
  454. this.no.callback = null;
  455. this.moreInfo.classList.add('hidden');
  456. this.moreInfoLink.removeEventListener('click',
  457. this.moreInfoHandler);
  458. this.hideInfoLink.removeEventListener('click',
  459. this.hideInfoHandler);
  460. if (!this.hideInfoLink.classList.contains('hidden')) {
  461. this.toggleInfo();
  462. }
  463. // XXX: This is telling AppWindowManager to focus the active app.
  464. // After we are moving into AppWindow, we need to remove that
  465. // and call this.app.focus() instead.
  466. this.publish('permissiondialoghide');
  467. },
  468. publish: function(eventName, detail) {
  469. var event = document.createEvent('CustomEvent');
  470. event.initCustomEvent(eventName, true, true, detail);
  471. window.dispatchEvent(event);
  472. },
  473. /**
  474. * Show the next request, if we have one.
  475. * @memberof PermissionManager.prototype
  476. */
  477. showNextPendingRequest: function pm_showNextPendingRequest() {
  478. if (this.pending.length === 0) {
  479. return;
  480. }
  481. var request = this.pending.shift();
  482. if ((this.currentOrigin === request.origin) &&
  483. (this.permissionType === Object.keys(request.permissions)[0])) {
  484. this.dispatchResponse(request.id, this.responseStatus,
  485. this.remember.checked);
  486. this.showNextPendingRequest();
  487. return;
  488. }
  489. this.handlePermissionPrompt(request);
  490. },
  491. /**
  492. * Event listener function for the yes/no buttons.
  493. * @memberof PermissionManager.prototype
  494. */
  495. clickHandler: function pm_clickHandler(evt) {
  496. var callback = null;
  497. if (evt.target === this.yes && this.yes.callback) {
  498. callback = this.yes.callback;
  499. this.responseStatus = 'permission-allow';
  500. } else if (evt.target === this.no && this.no.callback) {
  501. callback = this.no.callback;
  502. this.responseStatus = 'permission-deny';
  503. } else if (evt.target === this.moreInfoLink ||
  504. evt.target === this.hideInfoLink) {
  505. this.toggleInfo();
  506. return;
  507. }
  508. this.hidePermissionPrompt();
  509. // Call the appropriate callback, if it is defined.
  510. if (callback) {
  511. window.setTimeout(callback, 0);
  512. }
  513. this.showNextPendingRequest();
  514. },
  515. toggleInfo: function pm_toggleInfo() {
  516. this.moreInfoLink.classList.toggle('hidden');
  517. this.hideInfoLink.classList.toggle('hidden');
  518. this.moreInfoBox.classList.toggle('hidden');
  519. },
  520. /**
  521. * Form the media source selection list
  522. * @memberof PermissionManager.prototype
  523. */
  524. listDeviceOptions: function pm_listDeviceOptions() {
  525. var checked;
  526. // show description
  527. this.deviceSelector.classList.remove('hidden');
  528. // build device list
  529. this.currentPermissions['video-capture'].forEach(option => {
  530. // Match currentChoices
  531. checked = (this.currentChoices['video-capture'] === option) ?
  532. 'checked=true disabled=true' : '';
  533. if (checked) {
  534. this.currentChoices['video-capture'] = option;
  535. }
  536. var item_li = document.createElement('li');
  537. item_li.className = 'device-cell';
  538. item_li.innerHTML = this.deviceOptionView({
  539. id: option,
  540. checked: checked,
  541. label: 'device-' + option
  542. });
  543. this.devices.appendChild(item_li);
  544. });
  545. this.devices.addEventListener('click',
  546. this.optionClickhandler.bind(this));
  547. this.devices.classList.add('visible');
  548. },
  549. /**
  550. * Put the message in the dialog.
  551. * @memberof PermissionManager.prototype
  552. */
  553. showPermissionPrompt:
  554. function pm_showPermissionPrompt(detail, yescallback, nocallback) {
  555. // Note plain text since this may include text from
  556. // untrusted app manifests, for example.
  557. var text = this.getStrings(detail);
  558. if (typeof(text.message) === 'object') {
  559. navigator.mozL10n.setAttributes(this.message,
  560. text.message.id, text.message.args);
  561. } else {
  562. this.message.setAttribute('data-l10n-id', text.message);
  563. }
  564. if (text.moreInfoText) {
  565. // Show the "More info… " link.
  566. this.moreInfo.classList.remove('hidden');
  567. this.moreInfoHandler = this.clickHandler.bind(this);
  568. this.hideInfoHandler = this.clickHandler.bind(this);
  569. this.moreInfoLink.addEventListener('click', this.moreInfoHandler);
  570. this.hideInfoLink.addEventListener('click', this.hideInfoHandler);
  571. this.moreInfoBox.setAttribute('data-l10n-id', text.moreInfoText);
  572. }
  573. this.currentRequestId = detail.id;
  574. // Not show the list if there's only 1 option
  575. if (this.isVideo && this.currentPermissions['video-capture'].length > 1) {
  576. this.listDeviceOptions();
  577. }
  578. // Set event listeners for the yes and no buttons
  579. var isSharedPermission = this.isVideo || this.isAudio ||
  580. this.permissionType === 'geolocation';
  581. this.yes.setAttribute('data-l10n-id',
  582. isSharedPermission ? 'share-' + this.permissionType : 'allow');
  583. this.yesHandler = this.clickHandler.bind(this);
  584. this.yes.addEventListener('click', this.yesHandler);
  585. this.yes.callback = yescallback;
  586. this.no.setAttribute('data-l10n-id', isSharedPermission ?
  587. 'dontshare-' + this.permissionType : 'dontallow');
  588. this.noHandler = this.clickHandler.bind(this);
  589. this.no.addEventListener('click', this.noHandler);
  590. this.no.callback = nocallback;
  591. // customize camera selector dialog
  592. if (this.isCamSelector) {
  593. this.message.classList.add('hidden');
  594. this.rememberSection.style.display = 'none';
  595. this.buttons.dataset.items = 1;
  596. this.no.style.display = 'none';
  597. this.yes.setAttribute('data-l10n-id', 'ok');
  598. }
  599. // Make the screen visible
  600. this.overlay.classList.add('visible');
  601. },
  602. /**
  603. * Cancels a request with a specfied id. Request can either be
  604. * currently showing, or pending. If there are further pending requests,
  605. * the next is shown.
  606. * @memberof PermissionManager.prototype
  607. */
  608. cancelRequest: function pm_cancelRequest(id) {
  609. if (this.currentRequestId === id) {
  610. // Request is currently being displayed. Hide the permission prompt,
  611. // and show the next request, if we have any.
  612. this.hidePermissionPrompt();
  613. this.showNextPendingRequest();
  614. } else {
  615. // The request is currently not being displayed. Search through the
  616. // list of pending requests, and remove it from the list if present.
  617. for (var i = 0; i < this.pending.length; i++) {
  618. if (this.pending[i].id === id) {
  619. this.pending.splice(i, 1);
  620. break;
  621. }
  622. }
  623. }
  624. },
  625. /**
  626. * Clean current request queue and
  627. * send refuse permission request message to gecko
  628. * @memberof PermissionManager.prototype
  629. */
  630. discardPermissionRequest: function pm_discardPermissionRequest() {
  631. if (this.currentRequestId === undefined ||
  632. this.currentRequestId === null) {
  633. return;
  634. }
  635. if (this.currentRequestId == 'fullscreen') {
  636. if (this.no.callback) {
  637. this.no.callback();
  638. }
  639. this.isFullscreenRequest = false;
  640. } else {
  641. this.dispatchResponse(this.currentRequestId, 'permission-deny', false);
  642. }
  643. this.hidePermissionPrompt();
  644. this.pending = [];
  645. }
  646. };
  647. exports.PermissionManager = PermissionManager;
  648. })(window);