material_raingular 0.0.7 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/lib/material_raingular/version.rb +1 -1
- data/vendor/assets/angular/angular-animate.js +1027 -620
- data/vendor/assets/angular/angular-aria.js +111 -91
- data/vendor/assets/angular/angular-cookies.js +17 -15
- data/vendor/assets/angular/angular-loader.js +70 -15
- data/vendor/assets/angular/angular-material.min.css +2 -2
- data/vendor/assets/angular/angular-material.min.js +10 -9
- data/vendor/assets/angular/angular-message-format.js +2 -2
- data/vendor/assets/angular/angular-messages.js +77 -33
- data/vendor/assets/angular/angular-resource.js +140 -40
- data/vendor/assets/angular/angular-route.js +52 -21
- data/vendor/assets/angular/angular-sanitize.js +283 -249
- data/vendor/assets/angular/angular-touch.js +117 -15
- data/vendor/assets/angular/angular.js +4528 -1947
- metadata +2 -2
@@ -1,6 +1,6 @@
|
|
1
1
|
/**
|
2
|
-
* @license AngularJS v1.
|
3
|
-
* (c) 2010-
|
2
|
+
* @license AngularJS v1.5.3
|
3
|
+
* (c) 2010-2016 Google, Inc. http://angularjs.org
|
4
4
|
* License: MIT
|
5
5
|
*/
|
6
6
|
(function(window, angular, undefined) {'use strict';
|
@@ -22,7 +22,11 @@
|
|
22
22
|
*/
|
23
23
|
/* global -ngRouteModule */
|
24
24
|
var ngRouteModule = angular.module('ngRoute', ['ng']).
|
25
|
-
provider('$route', $RouteProvider)
|
25
|
+
provider('$route', $RouteProvider).
|
26
|
+
// Ensure `$route` will be instantiated in time to capture the initial
|
27
|
+
// `$locationChangeSuccess` event. This is necessary in case `ngView` is
|
28
|
+
// included in an asynchronously loaded template.
|
29
|
+
run(['$route', angular.noop]),
|
26
30
|
$routeMinErr = angular.$$minErr('ngRoute');
|
27
31
|
|
28
32
|
/**
|
@@ -105,8 +109,17 @@ function $RouteProvider() {
|
|
105
109
|
* If all the promises are resolved successfully, the values of the resolved promises are
|
106
110
|
* injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is
|
107
111
|
* fired. If any of the promises are rejected the
|
108
|
-
* {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired.
|
109
|
-
*
|
112
|
+
* {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired.
|
113
|
+
* For easier access to the resolved dependencies from the template, the `resolve` map will
|
114
|
+
* be available on the scope of the route, under `$resolve` (by default) or a custom name
|
115
|
+
* specified by the `resolveAs` property (see below). This can be particularly useful, when
|
116
|
+
* working with {@link angular.Module#component components} as route templates.<br />
|
117
|
+
* <div class="alert alert-warning">
|
118
|
+
* **Note:** If your scope already contains a property with this name, it will be hidden
|
119
|
+
* or overwritten. Make sure, you specify an appropriate name for this property, that
|
120
|
+
* does not collide with other properties on the scope.
|
121
|
+
* </div>
|
122
|
+
* The map object is:
|
110
123
|
*
|
111
124
|
* - `key` – `{string}`: a name of a dependency to be injected into the controller.
|
112
125
|
* - `factory` - `{string|function}`: If `string` then it is an alias for a service.
|
@@ -116,7 +129,10 @@ function $RouteProvider() {
|
|
116
129
|
* `ngRoute.$routeParams` will still refer to the previous route within these resolve
|
117
130
|
* functions. Use `$route.current.params` to access the new route parameters, instead.
|
118
131
|
*
|
119
|
-
* - `
|
132
|
+
* - `resolveAs` - `{string=}` - The name under which the `resolve` map will be available on
|
133
|
+
* the scope of the route. If omitted, defaults to `$resolve`.
|
134
|
+
*
|
135
|
+
* - `redirectTo` – `{(string|function())=}` – value to update
|
120
136
|
* {@link ng.$location $location} path with and trigger route redirection.
|
121
137
|
*
|
122
138
|
* If `redirectTo` is a function, it will be called with the following parameters:
|
@@ -129,13 +145,13 @@ function $RouteProvider() {
|
|
129
145
|
* The custom `redirectTo` function is expected to return a string which will be used
|
130
146
|
* to update `$location.path()` and `$location.search()`.
|
131
147
|
*
|
132
|
-
* - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()`
|
148
|
+
* - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()`
|
133
149
|
* or `$location.hash()` changes.
|
134
150
|
*
|
135
151
|
* If the option is set to `false` and url in the browser changes, then
|
136
152
|
* `$routeUpdate` event is broadcasted on the root scope.
|
137
153
|
*
|
138
|
-
* - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive
|
154
|
+
* - `[caseInsensitiveMatch=false]` - `{boolean=}` - match routes without being case sensitive
|
139
155
|
*
|
140
156
|
* If the option is set to `true`, then the particular route can be matched without being
|
141
157
|
* case sensitive
|
@@ -206,9 +222,9 @@ function $RouteProvider() {
|
|
206
222
|
|
207
223
|
path = path
|
208
224
|
.replace(/([().])/g, '\\$1')
|
209
|
-
.replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) {
|
210
|
-
var optional = option === '?'
|
211
|
-
var star = option === '*'
|
225
|
+
.replace(/(\/)?:(\w+)(\*\?|[\?\*])?/g, function(_, slash, key, option) {
|
226
|
+
var optional = (option === '?' || option === '*?') ? '?' : null;
|
227
|
+
var star = (option === '*' || option === '*?') ? '*' : null;
|
212
228
|
keys.push({ name: key, optional: !!optional });
|
213
229
|
slash = slash || '';
|
214
230
|
return ''
|
@@ -265,7 +281,7 @@ function $RouteProvider() {
|
|
265
281
|
* @property {Object} current Reference to the current route definition.
|
266
282
|
* The route definition contains:
|
267
283
|
*
|
268
|
-
* - `controller`: The controller constructor as
|
284
|
+
* - `controller`: The controller constructor as defined in the route definition.
|
269
285
|
* - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for
|
270
286
|
* controller instantiation. The `locals` contain
|
271
287
|
* the resolved values of the `resolve` map. Additionally the `locals` also contain:
|
@@ -273,6 +289,10 @@ function $RouteProvider() {
|
|
273
289
|
* - `$scope` - The current route scope.
|
274
290
|
* - `$template` - The current route template HTML.
|
275
291
|
*
|
292
|
+
* The `locals` will be assigned to the route scope's `$resolve` property. You can override
|
293
|
+
* the property name, using `resolveAs` in the route definition. See
|
294
|
+
* {@link ngRoute.$routeProvider $routeProvider} for more info.
|
295
|
+
*
|
276
296
|
* @property {Object} routes Object with all route configuration Objects as its properties.
|
277
297
|
*
|
278
298
|
* @description
|
@@ -412,7 +432,9 @@ function $RouteProvider() {
|
|
412
432
|
* @name $route#$routeChangeSuccess
|
413
433
|
* @eventType broadcast on root scope
|
414
434
|
* @description
|
415
|
-
* Broadcasted after a route
|
435
|
+
* Broadcasted after a route change has happened successfully.
|
436
|
+
* The `resolve` dependencies are now available in the `current.locals` property.
|
437
|
+
*
|
416
438
|
* {@link ngRoute.directive:ngView ngView} listens for the directive
|
417
439
|
* to instantiate the controller and render the view.
|
418
440
|
*
|
@@ -466,10 +488,18 @@ function $RouteProvider() {
|
|
466
488
|
*/
|
467
489
|
reload: function() {
|
468
490
|
forceReload = true;
|
491
|
+
|
492
|
+
var fakeLocationEvent = {
|
493
|
+
defaultPrevented: false,
|
494
|
+
preventDefault: function fakePreventDefault() {
|
495
|
+
this.defaultPrevented = true;
|
496
|
+
forceReload = false;
|
497
|
+
}
|
498
|
+
};
|
499
|
+
|
469
500
|
$rootScope.$evalAsync(function() {
|
470
|
-
|
471
|
-
|
472
|
-
commitRoute();
|
501
|
+
prepareRoute(fakeLocationEvent);
|
502
|
+
if (!fakeLocationEvent.defaultPrevented) commitRoute();
|
473
503
|
});
|
474
504
|
},
|
475
505
|
|
@@ -596,9 +626,8 @@ function $RouteProvider() {
|
|
596
626
|
if (angular.isFunction(templateUrl)) {
|
597
627
|
templateUrl = templateUrl(nextRoute.params);
|
598
628
|
}
|
599
|
-
templateUrl = $sce.getTrustedResourceUrl(templateUrl);
|
600
629
|
if (angular.isDefined(templateUrl)) {
|
601
|
-
nextRoute.loadedTemplateUrl = templateUrl;
|
630
|
+
nextRoute.loadedTemplateUrl = $sce.valueOf(templateUrl);
|
602
631
|
template = $templateRequest(templateUrl);
|
603
632
|
}
|
604
633
|
}
|
@@ -724,8 +753,10 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory);
|
|
724
753
|
* Requires the {@link ngRoute `ngRoute`} module to be installed.
|
725
754
|
*
|
726
755
|
* @animations
|
727
|
-
*
|
728
|
-
*
|
756
|
+
* | Animation | Occurs |
|
757
|
+
* |----------------------------------|-------------------------------------|
|
758
|
+
* | {@link ng.$animate#enter enter} | when the new element is inserted to the DOM |
|
759
|
+
* | {@link ng.$animate#leave leave} | when the old element is removed from to the DOM |
|
729
760
|
*
|
730
761
|
* The enter and leave animation occur concurrently.
|
731
762
|
*
|
@@ -795,7 +826,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory);
|
|
795
826
|
}
|
796
827
|
|
797
828
|
.view-animate.ng-enter, .view-animate.ng-leave {
|
798
|
-
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
|
799
829
|
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
|
800
830
|
|
801
831
|
display:block;
|
@@ -981,6 +1011,7 @@ function ngViewFillContentFactory($compile, $controller, $route) {
|
|
981
1011
|
$element.data('$ngControllerController', controller);
|
982
1012
|
$element.children().data('$ngControllerController', controller);
|
983
1013
|
}
|
1014
|
+
scope[current.resolveAs || '$resolve'] = locals;
|
984
1015
|
|
985
1016
|
link(scope);
|
986
1017
|
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
/**
|
2
|
-
* @license AngularJS v1.
|
3
|
-
* (c) 2010-
|
2
|
+
* @license AngularJS v1.5.3
|
3
|
+
* (c) 2010-2016 Google, Inc. http://angularjs.org
|
4
4
|
* License: MIT
|
5
5
|
*/
|
6
6
|
(function(window, angular, undefined) {'use strict';
|
@@ -33,36 +33,23 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
|
|
33
33
|
* See {@link ngSanitize.$sanitize `$sanitize`} for usage.
|
34
34
|
*/
|
35
35
|
|
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
36
|
/**
|
54
37
|
* @ngdoc service
|
55
38
|
* @name $sanitize
|
56
39
|
* @kind function
|
57
40
|
*
|
58
41
|
* @description
|
42
|
+
* Sanitizes an html string by stripping all potentially dangerous tokens.
|
43
|
+
*
|
59
44
|
* The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
|
60
45
|
* then serialized back to properly escaped html string. This means that no unsafe input can make
|
61
|
-
* it into the returned string
|
62
|
-
*
|
63
|
-
*
|
64
|
-
*
|
65
|
-
*
|
46
|
+
* it into the returned string.
|
47
|
+
*
|
48
|
+
* The whitelist for URL sanitization of attribute values is configured using the functions
|
49
|
+
* `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider
|
50
|
+
* `$compileProvider`}.
|
51
|
+
*
|
52
|
+
* The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
|
66
53
|
*
|
67
54
|
* @param {string} html HTML input.
|
68
55
|
* @returns {string} Sanitized HTML.
|
@@ -148,16 +135,70 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
|
|
148
135
|
</file>
|
149
136
|
</example>
|
150
137
|
*/
|
138
|
+
|
139
|
+
|
140
|
+
/**
|
141
|
+
* @ngdoc provider
|
142
|
+
* @name $sanitizeProvider
|
143
|
+
*
|
144
|
+
* @description
|
145
|
+
* Creates and configures {@link $sanitize} instance.
|
146
|
+
*/
|
151
147
|
function $SanitizeProvider() {
|
148
|
+
var svgEnabled = false;
|
149
|
+
|
152
150
|
this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
|
151
|
+
if (svgEnabled) {
|
152
|
+
angular.extend(validElements, svgElements);
|
153
|
+
}
|
153
154
|
return function(html) {
|
154
155
|
var buf = [];
|
155
156
|
htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
|
156
|
-
return !/^unsafe
|
157
|
+
return !/^unsafe:/.test($$sanitizeUri(uri, isImage));
|
157
158
|
}));
|
158
159
|
return buf.join('');
|
159
160
|
};
|
160
161
|
}];
|
162
|
+
|
163
|
+
|
164
|
+
/**
|
165
|
+
* @ngdoc method
|
166
|
+
* @name $sanitizeProvider#enableSvg
|
167
|
+
* @kind function
|
168
|
+
*
|
169
|
+
* @description
|
170
|
+
* Enables a subset of svg to be supported by the sanitizer.
|
171
|
+
*
|
172
|
+
* <div class="alert alert-warning">
|
173
|
+
* <p>By enabling this setting without taking other precautions, you might expose your
|
174
|
+
* application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned
|
175
|
+
* outside of the containing element and be rendered over other elements on the page (e.g. a login
|
176
|
+
* link). Such behavior can then result in phishing incidents.</p>
|
177
|
+
*
|
178
|
+
* <p>To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg
|
179
|
+
* tags within the sanitized content:</p>
|
180
|
+
*
|
181
|
+
* <br>
|
182
|
+
*
|
183
|
+
* <pre><code>
|
184
|
+
* .rootOfTheIncludedContent svg {
|
185
|
+
* overflow: hidden !important;
|
186
|
+
* }
|
187
|
+
* </code></pre>
|
188
|
+
* </div>
|
189
|
+
*
|
190
|
+
* @param {boolean=} regexp New regexp to whitelist urls with.
|
191
|
+
* @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called
|
192
|
+
* without an argument or self for chaining otherwise.
|
193
|
+
*/
|
194
|
+
this.enableSvg = function(enableSvg) {
|
195
|
+
if (angular.isDefined(enableSvg)) {
|
196
|
+
svgEnabled = enableSvg;
|
197
|
+
return this;
|
198
|
+
} else {
|
199
|
+
return svgEnabled;
|
200
|
+
}
|
201
|
+
};
|
161
202
|
}
|
162
203
|
|
163
204
|
function sanitizeText(chars) {
|
@@ -169,18 +210,9 @@ function sanitizeText(chars) {
|
|
169
210
|
|
170
211
|
|
171
212
|
// Regular Expressions for parsing tags and attributes
|
172
|
-
var
|
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,
|
213
|
+
var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
|
182
214
|
// Match everything outside of normal chars and " (quote character)
|
183
|
-
NON_ALPHANUMERIC_REGEXP = /([
|
215
|
+
NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g;
|
184
216
|
|
185
217
|
|
186
218
|
// Good source of info about elements and attributes
|
@@ -189,23 +221,23 @@ var START_TAG_REGEXP =
|
|
189
221
|
|
190
222
|
// Safe Void Elements - HTML5
|
191
223
|
// http://dev.w3.org/html5/spec/Overview.html#void-elements
|
192
|
-
var voidElements =
|
224
|
+
var voidElements = toMap("area,br,col,hr,img,wbr");
|
193
225
|
|
194
226
|
// Elements that you can, intentionally, leave open (and which close themselves)
|
195
227
|
// http://dev.w3.org/html5/spec/Overview.html#optional-tags
|
196
|
-
var optionalEndTagBlockElements =
|
197
|
-
optionalEndTagInlineElements =
|
228
|
+
var optionalEndTagBlockElements = toMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
|
229
|
+
optionalEndTagInlineElements = toMap("rp,rt"),
|
198
230
|
optionalEndTagElements = angular.extend({},
|
199
231
|
optionalEndTagInlineElements,
|
200
232
|
optionalEndTagBlockElements);
|
201
233
|
|
202
234
|
// Safe Block Elements - HTML5
|
203
|
-
var blockElements = angular.extend({}, optionalEndTagBlockElements,
|
235
|
+
var blockElements = angular.extend({}, optionalEndTagBlockElements, toMap("address,article," +
|
204
236
|
"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,
|
237
|
+
"h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul"));
|
206
238
|
|
207
239
|
// Inline Elements - HTML5
|
208
|
-
var inlineElements = angular.extend({}, optionalEndTagInlineElements,
|
240
|
+
var inlineElements = angular.extend({}, optionalEndTagInlineElements, toMap("a,abbr,acronym,b," +
|
209
241
|
"bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
|
210
242
|
"samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
|
211
243
|
|
@@ -213,32 +245,31 @@ var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a
|
|
213
245
|
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
|
214
246
|
// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
|
215
247
|
// They can potentially allow for arbitrary javascript to be executed. See #11290
|
216
|
-
var svgElements =
|
248
|
+
var svgElements = toMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," +
|
217
249
|
"hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," +
|
218
|
-
"radialGradient,rect,stop,svg,switch,text,title,tspan
|
250
|
+
"radialGradient,rect,stop,svg,switch,text,title,tspan");
|
219
251
|
|
220
|
-
//
|
221
|
-
var
|
252
|
+
// Blocked Elements (will be stripped)
|
253
|
+
var blockedElements = toMap("script,style");
|
222
254
|
|
223
255
|
var validElements = angular.extend({},
|
224
256
|
voidElements,
|
225
257
|
blockElements,
|
226
258
|
inlineElements,
|
227
|
-
optionalEndTagElements
|
228
|
-
svgElements);
|
259
|
+
optionalEndTagElements);
|
229
260
|
|
230
261
|
//Attributes that have href and hence need to be sanitized
|
231
|
-
var uriAttrs =
|
262
|
+
var uriAttrs = toMap("background,cite,href,longdesc,src,xlink:href");
|
232
263
|
|
233
|
-
var htmlAttrs =
|
264
|
+
var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
|
234
265
|
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
|
235
266
|
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
|
236
|
-
'scope,scrolling,shape,size,span,start,summary,target,title,type,' +
|
267
|
+
'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
|
237
268
|
'valign,value,vspace,width');
|
238
269
|
|
239
270
|
// SVG attributes (without "id" and "name" attributes)
|
240
271
|
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
|
241
|
-
var svgAttrs =
|
272
|
+
var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
|
242
273
|
'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
|
243
274
|
'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
|
244
275
|
'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
|
@@ -259,7 +290,7 @@ var validAttrs = angular.extend({},
|
|
259
290
|
svgAttrs,
|
260
291
|
htmlAttrs);
|
261
292
|
|
262
|
-
function
|
293
|
+
function toMap(str, lowercaseKeys) {
|
263
294
|
var obj = {}, items = str.split(','), i;
|
264
295
|
for (i = 0; i < items.length; i++) {
|
265
296
|
obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true;
|
@@ -267,11 +298,32 @@ function makeMap(str, lowercaseKeys) {
|
|
267
298
|
return obj;
|
268
299
|
}
|
269
300
|
|
301
|
+
var inertBodyElement;
|
302
|
+
(function(window) {
|
303
|
+
var doc;
|
304
|
+
if (window.document && window.document.implementation) {
|
305
|
+
doc = window.document.implementation.createHTMLDocument("inert");
|
306
|
+
} else {
|
307
|
+
throw $sanitizeMinErr('noinert', "Can't create an inert html document");
|
308
|
+
}
|
309
|
+
var docElement = doc.documentElement || doc.getDocumentElement();
|
310
|
+
var bodyElements = docElement.getElementsByTagName('body');
|
311
|
+
|
312
|
+
// usually there should be only one body element in the document, but IE doesn't have any, so we need to create one
|
313
|
+
if (bodyElements.length === 1) {
|
314
|
+
inertBodyElement = bodyElements[0];
|
315
|
+
} else {
|
316
|
+
var html = doc.createElement('html');
|
317
|
+
inertBodyElement = doc.createElement('body');
|
318
|
+
html.appendChild(inertBodyElement);
|
319
|
+
doc.appendChild(html);
|
320
|
+
}
|
321
|
+
})(window);
|
270
322
|
|
271
323
|
/**
|
272
324
|
* @example
|
273
325
|
* htmlParser(htmlString, {
|
274
|
-
* start: function(tag, attrs
|
326
|
+
* start: function(tag, attrs) {},
|
275
327
|
* end: function(tag) {},
|
276
328
|
* chars: function(text) {},
|
277
329
|
* comment: function(text) {}
|
@@ -281,169 +333,74 @@ function makeMap(str, lowercaseKeys) {
|
|
281
333
|
* @param {object} handler
|
282
334
|
*/
|
283
335
|
function htmlParser(html, handler) {
|
284
|
-
if (
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
html = '' + html;
|
289
|
-
}
|
336
|
+
if (html === null || html === undefined) {
|
337
|
+
html = '';
|
338
|
+
} else if (typeof html !== 'string') {
|
339
|
+
html = '' + html;
|
290
340
|
}
|
291
|
-
|
292
|
-
stack.last = function() { return stack[stack.length - 1]; };
|
341
|
+
inertBodyElement.innerHTML = html;
|
293
342
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
// Comment
|
302
|
-
if (html.indexOf("<!--") === 0) {
|
303
|
-
// comments containing -- are not allowed unless they terminate the comment
|
304
|
-
index = html.indexOf("--", 4);
|
305
|
-
|
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);
|
314
|
-
|
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
|
-
}
|
343
|
+
//mXSS protection
|
344
|
+
var mXSSAttempts = 5;
|
345
|
+
do {
|
346
|
+
if (mXSSAttempts === 0) {
|
347
|
+
throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable");
|
348
|
+
}
|
349
|
+
mXSSAttempts--;
|
328
350
|
|
329
|
-
|
330
|
-
|
331
|
-
|
351
|
+
// strip custom-namespaced attributes on IE<=11
|
352
|
+
if (document.documentMode <= 11) {
|
353
|
+
stripCustomNsAttrs(inertBodyElement);
|
354
|
+
}
|
355
|
+
html = inertBodyElement.innerHTML; //trigger mXSS
|
356
|
+
inertBodyElement.innerHTML = html;
|
357
|
+
} while (html !== inertBodyElement.innerHTML);
|
358
|
+
|
359
|
+
var node = inertBodyElement.firstChild;
|
360
|
+
while (node) {
|
361
|
+
switch (node.nodeType) {
|
362
|
+
case 1: // ELEMENT_NODE
|
363
|
+
handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes));
|
364
|
+
break;
|
365
|
+
case 3: // TEXT NODE
|
366
|
+
handler.chars(node.textContent);
|
367
|
+
break;
|
368
|
+
}
|
332
369
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
370
|
+
var nextNode;
|
371
|
+
if (!(nextNode = node.firstChild)) {
|
372
|
+
if (node.nodeType == 1) {
|
373
|
+
handler.end(node.nodeName.toLowerCase());
|
374
|
+
}
|
375
|
+
nextNode = node.nextSibling;
|
376
|
+
if (!nextNode) {
|
377
|
+
while (nextNode == null) {
|
378
|
+
node = node.parentNode;
|
379
|
+
if (node === inertBodyElement) break;
|
380
|
+
nextNode = node.nextSibling;
|
381
|
+
if (node.nodeType == 1) {
|
382
|
+
handler.end(node.nodeName.toLowerCase());
|
338
383
|
}
|
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
384
|
}
|
345
385
|
}
|
346
|
-
|
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));
|
354
|
-
}
|
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());
|
368
|
-
}
|
369
|
-
|
370
|
-
if (html == last) {
|
371
|
-
throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
|
372
|
-
"of html: {0}", html);
|
373
386
|
}
|
374
|
-
|
387
|
+
node = nextNode;
|
375
388
|
}
|
376
389
|
|
377
|
-
|
378
|
-
|
379
|
-
|
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());
|
385
|
-
}
|
386
|
-
}
|
387
|
-
|
388
|
-
if (optionalEndTagElements[tagName] && stack.last() == tagName) {
|
389
|
-
parseEndTag("", tagName);
|
390
|
-
}
|
391
|
-
|
392
|
-
unary = voidElements[tagName] || !!unary;
|
393
|
-
|
394
|
-
if (!unary) {
|
395
|
-
stack.push(tagName);
|
396
|
-
}
|
397
|
-
|
398
|
-
var attrs = {};
|
399
|
-
|
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);
|
390
|
+
while (node = inertBodyElement.firstChild) {
|
391
|
+
inertBodyElement.removeChild(node);
|
410
392
|
}
|
393
|
+
}
|
411
394
|
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
for (pos = stack.length - 1; pos >= 0; pos--) {
|
418
|
-
if (stack[pos] == tagName) break;
|
419
|
-
}
|
420
|
-
}
|
421
|
-
|
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;
|
429
|
-
}
|
395
|
+
function attrToMap(attrs) {
|
396
|
+
var map = {};
|
397
|
+
for (var i = 0, ii = attrs.length; i < ii; i++) {
|
398
|
+
var attr = attrs[i];
|
399
|
+
map[attr.name] = attr.value;
|
430
400
|
}
|
401
|
+
return map;
|
431
402
|
}
|
432
403
|
|
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 ''; }
|
441
|
-
|
442
|
-
hiddenPre.innerHTML = value.replace(/</g,"<");
|
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
404
|
|
448
405
|
/**
|
449
406
|
* Escapes all potentially dangerous characters, so that the
|
@@ -469,24 +426,24 @@ function encodeEntities(value) {
|
|
469
426
|
|
470
427
|
/**
|
471
428
|
* create an HTML/XML writer which writes to buffer
|
472
|
-
* @param {Array} buf use buf.
|
429
|
+
* @param {Array} buf use buf.join('') to get out sanitized html string
|
473
430
|
* @returns {object} in the form of {
|
474
|
-
* start: function(tag, attrs
|
431
|
+
* start: function(tag, attrs) {},
|
475
432
|
* end: function(tag) {},
|
476
433
|
* chars: function(text) {},
|
477
434
|
* comment: function(text) {}
|
478
435
|
* }
|
479
436
|
*/
|
480
437
|
function htmlSanitizeWriter(buf, uriValidator) {
|
481
|
-
var
|
438
|
+
var ignoreCurrentElement = false;
|
482
439
|
var out = angular.bind(buf, buf.push);
|
483
440
|
return {
|
484
|
-
start: function(tag, attrs
|
441
|
+
start: function(tag, attrs) {
|
485
442
|
tag = angular.lowercase(tag);
|
486
|
-
if (!
|
487
|
-
|
443
|
+
if (!ignoreCurrentElement && blockedElements[tag]) {
|
444
|
+
ignoreCurrentElement = tag;
|
488
445
|
}
|
489
|
-
if (!
|
446
|
+
if (!ignoreCurrentElement && validElements[tag] === true) {
|
490
447
|
out('<');
|
491
448
|
out(tag);
|
492
449
|
angular.forEach(attrs, function(value, key) {
|
@@ -501,29 +458,63 @@ function htmlSanitizeWriter(buf, uriValidator) {
|
|
501
458
|
out('"');
|
502
459
|
}
|
503
460
|
});
|
504
|
-
out(
|
461
|
+
out('>');
|
505
462
|
}
|
506
463
|
},
|
507
464
|
end: function(tag) {
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
465
|
+
tag = angular.lowercase(tag);
|
466
|
+
if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) {
|
467
|
+
out('</');
|
468
|
+
out(tag);
|
469
|
+
out('>');
|
470
|
+
}
|
471
|
+
if (tag == ignoreCurrentElement) {
|
472
|
+
ignoreCurrentElement = false;
|
473
|
+
}
|
474
|
+
},
|
518
475
|
chars: function(chars) {
|
519
|
-
|
520
|
-
|
521
|
-
}
|
476
|
+
if (!ignoreCurrentElement) {
|
477
|
+
out(encodeEntities(chars));
|
522
478
|
}
|
479
|
+
}
|
523
480
|
};
|
524
481
|
}
|
525
482
|
|
526
483
|
|
484
|
+
/**
|
485
|
+
* When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare
|
486
|
+
* ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want
|
487
|
+
* to allow any of these custom attributes. This method strips them all.
|
488
|
+
*
|
489
|
+
* @param node Root element to process
|
490
|
+
*/
|
491
|
+
function stripCustomNsAttrs(node) {
|
492
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
493
|
+
var attrs = node.attributes;
|
494
|
+
for (var i = 0, l = attrs.length; i < l; i++) {
|
495
|
+
var attrNode = attrs[i];
|
496
|
+
var attrName = attrNode.name.toLowerCase();
|
497
|
+
if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
|
498
|
+
node.removeAttributeNode(attrNode);
|
499
|
+
i--;
|
500
|
+
l--;
|
501
|
+
}
|
502
|
+
}
|
503
|
+
}
|
504
|
+
|
505
|
+
var nextNode = node.firstChild;
|
506
|
+
if (nextNode) {
|
507
|
+
stripCustomNsAttrs(nextNode);
|
508
|
+
}
|
509
|
+
|
510
|
+
nextNode = node.nextSibling;
|
511
|
+
if (nextNode) {
|
512
|
+
stripCustomNsAttrs(nextNode);
|
513
|
+
}
|
514
|
+
}
|
515
|
+
|
516
|
+
|
517
|
+
|
527
518
|
// define ngSanitize module and register $sanitize service
|
528
519
|
angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
|
529
520
|
|
@@ -535,14 +526,25 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
|
|
535
526
|
* @kind function
|
536
527
|
*
|
537
528
|
* @description
|
538
|
-
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
|
529
|
+
* Finds links in text input and turns them into html links. Supports `http/https/ftp/mailto` and
|
539
530
|
* plain email address links.
|
540
531
|
*
|
541
532
|
* Requires the {@link ngSanitize `ngSanitize`} module to be installed.
|
542
533
|
*
|
543
534
|
* @param {string} text Input text.
|
544
|
-
* @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
|
545
|
-
* @
|
535
|
+
* @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in.
|
536
|
+
* @param {object|function(url)} [attributes] Add custom attributes to the link element.
|
537
|
+
*
|
538
|
+
* Can be one of:
|
539
|
+
*
|
540
|
+
* - `object`: A map of attributes
|
541
|
+
* - `function`: Takes the url as a parameter and returns a map of attributes
|
542
|
+
*
|
543
|
+
* If the map of attributes contains a value for `target`, it overrides the value of
|
544
|
+
* the target parameter.
|
545
|
+
*
|
546
|
+
*
|
547
|
+
* @returns {string} Html-linkified and {@link $sanitize sanitized} text.
|
546
548
|
*
|
547
549
|
* @usage
|
548
550
|
<span ng-bind-html="linky_expression | linky"></span>
|
@@ -550,25 +552,13 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
|
|
550
552
|
* @example
|
551
553
|
<example module="linkyExample" deps="angular-sanitize.js">
|
552
554
|
<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
555
|
<div ng-controller="ExampleController">
|
566
556
|
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
|
567
557
|
<table>
|
568
558
|
<tr>
|
569
|
-
<
|
570
|
-
<
|
571
|
-
<
|
559
|
+
<th>Filter</th>
|
560
|
+
<th>Source</th>
|
561
|
+
<th>Rendered</th>
|
572
562
|
</tr>
|
573
563
|
<tr id="linky-filter">
|
574
564
|
<td>linky filter</td>
|
@@ -582,10 +572,19 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
|
|
582
572
|
<tr id="linky-target">
|
583
573
|
<td>linky target</td>
|
584
574
|
<td>
|
585
|
-
<pre><div ng-bind-html="
|
575
|
+
<pre><div ng-bind-html="snippetWithSingleURL | linky:'_blank'"><br></div></pre>
|
576
|
+
</td>
|
577
|
+
<td>
|
578
|
+
<div ng-bind-html="snippetWithSingleURL | linky:'_blank'"></div>
|
579
|
+
</td>
|
580
|
+
</tr>
|
581
|
+
<tr id="linky-custom-attributes">
|
582
|
+
<td>linky custom attributes</td>
|
583
|
+
<td>
|
584
|
+
<pre><div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"><br></div></pre>
|
586
585
|
</td>
|
587
586
|
<td>
|
588
|
-
<div ng-bind-html="
|
587
|
+
<div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"></div>
|
589
588
|
</td>
|
590
589
|
</tr>
|
591
590
|
<tr id="escaped-html">
|
@@ -595,6 +594,18 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
|
|
595
594
|
</tr>
|
596
595
|
</table>
|
597
596
|
</file>
|
597
|
+
<file name="script.js">
|
598
|
+
angular.module('linkyExample', ['ngSanitize'])
|
599
|
+
.controller('ExampleController', ['$scope', function($scope) {
|
600
|
+
$scope.snippet =
|
601
|
+
'Pretty text with some links:\n'+
|
602
|
+
'http://angularjs.org/,\n'+
|
603
|
+
'mailto:us@somewhere.org,\n'+
|
604
|
+
'another@somewhere.org,\n'+
|
605
|
+
'and one more: ftp://127.0.0.1/.';
|
606
|
+
$scope.snippetWithSingleURL = 'http://angularjs.org/';
|
607
|
+
}]);
|
608
|
+
</file>
|
598
609
|
<file name="protractor.js" type="protractor">
|
599
610
|
it('should linkify the snippet with urls', function() {
|
600
611
|
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
|
@@ -622,20 +633,32 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
|
|
622
633
|
|
623
634
|
it('should work with the target property', function() {
|
624
635
|
expect(element(by.id('linky-target')).
|
625
|
-
element(by.binding("
|
636
|
+
element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()).
|
626
637
|
toBe('http://angularjs.org/');
|
627
638
|
expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
|
628
639
|
});
|
640
|
+
|
641
|
+
it('should optionally add custom attributes', function() {
|
642
|
+
expect(element(by.id('linky-custom-attributes')).
|
643
|
+
element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()).
|
644
|
+
toBe('http://angularjs.org/');
|
645
|
+
expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow');
|
646
|
+
});
|
629
647
|
</file>
|
630
648
|
</example>
|
631
649
|
*/
|
632
650
|
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
|
633
651
|
var LINKY_URL_REGEXP =
|
634
|
-
/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"
|
635
|
-
MAILTO_REGEXP = /^mailto
|
652
|
+
/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
|
653
|
+
MAILTO_REGEXP = /^mailto:/i;
|
654
|
+
|
655
|
+
var linkyMinErr = angular.$$minErr('linky');
|
656
|
+
var isString = angular.isString;
|
657
|
+
|
658
|
+
return function(text, target, attributes) {
|
659
|
+
if (text == null || text === '') return text;
|
660
|
+
if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text);
|
636
661
|
|
637
|
-
return function(text, target) {
|
638
|
-
if (!text) return text;
|
639
662
|
var match;
|
640
663
|
var raw = text;
|
641
664
|
var html = [];
|
@@ -664,8 +687,19 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
|
|
664
687
|
}
|
665
688
|
|
666
689
|
function addLink(url, text) {
|
690
|
+
var key;
|
667
691
|
html.push('<a ');
|
668
|
-
if (angular.
|
692
|
+
if (angular.isFunction(attributes)) {
|
693
|
+
attributes = attributes(url);
|
694
|
+
}
|
695
|
+
if (angular.isObject(attributes)) {
|
696
|
+
for (key in attributes) {
|
697
|
+
html.push(key + '="' + attributes[key] + '" ');
|
698
|
+
}
|
699
|
+
} else {
|
700
|
+
attributes = {};
|
701
|
+
}
|
702
|
+
if (angular.isDefined(target) && !('target' in attributes)) {
|
669
703
|
html.push('target="',
|
670
704
|
target,
|
671
705
|
'" ');
|