linkdialogplugin.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview A plugin for the LinkDialog.
  16. *
  17. * @author nicksantos@google.com (Nick Santos)
  18. * @author robbyw@google.com (Robby Walker)
  19. */
  20. goog.provide('goog.editor.plugins.LinkDialogPlugin');
  21. goog.require('goog.array');
  22. goog.require('goog.dom');
  23. goog.require('goog.editor.Command');
  24. goog.require('goog.editor.plugins.AbstractDialogPlugin');
  25. goog.require('goog.events.EventHandler');
  26. goog.require('goog.functions');
  27. goog.require('goog.ui.editor.AbstractDialog');
  28. goog.require('goog.ui.editor.LinkDialog');
  29. goog.require('goog.uri.utils');
  30. /**
  31. * A plugin that opens the link dialog.
  32. * @constructor
  33. * @extends {goog.editor.plugins.AbstractDialogPlugin}
  34. */
  35. goog.editor.plugins.LinkDialogPlugin = function() {
  36. goog.editor.plugins.LinkDialogPlugin.base(
  37. this, 'constructor', goog.editor.Command.MODAL_LINK_EDITOR);
  38. /**
  39. * Event handler for this object.
  40. * @type {goog.events.EventHandler<!goog.editor.plugins.LinkDialogPlugin>}
  41. * @private
  42. */
  43. this.eventHandler_ = new goog.events.EventHandler(this);
  44. /**
  45. * A list of whitelisted URL schemes which are safe to open.
  46. * @type {Array<string>}
  47. * @private
  48. */
  49. this.safeToOpenSchemes_ = ['http', 'https', 'ftp'];
  50. };
  51. goog.inherits(
  52. goog.editor.plugins.LinkDialogPlugin,
  53. goog.editor.plugins.AbstractDialogPlugin);
  54. /**
  55. * Link object that the dialog is editing.
  56. * @type {goog.editor.Link}
  57. * @protected
  58. */
  59. goog.editor.plugins.LinkDialogPlugin.prototype.currentLink_;
  60. /**
  61. * Optional warning to show about email addresses.
  62. * @type {goog.html.SafeHtml}
  63. * @private
  64. */
  65. goog.editor.plugins.LinkDialogPlugin.prototype.emailWarning_;
  66. /**
  67. * Whether to show a checkbox where the user can choose to have the link open in
  68. * a new window.
  69. * @type {boolean}
  70. * @private
  71. */
  72. goog.editor.plugins.LinkDialogPlugin.prototype.showOpenLinkInNewWindow_ = false;
  73. /**
  74. * Whether the "open link in new window" checkbox should be checked when the
  75. * dialog is shown, and also whether it was checked last time the dialog was
  76. * closed.
  77. * @type {boolean}
  78. * @private
  79. */
  80. goog.editor.plugins.LinkDialogPlugin.prototype.isOpenLinkInNewWindowChecked_ =
  81. false;
  82. /**
  83. * Weather to show a checkbox where the user can choose to add 'rel=nofollow'
  84. * attribute added to the link.
  85. * @type {boolean}
  86. * @private
  87. */
  88. goog.editor.plugins.LinkDialogPlugin.prototype.showRelNoFollow_ = false;
  89. /**
  90. * Whether to stop referrer leaks. Defaults to false.
  91. * @type {boolean}
  92. * @private
  93. */
  94. goog.editor.plugins.LinkDialogPlugin.prototype.stopReferrerLeaks_ = false;
  95. /**
  96. * Whether to block opening links with a non-whitelisted URL scheme.
  97. * @type {boolean}
  98. * @private
  99. */
  100. goog.editor.plugins.LinkDialogPlugin.prototype.blockOpeningUnsafeSchemes_ =
  101. true;
  102. /** @override */
  103. goog.editor.plugins.LinkDialogPlugin.prototype.getTrogClassId =
  104. goog.functions.constant('LinkDialogPlugin');
  105. /**
  106. * Tells the plugin whether to block URLs with schemes not in the whitelist.
  107. * If blocking is enabled, this plugin will stop the 'Test Link' popup
  108. * window from being created. Blocking doesn't affect link creation--if the
  109. * user clicks the 'OK' button with an unsafe URL, the link will still be
  110. * created as normal.
  111. * @param {boolean} blockOpeningUnsafeSchemes Whether to block non-whitelisted
  112. * schemes.
  113. */
  114. goog.editor.plugins.LinkDialogPlugin.prototype.setBlockOpeningUnsafeSchemes =
  115. function(blockOpeningUnsafeSchemes) {
  116. this.blockOpeningUnsafeSchemes_ = blockOpeningUnsafeSchemes;
  117. };
  118. /**
  119. * Sets a whitelist of allowed URL schemes that are safe to open.
  120. * Schemes should all be in lowercase. If the plugin is set to block opening
  121. * unsafe schemes, user-entered URLs will be converted to lowercase and checked
  122. * against this list. The whitelist has no effect if blocking is not enabled.
  123. * @param {Array<string>} schemes String array of URL schemes to allow (http,
  124. * https, etc.).
  125. */
  126. goog.editor.plugins.LinkDialogPlugin.prototype.setSafeToOpenSchemes = function(
  127. schemes) {
  128. this.safeToOpenSchemes_ = schemes;
  129. };
  130. /**
  131. * Tells the dialog to show a checkbox where the user can choose to have the
  132. * link open in a new window.
  133. * @param {boolean} startChecked Whether to check the checkbox the first
  134. * time the dialog is shown. Subesquent times the checkbox will remember its
  135. * previous state.
  136. */
  137. goog.editor.plugins.LinkDialogPlugin.prototype.showOpenLinkInNewWindow =
  138. function(startChecked) {
  139. this.showOpenLinkInNewWindow_ = true;
  140. this.isOpenLinkInNewWindowChecked_ = startChecked;
  141. };
  142. /**
  143. * Tells the dialog to show a checkbox where the user can choose to have
  144. * 'rel=nofollow' attribute added to the link.
  145. */
  146. goog.editor.plugins.LinkDialogPlugin.prototype.showRelNoFollow = function() {
  147. this.showRelNoFollow_ = true;
  148. };
  149. /**
  150. * Returns whether the"open link in new window" checkbox was checked last time
  151. * the dialog was closed.
  152. * @return {boolean} Whether the"open link in new window" checkbox was checked
  153. * last time the dialog was closed.
  154. */
  155. goog.editor.plugins.LinkDialogPlugin.prototype
  156. .getOpenLinkInNewWindowCheckedState = function() {
  157. return this.isOpenLinkInNewWindowChecked_;
  158. };
  159. /**
  160. * Tells the plugin to stop leaking the page's url via the referrer header when
  161. * the "test this link" link is clicked. When the user clicks on a link, the
  162. * browser makes a request for the link url, passing the url of the current page
  163. * in the request headers. If the user wants the current url to be kept secret
  164. * (e.g. an unpublished document), the owner of the url that was clicked will
  165. * see the secret url in the request headers, and it will no longer be a secret.
  166. * Calling this method will not send a referrer header in the request, just as
  167. * if the user had opened a blank window and typed the url in themselves.
  168. */
  169. goog.editor.plugins.LinkDialogPlugin.prototype.stopReferrerLeaks = function() {
  170. this.stopReferrerLeaks_ = true;
  171. };
  172. /**
  173. * Sets the warning message to show to users about including email addresses on
  174. * public web pages.
  175. * @param {!goog.html.SafeHtml} emailWarning Warning message to show users about
  176. * including email addresses on the web.
  177. */
  178. goog.editor.plugins.LinkDialogPlugin.prototype.setEmailWarning = function(
  179. emailWarning) {
  180. this.emailWarning_ = emailWarning;
  181. };
  182. /**
  183. * Handles execCommand by opening the dialog.
  184. * @param {string} command The command to execute.
  185. * @param {*=} opt_arg {@link A goog.editor.Link} object representing the link
  186. * being edited.
  187. * @return {*} Always returns true, indicating the dialog was shown.
  188. * @protected
  189. * @override
  190. */
  191. goog.editor.plugins.LinkDialogPlugin.prototype.execCommandInternal = function(
  192. command, opt_arg) {
  193. this.currentLink_ = /** @type {goog.editor.Link} */ (opt_arg);
  194. return goog.editor.plugins.LinkDialogPlugin.base(
  195. this, 'execCommandInternal', command, opt_arg);
  196. };
  197. /**
  198. * Handles when the dialog closes.
  199. * @param {goog.events.Event} e The AFTER_HIDE event object.
  200. * @override
  201. * @protected
  202. */
  203. goog.editor.plugins.LinkDialogPlugin.prototype.handleAfterHide = function(e) {
  204. goog.editor.plugins.LinkDialogPlugin.base(this, 'handleAfterHide', e);
  205. this.currentLink_ = null;
  206. };
  207. /**
  208. * @return {goog.events.EventHandler<T>} The event handler.
  209. * @protected
  210. * @this {T}
  211. * @template T
  212. */
  213. goog.editor.plugins.LinkDialogPlugin.prototype.getEventHandler = function() {
  214. return this.eventHandler_;
  215. };
  216. /**
  217. * @return {goog.editor.Link} The link being edited.
  218. * @protected
  219. */
  220. goog.editor.plugins.LinkDialogPlugin.prototype.getCurrentLink = function() {
  221. return this.currentLink_;
  222. };
  223. /**
  224. * Creates a new instance of the dialog and registers for the relevant events.
  225. * @param {goog.dom.DomHelper} dialogDomHelper The dom helper to be used to
  226. * create the dialog.
  227. * @param {*=} opt_link The target link (should be a goog.editor.Link).
  228. * @return {!goog.ui.editor.LinkDialog} The dialog.
  229. * @override
  230. * @protected
  231. */
  232. goog.editor.plugins.LinkDialogPlugin.prototype.createDialog = function(
  233. dialogDomHelper, opt_link) {
  234. var dialog = new goog.ui.editor.LinkDialog(
  235. dialogDomHelper,
  236. /** @type {goog.editor.Link} */ (opt_link));
  237. if (this.emailWarning_) {
  238. dialog.setEmailWarning(this.emailWarning_);
  239. }
  240. if (this.showOpenLinkInNewWindow_) {
  241. dialog.showOpenLinkInNewWindow(this.isOpenLinkInNewWindowChecked_);
  242. }
  243. if (this.showRelNoFollow_) {
  244. dialog.showRelNoFollow();
  245. }
  246. dialog.setStopReferrerLeaks(this.stopReferrerLeaks_);
  247. this.eventHandler_
  248. .listen(dialog, goog.ui.editor.AbstractDialog.EventType.OK, this.handleOk)
  249. .listen(
  250. dialog, goog.ui.editor.AbstractDialog.EventType.CANCEL,
  251. this.handleCancel_)
  252. .listen(
  253. dialog, goog.ui.editor.LinkDialog.EventType.BEFORE_TEST_LINK,
  254. this.handleBeforeTestLink);
  255. return dialog;
  256. };
  257. /** @override */
  258. goog.editor.plugins.LinkDialogPlugin.prototype.disposeInternal = function() {
  259. goog.editor.plugins.LinkDialogPlugin.base(this, 'disposeInternal');
  260. this.eventHandler_.dispose();
  261. };
  262. /**
  263. * Handles the OK event from the dialog by updating the link in the field.
  264. * @param {goog.ui.editor.LinkDialog.OkEvent} e OK event object.
  265. * @protected
  266. */
  267. goog.editor.plugins.LinkDialogPlugin.prototype.handleOk = function(e) {
  268. // We're not restoring the original selection, so clear it out.
  269. this.disposeOriginalSelection();
  270. this.currentLink_.setTextAndUrl(e.linkText, e.linkUrl);
  271. if (this.showOpenLinkInNewWindow_) {
  272. // Save checkbox state for next time.
  273. this.isOpenLinkInNewWindowChecked_ = e.openInNewWindow;
  274. }
  275. var anchor = this.currentLink_.getAnchor();
  276. this.touchUpAnchorOnOk_(anchor, e);
  277. var extraAnchors = this.currentLink_.getExtraAnchors();
  278. for (var i = 0; i < extraAnchors.length; ++i) {
  279. extraAnchors[i].href = anchor.href;
  280. this.touchUpAnchorOnOk_(extraAnchors[i], e);
  281. }
  282. // Place cursor to the right of the modified link.
  283. this.currentLink_.placeCursorRightOf();
  284. this.getFieldObject().focus();
  285. this.getFieldObject().dispatchSelectionChangeEvent();
  286. this.getFieldObject().dispatchChange();
  287. this.eventHandler_.removeAll();
  288. };
  289. /**
  290. * Apply the necessary properties to a link upon Ok being clicked in the dialog.
  291. * @param {HTMLAnchorElement} anchor The anchor to set properties on.
  292. * @param {goog.events.Event} e Event object.
  293. * @private
  294. */
  295. goog.editor.plugins.LinkDialogPlugin.prototype.touchUpAnchorOnOk_ = function(
  296. anchor, e) {
  297. if (this.showOpenLinkInNewWindow_) {
  298. if (e.openInNewWindow) {
  299. anchor.target = '_blank';
  300. } else {
  301. if (anchor.target == '_blank') {
  302. anchor.target = '';
  303. }
  304. // If user didn't indicate to open in a new window but the link already
  305. // had a target other than '_blank', let's leave what they had before.
  306. }
  307. }
  308. if (this.showRelNoFollow_) {
  309. var alreadyPresent = goog.ui.editor.LinkDialog.hasNoFollow(anchor.rel);
  310. if (alreadyPresent && !e.noFollow) {
  311. anchor.rel = goog.ui.editor.LinkDialog.removeNoFollow(anchor.rel);
  312. } else if (!alreadyPresent && e.noFollow) {
  313. anchor.rel = anchor.rel ? anchor.rel + ' nofollow' : 'nofollow';
  314. }
  315. }
  316. };
  317. /**
  318. * Handles the CANCEL event from the dialog by clearing the anchor if needed.
  319. * @param {goog.events.Event} e Event object.
  320. * @private
  321. */
  322. goog.editor.plugins.LinkDialogPlugin.prototype.handleCancel_ = function(e) {
  323. if (this.currentLink_.isNew()) {
  324. goog.dom.flattenElement(this.currentLink_.getAnchor());
  325. var extraAnchors = this.currentLink_.getExtraAnchors();
  326. for (var i = 0; i < extraAnchors.length; ++i) {
  327. goog.dom.flattenElement(extraAnchors[i]);
  328. }
  329. // Make sure listeners know the anchor was flattened out.
  330. this.getFieldObject().dispatchChange();
  331. }
  332. this.eventHandler_.removeAll();
  333. };
  334. /**
  335. * Handles the BeforeTestLink event fired when the 'test' link is clicked.
  336. * @param {goog.ui.editor.LinkDialog.BeforeTestLinkEvent} e BeforeTestLink event
  337. * object.
  338. * @protected
  339. */
  340. goog.editor.plugins.LinkDialogPlugin.prototype.handleBeforeTestLink = function(
  341. e) {
  342. if (!this.shouldOpenUrl(e.url)) {
  343. /** @desc Message when the user tries to test (preview) a link, but the
  344. * link cannot be tested. */
  345. var MSG_UNSAFE_LINK = goog.getMsg('This link cannot be tested.');
  346. alert(MSG_UNSAFE_LINK);
  347. e.preventDefault();
  348. }
  349. };
  350. /**
  351. * Checks whether the plugin should open the given url in a new window.
  352. * @param {string} url The url to check.
  353. * @return {boolean} If the plugin should open the given url in a new window.
  354. * @protected
  355. */
  356. goog.editor.plugins.LinkDialogPlugin.prototype.shouldOpenUrl = function(url) {
  357. return !this.blockOpeningUnsafeSchemes_ || this.isSafeSchemeToOpen_(url);
  358. };
  359. /**
  360. * Determines whether or not a url has a scheme which is safe to open.
  361. * Schemes like javascript are unsafe due to the possibility of XSS.
  362. * @param {string} url A url.
  363. * @return {boolean} Whether the url has a safe scheme.
  364. * @private
  365. */
  366. goog.editor.plugins.LinkDialogPlugin.prototype.isSafeSchemeToOpen_ = function(
  367. url) {
  368. var scheme = goog.uri.utils.getScheme(url) || 'http';
  369. return goog.array.contains(this.safeToOpenSchemes_, scheme.toLowerCase());
  370. };