kms 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/kms/application.js +1 -0
  3. data/app/assets/javascripts/kms/application/controllers/assets_controller.coffee.erb +14 -4
  4. data/app/assets/javascripts/kms/application/controllers/pages_controller.coffee.erb +12 -2
  5. data/app/assets/javascripts/kms/application/controllers/snippets_controller.coffee.erb +13 -3
  6. data/app/assets/javascripts/kms/application/controllers/templates_controller.coffee.erb +13 -3
  7. data/app/assets/javascripts/kms/application/controllers/users_controller.coffee +5 -5
  8. data/app/assets/javascripts/kms/application/module.coffee +6 -2
  9. data/app/assets/javascripts/kms/application/routes.coffee.erb +10 -0
  10. data/app/assets/javascripts/templates/assets/edit.html.slim +2 -1
  11. data/app/assets/javascripts/templates/assets/form.html.slim +1 -1
  12. data/app/assets/javascripts/templates/pages/edit.html.slim +1 -0
  13. data/app/assets/javascripts/templates/shared/hotkey_notification.html.slim +6 -0
  14. data/app/assets/javascripts/templates/snippets/edit.html.slim +1 -0
  15. data/app/assets/javascripts/templates/templates/edit.html.slim +1 -0
  16. data/app/assets/javascripts/templates/users/edit.html.slim +5 -0
  17. data/app/assets/javascripts/templates/users/form.html.slim +3 -2
  18. data/app/assets/javascripts/templates/users/index.html.slim +2 -1
  19. data/app/assets/stylesheets/kms/custom.css.scss +10 -0
  20. data/app/controllers/kms/assets_controller.rb +6 -3
  21. data/app/controllers/kms/users_controller.rb +14 -0
  22. data/app/services/kms/resource_service.rb +3 -1
  23. data/app/views/layouts/kms/kms.html.erb +1 -1
  24. data/config/initializers/devise.rb +9 -0
  25. data/config/locales/en.yml +12 -0
  26. data/config/locales/ru.yml +12 -0
  27. data/config/routes.rb +1 -1
  28. data/lib/kms/engine.rb +1 -1
  29. data/lib/kms/version.rb +1 -1
  30. data/spec/controllers/kms/assets_controller_spec.rb +28 -10
  31. data/spec/controllers/kms/users_controller_spec.rb +23 -0
  32. data/spec/internal/config/routes.rb +1 -1
  33. data/spec/internal/log/test.log +0 -105823
  34. data/vendor/assets/bower.json +5 -4
  35. data/vendor/assets/bower_components/angular-cookies/angular-cookies.js +22 -18
  36. data/vendor/assets/bower_components/angular-cookies/angular-cookies.min.js +4 -4
  37. data/vendor/assets/bower_components/angular-cookies/angular-cookies.min.js.map +2 -2
  38. data/vendor/assets/bower_components/angular-cookies/bower.json +2 -2
  39. data/vendor/assets/bower_components/angular-cookies/package.json +1 -1
  40. data/vendor/assets/bower_components/angular-hotkeys/Gruntfile.js +118 -0
  41. data/vendor/assets/bower_components/angular-hotkeys/LICENSE +20 -0
  42. data/vendor/assets/bower_components/angular-hotkeys/README.md +248 -0
  43. data/vendor/assets/bower_components/angular-hotkeys/bower.json +19 -0
  44. data/vendor/assets/bower_components/angular-hotkeys/build/hotkeys.css +110 -0
  45. data/vendor/assets/bower_components/angular-hotkeys/build/hotkeys.js +1661 -0
  46. data/vendor/assets/bower_components/angular-hotkeys/build/hotkeys.min.css +1 -0
  47. data/vendor/assets/bower_components/angular-hotkeys/build/hotkeys.min.js +7 -0
  48. data/vendor/assets/bower_components/angular-hotkeys/package.json +45 -0
  49. data/vendor/assets/bower_components/angular-hotkeys/src/hotkeys.css +104 -0
  50. data/vendor/assets/bower_components/angular-hotkeys/src/hotkeys.js +633 -0
  51. data/vendor/assets/bower_components/angular-loading-bar/CHANGELOG.md +33 -0
  52. data/vendor/assets/bower_components/angular-loading-bar/CONTRIBUTING.md +17 -0
  53. data/vendor/assets/bower_components/angular-loading-bar/Gruntfile.js +9 -1
  54. data/vendor/assets/bower_components/angular-loading-bar/ISSUE_TEMPLATE.md +14 -0
  55. data/vendor/assets/bower_components/angular-loading-bar/PULL_REQUEST_TEMPLATE.md +13 -0
  56. data/vendor/assets/bower_components/angular-loading-bar/README.md +30 -3
  57. data/vendor/assets/bower_components/angular-loading-bar/bower.json +11 -6
  58. data/vendor/assets/bower_components/angular-loading-bar/build/loading-bar.css +5 -5
  59. data/vendor/assets/bower_components/angular-loading-bar/build/loading-bar.js +39 -12
  60. data/vendor/assets/bower_components/angular-loading-bar/build/loading-bar.min.css +1 -8
  61. data/vendor/assets/bower_components/angular-loading-bar/build/loading-bar.min.js +3 -3
  62. data/vendor/assets/bower_components/angular-loading-bar/index.js +2 -0
  63. data/vendor/assets/bower_components/angular-loading-bar/package.json +12 -15
  64. data/vendor/assets/bower_components/angular-loading-bar/src/loading-bar.css +3 -3
  65. data/vendor/assets/bower_components/angular-loading-bar/src/loading-bar.js +37 -10
  66. data/vendor/assets/bower_components/angular-sanitize/angular-sanitize.js +504 -386
  67. data/vendor/assets/bower_components/angular-sanitize/angular-sanitize.min.js +13 -12
  68. data/vendor/assets/bower_components/angular-sanitize/angular-sanitize.min.js.map +3 -3
  69. data/vendor/assets/bower_components/angular-sanitize/bower.json +2 -2
  70. data/vendor/assets/bower_components/angular-sanitize/package.json +1 -1
  71. data/vendor/assets/bower_components/angular-ui-router/CHANGELOG.md +1410 -0
  72. data/vendor/assets/bower_components/angular-ui-router/CONTRIBUTING.md +64 -16
  73. data/vendor/assets/bower_components/angular-ui-router/DOCS.md +48 -0
  74. data/vendor/assets/bower_components/angular-ui-router/ISSUE_TEMPLATE.md +53 -0
  75. data/vendor/assets/bower_components/angular-ui-router/LICENSE +1 -1
  76. data/vendor/assets/bower_components/angular-ui-router/README.md +24 -211
  77. data/vendor/assets/bower_components/angular-ui-router/artifacts.json +8 -0
  78. data/vendor/assets/bower_components/angular-ui-router/bower.json +1 -23
  79. data/vendor/assets/bower_components/angular-ui-router/karma.conf.js +105 -0
  80. data/vendor/assets/bower_components/angular-ui-router/release/angular-ui-router.js +9744 -3901
  81. data/vendor/assets/bower_components/angular-ui-router/release/angular-ui-router.js.map +192 -0
  82. data/vendor/assets/bower_components/angular-ui-router/release/angular-ui-router.min.js +9 -4
  83. data/vendor/assets/bower_components/angular-ui-router/release/angular-ui-router.min.js.map +1679 -0
  84. data/vendor/assets/bower_components/angular-ui-router/release/resolveService.js +83 -0
  85. data/vendor/assets/bower_components/angular-ui-router/release/resolveService.js.map +19 -0
  86. data/vendor/assets/bower_components/angular-ui-router/release/resolveService.min.js +8 -0
  87. data/vendor/assets/bower_components/angular-ui-router/release/resolveService.min.js.map +47 -0
  88. data/vendor/assets/bower_components/angular-ui-router/release/stateEvents.js +294 -0
  89. data/vendor/assets/bower_components/angular-ui-router/release/stateEvents.js.map +17 -0
  90. data/vendor/assets/bower_components/angular-ui-router/release/stateEvents.min.js +8 -0
  91. data/vendor/assets/bower_components/angular-ui-router/release/stateEvents.min.js.map +102 -0
  92. data/vendor/assets/bower_components/angular-ui-router/release/ui-router-angularjs.js +2014 -0
  93. data/vendor/assets/bower_components/angular-ui-router/release/ui-router-angularjs.js.map +70 -0
  94. data/vendor/assets/bower_components/angular-ui-router/release/ui-router-angularjs.min.js +9 -0
  95. data/vendor/assets/bower_components/angular-ui-router/release/ui-router-angularjs.min.js.map +541 -0
  96. data/vendor/assets/bower_components/angular-ui-router/rollup.config.js +116 -0
  97. data/vendor/assets/bower_components/angular-ui-router/tslint.json +60 -0
  98. data/vendor/assets/bower_components/angular-ui-router/yarn.lock +4146 -0
  99. data/vendor/assets/bower_components/angular-ui-tree/yarn.lock +4945 -0
  100. data/vendor/assets/bower_components/angular/angular.js +4019 -2449
  101. data/vendor/assets/bower_components/angular/angular.min.js +331 -319
  102. data/vendor/assets/bower_components/angular/angular.min.js.gzip +0 -0
  103. data/vendor/assets/bower_components/angular/angular.min.js.map +3 -3
  104. data/vendor/assets/bower_components/angular/bower.json +1 -1
  105. data/vendor/assets/bower_components/angular/package.json +1 -1
  106. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/LICENSE +21 -0
  107. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/README.md +14 -14
  108. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/bower.json +25 -12
  109. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/development_index.html +59 -52
  110. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/dist/angularjs-dropdown-multiselect.min.js +1 -1
  111. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/index.html +73 -0
  112. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/package.json +19 -7
  113. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/pages/javascripts/pages/home/ExampleCtrl.js +126 -3
  114. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/pages/javascripts/pages/home/home.html +1262 -852
  115. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/pages/stylesheets/stylesheet.css +10 -5
  116. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/src/angularjs-dropdown-multiselect.js +612 -287
  117. metadata +66 -169
  118. data/spec/internal/config/database.yml +0 -7
  119. data/spec/internal/public/uploads/kms/asset/file/1/avatar.jpg +0 -0
  120. data/spec/internal/public/uploads/kms/asset/file/2/avatar.jpg +0 -0
  121. data/spec/internal/public/uploads/kms/asset/file/2/style.css +0 -1
  122. data/spec/internal/public/uploads/kms/asset/file/3/style.css +0 -1
  123. data/spec/internal/public/uploads/kms/asset/file/4/style.css +0 -1
  124. data/spec/internal/public/uploads/tmp/1500976987-41025-0002-0883/style.css +0 -1
  125. data/spec/internal/public/uploads/tmp/1500977082-41195-0002-6495/style.css +0 -1
  126. data/spec/internal/public/uploads/tmp/1500977109-41364-0002-4518/style.css +0 -1
  127. data/spec/internal/public/uploads/tmp/1500977152-41405-0002-2345/style.css +0 -1
  128. data/spec/internal/public/uploads/tmp/1500977327-41694-0002-5448/style.css +0 -1
  129. data/spec/internal/public/uploads/tmp/1500977376-41732-0002-7916/style.css +0 -1
  130. data/spec/internal/public/uploads/tmp/1500977392-41759-0002-7593/style.css +0 -1
  131. data/spec/internal/public/uploads/tmp/1500977410-42259-0002-7527/style.css +0 -1
  132. data/spec/internal/public/uploads/tmp/1500977429-42306-0002-5937/style.css +0 -1
  133. data/spec/internal/public/uploads/tmp/1500977437-42324-0002-5880/style.css +0 -1
  134. data/spec/internal/public/uploads/tmp/1500983228-53594-0002-4559/style.css +0 -1
  135. data/spec/internal/public/uploads/tmp/1500983284-53632-0002-6590/style.css +0 -1
  136. data/spec/internal/public/uploads/tmp/1500983360-53784-0002-7289/style.css +0 -1
  137. data/spec/internal/public/uploads/tmp/1500983469-54321-0002-0386/avatar.jpg +0 -0
  138. data/spec/internal/public/uploads/tmp/1500983469-54321-0004-5691/style.css +0 -1
  139. data/spec/internal/public/uploads/tmp/1500983511-54352-0002-5720/avatar.jpg +0 -0
  140. data/spec/internal/public/uploads/tmp/1500983511-54352-0004-1399/style.css +0 -1
  141. data/spec/internal/public/uploads/tmp/1500983610-54507-0002-4280/avatar.jpg +0 -0
  142. data/spec/internal/public/uploads/tmp/1500983610-54507-0004-9758/style.css +0 -1
  143. data/spec/internal/public/uploads/tmp/1500984466-57012-0002-4146/avatar.jpg +0 -0
  144. data/spec/internal/public/uploads/tmp/1500984466-57012-0004-5895/style.css +0 -1
  145. data/spec/internal/public/uploads/tmp/1500984509-57158-0002-9657/avatar.jpg +0 -0
  146. data/spec/internal/public/uploads/tmp/1500984509-57158-0004-5003/style.css +0 -1
  147. data/spec/internal/public/uploads/tmp/1500984616-57697-0002-7201/avatar.jpg +0 -0
  148. data/spec/internal/public/uploads/tmp/1500984616-57697-0004-6255/style.css +0 -1
  149. data/spec/internal/public/uploads/tmp/1500985257-58947-0002-3629/avatar.jpg +0 -0
  150. data/spec/internal/public/uploads/tmp/1500985257-58947-0004-5338/style.css +0 -1
  151. data/spec/internal/public/uploads/tmp/1500985407-58947-0006-5929/style.css +0 -1
  152. data/spec/internal/public/uploads/tmp/1500985473-59264-0002-0397/avatar.jpg +0 -0
  153. data/spec/internal/public/uploads/tmp/1500985473-59264-0004-6493/style.css +0 -1
  154. data/spec/internal/public/uploads/tmp/1500985475-59264-0007-8674/style.css +0 -1
  155. data/spec/internal/public/uploads/tmp/1500985538-59468-0002-9206/avatar.jpg +0 -0
  156. data/spec/internal/public/uploads/tmp/1500985538-59468-0004-2586/style.css +0 -1
  157. data/spec/internal/public/uploads/tmp/1500985538-59468-0007-6200/style.css +0 -1
  158. data/spec/internal/public/uploads/tmp/1500988358-65877-0002-4528/avatar.jpg +0 -0
  159. data/spec/internal/public/uploads/tmp/1500988358-65877-0004-5904/style.css +0 -1
  160. data/spec/internal/public/uploads/tmp/1500988358-65877-0007-7320/style.css +0 -1
  161. data/spec/internal/public/uploads/tmp/1500988407-65916-0002-3138/avatar.jpg +0 -0
  162. data/spec/internal/public/uploads/tmp/1500988407-65916-0004-5400/style.css +0 -1
  163. data/spec/internal/public/uploads/tmp/1500988407-65916-0007-1655/style.css +0 -1
  164. data/spec/internal/public/uploads/tmp/1500988421-65950-0002-9415/avatar.jpg +0 -0
  165. data/spec/internal/public/uploads/tmp/1500988421-65950-0004-7130/style.css +0 -1
  166. data/spec/internal/public/uploads/tmp/1500988421-65950-0007-9886/style.css +0 -1
  167. data/spec/internal/public/uploads/tmp/1500988435-65981-0002-3228/avatar.jpg +0 -0
  168. data/spec/internal/public/uploads/tmp/1500988435-65981-0004-3682/style.css +0 -1
  169. data/spec/internal/public/uploads/tmp/1500988435-65981-0007-1582/style.css +0 -1
  170. data/spec/internal/public/uploads/tmp/1500988475-66122-0002-9516/avatar.jpg +0 -0
  171. data/spec/internal/public/uploads/tmp/1500988475-66122-0004-5634/style.css +0 -1
  172. data/spec/internal/public/uploads/tmp/1500988530-66122-0007-2272/style.css +0 -1
  173. data/spec/internal/public/uploads/tmp/1500988554-66315-0002-6262/avatar.jpg +0 -0
  174. data/spec/internal/public/uploads/tmp/1500988554-66315-0004-6099/style.css +0 -1
  175. data/spec/internal/public/uploads/tmp/1500988554-66315-0007-1632/style.css +0 -1
  176. data/spec/internal/public/uploads/tmp/1500991751-73722-0002-9937/avatar.jpg +0 -0
  177. data/spec/internal/public/uploads/tmp/1500991751-73722-0004-8034/style.css +0 -1
  178. data/spec/internal/public/uploads/tmp/1500991751-73722-0007-7763/style.css +0 -1
  179. data/spec/internal/public/uploads/tmp/1501233238-34385-0002-3210/avatar.jpg +0 -0
  180. data/spec/internal/public/uploads/tmp/1501233238-34385-0004-5881/style.css +0 -1
  181. data/spec/internal/public/uploads/tmp/1501233238-34385-0007-6280/style.css +0 -1
  182. data/spec/internal/tmp/cache/assets/test/sprockets/v3.0/1XyAFYlYI0pK7WAgjR4PgXV6BgU6huJSviWmHetdCRs.cache +0 -1
  183. data/vendor/assets/bower_components/angular-ui-router/api/angular-ui-router.d.ts +0 -126
  184. data/vendor/assets/bower_components/angular-ui-router/src/common.js +0 -292
  185. data/vendor/assets/bower_components/angular-ui-router/src/resolve.js +0 -252
  186. data/vendor/assets/bower_components/angular-ui-router/src/state.js +0 -1373
  187. data/vendor/assets/bower_components/angular-ui-router/src/stateDirectives.js +0 -268
  188. data/vendor/assets/bower_components/angular-ui-router/src/stateFilters.js +0 -39
  189. data/vendor/assets/bower_components/angular-ui-router/src/templateFactory.js +0 -110
  190. data/vendor/assets/bower_components/angular-ui-router/src/urlMatcherFactory.js +0 -1036
  191. data/vendor/assets/bower_components/angular-ui-router/src/urlRouter.js +0 -413
  192. data/vendor/assets/bower_components/angular-ui-router/src/view.js +0 -71
  193. data/vendor/assets/bower_components/angular-ui-router/src/viewDirective.js +0 -302
  194. data/vendor/assets/bower_components/angular-ui-router/src/viewScroll.js +0 -52
  195. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/pages/index.html +0 -67
  196. data/vendor/assets/bower_components/bootstrap/Gemfile.lock +0 -43
@@ -26,7 +26,7 @@ angular.module('chieffancypants.loadingBar', ['cfp.loadingBarInterceptor']);
26
26
  angular.module('cfp.loadingBarInterceptor', ['cfp.loadingBar'])
27
27
  .config(['$httpProvider', function ($httpProvider) {
28
28
 
29
- var interceptor = ['$q', '$cacheFactory', '$timeout', '$rootScope', 'cfpLoadingBar', function ($q, $cacheFactory, $timeout, $rootScope, cfpLoadingBar) {
29
+ var interceptor = ['$q', '$cacheFactory', '$timeout', '$rootScope', '$log', 'cfpLoadingBar', function ($q, $cacheFactory, $timeout, $rootScope, $log, cfpLoadingBar) {
30
30
 
31
31
  /**
32
32
  * The total number of requests made
@@ -107,9 +107,14 @@ angular.module('cfp.loadingBarInterceptor', ['cfp.loadingBar'])
107
107
  },
108
108
 
109
109
  'response': function(response) {
110
+ if (!response || !response.config) {
111
+ $log.error('Broken interceptor detected: Config object not supplied in response:\n https://github.com/chieffancypants/angular-loading-bar/pull/50');
112
+ return response;
113
+ }
114
+
110
115
  if (!response.config.ignoreLoadingBar && !isCached(response.config)) {
111
116
  reqsCompleted++;
112
- $rootScope.$broadcast('cfpLoadingBar:loaded', {url: response.config.url});
117
+ $rootScope.$broadcast('cfpLoadingBar:loaded', {url: response.config.url, result: response});
113
118
  if (reqsCompleted >= reqsTotal) {
114
119
  setComplete();
115
120
  } else {
@@ -120,9 +125,14 @@ angular.module('cfp.loadingBarInterceptor', ['cfp.loadingBar'])
120
125
  },
121
126
 
122
127
  'responseError': function(rejection) {
128
+ if (!rejection || !rejection.config) {
129
+ $log.error('Broken interceptor detected: Config object not supplied in rejection:\n https://github.com/chieffancypants/angular-loading-bar/pull/50');
130
+ return $q.reject(rejection);
131
+ }
132
+
123
133
  if (!rejection.config.ignoreLoadingBar && !isCached(rejection.config)) {
124
134
  reqsCompleted++;
125
- $rootScope.$broadcast('cfpLoadingBar:loaded', {url: rejection.config.url});
135
+ $rootScope.$broadcast('cfpLoadingBar:loaded', {url: rejection.config.url, result: rejection});
126
136
  if (reqsCompleted >= reqsTotal) {
127
137
  setComplete();
128
138
  } else {
@@ -150,6 +160,7 @@ angular.module('cfp.loadingBarInterceptor', ['cfp.loadingBar'])
150
160
  angular.module('cfp.loadingBar', [])
151
161
  .provider('cfpLoadingBar', function() {
152
162
 
163
+ this.autoIncrement = true;
153
164
  this.includeSpinner = true;
154
165
  this.includeBar = true;
155
166
  this.latencyThreshold = 100;
@@ -170,6 +181,7 @@ angular.module('cfp.loadingBar', [])
170
181
  started = false,
171
182
  status = 0;
172
183
 
184
+ var autoIncrement = this.autoIncrement;
173
185
  var includeSpinner = this.includeSpinner;
174
186
  var includeBar = this.includeBar;
175
187
  var startSize = this.startSize;
@@ -182,7 +194,6 @@ angular.module('cfp.loadingBar', [])
182
194
  $animate = $injector.get('$animate');
183
195
  }
184
196
 
185
- var $parent = $document.find($parentSelector).eq(0);
186
197
  $timeout.cancel(completeTimeout);
187
198
 
188
199
  // do not continually broadcast the started event:
@@ -190,15 +201,28 @@ angular.module('cfp.loadingBar', [])
190
201
  return;
191
202
  }
192
203
 
204
+ var document = $document[0];
205
+ var parent = document.querySelector ?
206
+ document.querySelector($parentSelector)
207
+ : $document.find($parentSelector)[0]
208
+ ;
209
+
210
+ if (! parent) {
211
+ parent = document.getElementsByTagName('body')[0];
212
+ }
213
+
214
+ var $parent = angular.element(parent);
215
+ var $after = parent.lastChild && angular.element(parent.lastChild);
216
+
193
217
  $rootScope.$broadcast('cfpLoadingBar:started');
194
218
  started = true;
195
219
 
196
220
  if (includeBar) {
197
- $animate.enter(loadingBarContainer, $parent);
221
+ $animate.enter(loadingBarContainer, $parent, $after);
198
222
  }
199
223
 
200
224
  if (includeSpinner) {
201
- $animate.enter(spinner, $parent);
225
+ $animate.enter(spinner, $parent, loadingBarContainer);
202
226
  }
203
227
 
204
228
  _set(startSize);
@@ -220,10 +244,12 @@ angular.module('cfp.loadingBar', [])
220
244
  // increment loadingbar to give the illusion that there is always
221
245
  // progress but make sure to cancel the previous timeouts so we don't
222
246
  // have multiple incs running at the same time.
223
- $timeout.cancel(incTimeout);
224
- incTimeout = $timeout(function() {
225
- _inc();
226
- }, 250);
247
+ if (autoIncrement) {
248
+ $timeout.cancel(incTimeout);
249
+ incTimeout = $timeout(function() {
250
+ _inc();
251
+ }, 250);
252
+ }
227
253
  }
228
254
 
229
255
  /**
@@ -296,6 +322,7 @@ angular.module('cfp.loadingBar', [])
296
322
  status : _status,
297
323
  inc : _inc,
298
324
  complete : _complete,
325
+ autoIncrement : this.autoIncrement,
299
326
  includeSpinner : this.includeSpinner,
300
327
  latencyThreshold : this.latencyThreshold,
301
328
  parentSelector : this.parentSelector,
@@ -1,9 +1,9 @@
1
1
  /**
2
- * @license AngularJS v1.4.14
3
- * (c) 2010-2015 Google, Inc. http://angularjs.org
2
+ * @license AngularJS v1.6.7
3
+ * (c) 2010-2017 Google, Inc. http://angularjs.org
4
4
  * License: MIT
5
5
  */
6
- (function(window, angular, undefined) {'use strict';
6
+ (function(window, angular) {'use strict';
7
7
 
8
8
  /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
9
9
  * Any commits to this file should be reviewed with security in mind. *
@@ -17,58 +17,49 @@
17
17
  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
18
18
 
19
19
  var $sanitizeMinErr = angular.$$minErr('$sanitize');
20
+ var bind;
21
+ var extend;
22
+ var forEach;
23
+ var isDefined;
24
+ var lowercase;
25
+ var noop;
26
+ var nodeContains;
27
+ var htmlParser;
28
+ var htmlSanitizeWriter;
20
29
 
21
30
  /**
22
31
  * @ngdoc module
23
32
  * @name ngSanitize
24
33
  * @description
25
34
  *
26
- * # ngSanitize
27
- *
28
35
  * The `ngSanitize` module provides functionality to sanitize HTML.
29
36
  *
30
- *
31
- * <div doc-module-components="ngSanitize"></div>
32
- *
33
37
  * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
34
38
  */
35
39
 
36
- /*
37
- * HTML Parser By Misko Hevery (misko@hevery.com)
38
- * based on: HTML Parser By John Resig (ejohn.org)
39
- * Original code by Erik Arvidsson, Mozilla Public License
40
- * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
41
- *
42
- * // Use like so:
43
- * htmlParser(htmlString, {
44
- * start: function(tag, attrs, unary) {},
45
- * end: function(tag) {},
46
- * chars: function(text) {},
47
- * comment: function(text) {}
48
- * });
49
- *
50
- */
51
-
52
-
53
40
  /**
54
41
  * @ngdoc service
55
42
  * @name $sanitize
56
43
  * @kind function
57
44
  *
58
45
  * @description
46
+ * Sanitizes an html string by stripping all potentially dangerous tokens.
47
+ *
59
48
  * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
60
49
  * then serialized back to properly escaped html string. This means that no unsafe input can make
61
- * it into the returned string, however, since our parser is more strict than a typical browser
62
- * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
63
- * browser, won't make it through the sanitizer. The input may also contain SVG markup.
64
- * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
65
- * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
50
+ * it into the returned string.
51
+ *
52
+ * The whitelist for URL sanitization of attribute values is configured using the functions
53
+ * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider
54
+ * `$compileProvider`}.
55
+ *
56
+ * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
66
57
  *
67
58
  * @param {string} html HTML input.
68
59
  * @returns {string} Sanitized HTML.
69
60
  *
70
61
  * @example
71
- <example module="sanitizeExample" deps="angular-sanitize.js">
62
+ <example module="sanitizeExample" deps="angular-sanitize.js" name="sanitize-service">
72
63
  <file name="index.html">
73
64
  <script>
74
65
  angular.module('sanitizeExample', ['ngSanitize'])
@@ -117,19 +108,19 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
117
108
  </file>
118
109
  <file name="protractor.js" type="protractor">
119
110
  it('should sanitize the html snippet by default', function() {
120
- expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
111
+ expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')).
121
112
  toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
122
113
  });
123
114
 
124
115
  it('should inline raw snippet if bound to a trusted value', function() {
125
- expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
116
+ expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')).
126
117
  toBe("<p style=\"color:blue\">an html\n" +
127
118
  "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
128
119
  "snippet</p>");
129
120
  });
130
121
 
131
122
  it('should escape snippet without any filter', function() {
132
- expect(element(by.css('#bind-default div')).getInnerHtml()).
123
+ expect(element(by.css('#bind-default div')).getAttribute('innerHTML')).
133
124
  toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
134
125
  "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
135
126
  "snippet&lt;/p&gt;");
@@ -138,396 +129,477 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
138
129
  it('should update', function() {
139
130
  element(by.model('snippet')).clear();
140
131
  element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
141
- expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
132
+ expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')).
142
133
  toBe('new <b>text</b>');
143
- expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
134
+ expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')).toBe(
144
135
  'new <b onclick="alert(1)">text</b>');
145
- expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
136
+ expect(element(by.css('#bind-default div')).getAttribute('innerHTML')).toBe(
146
137
  "new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
147
138
  });
148
139
  </file>
149
140
  </example>
150
141
  */
142
+
143
+
144
+ /**
145
+ * @ngdoc provider
146
+ * @name $sanitizeProvider
147
+ * @this
148
+ *
149
+ * @description
150
+ * Creates and configures {@link $sanitize} instance.
151
+ */
151
152
  function $SanitizeProvider() {
153
+ var svgEnabled = false;
154
+
152
155
  this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
156
+ if (svgEnabled) {
157
+ extend(validElements, svgElements);
158
+ }
153
159
  return function(html) {
154
160
  var buf = [];
155
161
  htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
156
- return !/^unsafe/.test($$sanitizeUri(uri, isImage));
162
+ return !/^unsafe:/.test($$sanitizeUri(uri, isImage));
157
163
  }));
158
164
  return buf.join('');
159
165
  };
160
166
  }];
161
- }
162
167
 
163
- function sanitizeText(chars) {
164
- var buf = [];
165
- var writer = htmlSanitizeWriter(buf, angular.noop);
166
- writer.chars(chars);
167
- return buf.join('');
168
- }
169
168
 
170
-
171
- // Regular Expressions for parsing tags and attributes
172
- var START_TAG_REGEXP =
173
- /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
174
- END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/,
175
- ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
176
- BEGIN_TAG_REGEXP = /^</,
177
- BEGING_END_TAGE_REGEXP = /^<\//,
178
- COMMENT_REGEXP = /<!--(.*?)-->/g,
179
- DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
180
- CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
181
- SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
182
- // Match everything outside of normal chars and " (quote character)
183
- NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
184
-
185
-
186
- // Good source of info about elements and attributes
187
- // http://dev.w3.org/html5/spec/Overview.html#semantics
188
- // http://simon.html5.org/html-elements
189
-
190
- // Safe Void Elements - HTML5
191
- // http://dev.w3.org/html5/spec/Overview.html#void-elements
192
- var voidElements = makeMap("area,br,col,hr,img,wbr");
193
-
194
- // Elements that you can, intentionally, leave open (and which close themselves)
195
- // http://dev.w3.org/html5/spec/Overview.html#optional-tags
196
- var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
197
- optionalEndTagInlineElements = makeMap("rp,rt"),
198
- optionalEndTagElements = angular.extend({},
199
- optionalEndTagInlineElements,
200
- optionalEndTagBlockElements);
201
-
202
- // Safe Block Elements - HTML5
203
- var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
204
- "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
205
- "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
206
-
207
- // Inline Elements - HTML5
208
- var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
209
- "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
210
- "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
211
-
212
- // SVG Elements
213
- // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
214
- // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
215
- // They can potentially allow for arbitrary javascript to be executed. See #11290
216
- var svgElements = makeMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," +
217
- "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," +
218
- "radialGradient,rect,stop,svg,switch,text,title,tspan,use");
219
-
220
- // Special Elements (can contain anything)
221
- var specialElements = makeMap("script,style");
222
-
223
- var validElements = angular.extend({},
224
- voidElements,
225
- blockElements,
226
- inlineElements,
227
- optionalEndTagElements,
228
- svgElements);
229
-
230
- //Attributes that have href and hence need to be sanitized
231
- var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href");
232
-
233
- var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
234
- 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
235
- 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
236
- 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
237
- 'valign,value,vspace,width');
238
-
239
- // SVG attributes (without "id" and "name" attributes)
240
- // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
241
- var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
242
- 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
243
- 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
244
- 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
245
- 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' +
246
- 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' +
247
- 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' +
248
- 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' +
249
- 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' +
250
- 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' +
251
- 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' +
252
- 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' +
253
- 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' +
254
- 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' +
255
- 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true);
256
-
257
- var validAttrs = angular.extend({},
258
- uriAttrs,
259
- svgAttrs,
260
- htmlAttrs);
261
-
262
- function makeMap(str, lowercaseKeys) {
263
- var obj = {}, items = str.split(','), i;
264
- for (i = 0; i < items.length; i++) {
265
- obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true;
266
- }
267
- return obj;
268
- }
269
-
270
-
271
- /**
272
- * @example
273
- * htmlParser(htmlString, {
274
- * start: function(tag, attrs, unary) {},
275
- * end: function(tag) {},
276
- * chars: function(text) {},
277
- * comment: function(text) {}
278
- * });
279
- *
280
- * @param {string} html string
281
- * @param {object} handler
282
- */
283
- function htmlParser(html, handler) {
284
- if (typeof html !== 'string') {
285
- if (html === null || typeof html === 'undefined') {
286
- html = '';
169
+ /**
170
+ * @ngdoc method
171
+ * @name $sanitizeProvider#enableSvg
172
+ * @kind function
173
+ *
174
+ * @description
175
+ * Enables a subset of svg to be supported by the sanitizer.
176
+ *
177
+ * <div class="alert alert-warning">
178
+ * <p>By enabling this setting without taking other precautions, you might expose your
179
+ * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned
180
+ * outside of the containing element and be rendered over other elements on the page (e.g. a login
181
+ * link). Such behavior can then result in phishing incidents.</p>
182
+ *
183
+ * <p>To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg
184
+ * tags within the sanitized content:</p>
185
+ *
186
+ * <br>
187
+ *
188
+ * <pre><code>
189
+ * .rootOfTheIncludedContent svg {
190
+ * overflow: hidden !important;
191
+ * }
192
+ * </code></pre>
193
+ * </div>
194
+ *
195
+ * @param {boolean=} flag Enable or disable SVG support in the sanitizer.
196
+ * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called
197
+ * without an argument or self for chaining otherwise.
198
+ */
199
+ this.enableSvg = function(enableSvg) {
200
+ if (isDefined(enableSvg)) {
201
+ svgEnabled = enableSvg;
202
+ return this;
287
203
  } else {
288
- html = '' + html;
204
+ return svgEnabled;
289
205
  }
290
- }
291
- var index, chars, match, stack = [], last = html, text;
292
- stack.last = function() { return stack[stack.length - 1]; };
206
+ };
293
207
 
294
- while (html) {
295
- text = '';
296
- chars = true;
208
+ //////////////////////////////////////////////////////////////////////////////////////////////////
209
+ // Private stuff
210
+ //////////////////////////////////////////////////////////////////////////////////////////////////
297
211
 
298
- // Make sure we're not in a script or style element
299
- if (!stack.last() || !specialElements[stack.last()]) {
212
+ bind = angular.bind;
213
+ extend = angular.extend;
214
+ forEach = angular.forEach;
215
+ isDefined = angular.isDefined;
216
+ lowercase = angular.lowercase;
217
+ noop = angular.noop;
300
218
 
301
- // Comment
302
- if (html.indexOf("<!--") === 0) {
303
- // comments containing -- are not allowed unless they terminate the comment
304
- index = html.indexOf("--", 4);
219
+ htmlParser = htmlParserImpl;
220
+ htmlSanitizeWriter = htmlSanitizeWriterImpl;
305
221
 
306
- if (index >= 0 && html.lastIndexOf("-->", index) === index) {
307
- if (handler.comment) handler.comment(html.substring(4, index));
308
- html = html.substring(index + 3);
309
- chars = false;
310
- }
311
- // DOCTYPE
312
- } else if (DOCTYPE_REGEXP.test(html)) {
313
- match = html.match(DOCTYPE_REGEXP);
222
+ nodeContains = window.Node.prototype.contains || /** @this */ function(arg) {
223
+ // eslint-disable-next-line no-bitwise
224
+ return !!(this.compareDocumentPosition(arg) & 16);
225
+ };
314
226
 
315
- if (match) {
316
- html = html.replace(match[0], '');
317
- chars = false;
318
- }
319
- // end tag
320
- } else if (BEGING_END_TAGE_REGEXP.test(html)) {
321
- match = html.match(END_TAG_REGEXP);
322
-
323
- if (match) {
324
- html = html.substring(match[0].length);
325
- match[0].replace(END_TAG_REGEXP, parseEndTag);
326
- chars = false;
327
- }
227
+ // Regular Expressions for parsing tags and attributes
228
+ var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
229
+ // Match everything outside of normal chars and " (quote character)
230
+ NON_ALPHANUMERIC_REGEXP = /([^#-~ |!])/g;
231
+
232
+
233
+ // Good source of info about elements and attributes
234
+ // http://dev.w3.org/html5/spec/Overview.html#semantics
235
+ // http://simon.html5.org/html-elements
236
+
237
+ // Safe Void Elements - HTML5
238
+ // http://dev.w3.org/html5/spec/Overview.html#void-elements
239
+ var voidElements = toMap('area,br,col,hr,img,wbr');
240
+
241
+ // Elements that you can, intentionally, leave open (and which close themselves)
242
+ // http://dev.w3.org/html5/spec/Overview.html#optional-tags
243
+ var optionalEndTagBlockElements = toMap('colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr'),
244
+ optionalEndTagInlineElements = toMap('rp,rt'),
245
+ optionalEndTagElements = extend({},
246
+ optionalEndTagInlineElements,
247
+ optionalEndTagBlockElements);
248
+
249
+ // Safe Block Elements - HTML5
250
+ var blockElements = extend({}, optionalEndTagBlockElements, toMap('address,article,' +
251
+ 'aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' +
252
+ 'h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul'));
253
+
254
+ // Inline Elements - HTML5
255
+ var inlineElements = extend({}, optionalEndTagInlineElements, toMap('a,abbr,acronym,b,' +
256
+ 'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,' +
257
+ 'samp,small,span,strike,strong,sub,sup,time,tt,u,var'));
258
+
259
+ // SVG Elements
260
+ // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
261
+ // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
262
+ // They can potentially allow for arbitrary javascript to be executed. See #11290
263
+ var svgElements = toMap('circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,' +
264
+ 'hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,' +
265
+ 'radialGradient,rect,stop,svg,switch,text,title,tspan');
266
+
267
+ // Blocked Elements (will be stripped)
268
+ var blockedElements = toMap('script,style');
269
+
270
+ var validElements = extend({},
271
+ voidElements,
272
+ blockElements,
273
+ inlineElements,
274
+ optionalEndTagElements);
275
+
276
+ //Attributes that have href and hence need to be sanitized
277
+ var uriAttrs = toMap('background,cite,href,longdesc,src,xlink:href');
278
+
279
+ var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
280
+ 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
281
+ 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
282
+ 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
283
+ 'valign,value,vspace,width');
284
+
285
+ // SVG attributes (without "id" and "name" attributes)
286
+ // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
287
+ var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
288
+ 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
289
+ 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
290
+ 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
291
+ 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' +
292
+ 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' +
293
+ 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' +
294
+ 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' +
295
+ 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' +
296
+ 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' +
297
+ 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' +
298
+ 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' +
299
+ 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' +
300
+ 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' +
301
+ 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true);
302
+
303
+ var validAttrs = extend({},
304
+ uriAttrs,
305
+ svgAttrs,
306
+ htmlAttrs);
307
+
308
+ function toMap(str, lowercaseKeys) {
309
+ var obj = {}, items = str.split(','), i;
310
+ for (i = 0; i < items.length; i++) {
311
+ obj[lowercaseKeys ? lowercase(items[i]) : items[i]] = true;
312
+ }
313
+ return obj;
314
+ }
328
315
 
329
- // start tag
330
- } else if (BEGIN_TAG_REGEXP.test(html)) {
331
- match = html.match(START_TAG_REGEXP);
316
+ /**
317
+ * Create an inert document that contains the dirty HTML that needs sanitizing
318
+ * Depending upon browser support we use one of three strategies for doing this.
319
+ * Support: Safari 10.x -> XHR strategy
320
+ * Support: Firefox -> DomParser strategy
321
+ */
322
+ var getInertBodyElement /* function(html: string): HTMLBodyElement */ = (function(window, document) {
323
+ var inertDocument;
324
+ if (document && document.implementation) {
325
+ inertDocument = document.implementation.createHTMLDocument('inert');
326
+ } else {
327
+ throw $sanitizeMinErr('noinert', 'Can\'t create an inert html document');
328
+ }
329
+ var inertBodyElement = (inertDocument.documentElement || inertDocument.getDocumentElement()).querySelector('body');
332
330
 
333
- if (match) {
334
- // We only have a valid start-tag if there is a '>'.
335
- if (match[4]) {
336
- html = html.substring(match[0].length);
337
- match[0].replace(START_TAG_REGEXP, parseStartTag);
338
- }
339
- chars = false;
340
- } else {
341
- // no ending tag found --- this piece should be encoded as an entity.
342
- text += '<';
343
- html = html.substring(1);
344
- }
331
+ // Check for the Safari 10.1 bug - which allows JS to run inside the SVG G element
332
+ inertBodyElement.innerHTML = '<svg><g onload="this.parentNode.remove()"></g></svg>';
333
+ if (!inertBodyElement.querySelector('svg')) {
334
+ return getInertBodyElement_XHR;
335
+ } else {
336
+ // Check for the Firefox bug - which prevents the inner img JS from being sanitized
337
+ inertBodyElement.innerHTML = '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">';
338
+ if (inertBodyElement.querySelector('svg img')) {
339
+ return getInertBodyElement_DOMParser;
340
+ } else {
341
+ return getInertBodyElement_InertDocument;
345
342
  }
343
+ }
346
344
 
347
- if (chars) {
348
- index = html.indexOf("<");
349
-
350
- text += index < 0 ? html : html.substring(0, index);
351
- html = index < 0 ? "" : html.substring(index);
352
-
353
- if (handler.chars) handler.chars(decodeEntities(text));
345
+ function getInertBodyElement_XHR(html) {
346
+ // We add this dummy element to ensure that the rest of the content is parsed as expected
347
+ // e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the `<head>` tag.
348
+ html = '<remove></remove>' + html;
349
+ try {
350
+ html = encodeURI(html);
351
+ } catch (e) {
352
+ return undefined;
354
353
  }
355
-
356
- } else {
357
- // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w].
358
- html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
359
- function(all, text) {
360
- text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
361
-
362
- if (handler.chars) handler.chars(decodeEntities(text));
363
-
364
- return "";
365
- });
366
-
367
- parseEndTag("", stack.last());
354
+ var xhr = new window.XMLHttpRequest();
355
+ xhr.responseType = 'document';
356
+ xhr.open('GET', 'data:text/html;charset=utf-8,' + html, false);
357
+ xhr.send(null);
358
+ var body = xhr.response.body;
359
+ body.firstChild.remove();
360
+ return body;
368
361
  }
369
362
 
370
- if (html == last) {
371
- throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
372
- "of html: {0}", html);
363
+ function getInertBodyElement_DOMParser(html) {
364
+ // We add this dummy element to ensure that the rest of the content is parsed as expected
365
+ // e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the `<head>` tag.
366
+ html = '<remove></remove>' + html;
367
+ try {
368
+ var body = new window.DOMParser().parseFromString(html, 'text/html').body;
369
+ body.firstChild.remove();
370
+ return body;
371
+ } catch (e) {
372
+ return undefined;
373
+ }
373
374
  }
374
- last = html;
375
- }
376
375
 
377
- // Clean up any remaining tags
378
- parseEndTag();
376
+ function getInertBodyElement_InertDocument(html) {
377
+ inertBodyElement.innerHTML = html;
379
378
 
380
- function parseStartTag(tag, tagName, rest, unary) {
381
- tagName = angular.lowercase(tagName);
382
- if (blockElements[tagName]) {
383
- while (stack.last() && inlineElements[stack.last()]) {
384
- parseEndTag("", stack.last());
379
+ // Support: IE 9-11 only
380
+ // strip custom-namespaced attributes on IE<=11
381
+ if (document.documentMode) {
382
+ stripCustomNsAttrs(inertBodyElement);
385
383
  }
386
- }
387
384
 
388
- if (optionalEndTagElements[tagName] && stack.last() == tagName) {
389
- parseEndTag("", tagName);
385
+ return inertBodyElement;
390
386
  }
391
-
392
- unary = voidElements[tagName] || !!unary;
393
-
394
- if (!unary) {
395
- stack.push(tagName);
387
+ })(window, window.document);
388
+
389
+ /**
390
+ * @example
391
+ * htmlParser(htmlString, {
392
+ * start: function(tag, attrs) {},
393
+ * end: function(tag) {},
394
+ * chars: function(text) {},
395
+ * comment: function(text) {}
396
+ * });
397
+ *
398
+ * @param {string} html string
399
+ * @param {object} handler
400
+ */
401
+ function htmlParserImpl(html, handler) {
402
+ if (html === null || html === undefined) {
403
+ html = '';
404
+ } else if (typeof html !== 'string') {
405
+ html = '' + html;
396
406
  }
397
407
 
398
- var attrs = {};
408
+ var inertBodyElement = getInertBodyElement(html);
409
+ if (!inertBodyElement) return '';
399
410
 
400
- rest.replace(ATTR_REGEXP,
401
- function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
402
- var value = doubleQuotedValue
403
- || singleQuotedValue
404
- || unquotedValue
405
- || '';
406
-
407
- attrs[name] = decodeEntities(value);
408
- });
409
- if (handler.start) handler.start(tagName, attrs, unary);
410
- }
411
+ //mXSS protection
412
+ var mXSSAttempts = 5;
413
+ do {
414
+ if (mXSSAttempts === 0) {
415
+ throw $sanitizeMinErr('uinput', 'Failed to sanitize html because the input is unstable');
416
+ }
417
+ mXSSAttempts--;
418
+
419
+ // trigger mXSS if it is going to happen by reading and writing the innerHTML
420
+ html = inertBodyElement.innerHTML;
421
+ inertBodyElement = getInertBodyElement(html);
422
+ } while (html !== inertBodyElement.innerHTML);
423
+
424
+ var node = inertBodyElement.firstChild;
425
+ while (node) {
426
+ switch (node.nodeType) {
427
+ case 1: // ELEMENT_NODE
428
+ handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes));
429
+ break;
430
+ case 3: // TEXT NODE
431
+ handler.chars(node.textContent);
432
+ break;
433
+ }
411
434
 
412
- function parseEndTag(tag, tagName) {
413
- var pos = 0, i;
414
- tagName = angular.lowercase(tagName);
415
- if (tagName) {
416
- // Find the closest opened tag of the same type
417
- for (pos = stack.length - 1; pos >= 0; pos--) {
418
- if (stack[pos] == tagName) break;
435
+ var nextNode;
436
+ if (!(nextNode = node.firstChild)) {
437
+ if (node.nodeType === 1) {
438
+ handler.end(node.nodeName.toLowerCase());
439
+ }
440
+ nextNode = getNonDescendant('nextSibling', node);
441
+ if (!nextNode) {
442
+ while (nextNode == null) {
443
+ node = getNonDescendant('parentNode', node);
444
+ if (node === inertBodyElement) break;
445
+ nextNode = getNonDescendant('nextSibling', node);
446
+ if (node.nodeType === 1) {
447
+ handler.end(node.nodeName.toLowerCase());
448
+ }
449
+ }
450
+ }
419
451
  }
452
+ node = nextNode;
420
453
  }
421
454
 
422
- if (pos >= 0) {
423
- // Close all the open elements, up the stack
424
- for (i = stack.length - 1; i >= pos; i--)
425
- if (handler.end) handler.end(stack[i]);
426
-
427
- // Remove the open elements from the stack
428
- stack.length = pos;
455
+ while ((node = inertBodyElement.firstChild)) {
456
+ inertBodyElement.removeChild(node);
429
457
  }
430
458
  }
431
- }
432
459
 
433
- var hiddenPre=document.createElement("pre");
434
- /**
435
- * decodes all entities into regular string
436
- * @param value
437
- * @returns {string} A string with decoded entities.
438
- */
439
- function decodeEntities(value) {
440
- if (!value) { return ''; }
460
+ function attrToMap(attrs) {
461
+ var map = {};
462
+ for (var i = 0, ii = attrs.length; i < ii; i++) {
463
+ var attr = attrs[i];
464
+ map[attr.name] = attr.value;
465
+ }
466
+ return map;
467
+ }
441
468
 
442
- hiddenPre.innerHTML = value.replace(/</g,"&lt;");
443
- // innerText depends on styling as it doesn't display hidden elements.
444
- // Therefore, it's better to use textContent not to cause unnecessary reflows.
445
- return hiddenPre.textContent;
446
- }
447
469
 
448
- /**
449
- * Escapes all potentially dangerous characters, so that the
450
- * resulting string can be safely inserted into attribute or
451
- * element text.
452
- * @param value
453
- * @returns {string} escaped text
454
- */
455
- function encodeEntities(value) {
456
- return value.
457
- replace(/&/g, '&amp;').
458
- replace(SURROGATE_PAIR_REGEXP, function(value) {
459
- var hi = value.charCodeAt(0);
460
- var low = value.charCodeAt(1);
461
- return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
462
- }).
463
- replace(NON_ALPHANUMERIC_REGEXP, function(value) {
464
- return '&#' + value.charCodeAt(0) + ';';
465
- }).
466
- replace(/</g, '&lt;').
467
- replace(/>/g, '&gt;');
468
- }
470
+ /**
471
+ * Escapes all potentially dangerous characters, so that the
472
+ * resulting string can be safely inserted into attribute or
473
+ * element text.
474
+ * @param value
475
+ * @returns {string} escaped text
476
+ */
477
+ function encodeEntities(value) {
478
+ return value.
479
+ replace(/&/g, '&amp;').
480
+ replace(SURROGATE_PAIR_REGEXP, function(value) {
481
+ var hi = value.charCodeAt(0);
482
+ var low = value.charCodeAt(1);
483
+ return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
484
+ }).
485
+ replace(NON_ALPHANUMERIC_REGEXP, function(value) {
486
+ return '&#' + value.charCodeAt(0) + ';';
487
+ }).
488
+ replace(/</g, '&lt;').
489
+ replace(/>/g, '&gt;');
490
+ }
469
491
 
470
- /**
471
- * create an HTML/XML writer which writes to buffer
472
- * @param {Array} buf use buf.jain('') to get out sanitized html string
473
- * @returns {object} in the form of {
474
- * start: function(tag, attrs, unary) {},
475
- * end: function(tag) {},
476
- * chars: function(text) {},
477
- * comment: function(text) {}
478
- * }
479
- */
480
- function htmlSanitizeWriter(buf, uriValidator) {
481
- var ignore = false;
482
- var out = angular.bind(buf, buf.push);
483
- return {
484
- start: function(tag, attrs, unary) {
485
- tag = angular.lowercase(tag);
486
- if (!ignore && specialElements[tag]) {
487
- ignore = tag;
488
- }
489
- if (!ignore && validElements[tag] === true) {
490
- out('<');
491
- out(tag);
492
- angular.forEach(attrs, function(value, key) {
493
- var lkey=angular.lowercase(key);
494
- var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
495
- if (validAttrs[lkey] === true &&
496
- (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
497
- out(' ');
498
- out(key);
499
- out('="');
500
- out(encodeEntities(value));
501
- out('"');
502
- }
503
- });
504
- out(unary ? '/>' : '>');
505
- }
506
- },
507
- end: function(tag) {
508
- tag = angular.lowercase(tag);
509
- if (!ignore && validElements[tag] === true) {
492
+ /**
493
+ * create an HTML/XML writer which writes to buffer
494
+ * @param {Array} buf use buf.join('') to get out sanitized html string
495
+ * @returns {object} in the form of {
496
+ * start: function(tag, attrs) {},
497
+ * end: function(tag) {},
498
+ * chars: function(text) {},
499
+ * comment: function(text) {}
500
+ * }
501
+ */
502
+ function htmlSanitizeWriterImpl(buf, uriValidator) {
503
+ var ignoreCurrentElement = false;
504
+ var out = bind(buf, buf.push);
505
+ return {
506
+ start: function(tag, attrs) {
507
+ tag = lowercase(tag);
508
+ if (!ignoreCurrentElement && blockedElements[tag]) {
509
+ ignoreCurrentElement = tag;
510
+ }
511
+ if (!ignoreCurrentElement && validElements[tag] === true) {
512
+ out('<');
513
+ out(tag);
514
+ forEach(attrs, function(value, key) {
515
+ var lkey = lowercase(key);
516
+ var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
517
+ if (validAttrs[lkey] === true &&
518
+ (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
519
+ out(' ');
520
+ out(key);
521
+ out('="');
522
+ out(encodeEntities(value));
523
+ out('"');
524
+ }
525
+ });
526
+ out('>');
527
+ }
528
+ },
529
+ end: function(tag) {
530
+ tag = lowercase(tag);
531
+ if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) {
510
532
  out('</');
511
533
  out(tag);
512
534
  out('>');
513
535
  }
514
- if (tag == ignore) {
515
- ignore = false;
536
+ // eslint-disable-next-line eqeqeq
537
+ if (tag == ignoreCurrentElement) {
538
+ ignoreCurrentElement = false;
516
539
  }
517
540
  },
518
- chars: function(chars) {
519
- if (!ignore) {
541
+ chars: function(chars) {
542
+ if (!ignoreCurrentElement) {
520
543
  out(encodeEntities(chars));
521
544
  }
522
545
  }
523
- };
546
+ };
547
+ }
548
+
549
+
550
+ /**
551
+ * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare
552
+ * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want
553
+ * to allow any of these custom attributes. This method strips them all.
554
+ *
555
+ * @param node Root element to process
556
+ */
557
+ function stripCustomNsAttrs(node) {
558
+ while (node) {
559
+ if (node.nodeType === window.Node.ELEMENT_NODE) {
560
+ var attrs = node.attributes;
561
+ for (var i = 0, l = attrs.length; i < l; i++) {
562
+ var attrNode = attrs[i];
563
+ var attrName = attrNode.name.toLowerCase();
564
+ if (attrName === 'xmlns:ns1' || attrName.lastIndexOf('ns1:', 0) === 0) {
565
+ node.removeAttributeNode(attrNode);
566
+ i--;
567
+ l--;
568
+ }
569
+ }
570
+ }
571
+
572
+ var nextNode = node.firstChild;
573
+ if (nextNode) {
574
+ stripCustomNsAttrs(nextNode);
575
+ }
576
+
577
+ node = getNonDescendant('nextSibling', node);
578
+ }
579
+ }
580
+
581
+ function getNonDescendant(propName, node) {
582
+ // An element is clobbered if its `propName` property points to one of its descendants
583
+ var nextNode = node[propName];
584
+ if (nextNode && nodeContains.call(node, nextNode)) {
585
+ throw $sanitizeMinErr('elclob', 'Failed to sanitize html because the element is clobbered: {0}', node.outerHTML || node.outerText);
586
+ }
587
+ return nextNode;
588
+ }
524
589
  }
525
590
 
591
+ function sanitizeText(chars) {
592
+ var buf = [];
593
+ var writer = htmlSanitizeWriter(buf, noop);
594
+ writer.chars(chars);
595
+ return buf.join('');
596
+ }
526
597
 
527
- // define ngSanitize module and register $sanitize service
528
- angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
529
598
 
530
- /* global sanitizeText: false */
599
+ // define ngSanitize module and register $sanitize service
600
+ angular.module('ngSanitize', [])
601
+ .provider('$sanitize', $SanitizeProvider)
602
+ .info({ angularVersion: '1.6.7' });
531
603
 
532
604
  /**
533
605
  * @ngdoc filter
@@ -535,40 +607,39 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
535
607
  * @kind function
536
608
  *
537
609
  * @description
538
- * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
610
+ * Finds links in text input and turns them into html links. Supports `http/https/ftp/sftp/mailto` and
539
611
  * plain email address links.
540
612
  *
541
613
  * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
542
614
  *
543
615
  * @param {string} text Input text.
544
- * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
545
- * @returns {string} Html-linkified text.
616
+ * @param {string} [target] Window (`_blank|_self|_parent|_top`) or named frame to open links in.
617
+ * @param {object|function(url)} [attributes] Add custom attributes to the link element.
618
+ *
619
+ * Can be one of:
620
+ *
621
+ * - `object`: A map of attributes
622
+ * - `function`: Takes the url as a parameter and returns a map of attributes
623
+ *
624
+ * If the map of attributes contains a value for `target`, it overrides the value of
625
+ * the target parameter.
626
+ *
627
+ *
628
+ * @returns {string} Html-linkified and {@link $sanitize sanitized} text.
546
629
  *
547
630
  * @usage
548
631
  <span ng-bind-html="linky_expression | linky"></span>
549
632
  *
550
633
  * @example
551
- <example module="linkyExample" deps="angular-sanitize.js">
634
+ <example module="linkyExample" deps="angular-sanitize.js" name="linky-filter">
552
635
  <file name="index.html">
553
- <script>
554
- angular.module('linkyExample', ['ngSanitize'])
555
- .controller('ExampleController', ['$scope', function($scope) {
556
- $scope.snippet =
557
- 'Pretty text with some links:\n'+
558
- 'http://angularjs.org/,\n'+
559
- 'mailto:us@somewhere.org,\n'+
560
- 'another@somewhere.org,\n'+
561
- 'and one more: ftp://127.0.0.1/.';
562
- $scope.snippetWithTarget = 'http://angularjs.org/';
563
- }]);
564
- </script>
565
636
  <div ng-controller="ExampleController">
566
637
  Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
567
638
  <table>
568
639
  <tr>
569
- <td>Filter</td>
570
- <td>Source</td>
571
- <td>Rendered</td>
640
+ <th>Filter</th>
641
+ <th>Source</th>
642
+ <th>Rendered</th>
572
643
  </tr>
573
644
  <tr id="linky-filter">
574
645
  <td>linky filter</td>
@@ -582,10 +653,19 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
582
653
  <tr id="linky-target">
583
654
  <td>linky target</td>
584
655
  <td>
585
- <pre>&lt;div ng-bind-html="snippetWithTarget | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
656
+ <pre>&lt;div ng-bind-html="snippetWithSingleURL | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
586
657
  </td>
587
658
  <td>
588
- <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
659
+ <div ng-bind-html="snippetWithSingleURL | linky:'_blank'"></div>
660
+ </td>
661
+ </tr>
662
+ <tr id="linky-custom-attributes">
663
+ <td>linky custom attributes</td>
664
+ <td>
665
+ <pre>&lt;div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"&gt;<br>&lt;/div&gt;</pre>
666
+ </td>
667
+ <td>
668
+ <div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"></div>
589
669
  </td>
590
670
  </tr>
591
671
  <tr id="escaped-html">
@@ -595,6 +675,18 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
595
675
  </tr>
596
676
  </table>
597
677
  </file>
678
+ <file name="script.js">
679
+ angular.module('linkyExample', ['ngSanitize'])
680
+ .controller('ExampleController', ['$scope', function($scope) {
681
+ $scope.snippet =
682
+ 'Pretty text with some links:\n' +
683
+ 'http://angularjs.org/,\n' +
684
+ 'mailto:us@somewhere.org,\n' +
685
+ 'another@somewhere.org,\n' +
686
+ 'and one more: ftp://127.0.0.1/.';
687
+ $scope.snippetWithSingleURL = 'http://angularjs.org/';
688
+ }]);
689
+ </file>
598
690
  <file name="protractor.js" type="protractor">
599
691
  it('should linkify the snippet with urls', function() {
600
692
  expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
@@ -622,20 +714,40 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
622
714
 
623
715
  it('should work with the target property', function() {
624
716
  expect(element(by.id('linky-target')).
625
- element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
717
+ element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()).
626
718
  toBe('http://angularjs.org/');
627
719
  expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
628
720
  });
721
+
722
+ it('should optionally add custom attributes', function() {
723
+ expect(element(by.id('linky-custom-attributes')).
724
+ element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()).
725
+ toBe('http://angularjs.org/');
726
+ expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow');
727
+ });
629
728
  </file>
630
729
  </example>
631
730
  */
632
731
  angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
633
732
  var LINKY_URL_REGEXP =
634
- /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
733
+ /((s?ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
635
734
  MAILTO_REGEXP = /^mailto:/i;
636
735
 
637
- return function(text, target) {
638
- if (!text) return text;
736
+ var linkyMinErr = angular.$$minErr('linky');
737
+ var isDefined = angular.isDefined;
738
+ var isFunction = angular.isFunction;
739
+ var isObject = angular.isObject;
740
+ var isString = angular.isString;
741
+
742
+ return function(text, target, attributes) {
743
+ if (text == null || text === '') return text;
744
+ if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text);
745
+
746
+ var attributesFn =
747
+ isFunction(attributes) ? attributes :
748
+ isObject(attributes) ? function getAttributesObject() {return attributes;} :
749
+ function getEmptyAttributesObject() {return {};};
750
+
639
751
  var match;
640
752
  var raw = text;
641
753
  var html = [];
@@ -664,8 +776,14 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
664
776
  }
665
777
 
666
778
  function addLink(url, text) {
779
+ var key, linkAttributes = attributesFn(url);
667
780
  html.push('<a ');
668
- if (angular.isDefined(target)) {
781
+
782
+ for (key in linkAttributes) {
783
+ html.push(key + '="' + linkAttributes[key] + '" ');
784
+ }
785
+
786
+ if (isDefined(target) && !('target' in linkAttributes)) {
669
787
  html.push('target="',
670
788
  target,
671
789
  '" ');