browser_misused_characters_in_strings.js (6795B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /* This list allows pre-existing or 'unfixable' issues to remain, while we 5 * detect newly occurring issues in shipping files. It is a list of objects 6 * specifying conditions under which an error should be ignored. 7 * 8 * As each issue is found in the exceptions list, it is removed from the list. 9 * At the end of the test, there is an assertion that all items have been 10 * removed from the exceptions list, thus ensuring there are no stale 11 * entries. */ 12 let gExceptionsList = [ 13 { 14 file: "layout_errors.properties", 15 key: "ImageMapRectBoundsError", 16 type: "double-quote", 17 }, 18 { 19 file: "layout_errors.properties", 20 key: "ImageMapCircleWrongNumberOfCoords", 21 type: "double-quote", 22 }, 23 { 24 file: "layout_errors.properties", 25 key: "ImageMapCircleNegativeRadius", 26 type: "double-quote", 27 }, 28 { 29 file: "layout_errors.properties", 30 key: "ImageMapPolyWrongNumberOfCoords", 31 type: "double-quote", 32 }, 33 { 34 file: "layout_errors.properties", 35 key: "ImageMapPolyOddNumberOfCoords", 36 type: "double-quote", 37 }, 38 { 39 file: "dom.properties", 40 key: "ImportMapExternalNotSupported", 41 type: "single-quote", 42 }, 43 // dom.properties is packaged twice so we need to have two exceptions for this string. 44 { 45 file: "dom.properties", 46 key: "ImportMapExternalNotSupported", 47 type: "single-quote", 48 }, 49 { 50 file: "dom.properties", 51 key: "MathML_DeprecatedMathVariantWarning", 52 type: "single-quote", 53 }, 54 // dom.properties is packaged twice so we need to have two exceptions for this string. 55 { 56 file: "dom.properties", 57 key: "MathML_DeprecatedMathVariantWarning", 58 type: "single-quote", 59 }, 60 // These error messages contain references to the CSP keywords like 'unsafe-eval', 61 // and those keywords contain actual single-quotes: https://w3c.github.io/webappsec-csp/#grammardef-keyword-source 62 { 63 file: "csp.properties", 64 key: "CSPInlineStyleViolation2", 65 type: "single-quote", 66 }, 67 { 68 file: "csp.properties", 69 key: "CSPROInlineStyleViolation2", 70 type: "single-quote", 71 }, 72 { 73 file: "csp.properties", 74 key: "CSPInlineScriptViolation2", 75 type: "single-quote", 76 }, 77 { 78 file: "csp.properties", 79 key: "CSPROInlineScriptViolation2", 80 type: "single-quote", 81 }, 82 { 83 file: "csp.properties", 84 key: "CSPEventHandlerScriptViolation2", 85 type: "single-quote", 86 }, 87 { 88 file: "csp.properties", 89 key: "CSPROEventHandlerScriptViolation2", 90 type: "single-quote", 91 }, 92 { 93 file: "csp.properties", 94 key: "CSPEvalScriptViolation", 95 type: "single-quote", 96 }, 97 { 98 file: "csp.properties", 99 key: "CSPROEvalScriptViolation", 100 type: "single-quote", 101 }, 102 { 103 file: "csp.properties", 104 key: "CSPWasmEvalScriptViolation", 105 type: "single-quote", 106 }, 107 { 108 file: "csp.properties", 109 key: "CSPROWasmEvalScriptViolation", 110 type: "single-quote", 111 }, 112 ]; 113 114 /** 115 * Check if an error should be ignored due to matching one of the exceptions 116 * defined in gExceptionsList. 117 * 118 * @param filepath The URI spec of the locale file 119 * @param key The key of the entity that is being checked 120 * @param type The type of error that has been found 121 * @return true if the error should be ignored, false otherwise. 122 */ 123 function ignoredError(filepath, key, type) { 124 for (let index in gExceptionsList) { 125 let exceptionItem = gExceptionsList[index]; 126 if ( 127 filepath.endsWith(exceptionItem.file) && 128 key == exceptionItem.key && 129 type == exceptionItem.type 130 ) { 131 gExceptionsList.splice(index, 1); 132 return true; 133 } 134 } 135 return false; 136 } 137 138 function testForError(filepath, key, str, pattern, type, helpText) { 139 if (str.match(pattern) && !ignoredError(filepath, key, type)) { 140 ok(false, `${filepath} with key=${key} has a misused ${type}. ${helpText}`); 141 } 142 } 143 144 function testForErrors(filepath, key, str) { 145 testForError( 146 filepath, 147 key, 148 str, 149 /(\w|^)'\w/, 150 "apostrophe", 151 "Strings with apostrophes should use foo\u2019s instead of foo's." 152 ); 153 testForError( 154 filepath, 155 key, 156 str, 157 /\w\u2018\w/, 158 "incorrect-apostrophe", 159 "Strings with apostrophes should use foo\u2019s instead of foo\u2018s." 160 ); 161 testForError( 162 filepath, 163 key, 164 str, 165 /'.+'/, 166 "single-quote", 167 "Single-quoted strings should use Unicode \u2018foo\u2019 instead of 'foo'." 168 ); 169 testForError( 170 filepath, 171 key, 172 str, 173 /"/, 174 "double-quote", 175 'Double-quoted strings should use Unicode \u201cfoo\u201d instead of "foo".' 176 ); 177 testForError( 178 filepath, 179 key, 180 str, 181 /\.\.\./, 182 "ellipsis", 183 "Strings with an ellipsis should use the Unicode \u2026 character instead of three periods." 184 ); 185 } 186 187 async function getAllTheFiles(extension) { 188 let appDirGreD = Services.dirsvc.get("GreD", Ci.nsIFile); 189 let appDirXCurProcD = Services.dirsvc.get("XCurProcD", Ci.nsIFile); 190 if (appDirGreD.contains(appDirXCurProcD)) { 191 return generateURIsFromDirTree(appDirGreD, [extension]); 192 } 193 if (appDirXCurProcD.contains(appDirGreD)) { 194 return generateURIsFromDirTree(appDirXCurProcD, [extension]); 195 } 196 let urisGreD = await generateURIsFromDirTree(appDirGreD, [extension]); 197 let urisXCurProcD = await generateURIsFromDirTree(appDirXCurProcD, [ 198 extension, 199 ]); 200 return Array.from(new Set(urisGreD.concat(urisXCurProcD))); 201 } 202 203 add_task(async function checkAllTheProperties() { 204 // This asynchronously produces a list of URLs (sadly, mostly sync on our 205 // test infrastructure because it runs against jarfiles there, and 206 // our zipreader APIs are all sync) 207 let uris = await getAllTheFiles(".properties"); 208 ok( 209 uris.length, 210 `Found ${uris.length} .properties files to scan for misused characters` 211 ); 212 213 for (let uri of uris) { 214 let bundle = Services.strings.createBundle(uri.spec); 215 216 for (let entity of bundle.getSimpleEnumeration()) { 217 testForErrors(uri.spec, entity.key, entity.value); 218 } 219 } 220 }); 221 222 add_task(async function checkAllTheFluents() { 223 let uris = await getAllTheFiles(".ftl"); 224 225 let domParser = new DOMParser(); 226 domParser.forceEnableDTD(); 227 228 for (let uri of uris) { 229 let rawContents = await fetchFile(uri.spec); 230 const resource = new FluentResource(rawContents); 231 232 for (const info of resource.textElements()) { 233 const key = info.attr ? `${info.id}.${info.attr}` : info.id; 234 235 const stripped_val = domParser.parseFromString( 236 "<!DOCTYPE html>" + info.text, 237 "text/html" 238 ).documentElement.textContent; 239 240 testForErrors(uri.spec, key, stripped_val); 241 } 242 } 243 }); 244 245 add_task(async function ensureExceptionsListIsEmpty() { 246 is(gExceptionsList.length, 0, "No remaining exceptions exist"); 247 });