vite-plugin-sri4 1.7.0 → 1.8.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.
- package/dist/index.cjs +65 -53
- package/dist/index.js +65 -53
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -12,23 +12,12 @@ const DEFAULT_OPTIONS = {
|
|
|
12
12
|
debug: false
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* Log debug messages if debug mode is enabled
|
|
17
|
-
* @param {string} message - Message to log
|
|
18
|
-
* @param {Object} options - Plugin options
|
|
19
|
-
*/
|
|
20
15
|
function log(message, options) {
|
|
21
16
|
if (options.debug) {
|
|
22
17
|
console.log(`${LOG_PREFIX} ${message}`);
|
|
23
18
|
}
|
|
24
19
|
}
|
|
25
20
|
|
|
26
|
-
/**
|
|
27
|
-
* Compute SRI hash for given content
|
|
28
|
-
* @param {string|Buffer} content - Content to hash
|
|
29
|
-
* @param {string} algorithm - Hash algorithm to use
|
|
30
|
-
* @returns {string|null} SRI hash string or null if failed
|
|
31
|
-
*/
|
|
32
21
|
function computeSri(content, algorithm = 'sha384') {
|
|
33
22
|
try {
|
|
34
23
|
const hash = node_crypto.createHash(algorithm)
|
|
@@ -41,17 +30,10 @@ function computeSri(content, algorithm = 'sha384') {
|
|
|
41
30
|
}
|
|
42
31
|
}
|
|
43
32
|
|
|
44
|
-
/**
|
|
45
|
-
* Check if external resource has CORS enabled
|
|
46
|
-
* @param {string} url - URL to check
|
|
47
|
-
* @param {Object} options - Plugin options
|
|
48
|
-
* @returns {Promise<boolean>} Whether CORS is enabled
|
|
49
|
-
*/
|
|
50
33
|
async function externalResourceIsCorsEnabled(url, options) {
|
|
51
34
|
try {
|
|
52
35
|
const response = await fetch(url, {
|
|
53
|
-
method: 'HEAD'
|
|
54
|
-
timeout: 5000
|
|
36
|
+
method: 'HEAD'
|
|
55
37
|
});
|
|
56
38
|
const acao = response.headers.get('access-control-allow-origin');
|
|
57
39
|
if (acao && (acao === '*' || acao.includes(options.domain || ''))) {
|
|
@@ -64,12 +46,6 @@ async function externalResourceIsCorsEnabled(url, options) {
|
|
|
64
46
|
}
|
|
65
47
|
}
|
|
66
48
|
|
|
67
|
-
/**
|
|
68
|
-
* Check if URL belongs to bypass domains
|
|
69
|
-
* @param {string} url - URL to check
|
|
70
|
-
* @param {string[]} bypassDomains - List of domains to bypass
|
|
71
|
-
* @returns {boolean} Whether URL belongs to bypass domains
|
|
72
|
-
*/
|
|
73
49
|
function isBypassDomain(url, bypassDomains = []) {
|
|
74
50
|
if (!bypassDomains.length) return false;
|
|
75
51
|
try {
|
|
@@ -85,29 +61,38 @@ function isBypassDomain(url, bypassDomains = []) {
|
|
|
85
61
|
}
|
|
86
62
|
}
|
|
87
63
|
|
|
88
|
-
/**
|
|
89
|
-
* Check if tag already has crossorigin attribute
|
|
90
|
-
* @param {string} tag - HTML tag to check
|
|
91
|
-
* @returns {boolean} Whether tag has crossorigin attribute
|
|
92
|
-
*/
|
|
93
64
|
function hasCrossOriginAttr(tag) {
|
|
94
65
|
return /(?:^|\s)crossorigin(?:=["']?[^"'\s>]*["']?)?(?:\s|>|$)/i.test(tag);
|
|
95
66
|
}
|
|
96
67
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
68
|
+
function getAllPossiblePaths(url) {
|
|
69
|
+
const paths = [
|
|
70
|
+
url,
|
|
71
|
+
url.startsWith('/') ? url.slice(1) : url,
|
|
72
|
+
url.startsWith('/static/') ? url.slice(8) : url,
|
|
73
|
+
!url.startsWith('/static/') ? `static/${url}` : url,
|
|
74
|
+
!url.startsWith('/static/') ? `/static/${url}` : url,
|
|
75
|
+
url.replace(/^\/static\//, ''),
|
|
76
|
+
url.replace(/^static\//, '')
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const withoutHash = url.replace(/-[a-zA-Z0-9]+\./, '.');
|
|
80
|
+
if (withoutHash !== url) {
|
|
81
|
+
paths.push(...getAllPossiblePaths(withoutHash));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [...new Set(paths)];
|
|
85
|
+
}
|
|
86
|
+
|
|
105
87
|
async function processTag(tag, url, options, sriMap) {
|
|
106
88
|
if (tag.includes('integrity=')) {
|
|
107
89
|
log(`Skip tag with existing integrity attribute: ${tag}`, options);
|
|
108
90
|
return tag;
|
|
109
91
|
}
|
|
110
92
|
|
|
93
|
+
log(`Processing tag: ${tag}`, options);
|
|
94
|
+
log(`URL: ${url}`, options);
|
|
95
|
+
|
|
111
96
|
// Handle external resources
|
|
112
97
|
if (/^(https?:)?\/\//i.test(url)) {
|
|
113
98
|
if (isBypassDomain(url, options.bypassDomains)) {
|
|
@@ -129,7 +114,9 @@ async function processTag(tag, url, options, sriMap) {
|
|
|
129
114
|
log(`Computing SRI for external resource ${url}: ${hash}`, options);
|
|
130
115
|
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
131
116
|
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
132
|
-
|
|
117
|
+
const newTag = tag.replace(/>$/, ` integrity="${hash}"${crossOriginAttr}>`);
|
|
118
|
+
log(`New tag: ${newTag}`, options);
|
|
119
|
+
return newTag;
|
|
133
120
|
}
|
|
134
121
|
} catch (error) {
|
|
135
122
|
log(`Failed to process external resource ${url}: ${error}`, options);
|
|
@@ -138,24 +125,33 @@ async function processTag(tag, url, options, sriMap) {
|
|
|
138
125
|
}
|
|
139
126
|
|
|
140
127
|
// Handle local resources
|
|
141
|
-
const
|
|
142
|
-
|
|
128
|
+
const possiblePaths = getAllPossiblePaths(url);
|
|
129
|
+
let integrity = null;
|
|
130
|
+
|
|
131
|
+
log(`Checking possible paths for ${url}:`, options);
|
|
132
|
+
for (const path of possiblePaths) {
|
|
133
|
+
log(`- Checking path: ${path}`, options);
|
|
134
|
+
if (sriMap.has(path)) {
|
|
135
|
+
integrity = sriMap.get(path);
|
|
136
|
+
log(`Found integrity for path ${path}: ${integrity}`, options);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
143
141
|
if (integrity) {
|
|
144
|
-
log(`Using precomputed SRI for ${
|
|
142
|
+
log(`Using precomputed SRI for ${url}: ${integrity}`, options);
|
|
145
143
|
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
146
144
|
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
147
|
-
|
|
145
|
+
const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
|
|
146
|
+
log(`New tag: ${newTag}`, options);
|
|
147
|
+
return newTag;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
log(`No SRI hash found for ${
|
|
150
|
+
log(`No SRI hash found for ${url}`, options);
|
|
151
|
+
log(`Available paths in sriMap: ${Array.from(sriMap.keys()).join(', ')}`, options);
|
|
151
152
|
return tag;
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
/**
|
|
155
|
-
* Vite plugin for adding Subresource Integrity (SRI) hashes to assets
|
|
156
|
-
* @param {Object} userOptions - Plugin options
|
|
157
|
-
* @returns {import('vite').Plugin} Vite plugin object
|
|
158
|
-
*/
|
|
159
155
|
function sri(userOptions = {}) {
|
|
160
156
|
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
161
157
|
const sriMap = new Map();
|
|
@@ -177,8 +173,10 @@ function sri(userOptions = {}) {
|
|
|
177
173
|
|
|
178
174
|
const hash = computeSri(code, options.algorithm);
|
|
179
175
|
if (hash) {
|
|
180
|
-
|
|
181
|
-
|
|
176
|
+
for (const path of getAllPossiblePaths(chunk.fileName)) {
|
|
177
|
+
sriMap.set(path, hash);
|
|
178
|
+
log(`Stored SRI for path ${path}: ${hash}`, options);
|
|
179
|
+
}
|
|
182
180
|
}
|
|
183
181
|
return null;
|
|
184
182
|
},
|
|
@@ -191,11 +189,18 @@ function sri(userOptions = {}) {
|
|
|
191
189
|
if (chunk.type === 'asset' && !sriMap.has(fileName)) {
|
|
192
190
|
const hash = computeSri(chunk.source, options.algorithm);
|
|
193
191
|
if (hash) {
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
for (const path of getAllPossiblePaths(fileName)) {
|
|
193
|
+
sriMap.set(path, hash);
|
|
194
|
+
log(`Computing SRI for asset ${path}: ${hash}`, options);
|
|
195
|
+
}
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
|
+
|
|
200
|
+
log('Final sriMap contents:', options);
|
|
201
|
+
for (const [key, value] of sriMap.entries()) {
|
|
202
|
+
log(`${key} => ${value}`, options);
|
|
203
|
+
}
|
|
199
204
|
},
|
|
200
205
|
|
|
201
206
|
async transformIndexHtml(html) {
|
|
@@ -205,18 +210,25 @@ function sri(userOptions = {}) {
|
|
|
205
210
|
}
|
|
206
211
|
|
|
207
212
|
try {
|
|
213
|
+
log('Starting HTML transformation', options);
|
|
214
|
+
log(`SRI Map size: ${sriMap.size}`, options);
|
|
215
|
+
|
|
208
216
|
// Process script tags
|
|
209
217
|
const scriptTags = html.match(/<script[^>]+src=["']([^"']+)["'][^>]*>/g) || [];
|
|
218
|
+
log(`Found ${scriptTags.length} script tags`, options);
|
|
210
219
|
for (const tag of scriptTags) {
|
|
211
220
|
const url = tag.match(/src=["']([^"']+)["']/)[1];
|
|
221
|
+
log(`Processing script: ${url}`, options);
|
|
212
222
|
const newTag = await processTag(tag, url, options, sriMap);
|
|
213
223
|
html = html.replace(tag, newTag);
|
|
214
224
|
}
|
|
215
225
|
|
|
216
226
|
// Process link tags
|
|
217
227
|
const linkTags = html.match(/<link[^>]+href=["']([^"']+)["'][^>]*>/g) || [];
|
|
228
|
+
log(`Found ${linkTags.length} link tags`, options);
|
|
218
229
|
for (const tag of linkTags) {
|
|
219
230
|
const url = tag.match(/href=["']([^"']+)["']/)[1];
|
|
231
|
+
log(`Processing link: ${url}`, options);
|
|
220
232
|
const newTag = await processTag(tag, url, options, sriMap);
|
|
221
233
|
html = html.replace(tag, newTag);
|
|
222
234
|
}
|
package/dist/index.js
CHANGED
|
@@ -10,23 +10,12 @@ const DEFAULT_OPTIONS = {
|
|
|
10
10
|
debug: false
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* Log debug messages if debug mode is enabled
|
|
15
|
-
* @param {string} message - Message to log
|
|
16
|
-
* @param {Object} options - Plugin options
|
|
17
|
-
*/
|
|
18
13
|
function log(message, options) {
|
|
19
14
|
if (options.debug) {
|
|
20
15
|
console.log(`${LOG_PREFIX} ${message}`);
|
|
21
16
|
}
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
/**
|
|
25
|
-
* Compute SRI hash for given content
|
|
26
|
-
* @param {string|Buffer} content - Content to hash
|
|
27
|
-
* @param {string} algorithm - Hash algorithm to use
|
|
28
|
-
* @returns {string|null} SRI hash string or null if failed
|
|
29
|
-
*/
|
|
30
19
|
function computeSri(content, algorithm = 'sha384') {
|
|
31
20
|
try {
|
|
32
21
|
const hash = createHash(algorithm)
|
|
@@ -39,17 +28,10 @@ function computeSri(content, algorithm = 'sha384') {
|
|
|
39
28
|
}
|
|
40
29
|
}
|
|
41
30
|
|
|
42
|
-
/**
|
|
43
|
-
* Check if external resource has CORS enabled
|
|
44
|
-
* @param {string} url - URL to check
|
|
45
|
-
* @param {Object} options - Plugin options
|
|
46
|
-
* @returns {Promise<boolean>} Whether CORS is enabled
|
|
47
|
-
*/
|
|
48
31
|
async function externalResourceIsCorsEnabled(url, options) {
|
|
49
32
|
try {
|
|
50
33
|
const response = await fetch(url, {
|
|
51
|
-
method: 'HEAD'
|
|
52
|
-
timeout: 5000
|
|
34
|
+
method: 'HEAD'
|
|
53
35
|
});
|
|
54
36
|
const acao = response.headers.get('access-control-allow-origin');
|
|
55
37
|
if (acao && (acao === '*' || acao.includes(options.domain || ''))) {
|
|
@@ -62,12 +44,6 @@ async function externalResourceIsCorsEnabled(url, options) {
|
|
|
62
44
|
}
|
|
63
45
|
}
|
|
64
46
|
|
|
65
|
-
/**
|
|
66
|
-
* Check if URL belongs to bypass domains
|
|
67
|
-
* @param {string} url - URL to check
|
|
68
|
-
* @param {string[]} bypassDomains - List of domains to bypass
|
|
69
|
-
* @returns {boolean} Whether URL belongs to bypass domains
|
|
70
|
-
*/
|
|
71
47
|
function isBypassDomain(url, bypassDomains = []) {
|
|
72
48
|
if (!bypassDomains.length) return false;
|
|
73
49
|
try {
|
|
@@ -83,29 +59,38 @@ function isBypassDomain(url, bypassDomains = []) {
|
|
|
83
59
|
}
|
|
84
60
|
}
|
|
85
61
|
|
|
86
|
-
/**
|
|
87
|
-
* Check if tag already has crossorigin attribute
|
|
88
|
-
* @param {string} tag - HTML tag to check
|
|
89
|
-
* @returns {boolean} Whether tag has crossorigin attribute
|
|
90
|
-
*/
|
|
91
62
|
function hasCrossOriginAttr(tag) {
|
|
92
63
|
return /(?:^|\s)crossorigin(?:=["']?[^"'\s>]*["']?)?(?:\s|>|$)/i.test(tag);
|
|
93
64
|
}
|
|
94
65
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
66
|
+
function getAllPossiblePaths(url) {
|
|
67
|
+
const paths = [
|
|
68
|
+
url,
|
|
69
|
+
url.startsWith('/') ? url.slice(1) : url,
|
|
70
|
+
url.startsWith('/static/') ? url.slice(8) : url,
|
|
71
|
+
!url.startsWith('/static/') ? `static/${url}` : url,
|
|
72
|
+
!url.startsWith('/static/') ? `/static/${url}` : url,
|
|
73
|
+
url.replace(/^\/static\//, ''),
|
|
74
|
+
url.replace(/^static\//, '')
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const withoutHash = url.replace(/-[a-zA-Z0-9]+\./, '.');
|
|
78
|
+
if (withoutHash !== url) {
|
|
79
|
+
paths.push(...getAllPossiblePaths(withoutHash));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return [...new Set(paths)];
|
|
83
|
+
}
|
|
84
|
+
|
|
103
85
|
async function processTag(tag, url, options, sriMap) {
|
|
104
86
|
if (tag.includes('integrity=')) {
|
|
105
87
|
log(`Skip tag with existing integrity attribute: ${tag}`, options);
|
|
106
88
|
return tag;
|
|
107
89
|
}
|
|
108
90
|
|
|
91
|
+
log(`Processing tag: ${tag}`, options);
|
|
92
|
+
log(`URL: ${url}`, options);
|
|
93
|
+
|
|
109
94
|
// Handle external resources
|
|
110
95
|
if (/^(https?:)?\/\//i.test(url)) {
|
|
111
96
|
if (isBypassDomain(url, options.bypassDomains)) {
|
|
@@ -127,7 +112,9 @@ async function processTag(tag, url, options, sriMap) {
|
|
|
127
112
|
log(`Computing SRI for external resource ${url}: ${hash}`, options);
|
|
128
113
|
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
129
114
|
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
130
|
-
|
|
115
|
+
const newTag = tag.replace(/>$/, ` integrity="${hash}"${crossOriginAttr}>`);
|
|
116
|
+
log(`New tag: ${newTag}`, options);
|
|
117
|
+
return newTag;
|
|
131
118
|
}
|
|
132
119
|
} catch (error) {
|
|
133
120
|
log(`Failed to process external resource ${url}: ${error}`, options);
|
|
@@ -136,24 +123,33 @@ async function processTag(tag, url, options, sriMap) {
|
|
|
136
123
|
}
|
|
137
124
|
|
|
138
125
|
// Handle local resources
|
|
139
|
-
const
|
|
140
|
-
|
|
126
|
+
const possiblePaths = getAllPossiblePaths(url);
|
|
127
|
+
let integrity = null;
|
|
128
|
+
|
|
129
|
+
log(`Checking possible paths for ${url}:`, options);
|
|
130
|
+
for (const path of possiblePaths) {
|
|
131
|
+
log(`- Checking path: ${path}`, options);
|
|
132
|
+
if (sriMap.has(path)) {
|
|
133
|
+
integrity = sriMap.get(path);
|
|
134
|
+
log(`Found integrity for path ${path}: ${integrity}`, options);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
141
139
|
if (integrity) {
|
|
142
|
-
log(`Using precomputed SRI for ${
|
|
140
|
+
log(`Using precomputed SRI for ${url}: ${integrity}`, options);
|
|
143
141
|
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
144
142
|
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
145
|
-
|
|
143
|
+
const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
|
|
144
|
+
log(`New tag: ${newTag}`, options);
|
|
145
|
+
return newTag;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
log(`No SRI hash found for ${
|
|
148
|
+
log(`No SRI hash found for ${url}`, options);
|
|
149
|
+
log(`Available paths in sriMap: ${Array.from(sriMap.keys()).join(', ')}`, options);
|
|
149
150
|
return tag;
|
|
150
151
|
}
|
|
151
152
|
|
|
152
|
-
/**
|
|
153
|
-
* Vite plugin for adding Subresource Integrity (SRI) hashes to assets
|
|
154
|
-
* @param {Object} userOptions - Plugin options
|
|
155
|
-
* @returns {import('vite').Plugin} Vite plugin object
|
|
156
|
-
*/
|
|
157
153
|
function sri(userOptions = {}) {
|
|
158
154
|
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
159
155
|
const sriMap = new Map();
|
|
@@ -175,8 +171,10 @@ function sri(userOptions = {}) {
|
|
|
175
171
|
|
|
176
172
|
const hash = computeSri(code, options.algorithm);
|
|
177
173
|
if (hash) {
|
|
178
|
-
|
|
179
|
-
|
|
174
|
+
for (const path of getAllPossiblePaths(chunk.fileName)) {
|
|
175
|
+
sriMap.set(path, hash);
|
|
176
|
+
log(`Stored SRI for path ${path}: ${hash}`, options);
|
|
177
|
+
}
|
|
180
178
|
}
|
|
181
179
|
return null;
|
|
182
180
|
},
|
|
@@ -189,11 +187,18 @@ function sri(userOptions = {}) {
|
|
|
189
187
|
if (chunk.type === 'asset' && !sriMap.has(fileName)) {
|
|
190
188
|
const hash = computeSri(chunk.source, options.algorithm);
|
|
191
189
|
if (hash) {
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
for (const path of getAllPossiblePaths(fileName)) {
|
|
191
|
+
sriMap.set(path, hash);
|
|
192
|
+
log(`Computing SRI for asset ${path}: ${hash}`, options);
|
|
193
|
+
}
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
+
|
|
198
|
+
log('Final sriMap contents:', options);
|
|
199
|
+
for (const [key, value] of sriMap.entries()) {
|
|
200
|
+
log(`${key} => ${value}`, options);
|
|
201
|
+
}
|
|
197
202
|
},
|
|
198
203
|
|
|
199
204
|
async transformIndexHtml(html) {
|
|
@@ -203,18 +208,25 @@ function sri(userOptions = {}) {
|
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
try {
|
|
211
|
+
log('Starting HTML transformation', options);
|
|
212
|
+
log(`SRI Map size: ${sriMap.size}`, options);
|
|
213
|
+
|
|
206
214
|
// Process script tags
|
|
207
215
|
const scriptTags = html.match(/<script[^>]+src=["']([^"']+)["'][^>]*>/g) || [];
|
|
216
|
+
log(`Found ${scriptTags.length} script tags`, options);
|
|
208
217
|
for (const tag of scriptTags) {
|
|
209
218
|
const url = tag.match(/src=["']([^"']+)["']/)[1];
|
|
219
|
+
log(`Processing script: ${url}`, options);
|
|
210
220
|
const newTag = await processTag(tag, url, options, sriMap);
|
|
211
221
|
html = html.replace(tag, newTag);
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
// Process link tags
|
|
215
225
|
const linkTags = html.match(/<link[^>]+href=["']([^"']+)["'][^>]*>/g) || [];
|
|
226
|
+
log(`Found ${linkTags.length} link tags`, options);
|
|
216
227
|
for (const tag of linkTags) {
|
|
217
228
|
const url = tag.match(/href=["']([^"']+)["']/)[1];
|
|
229
|
+
log(`Processing link: ${url}`, options);
|
|
218
230
|
const newTag = await processTag(tag, url, options, sriMap);
|
|
219
231
|
html = html.replace(tag, newTag);
|
|
220
232
|
}
|