Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: chrome/content/tests/policy.js

Issue 6280417539784704: Issue 101 - Fix policy tests (Closed)
Patch Set: Created July 9, 2014, 5:04 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 (function() 1 (function()
2 { 2 {
3 let server = null; 3 let server = null;
4 let frame = null; 4 let frame = null;
5 let requestNotifier = null; 5 let requestNotifier = null;
6 6
7 module("Content policy", { 7 module("Content policy", {
8 setup: function() 8 setup: function()
9 { 9 {
10 prepareFilterComponents.call(this); 10 prepareFilterComponents.call(this);
(...skipping 25 matching lines...) Expand all
36 36
37 start(); 37 start();
38 }); 38 });
39 } 39 }
40 }); 40 });
41 41
42 let tests = [ 42 let tests = [
43 [ 43 [
44 "HTML image with relative URL", 44 "HTML image with relative URL",
45 '<img src="test.gif">', 45 '<img src="test.gif">',
46 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 46 "http://127.0.0.1:1234/test.gif", "image", false, false
47 ], 47 ],
48 [ 48 [
49 "HTML image with absolute URL", 49 "HTML image with absolute URL",
50 '<img src="http://localhost:1234/test.gif">', 50 '<img src="http://localhost:1234/test.gif">',
51 "http://localhost:1234/test.gif", "image", "127.0.0.1", true 51 "http://localhost:1234/test.gif", "image", true, false
52 ], 52 ],
53 [ 53 [
54 "HTML image button", 54 "HTML image button",
55 '<input type="image" src="test.gif">', 55 '<input type="image" src="test.gif">',
56 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 56 "http://127.0.0.1:1234/test.gif", "image", false, false
57 ], 57 ],
58 [ 58 [
59 "HTML image button inside a frame", 59 "HTML image button inside a frame",
60 '<iframe src="data:text/html,%3Cinput%20type%3D%22image%22%20src%3D%22http %3A%2F%2F127.0.0.1:1234%2Ftest.gif%22%3E"></iframe>', 60 '<iframe src="data:text/html,%3Cinput%20type%3D%22image%22%20src%3D%22http %3A%2F%2F127.0.0.1:1234%2Ftest.gif%22%3E"></iframe>',
61 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 61 "http://127.0.0.1:1234/test.gif", "image", false, false
62 ], 62 ],
63 [ 63 [
64 "HTML image button inside a nested frame", 64 "HTML image button inside a nested frame",
65 '<iframe src="data:text/html,%3Ciframe%20src%3D%22data%3Atext%2Fhtml%2C%25 3Cinput%2520type%253D%2522image%2522%2520src%253D%2522http%253A%252F%252F127.0.0 .1%3A1234%252Ftest.gif%2522%253E%22%3E%3C%2Fiframe%3E"></iframe>', 65 '<iframe src="data:text/html,%3Ciframe%20src%3D%22data%3Atext%2Fhtml%2C%25 3Cinput%2520type%253D%2522image%2522%2520src%253D%2522http%253A%252F%252F127.0.0 .1%3A1234%252Ftest.gif%2522%253E%22%3E%3C%2Fiframe%3E"></iframe>',
66 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 66 "http://127.0.0.1:1234/test.gif", "image", false, false
67 ], 67 ],
68 [ 68 [
69 "Dynamically inserted image button", 69 "Dynamically inserted image button",
70 '<iframe src="about:blank"></iframe><script>window.addEventListener("DOMCo ntentLoaded", function() {frames[0].document.body.innerHTML = \'<input type="ima ge" src="test.gif">\';}, false);<' + '/script>', 70 '<div id="insert"></div><script>window.addEventListener("DOMContentLoaded" , function() { var div = document.getElementById("insert"); div.innerHTML = \'<i nput type="image" id="image" src="test.gif">\'; var image = document.getElementB yId("image"); image.onload = image.onerror = function () { parent.postMessage("l oaded", "*"); }; }, false);<' + '/script>',
71 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 71 "http://127.0.0.1:1234/test.gif", "image", false, true
72 ], 72 ],
73 [ 73 [
74 "CSS background-image", 74 "CSS background-image",
75 '<div style="background-image: url(test.gif)"></div>', 75 '<div style="background-image: url(test.gif)"></div>',
76 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 76 "http://127.0.0.1:1234/test.gif", "image", false, false
77 ], 77 ],
78 [ 78 [
79 "CSS cursor", 79 "CSS cursor",
80 '<div style="cursor: url(test.gif), pointer"></div>', 80 '<div style="cursor: url(test.gif), pointer"></div>',
81 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 81 "http://127.0.0.1:1234/test.gif", "image", false, false
82 ], 82 ],
83 [ 83 [
84 "CSS list-style-image", 84 "CSS list-style-image",
85 '<ol>' + 85 '<ol>' +
86 '<li style="list-style-image: url(test.gif)">foo</li>' + 86 '<li style="list-style-image: url(test.gif)">foo</li>' +
87 '</ol>', 87 '</ol>',
88 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 88 "http://127.0.0.1:1234/test.gif", "image", false, false
89 ], 89 ],
90 [ 90 [
91 "CSS generated content", 91 "CSS generated content",
92 '<style>div:before { content: url(test.gif); }</style><div>foo</div>', 92 '<style>div:before { content: url(test.gif); }</style><div>foo</div>',
93 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 93 "http://127.0.0.1:1234/test.gif", "image", false, false
94 ], 94 ],
95 [ 95 [
96 "HTML embed (image)", 96 "HTML embed (image)",
97 '<embed type="image/gif" src="test.gif"></embed>', 97 '<embed type="image/gif" src="test.gif"></embed>',
98 "http://127.0.0.1:1234/test.gif", "object", "127.0.0.1", false 98 "http://127.0.0.1:1234/test.gif", "object", false, false
99 ], 99 ],
100 [ 100 [
101 "HTML object (image)", 101 "HTML object (image)",
102 '<object type="image/gif" data="test.gif"></object>', 102 '<object type="image/gif" data="test.gif"></object>',
103 "http://127.0.0.1:1234/test.gif", "object", "127.0.0.1", false 103 "http://127.0.0.1:1234/test.gif", "object", false, false
104 ], 104 ],
105 [ 105 [
106 "SVG image", 106 "SVG image",
107 '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/19 99/xlink">' + 107 '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/19 99/xlink">' +
108 '<image xlink:href="test.gif"/>' + 108 '<image xlink:href="test.gif"/>' +
109 '</svg>', 109 '</svg>',
110 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 110 "http://127.0.0.1:1234/test.gif", "image", false, false
111 ], 111 ],
112 [ 112 [
113 "SVG filter image", 113 "SVG filter image",
114 '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/19 99/xlink">' + 114 '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/19 99/xlink">' +
115 '<filter>' + 115 '<filter>' +
116 '<feImage xlink:href="test.gif"/>' + 116 '<feImage xlink:href="test.gif"/>' +
117 '</filter>' + 117 '</filter>' +
118 '</svg>', 118 '</svg>',
119 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 119 "http://127.0.0.1:1234/test.gif", "image", false, false
120 ], 120 ],
121 [ 121 [
122 "HTML script", 122 "HTML script",
123 '<script src="test.js"></script>', 123 '<script src="test.js"></script>',
124 "http://127.0.0.1:1234/test.js", "script", "127.0.0.1", false 124 "http://127.0.0.1:1234/test.js", "script", false, false
125 ], 125 ],
126 [ 126 [
127 "SVG script", 127 "SVG script",
128 '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/19 99/xlink">' + 128 '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/19 99/xlink">' +
129 '<script xlink:href="test.js"/>' + 129 '<script xlink:href="test.js"/>' +
130 '</svg>', 130 '</svg>',
131 "http://127.0.0.1:1234/test.js", "script", "127.0.0.1", false 131 "http://127.0.0.1:1234/test.js", "script", false, false
132 ], 132 ],
133 [ 133 [
134 "HTML stylesheet", 134 "HTML stylesheet",
135 '<link rel="stylesheet" type="text/css" href="test.css">', 135 '<link rel="stylesheet" type="text/css" href="test.css">',
136 "http://127.0.0.1:1234/test.css", "stylesheet", "127.0.0.1", false 136 "http://127.0.0.1:1234/test.css", "stylesheet", false, false
137 ], 137 ],
138 [ 138 [
139 "HTML image with redirect", 139 "HTML image with redirect",
140 '<img src="redirect.gif">', 140 '<img src="redirect.gif">',
141 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 141 "http://127.0.0.1:1234/test.gif", "image", false, false
142 ], 142 ],
143 [ 143 [
144 "HTML image with multiple redirects", 144 "HTML image with multiple redirects",
145 '<img src="redirect2.gif">', 145 '<img src="redirect2.gif">',
146 "http://127.0.0.1:1234/test.gif", "image", "127.0.0.1", false 146 "http://127.0.0.1:1234/test.gif", "image", false, false
147 ], 147 ],
148 [ 148 [
149 "CSS fonts", 149 "CSS fonts",
150 '<style type="text/css">@font-face { font-family: Test; src: url("test.otf "); } html { font-family: Test; }</style>', 150 '<style type="text/css">@font-face { font-family: Test; src: url("test.otf "); } html { font-family: Test; }</style>',
151 "http://127.0.0.1:1234/test.otf", "font", "127.0.0.1", false 151 "http://127.0.0.1:1234/test.otf", "font", false, false
152 ], 152 ],
153 [ 153 [
154 "XMLHttpRequest loading", 154 "XMLHttpRequest loading",
155 '<script>var request = new XMLHttpRequest();request.open("GET", "test.xml" , false);request.send(null);</script>', 155 '<script>var request = new XMLHttpRequest();request.open("GET", "test.xml" , false);request.send(null);</script>',
156 "http://127.0.0.1:1234/test.xml", "xmlhttprequest", "127.0.0.1", false 156 "http://127.0.0.1:1234/test.xml", "xmlhttprequest", false, false
157 ], 157 ],
158 [ 158 [
159 "XML document loading", 159 "XML document loading",
160 '<script>var xmlDoc = document.implementation.createDocument(null, "root", null);xmlDoc.async = false;xmlDoc.load("test.xml")</script>', 160 '<script>var xmlDoc = document.implementation.createDocument(null, "root", null);xmlDoc.async = false;xmlDoc.load("test.xml")</script>',
161 "http://127.0.0.1:1234/test.xml", "xmlhttprequest", "127.0.0.1", false 161 "http://127.0.0.1:1234/test.xml", "xmlhttprequest", false, false
162 ], 162 ],
163 [ 163 [
164 "Web worker", 164 "Web worker",
165 '<script>new Worker("test.js");</script>' + 165 '<script>try { var worker = new Worker("test.js"); worker.onerror = functi on() { parent.postMessage("loaded", "*"); }; } catch (e) { parent.postMessage("e rror", "*"); }</script>',
166 '<script>var r = new XMLHttpRequest();r.open("GET", "", false);r.send(nu ll);</script>', 166 "http://127.0.0.1:1234/test.js", "script", false, true
167 "http://127.0.0.1:1234/test.js", "script", "127.0.0.1", false
168 ], 167 ],
169 ]; 168 ];
170 169
171 if (window.navigator.mimeTypes["application/x-shockwave-flash"] && window.navi gator.mimeTypes["application/x-shockwave-flash"].enabledPlugin) 170 if (window.navigator.mimeTypes["application/x-shockwave-flash"] && window.navi gator.mimeTypes["application/x-shockwave-flash"].enabledPlugin)
172 { 171 {
173 tests.push([ 172 tests.push([
174 "HTML embed (Flash)", 173 "HTML embed (Flash)",
175 '<embed type="application/x-shockwave-flash" src="test.swf"></embed>' + 174 '<embed type="application/x-shockwave-flash" src="test.swf"></embed>' +
176 '<script>var r = new XMLHttpRequest();r.open("GET", "", false);r.send(nu ll);</script>', 175 '<script>var r = new XMLHttpRequest();r.open("GET", "", false);r.send(nu ll);</script>',
177 "http://127.0.0.1:1234/test.swf", "object", "127.0.0.1", false 176 "http://127.0.0.1:1234/test.swf", "object", false, false
178 ], 177 ],
179 [ 178 [
180 "HTML object (Flash)", 179 "HTML object (Flash)",
181 '<object type="application/x-shockwave-flash" data="test.swf"></object>' + 180 '<object type="application/x-shockwave-flash" data="test.swf"></object>' +
182 '<script>var r = new XMLHttpRequest();r.open("GET", "", false);r.send(nu ll);</script>', 181 '<script>var r = new XMLHttpRequest();r.open("GET", "", false);r.send(nu ll);</script>',
183 "http://127.0.0.1:1234/test.swf", "object", "127.0.0.1", false 182 "http://127.0.0.1:1234/test.swf", "object", false, false
184 ]); 183 ]);
185 } 184 }
186 185
187 if (window.navigator.mimeTypes["application/x-java-applet"] && window.navigato r.mimeTypes["application/x-java-applet"].enabledPlugin) 186 if (window.navigator.mimeTypes["application/x-java-applet"] && window.navigato r.mimeTypes["application/x-java-applet"].enabledPlugin)
188 { 187 {
189 // Note: this could use some improvement but Gecko will fail badly with more complicated tests (bug 364400) 188 // Note: this could use some improvement but Gecko will fail badly with more complicated tests (bug 364400)
190 // Note: <applet> is not on the list because it shows some weird async behav ior (data is loaded after page load in some strange way) 189 // Note: <applet> is not on the list because it shows some weird async behav ior (data is loaded after page load in some strange way)
191 tests.push([ 190 tests.push([
192 "HTML embed (Java)", 191 "HTML embed (Java)",
193 '<embed type="application/x-java-applet" code="test.class" src="test.class "></embed>', 192 '<embed type="application/x-java-applet" code="test.class" src="test.class "></embed>',
194 "http://127.0.0.1:1234/test.class", "object", "127.0.0.1", false 193 "http://127.0.0.1:1234/test.class", "object", false, false
195 ], 194 ],
196 [ 195 [
197 "HTML object (Java)", 196 "HTML object (Java)",
198 '<object type="application/x-java-applet" data="test.class"></object>', 197 '<object type="application/x-java-applet" data="test.class"></object>',
199 "http://127.0.0.1:1234/test.class", "object", "127.0.0.1", false 198 "http://127.0.0.1:1234/test.class", "object", false, false
200 ]); 199 ]);
201 } 200 }
202 201
203 let policyHits = []; 202 let policyHits = [];
204 function onPolicyHit(wnd, node, item, scanComplete) 203 function onPolicyHit(wnd, node, item, scanComplete)
205 { 204 {
205 if (!item) {
Wladimir Palant 2014/07/09 18:48:32 Nit: opening { belongs onto the next line. Alterna
206 return;
207 }
206 if (item.location == "http://127.0.0.1:1234/test" || 208 if (item.location == "http://127.0.0.1:1234/test" ||
207 item.location == "http://127.0.0.1:1234/redirect.gif" || 209 item.location == "http://127.0.0.1:1234/redirect.gif" ||
208 item.location == "http://127.0.0.1:1234/redirect2.gif") 210 item.location == "http://127.0.0.1:1234/redirect2.gif")
209 { 211 {
210 return; 212 return;
211 } 213 }
212 if (item.filter instanceof WhitelistFilter) 214 if (item.filter instanceof WhitelistFilter)
213 return; 215 return;
214 216
215 if (policyHits.length > 0) 217 if (policyHits.length > 0)
216 { 218 {
217 // Ignore duplicate policy calls (possible due to prefetching) 219 // Ignore duplicate policy calls (possible due to prefetching)
218 let [prevWnd, prevNode, prevItem] = policyHits[policyHits.length - 1]; 220 let [prevWnd, prevNode, prevItem] = policyHits[policyHits.length - 1];
219 if (prevWnd == wnd && prevItem.location == item.location && prevItem.type == item.type && prevItem.docDomain == item.docDomain) 221 if (prevWnd == wnd && prevItem.location == item.location && prevItem.type == item.type && prevItem.docDomain == item.docDomain)
220 policyHits.pop(); 222 policyHits.pop();
221 } 223 }
222 policyHits.push([wnd, node, item]); 224 policyHits.push([wnd, node, item]);
223 } 225 }
224 226
225 function runTest([name, body, expectedURL, expectedType, expectedDomain, expec tedThirdParty], stage) 227 function runTest([name, body, expectedURL, expectedType, expectedThirdParty, w aitForMessage], stage)
226 { 228 {
227 defaultMatcher.clear(); 229 defaultMatcher.clear();
228 230
229 if (stage > 1) 231 if (stage > 1)
230 defaultMatcher.add(Filter.fromText(expectedURL)); 232 defaultMatcher.add(Filter.fromText(expectedURL));
231 if (stage == 3) 233 if (stage == 3)
232 defaultMatcher.add(Filter.fromText("@@||127.0.0.1:1234/test|$document")); 234 defaultMatcher.add(Filter.fromText("@@||127.0.0.1:1234/test|$document"));
233 if (stage == 4) 235 if (stage == 4)
234 defaultMatcher.add(Filter.fromText("@@||127.0.0.1:1234/test|$~document")); 236 defaultMatcher.add(Filter.fromText("@@||127.0.0.1:1234/test|$~document"));
235 237
(...skipping 16 matching lines...) Expand all
252 }); 254 });
253 server.registerPathHandler("/redirect2.gif", function(metadata, response) 255 server.registerPathHandler("/redirect2.gif", function(metadata, response)
254 { 256 {
255 response.setStatusLine("1.1", "302", "Moved Temporarily"); 257 response.setStatusLine("1.1", "302", "Moved Temporarily");
256 response.setHeader("Location", "http://127.0.0.1:1234/redirect.gif"); 258 response.setHeader("Location", "http://127.0.0.1:1234/redirect.gif");
257 }); 259 });
258 server.registerPathHandler(expectedURL.replace(/http:\/\/[^\/]+/, ""), funct ion(metadata, response) 260 server.registerPathHandler(expectedURL.replace(/http:\/\/[^\/]+/, ""), funct ion(metadata, response)
259 { 261 {
260 serverHit = true; 262 serverHit = true;
261 response.setStatusLine("1.1", "404", "Not Found"); 263 response.setStatusLine("1.1", "404", "Not Found");
264 response.setHeader("Content-Type", "text/html");
265
266 // Work around weird Firefox behavior, where work scripts succesfully load with empty 404 pages.
267 var error = "<b>Not found...<b>";
268 response.bodyOutputStream.write(error, error.length);
262 }); 269 });
263 270
264 policyHits = []; 271 policyHits = [];
265 frame.onload = function() 272 var callback = function()
266 { 273 {
267 let expectedStatus = "allowed"; 274 let expectedStatus = "allowed";
268 if (stage == 3) 275 if (stage == 3)
269 equal(policyHits.length, 0, "Number of policy hits"); 276 equal(policyHits.length, 0, "Number of policy hits");
270 else 277 else
271 { 278 {
272 equal(policyHits.length, 1, "Number of policy hits"); 279 equal(policyHits.length, 1, "Number of policy hits");
273 if (policyHits.length == 1) 280 if (policyHits.length == 1)
274 { 281 {
275 let [wnd, node, item] = policyHits[0]; 282 let [wnd, node, item] = policyHits[0];
276 283
277 equal(item.location, expectedURL, "Request URL"); 284 equal(item.location, expectedURL, "Request URL");
278 285
279 expectedStatus = (stage == 1 ? "allowed" : "blocked"); 286 expectedStatus = (stage == 1 ? "allowed" : "blocked");
280 let actualStatus = (item.filter ? "blocked" : "allowed"); 287 let actualStatus = (item.filter ? "blocked" : "allowed");
281 288
282 equal(actualStatus, expectedStatus, "Request blocked"); 289 equal(actualStatus, expectedStatus, "Request blocked");
283 equal(item.typeDescr.toLowerCase(), expectedType, "Request type"); 290 equal(item.typeDescr.toLowerCase(), expectedType, "Request type");
284 equal(item.thirdParty, expectedThirdParty, "Third-party flag"); 291 equal(item.thirdParty, expectedThirdParty, "Third-party flag");
285 equal(item.docDomain, expectedDomain, "Document domain"); 292 equal(item.docDomain, "127.0.0.1", "Document domain");
286 } 293 }
287 } 294 }
288
289 server.registerPathHandler(expectedURL.replace(/http:\/\/[^\/]+/, ""), nul l); 295 server.registerPathHandler(expectedURL.replace(/http:\/\/[^\/]+/, ""), nul l);
290 equal(serverHit, expectedStatus == "allowed", "Request received by server" ); 296 equal(serverHit, expectedStatus == "allowed", "Request received by server" );
291 297
298 if (waitForMessage) {
Wladimir Palant 2014/07/09 18:48:32 Nit: As above, the opening { belongs onto the next
299 window.removeEventListener("message", callback, true)
300 }
Wladimir Palant 2014/07/09 18:48:32 No need to remove conditionally, we can simply alw
301
292 start(); 302 start();
293 }; 303 };
294 frame.contentWindow.location.href = "http://127.0.0.1:1234/test"; 304 frame.contentWindow.location.href = "http://127.0.0.1:1234/test";
305
306 if (waitForMessage) {
Wladimir Palant 2014/07/09 18:48:32 Nit: As above, the opening { belongs onto the next
307 window.addEventListener("message", callback, true, true);
308 } else {
309 frame.onload = callback;
Wladimir Palant 2014/07/09 18:48:32 Feel free to use frame.addEventListener("load", ..
310 }
311
295 } 312 }
296 313
297 let stageDescriptions = { 314 let stageDescriptions = {
298 1: "running without filters", 315 1: "running without filters",
299 2: "running with filter %S", 316 2: "running with filter %S",
300 3: "running with filter %S and site exception", 317 3: "running with filter %S and site exception",
301 4: "running with filter %S and exception not applicable to sites", 318 4: "running with filter %S and exception not applicable to sites",
302 }; 319 };
303 320
304 for (let test = 0; test < tests.length; test++) 321 for (let test = 0; test < tests.length; test++)
305 { 322 {
306 let [name, body, expectedURL, expectedType, expectedDomain, expectedThirdPar ty] = tests[test]; 323 let [name, body, expectedURL, expectedType, expectedDomain, expectedThirdPar ty] = tests[test];
307 for (let stage = 1; stage in stageDescriptions; stage++) 324 for (let stage = 1; stage in stageDescriptions; stage++)
308 { 325 {
309 let stageDescription = stageDescriptions[stage]; 326 let stageDescription = stageDescriptions[stage];
310 if (stageDescription.indexOf("%S") >= 0) 327 if (stageDescription.indexOf("%S") >= 0)
311 stageDescription = stageDescription.replace("%S", expectedURL); 328 stageDescription = stageDescription.replace("%S", expectedURL);
312 329
313 asyncTest(name + " (" + stageDescription + ")", runTest.bind(null, tests[t est], stage)); 330 asyncTest(name + " (" + stageDescription + ")", runTest.bind(null, tests[t est], stage));
314 } 331 }
315 } 332 }
316 })(); 333 })();
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld