extlink.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. /**
  2. * @file
  3. * External links js file.
  4. */
  5. (function ($, Drupal, drupalSettings) {
  6. 'use strict';
  7. Drupal.extlink = Drupal.extlink || {};
  8. Drupal.extlink.attach = function (context, drupalSettings) {
  9. if (typeof drupalSettings.data === 'undefined' || !drupalSettings.data.hasOwnProperty('extlink')) {
  10. return;
  11. }
  12. // Define the jQuery method (either 'append' or 'prepend') of placing the
  13. // icon, defaults to 'append'.
  14. var extIconPlacement = 'append';
  15. if (drupalSettings.data.extlink.extIconPlacement && drupalSettings.data.extlink.extIconPlacement != '0') {
  16. extIconPlacement = drupalSettings.data.extlink.extIconPlacement;
  17. }
  18. // Strip the host name down, removing ports, subdomains, or www.
  19. var pattern = /^(([^\/:]+?\.)*)([^\.:]{1,})((\.[a-z0-9]{1,253})*)(:[0-9]{1,5})?$/;
  20. var host = window.location.host.replace(pattern, '$2$3$6');
  21. var subdomain = window.location.host.replace(host, '');
  22. // Determine what subdomains are considered internal.
  23. var subdomains;
  24. if (drupalSettings.data.extlink.extSubdomains) {
  25. subdomains = '([^/]*\\.)?';
  26. }
  27. else if (subdomain === 'www.' || subdomain === '') {
  28. subdomains = '(www\\.)?';
  29. }
  30. else {
  31. subdomains = subdomain.replace('.', '\\.');
  32. }
  33. // Whitelisted domains.
  34. var whitelistedDomains = false;
  35. if (drupalSettings.data.extlink.whitelistedDomains) {
  36. whitelistedDomains = [];
  37. for (var i = 0; i < drupalSettings.data.extlink.whitelistedDomains.length; i++) {
  38. whitelistedDomains.push(new RegExp('^https?:\\/\\/' + drupalSettings.data.extlink.whitelistedDomains[i].replace(/(\r\n|\n|\r)/gm,'') + '.*$', 'i'));
  39. }
  40. }
  41. // Build regular expressions that define an internal link.
  42. var internal_link = new RegExp('^https?://([^@]*@)?' + subdomains + host, 'i');
  43. // Extra internal link matching.
  44. var extInclude = false;
  45. if (drupalSettings.data.extlink.extInclude) {
  46. extInclude = new RegExp(drupalSettings.data.extlink.extInclude.replace(/\\/, '\\'), 'i');
  47. }
  48. // Extra external link matching.
  49. var extExclude = false;
  50. if (drupalSettings.data.extlink.extExclude) {
  51. extExclude = new RegExp(drupalSettings.data.extlink.extExclude.replace(/\\/, '\\'), 'i');
  52. }
  53. // Extra external link CSS selector exclusion.
  54. var extCssExclude = false;
  55. if (drupalSettings.data.extlink.extCssExclude) {
  56. extCssExclude = drupalSettings.data.extlink.extCssExclude;
  57. }
  58. // Extra external link CSS selector explicit.
  59. var extCssExplicit = false;
  60. if (drupalSettings.data.extlink.extCssExplicit) {
  61. extCssExplicit = drupalSettings.data.extlink.extCssExplicit;
  62. }
  63. // Find all links which are NOT internal and begin with http as opposed
  64. // to ftp://, javascript:, etc. other kinds of links.
  65. // When operating on the 'this' variable, the host has been appended to
  66. // all links by the browser, even local ones.
  67. // In jQuery 1.1 and higher, we'd use a filter method here, but it is not
  68. // available in jQuery 1.0 (Drupal 5 default).
  69. var external_links = [];
  70. var mailto_links = [];
  71. $('a:not([data-extlink]), area:not([data-extlink])', context).each(function (el) {
  72. try {
  73. var url = '';
  74. if (typeof this.href == 'string') {
  75. url = this.href.toLowerCase();
  76. }
  77. // Handle SVG links (xlink:href).
  78. else if (typeof this.href == 'object') {
  79. url = this.href.baseVal;
  80. }
  81. if (url.indexOf('http') === 0
  82. && ((!internal_link.test(url) && !(extExclude && extExclude.test(url))) || (extInclude && extInclude.test(url)))
  83. && !(extCssExclude && $(this).is(extCssExclude))
  84. && !(extCssExclude && $(this).parents(extCssExclude).length > 0)
  85. && !(extCssExplicit && $(this).parents(extCssExplicit).length < 1)) {
  86. var match = false;
  87. if (whitelistedDomains) {
  88. for (var i = 0; i < whitelistedDomains.length; i++) {
  89. if (whitelistedDomains[i].test(url)) {
  90. match = true;
  91. break;
  92. }
  93. }
  94. }
  95. if (!match) {
  96. external_links.push(this);
  97. }
  98. }
  99. // Do not include area tags with begin with mailto: (this prohibits
  100. // icons from being added to image-maps).
  101. else if (this.tagName !== 'AREA'
  102. && url.indexOf('mailto:') === 0
  103. && !(extCssExclude && $(this).parents(extCssExclude).length > 0)
  104. && !(extCssExplicit && $(this).parents(extCssExplicit).length < 1)) {
  105. mailto_links.push(this);
  106. }
  107. }
  108. // IE7 throws errors often when dealing with irregular links, such as:
  109. // <a href="node/10"></a> Empty tags.
  110. // <a href="http://user:pass@example.com">example</a> User:pass syntax.
  111. catch (error) {
  112. return false;
  113. }
  114. });
  115. if (drupalSettings.data.extlink.extClass !== '0' && drupalSettings.data.extlink.extClass !== '') {
  116. Drupal.extlink.applyClassAndSpan(external_links, drupalSettings.data.extlink.extClass, extIconPlacement);
  117. }
  118. if (drupalSettings.data.extlink.mailtoClass !== '0' && drupalSettings.data.extlink.mailtoClass !== '') {
  119. Drupal.extlink.applyClassAndSpan(mailto_links, drupalSettings.data.extlink.mailtoClass, extIconPlacement);
  120. }
  121. if (drupalSettings.data.extlink.extTarget) {
  122. // Apply the target attribute to all links.
  123. $(external_links).filter(function () {
  124. // Filter out links with target set if option specified.
  125. return !(drupalSettings.data.extlink.extTargetNoOverride && $(this).is('a[target]'));
  126. }).attr({target: '_blank'});
  127. // Add noopener rel attribute to combat phishing.
  128. $(external_links).attr('rel', function (i, val) {
  129. // If no rel attribute is present, create one with the value noopener.
  130. if (val === null || typeof val === 'undefined') {
  131. return 'noopener';
  132. }
  133. // Check to see if rel contains noopener. Add what doesn't exist.
  134. if (val.indexOf('noopener') > -1) {
  135. if (val.indexOf('noopener') === -1) {
  136. return val + ' noopener';
  137. }
  138. // Noopener exists. Nothing needs to be added.
  139. else {
  140. return val;
  141. }
  142. }
  143. // Else, append noopener to val.
  144. else {
  145. return val + ' noopener';
  146. }
  147. });
  148. }
  149. if (drupalSettings.data.extlink.extNofollow) {
  150. $(external_links).attr('rel', function (i, val) {
  151. // When the link does not have a rel attribute set it to 'nofollow'.
  152. if (val === null || typeof val === 'undefined') {
  153. return 'nofollow';
  154. }
  155. var target = 'nofollow';
  156. // Change the target, if not overriding follow.
  157. if (drupalSettings.data.extlink.extFollowNoOverride) {
  158. target = 'follow';
  159. }
  160. if (val.indexOf(target) === -1) {
  161. return val + ' nofollow';
  162. }
  163. return val;
  164. });
  165. }
  166. if (drupalSettings.data.extlink.extNoreferrer) {
  167. $(external_links).attr('rel', function (i, val) {
  168. // When the link does not have a rel attribute set it to 'noreferrer'.
  169. if (val === null || typeof val === 'undefined') {
  170. return 'noreferrer';
  171. }
  172. if (val.indexOf('noreferrer') === -1) {
  173. return val + ' noreferrer';
  174. }
  175. return val;
  176. });
  177. }
  178. Drupal.extlink = Drupal.extlink || {};
  179. // Set up default click function for the external links popup. This should be
  180. // overridden by modules wanting to alter the popup.
  181. Drupal.extlink.popupClickHandler = Drupal.extlink.popupClickHandler || function () {
  182. if (drupalSettings.data.extlink.extAlert) {
  183. return confirm(drupalSettings.data.extlink.extAlertText);
  184. }
  185. };
  186. $(external_links).off("click.extlink");
  187. $(external_links).on("click.extlink", function (e) {
  188. return Drupal.extlink.popupClickHandler(e, this);
  189. });
  190. };
  191. /**
  192. * Apply a class and a trailing <span> to all links not containing images.
  193. *
  194. * @param {object[]} links
  195. * An array of DOM elements representing the links.
  196. * @param {string} class_name
  197. * The class to apply to the links.
  198. * @param {string} icon_placement
  199. * 'append' or 'prepend' the icon to the link.
  200. */
  201. Drupal.extlink.applyClassAndSpan = function (links, class_name, icon_placement) {
  202. var $links_to_process;
  203. if (drupalSettings.data.extlink.extImgClass) {
  204. $links_to_process = $(links);
  205. }
  206. else {
  207. var links_with_images = $(links).find('img, svg').parents('a');
  208. $links_to_process = $(links).not(links_with_images);
  209. }
  210. if (class_name !== '0') {
  211. $links_to_process.addClass(class_name);
  212. }
  213. // Add data-extlink attribute.
  214. $links_to_process.attr('data-extlink', '');
  215. var i;
  216. var length = $links_to_process.length;
  217. for (i = 0; i < length; i++) {
  218. var $link = $($links_to_process[i]);
  219. if (drupalSettings.data.extlink.extUseFontAwesome) {
  220. if (class_name === drupalSettings.data.extlink.mailtoClass) {
  221. $link[icon_placement]('<span class="fa-' + class_name + ' extlink"><span class="' + drupalSettings.data.extlink.extFaMailtoClasses + '" aria-label="' + drupalSettings.data.extlink.mailtoLabel + '"></span></span>');
  222. }
  223. else {
  224. $link[icon_placement]('<span class="fa-' + class_name + ' extlink"><span class="' + drupalSettings.data.extlink.extFaLinkClasses + '" aria-label="' + drupalSettings.data.extlink.extLabel + '"></span></span>');
  225. }
  226. }
  227. else {
  228. if (class_name === drupalSettings.data.extlink.mailtoClass) {
  229. $link[icon_placement]('<svg focusable="false" class="' + class_name + '" role="img" aria-label="' + drupalSettings.data.extlink.mailtoLabel + '" xmlns="http://www.w3.org/2000/svg" viewBox="0 10 70 20"><metadata><sfw xmlns="http://ns.adobe.com/SaveForWeb/1.0/"><sliceSourceBounds y="-8160" x="-8165" width="16389" height="16384" bottomLeftOrigin="true"/><optimizationSettings><targetSettings targetSettingsID="0" fileFormat="PNG24Format"><PNG24Format transparency="true" filtered="false" interlaced="false" noMatteColor="false" matteColor="#FFFFFF"/></targetSettings></optimizationSettings></sfw></metadata><title>' + drupalSettings.data.extlink.mailtoLabel + '</title><path d="M56 14H8c-1.1 0-2 0.9-2 2v32c0 1.1 0.9 2 2 2h48c1.1 0 2-0.9 2-2V16C58 14.9 57.1 14 56 14zM50.5 18L32 33.4 13.5 18H50.5zM10 46V20.3l20.7 17.3C31.1 37.8 31.5 38 32 38s0.9-0.2 1.3-0.5L54 20.3V46H10z"/></svg>');
  230. }
  231. else {
  232. $link[icon_placement]('<svg focusable="false" class="' + class_name + '" role="img" aria-label="' + drupalSettings.data.extlink.extLabel + '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 40"><metadata><sfw xmlns="http://ns.adobe.com/SaveForWeb/1.0/"><sliceSourceBounds y="-8160" x="-8165" width="16389" height="16384" bottomLeftOrigin="true"/><optimizationSettings><targetSettings targetSettingsID="0" fileFormat="PNG24Format"><PNG24Format transparency="true" filtered="false" interlaced="false" noMatteColor="false" matteColor="#FFFFFF"/></targetSettings></optimizationSettings></sfw></metadata><title>' + drupalSettings.data.extlink.extLabel + '</title><path d="M48 26c-1.1 0-2 0.9-2 2v26H10V18h26c1.1 0 2-0.9 2-2s-0.9-2-2-2H8c-1.1 0-2 0.9-2 2v40c0 1.1 0.9 2 2 2h40c1.1 0 2-0.9 2-2V28C50 26.9 49.1 26 48 26z"/><path d="M56 6H44c-1.1 0-2 0.9-2 2s0.9 2 2 2h7.2L30.6 30.6c-0.8 0.8-0.8 2 0 2.8C31 33.8 31.5 34 32 34s1-0.2 1.4-0.6L54 12.8V20c0 1.1 0.9 2 2 2s2-0.9 2-2V8C58 6.9 57.1 6 56 6z"/></svg>');
  233. }
  234. }
  235. }
  236. };
  237. Drupal.behaviors.extlink = Drupal.behaviors.extlink || {};
  238. Drupal.behaviors.extlink.attach = function (context, drupalSettings) {
  239. // Backwards compatibility, for the benefit of modules overriding extlink
  240. // functionality by defining an "extlinkAttach" global function.
  241. if (typeof extlinkAttach === 'function') {
  242. extlinkAttach(context);
  243. }
  244. else {
  245. Drupal.extlink.attach(context, drupalSettings);
  246. }
  247. };
  248. })(jQuery, Drupal, drupalSettings);