| OLD | NEW | 
| (Empty) |  | 
 |    1 /* This Source Code Form is subject to the terms of the Mozilla Public | 
 |    2  * License, v. 2.0. If a copy of the MPL was not distributed with this file, | 
 |    3  * You can obtain one at http://mozilla.org/MPL/2.0/. */ | 
 |    4  | 
 |    5 Cu.import("resource://gre/modules/Services.jsm"); | 
 |    6 Cu.import("resource://gre/modules/FileUtils.jsm"); | 
 |    7  | 
 |    8 let {Prefs} = require("prefs"); | 
 |    9  | 
 |   10 let RULES_VERSION = 2; | 
 |   11  | 
 |   12 let CUSTOM_RULE_PRIORITY = 0x7FFFFFFF; | 
 |   13  | 
 |   14 let rules = {expressions: []}; | 
 |   15  | 
 |   16 loadRules(); | 
 |   17  | 
 |   18 // Make first attempt to update rules after five minutes | 
 |   19 let updateTimer = null; | 
 |   20 updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); | 
 |   21 updateTimer.initWithCallback(onTimer, 1000 * 60 * 5, Ci.nsITimer.TYPE_REPEATING_
     SLACK); | 
 |   22 onShutdown.add(function() updateTimer.cancel()); | 
 |   23  | 
 |   24 function loadRules() | 
 |   25 { | 
 |   26   loadRulesFrom(Services.io.newFileURI(getRuleFile()).spec, false, function(succ
     ess) | 
 |   27   { | 
 |   28     if (!success) | 
 |   29       loadRulesFrom(require("info").addonRoot + "defaults/typoRules.json", true)
     ; | 
 |   30   }); | 
 |   31 } | 
 |   32  | 
 |   33 function loadRulesFrom(url, ignoreVersion, callback) | 
 |   34 { | 
 |   35   if (typeof callback != "function") | 
 |   36     callback = function() {}; | 
 |   37  | 
 |   38   let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.
     nsIXMLHttpRequest); | 
 |   39   request.open("GET", url); | 
 |   40   request.overrideMimeType("text/plain"); | 
 |   41   request.addEventListener("load", function() | 
 |   42   { | 
 |   43     try | 
 |   44     { | 
 |   45       // Remove comments from the file if any | 
 |   46       let data = JSON.parse(request.responseText.replace(/^\s*\/\/.*/mg, "")); | 
 |   47       if (ignoreVersion || data.version == RULES_VERSION) | 
 |   48       { | 
 |   49         rules = data; | 
 |   50         callback(true); | 
 |   51  | 
 |   52         // Add user-defined rules after calling the callback - if the callback | 
 |   53         // saves the rules then the custom rules won't be included. | 
 |   54         addCustomRules(); | 
 |   55       } | 
 |   56       else | 
 |   57         callback(false); | 
 |   58     } | 
 |   59     catch (e) | 
 |   60     { | 
 |   61       Cu.reportError(e); | 
 |   62       callback(false); | 
 |   63     } | 
 |   64   }, false); | 
 |   65   request.addEventListener("error", function() | 
 |   66   { | 
 |   67     callback(false); | 
 |   68   }, false); | 
 |   69  | 
 |   70   try | 
 |   71   { | 
 |   72     request.send(null); | 
 |   73   } | 
 |   74   catch (e) | 
 |   75   { | 
 |   76     if (e.result != Cr.NS_ERROR_FILE_NOT_FOUND) | 
 |   77       Cu.reportError(e); | 
 |   78     callback(false); | 
 |   79   } | 
 |   80 } | 
 |   81  | 
 |   82 function getRuleFile() | 
 |   83 { | 
 |   84   let result = FileUtils.getFile("ProfD", [require("info").addonName + "-rules.j
     son"]); | 
 |   85  | 
 |   86   getRuleFile = function() result; | 
 |   87   return getRuleFile(); | 
 |   88 } | 
 |   89  | 
 |   90 function addCustomRules() | 
 |   91 { | 
 |   92   for (let domain in Prefs.whitelist) | 
 |   93     onWhitelistEntryAdded(domain); | 
 |   94 } | 
 |   95  | 
 |   96 function onWhitelistEntryAdded(domain) | 
 |   97 { | 
 |   98   let reverse = domain.split("").reverse().join(""); | 
 |   99   addSuffix(rules.domain, reverse, CUSTOM_RULE_PRIORITY); | 
 |  100 } | 
 |  101 exports.onWhitelistEntryAdded = onWhitelistEntryAdded; | 
 |  102  | 
 |  103 function onWhitelistEntryRemoved(domain) | 
 |  104 { | 
 |  105   let reverse = domain.split("").reverse().join(""); | 
 |  106   removeSuffix(rules.domain, reverse, CUSTOM_RULE_PRIORITY); | 
 |  107 } | 
 |  108 exports.onWhitelistEntryRemoved = onWhitelistEntryRemoved; | 
 |  109  | 
 |  110 function addSuffix(tree, suffix, priority) | 
 |  111 { | 
 |  112   if (suffix.length == 0) | 
 |  113   { | 
 |  114     // We are at the last character, just put our priority here | 
 |  115     tree[""] = " " + priority; | 
 |  116     return; | 
 |  117   } | 
 |  118  | 
 |  119   let c = suffix[0]; | 
 |  120   if (c in tree) | 
 |  121   { | 
 |  122     let existing = tree[c]; | 
 |  123     if (typeof existing == "string") | 
 |  124     { | 
 |  125       // Single choice for this suffix, maybe the same entry? | 
 |  126       if (existing.substr(0, suffix.length - 1) == suffix.substr(1) && existing[
     suffix.length - 1] == " ") | 
 |  127       { | 
 |  128         // Same entry, simply replace it by new priority | 
 |  129         tree[c] = suffix.substr(1) + " " + priority; | 
 |  130       } | 
 |  131       else | 
 |  132       { | 
 |  133         // Different entry, need to add a new branching point and go deeper | 
 |  134         if (existing[0] == " ") | 
 |  135           tree[c] = {"": existing}; | 
 |  136         else | 
 |  137         { | 
 |  138           tree[c] = {}; | 
 |  139           tree[c][existing[0]] = existing.substr(1); | 
 |  140         } | 
 |  141         addSuffix(tree[c], suffix.substr(1), priority); | 
 |  142       } | 
 |  143     } | 
 |  144     else | 
 |  145     { | 
 |  146       // Multiple choices for this suffix - go deeper | 
 |  147       addSuffix(existing, suffix.substr(1), priority); | 
 |  148     } | 
 |  149   } | 
 |  150   else | 
 |  151   { | 
 |  152     // No existing entry yet, just add ours | 
 |  153     tree[c] = suffix.substr(1) + " " + priority; | 
 |  154   } | 
 |  155 } | 
 |  156  | 
 |  157 function removeSuffix(tree, suffix, priority) | 
 |  158 { | 
 |  159   if (suffix.length == 0) | 
 |  160   { | 
 |  161     // We are at the last character, check whether there is an entry with | 
 |  162     // matching priority | 
 |  163     if ("" in tree && tree[""] == " " + priority) | 
 |  164       delete tree[""]; | 
 |  165     return; | 
 |  166   } | 
 |  167  | 
 |  168   let c = suffix[0]; | 
 |  169   if (!(c in tree)) | 
 |  170     return; | 
 |  171  | 
 |  172   if (typeof tree[c] == "string") | 
 |  173   { | 
 |  174     // Single entry - check whether it is the right one | 
 |  175     if (tree[c] == suffix.substr(1) + " " + priority) | 
 |  176       delete tree[c]; | 
 |  177   } | 
 |  178   else | 
 |  179   { | 
 |  180     // Multiple entries, need to go deeper | 
 |  181     removeSuffix(tree[c], suffix.substr(1), priority); | 
 |  182   } | 
 |  183 } | 
 |  184  | 
 |  185 function onTimer() | 
 |  186 { | 
 |  187   // Next check in 1 hour | 
 |  188   updateTimer.delay = 1000 * 60 * 60; | 
 |  189  | 
 |  190   // Only download rules every three days | 
 |  191   let nextUpdate = Prefs.lastRuleUpdate + 60 * 60 * 24 * 3; | 
 |  192   if (nextUpdate > Date.now() / 1000) | 
 |  193     return; | 
 |  194  | 
 |  195   loadRulesFrom("http://urlfixer.org/download/rules.json?version=" + RULES_VERSI
     ON, false, function(success) | 
 |  196   { | 
 |  197     if (success) | 
 |  198     { | 
 |  199       rules.timestamp = Date.now(); | 
 |  200  | 
 |  201       try | 
 |  202       { | 
 |  203         // Save the rules to file. | 
 |  204         let rulesText = JSON.stringify(rules); | 
 |  205         let fileStream = FileUtils.openSafeFileOutputStream(getRuleFile()); | 
 |  206         let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createIns
     tance(Ci.nsIConverterOutputStream); | 
 |  207         stream.init(fileStream, "UTF-8", 16384, Ci.nsIConverterInputStream.DEFAU
     LT_REPLACEMENT_CHARACTER); | 
 |  208         stream.writeString(rulesText); | 
 |  209         stream.flush(); | 
 |  210         FileUtils.closeSafeFileOutputStream(fileStream); | 
 |  211       } | 
 |  212       catch(e) | 
 |  213       { | 
 |  214         Cu.reportError(e); | 
 |  215       } | 
 |  216     } | 
 |  217   }); | 
 |  218  | 
 |  219   Prefs.lastRuleUpdate = Date.now() / 1000; | 
 |  220 } | 
 |  221  | 
 |  222 exports.getSchemeCorrection = getSchemeCorrection; | 
 |  223 function getSchemeCorrection(scheme) | 
 |  224 { | 
 |  225   return getBestMatch(scheme, rules.scheme, 1, null); | 
 |  226 } | 
 |  227  | 
 |  228 exports.isKnownScheme = isKnownScheme; | 
 |  229 function isKnownScheme(scheme) | 
 |  230 { | 
 |  231   return (getSimilarStrings(scheme, rules.scheme, 0, null).length > 0); | 
 |  232 } | 
 |  233  | 
 |  234 exports.getDomainCorrection = getDomainCorrection; | 
 |  235 function getDomainCorrection(domain) | 
 |  236 { | 
 |  237   // Apply user's custom changes first | 
 |  238   let customRules = Prefs.custom_replace; | 
 |  239   for (let searchString in customRules) | 
 |  240   { | 
 |  241     let replacement = customRules[searchString]; | 
 |  242     searchString = searchString.toLowerCase(); | 
 |  243     if (/^re:+/.test(searchString)) | 
 |  244       domain = domain.replace(new RegExp(RegExp.rightContext, "g"), replacement)
     ; | 
 |  245     else | 
 |  246       domain = domain.replace(searchString, replacement); | 
 |  247   } | 
 |  248  | 
 |  249   // Now apply our rules on the domain name | 
 |  250   for (let i = 0, l = rules.expressions.length; i < l; i++) | 
 |  251     domain = applyExpression(domain, rules.expressions[i]); | 
 |  252  | 
 |  253   // Find similar known domains, test domains without the www. prefix | 
 |  254   if (domain.substr(0, 4) == "www.") | 
 |  255     domain = "www." + getBestMatch(domain.substr(4), rules.domain, 1, "."); | 
 |  256   else | 
 |  257     domain = getBestMatch(domain, rules.domain, 1, "."); | 
 |  258  | 
 |  259   return domain; | 
 |  260 } | 
 |  261  | 
 |  262 exports.getDomainReferral = getDomainReferral; | 
 |  263 function getDomainReferral(domain) | 
 |  264 { | 
 |  265   if ("domainReferrals" in rules && domain in rules.domainReferrals) | 
 |  266     return rules.domainReferrals[domain]; | 
 |  267   else | 
 |  268     return null; | 
 |  269 } | 
 |  270  | 
 |  271 function applyExpression(string, expression) | 
 |  272 { | 
 |  273   if (expression.nomatch && new RegExp(expression.nomatch).test(string)) | 
 |  274     return string; | 
 |  275  | 
 |  276   return string.replace(new RegExp(expression.find, "g"), expression.replace); | 
 |  277 } | 
 |  278  | 
 |  279 function getSimilarStrings(input, dictionary, maxDistance, separator) | 
 |  280 { | 
 |  281   // We use a non-deterministic final automaton to perform a search on all | 
 |  282   // strings in the dictionary simultaneously (see | 
 |  283   // http://blog.notdot.net/2010/07/Damn-Cool-Algorithms-Levenshtein-Automata | 
 |  284   // for the basic algorithm). However, we use Damerau-Levenshtein distance | 
 |  285   // (transposition of two adjacent letters counts as one operation), meaning | 
 |  286   // one additional transition for the automaton. The number of automaton states | 
 |  287   // can theoretically be extremely large, the maxDistance parameter limits the | 
 |  288   // number of states we need to process however. We process the input string | 
 |  289   // backwards to allow matching domain names for a longer host name. | 
 |  290  | 
 |  291   let results = []; | 
 |  292  | 
 |  293   function processState(entry, distance, position, result) | 
 |  294   { | 
 |  295     let isString = (typeof entry == "string"); | 
 |  296  | 
 |  297     // Do we have a result? | 
 |  298     if (position < 0 || input[position] == separator) | 
 |  299     { | 
 |  300       if (!isString && "" in entry) | 
 |  301         results.push([result, input.substr(position + 1), distance, parseInt(ent
     ry[""], 10)]); | 
 |  302       else if (isString && entry[0] == " ") | 
 |  303         results.push([result, input.substr(position + 1), distance, parseInt(ent
     ry, 10)]); | 
 |  304     } | 
 |  305  | 
 |  306     // Maybe there is a match | 
 |  307     if (position >= 0) | 
 |  308     { | 
 |  309       let nextChar = input[position]; | 
 |  310       if (!isString && nextChar in entry) | 
 |  311         processState(entry[nextChar], distance, position - 1, nextChar + result)
     ; | 
 |  312       else if (isString && entry[0] == nextChar) | 
 |  313         processState(entry.substr(1), distance, position - 1, nextChar + result)
     ; | 
 |  314     } | 
 |  315  | 
 |  316     // Mistakes | 
 |  317     if (distance < maxDistance) | 
 |  318     { | 
 |  319       // Deletion and substitution | 
 |  320       if (!isString) | 
 |  321       { | 
 |  322         for (let c in entry) | 
 |  323         { | 
 |  324           if (c != "") | 
 |  325             processState(entry[c], distance + 1, position, c + result); | 
 |  326           if (c != "" && position >= 0) | 
 |  327             processState(entry[c], distance + 1, position - 1, c + result); | 
 |  328         } | 
 |  329       } | 
 |  330       else if (entry[0] != " ") | 
 |  331       { | 
 |  332         processState(entry.substr(1), distance + 1, position, entry[0] + result)
     ; | 
 |  333         if (position >= 0) | 
 |  334           processState(entry.substr(1), distance + 1, position - 1, entry[0] + r
     esult); | 
 |  335       } | 
 |  336  | 
 |  337       // Insertion | 
 |  338       if (position >= 0) | 
 |  339         processState(entry, distance + 1, position - 1, result); | 
 |  340  | 
 |  341       // Transposition | 
 |  342       if (position >= 1) | 
 |  343       { | 
 |  344         let nextChar1 = input[position]; | 
 |  345         let nextChar2 = input[position - 1]; | 
 |  346         if (isString) | 
 |  347         { | 
 |  348           if (entry[0] == nextChar2 && entry[1] == nextChar1) | 
 |  349             processState(entry.substr(2), distance + 1, position - 2, nextChar1 
     + nextChar2 + result); | 
 |  350         } | 
 |  351         else if (nextChar2 in entry) | 
 |  352         { | 
 |  353           let nextEntry = entry[nextChar2]; | 
 |  354           if (typeof nextEntry != "string") | 
 |  355           { | 
 |  356             if (nextChar1 in nextEntry) | 
 |  357               processState(nextEntry[nextChar1], distance + 1, position - 2, nex
     tChar1 + nextChar2 + result); | 
 |  358           } | 
 |  359           else | 
 |  360           { | 
 |  361             if (nextEntry[0] == nextChar1) | 
 |  362               processState(nextEntry.substr(1), distance + 1, position - 2, next
     Char1 + nextChar2 + result); | 
 |  363           } | 
 |  364         } | 
 |  365       } | 
 |  366     } | 
 |  367   } | 
 |  368  | 
 |  369   processState(dictionary, 0, input.length - 1, ""); | 
 |  370   return results; | 
 |  371 } | 
 |  372  | 
 |  373 function getBestMatch(input, dictionary, maxDistance, separator) | 
 |  374 { | 
 |  375   let suggestions = getSimilarStrings(input, dictionary, maxDistance, separator)
     ; | 
 |  376  | 
 |  377   let bestSuggestion = null; | 
 |  378   let bestSuggestionDistance; | 
 |  379   let bestSuggestionMatched; | 
 |  380   let bestSuggestionPriority; | 
 |  381   for (let i = 0; i < suggestions.length; i++) | 
 |  382   { | 
 |  383     let [suggestion, matchedString, distance, priority] = suggestions[i]; | 
 |  384     if (suggestion == input) | 
 |  385       return suggestion; | 
 |  386  | 
 |  387     let matchedLen = matchedString.length; | 
 |  388     if (priority < 0 && matchedLen == input.length) | 
 |  389     { | 
 |  390       // TLDs should never be proposed as a replacement for the entire host name | 
 |  391       continue; | 
 |  392     } | 
 |  393  | 
 |  394     if (!bestSuggestion || | 
 |  395         bestSuggestionMatched < matchedLen || | 
 |  396         (bestSuggestionMatched == matchedLen && bestSuggestionDistance > distanc
     e) || | 
 |  397         (bestSuggestionMatched == matchedLen && bestSuggestionDistance == distan
     ce && bestSuggestionPriority < priority)) | 
 |  398     { | 
 |  399       bestSuggestion = suggestion; | 
 |  400       bestSuggestionDistance = distance; | 
 |  401       bestSuggestionMatched = matchedLen; | 
 |  402       bestSuggestionPriority = priority; | 
 |  403     } | 
 |  404   } | 
 |  405   if (bestSuggestion) | 
 |  406     return input.substr(0, input.length - bestSuggestionMatched) + bestSuggestio
     n; | 
 |  407   else | 
 |  408     return input; | 
 |  409 } | 
| OLD | NEW |