Merge branch 'master' into translation-russian

main
Elbert Alias 5 years ago committed by GitHub
commit 2eb711330c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,9 +1,13 @@
# editorconfig.org
root = true root = true
[*] [*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space indent_style = space
insert_final_newline = true indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

@ -1,6 +1,20 @@
module.exports = { module.exports = {
"extends": "airbnb-base", root: true,
"rules": { env: {
"no-param-reassign": 0 browser: true,
} node: true
}; },
parserOptions: {
parser: 'babel-eslint'
},
extends: [
'@nuxtjs',
'prettier',
'prettier/vue',
'plugin:prettier/recommended',
'plugin:nuxt/recommended'
],
plugins: [
'prettier'
],
}

@ -0,0 +1,5 @@
{
"semi": false,
"arrowParens": "always",
"singleQuote": true
}

1781
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

@ -5,11 +5,17 @@
"read-chunk": "2.1.*" "read-chunk": "2.1.*"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.1.2", "@nuxtjs/eslint-config": "^1.0.1",
"eslint": "^4.19.1", "@nuxtjs/eslint-module": "^1.2.0",
"eslint-config-airbnb-base": "^13.0.0", "babel-eslint": "^10.1.0",
"eslint-plugin-import": "^2.13.0", "chai": "^4.2.0",
"mocha": "^5.2.0" "dotenv": "^8.2.0",
"eslint": "^6.1.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-nuxt": "^0.5.2",
"eslint-plugin-prettier": "^3.1.3",
"mocha": "^5.2.0",
"prettier": "^1.16.4"
}, },
"scripts": { "scripts": {
"test": "mocha -R spec src", "test": "mocha -R spec src",

@ -1,19 +1,25 @@
{ {
"title": "Wappalyzer Schema", "title": "Wappalyzer Schema",
"definitions": {
"non-empty-non-blank-sting": {
"type": "string",
"pattern": "^(?!\\s*$).+"
}
},
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["categories","apps"],
"properties": { "properties": {
"$schema": { "$schema": {
"type": "string" "type": "string"
}, },
"categories": { "categories": {
"type": "object", "type": "object",
"required": true, "minProperties": 64,
"additionalProperties": false, "additionalProperties": false,
"patternProperties": { "patternProperties": {
"^[0-9]+$": { "^[0-9]+$": {
"type": "object", "type": "object",
"required": true,
"properties": { "properties": {
"priority": { "priority": {
"type": "number" "type": "number"
@ -27,89 +33,124 @@
}, },
"apps": { "apps": {
"type": "object", "type": "object",
"required": true,
"additionalProperties": { "additionalProperties": {
"additionalProperties": false, "additionalProperties": false,
"required": ["cats","website"],
"properties": { "properties": {
"cats": { "cats": {
"type": "array", "type": "array",
"items": { "items": {
"type": "number" "type": "number"
}, },
"required": true "minItems": 1
}, },
"cpe": { "cpe": {
"type": "string" "$ref": "#/definitions/non-empty-non-blank-sting"
}, },
"cookies": { "cookies": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": false,
"type": "string" "patternProperties": {
"^.+$": {
},
"additionalProperties": {
"type": "string"
}
} }
}, },
"js": { "js": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": false,
"type": "string" "patternProperties": {
"^.+$": {
},
"additionalProperties": {
"type": "string"
}
} }
}, },
"headers": { "headers": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": false,
"type": "string" "patternProperties": {
"^.+$": {
},
"additionalProperties": {
"type": "string"
}
} }
}, },
"html": { "html": {
"type": [ "oneOf": [
"string", {
"array" "type": "array",
], "items": {
"items": { "$ref": "#/definitions/non-empty-non-blank-sting"
"type": "string" }
} },
{
"$ref": "#/definitions/non-empty-non-blank-sting"
}
]
}, },
"excludes": { "excludes": {
"type": [ "oneOf": [
"string", {
"array" "type": "array",
], "items": {
"items": { "$ref": "#/definitions/non-empty-non-blank-sting"
"type": "string" }
} },
{
"$ref": "#/definitions/non-empty-non-blank-sting"
}
]
}, },
"implies": { "implies": {
"type": [ "oneOf": [
"string", {
"array" "type": "array",
], "items": {
"items": { "$ref": "#/definitions/non-empty-non-blank-sting"
"type": "string" }
} },
{
"$ref": "#/definitions/non-empty-non-blank-sting"
}
]
}, },
"meta": { "meta": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": false,
"type": "string" "patternProperties": {
"^.+$": {
},
"additionalProperties": {
"type": "string"
}
} }
}, },
"script": { "script": {
"type": [ "oneOf": [
"string", {
"array" "type": "array",
], "items": {
"items": { "$ref": "#/definitions/non-empty-non-blank-sting"
"type": "string" }
} },
{
"$ref": "#/definitions/non-empty-non-blank-sting"
}
]
}, },
"url": { "url": {
"type": "string" "$ref": "#/definitions/non-empty-non-blank-sting"
}, },
"website": { "website": {
"type": "string", "$ref": "#/definitions/non-empty-non-blank-sting"
"required": true
}, },
"icon": { "icon": {
"type": "string" "$ref": "#/definitions/non-empty-non-blank-sting"
} }
} }
} }

@ -83,8 +83,14 @@
"cats": [ "cats": [
12 12
], ],
"html": "<[^>]+x-data[^<]+", "html": "<[^>]+[^\\w-]x-data[^\\w-][^<]+\\;confidence:75",
"js": {
"Alpine.version": "^(.+)$\\;version:\\1"
},
"icon": "Alpine.js.png", "icon": "Alpine.js.png",
"script": [
"/alpine(?:\\.min)?\\.js"
],
"website": "https://github.com/alpinejs/alpine" "website": "https://github.com/alpinejs/alpine"
}, },
"AOLserver": { "AOLserver": {
@ -154,9 +160,7 @@
5 5
], ],
"icon": "Accelerated-Mobile-Pages.svg", "icon": "Accelerated-Mobile-Pages.svg",
"implies": [ "implies": "WordPress",
"WordPress"
],
"meta": { "meta": {
"generator": "^AMP Plugin v(\\d+\\.\\d+.*)$\\;version:\\1" "generator": "^AMP Plugin v(\\d+\\.\\d+.*)$\\;version:\\1"
}, },
@ -1052,9 +1056,7 @@
"Server": "Artifactory(?:/([\\d.]+))?\\;version:\\1" "Server": "Artifactory(?:/([\\d.]+))?\\;version:\\1"
}, },
"icon": "Artifactory.svg", "icon": "Artifactory.svg",
"implies": [ "implies": "Artifactory",
"Artifactory"
],
"website": "http://jfrog.com/open-source/#os-arti" "website": "http://jfrog.com/open-source/#os-arti"
}, },
"ArvanCloud": { "ArvanCloud": {
@ -1608,7 +1610,6 @@
32 32
], ],
"html": "<meta property=\"cf:app_domain\" content=\"app\\.clickfunnels\\.com\"", "html": "<meta property=\"cf:app_domain\" content=\"app\\.clickfunnels\\.com\"",
"env": "Clickfunnels",
"icon": "ClickFunnels.png", "icon": "ClickFunnels.png",
"website": "https://www.clickfunnels.com" "website": "https://www.clickfunnels.com"
}, },
@ -2088,9 +2089,7 @@
}, },
"script": "clr-angular(?:\\.umd)?(?:\\.min)?\\.js", "script": "clr-angular(?:\\.umd)?(?:\\.min)?\\.js",
"icon": "clarity.svg", "icon": "clarity.svg",
"implies": [ "implies": "Angular",
"Angular"
],
"website": "https://clarity.design/" "website": "https://clarity.design/"
}, },
"ClickHeat": { "ClickHeat": {
@ -3181,9 +3180,7 @@
9 9
], ],
"icon": "EasyEngine.png", "icon": "EasyEngine.png",
"implies": [ "implies": "Docker",
"Docker"
],
"headers": { "headers": {
"x-powered-by": "^EasyEngine (.*)$\\;version:\\1" "x-powered-by": "^EasyEngine (.*)$\\;version:\\1"
}, },
@ -3353,9 +3350,7 @@
12 12
], ],
"icon": "ElementUI.svg", "icon": "ElementUI.svg",
"implies": [ "implies": "Vue.js",
"Vue"
],
"html": [ "html": [
"<(?:div|button) class=\"el-(?:table-column|table-filter|popper|pagination|pager|select-group|form|form-item|color-predefine|color-hue-slider|color-svpanel|color-alpha-slider|color-dropdown|color-picker|badge|tree|tree-node|select|message|dialog|checkbox|checkbox-button|checkbox-group|container|steps|carousel|menu|menu-item|submenu|menu-item-group|button|button-group|card|table|select-dropdown|row|tabs|notification|radio|progress|progress-bar|tag|popover|tooltip|cascader|cascader-menus|cascader-menu|time-spinner|spinner|spinner-inner|transfer|transfer-panel|rate|slider|dropdown|dropdown-menu|textarea|input|input-group|popup-parent|radio-group|main|breadcrumb|time-range-picker|date-range-picker|year-table|date-editor|range-editor|time-spinner|date-picker|time-panel|date-table|month-table|picker-panel|collapse|collapse-item|alert|select-dropdown|select-dropdown__empty|select-dropdown__wrap|select-dropdown__list|scrollbar|switch|carousel|upload|upload-dragger|upload-list|upload-cover|aside|input-number|header|message-box|footer|radio-button|step|autocomplete|autocomplete-suggestion|loading-parent|loading-mask|loading-spinner|)" "<(?:div|button) class=\"el-(?:table-column|table-filter|popper|pagination|pager|select-group|form|form-item|color-predefine|color-hue-slider|color-svpanel|color-alpha-slider|color-dropdown|color-picker|badge|tree|tree-node|select|message|dialog|checkbox|checkbox-button|checkbox-group|container|steps|carousel|menu|menu-item|submenu|menu-item-group|button|button-group|card|table|select-dropdown|row|tabs|notification|radio|progress|progress-bar|tag|popover|tooltip|cascader|cascader-menus|cascader-menu|time-spinner|spinner|spinner-inner|transfer|transfer-panel|rate|slider|dropdown|dropdown-menu|textarea|input|input-group|popup-parent|radio-group|main|breadcrumb|time-range-picker|date-range-picker|year-table|date-editor|range-editor|time-spinner|date-picker|time-panel|date-table|month-table|picker-panel|collapse|collapse-item|alert|select-dropdown|select-dropdown__empty|select-dropdown__wrap|select-dropdown__list|scrollbar|switch|carousel|upload|upload-dragger|upload-list|upload-cover|aside|input-number|header|message-box|footer|radio-button|step|autocomplete|autocomplete-suggestion|loading-parent|loading-mask|loading-spinner|)"
], ],
@ -3369,8 +3364,7 @@
"icon": "elm.svg", "icon": "elm.svg",
"js": { "js": {
"Elm.Main.init": "\\;version:0.19", "Elm.Main.init": "\\;version:0.19",
"Elm.Main.embed": "\\;version:0.18", "Elm.Main.embed": "\\;version:0.18"
"Elm": "\\;confidence:50"
}, },
"website": "https://elm-lang.org/" "website": "https://elm-lang.org/"
}, },
@ -3849,9 +3843,7 @@
"x-fw-static": "", "x-fw-static": "",
"x-fw-type": "" "x-fw-type": ""
}, },
"implies": [ "implies": "WordPress",
"WordPress"
],
"icon": "flywheel.svg", "icon": "flywheel.svg",
"website": "https://getflywheel.com" "website": "https://getflywheel.com"
}, },
@ -4213,9 +4205,7 @@
"X-GitHub-Request-Id": "" "X-GitHub-Request-Id": ""
}, },
"icon": "GitHub.svg", "icon": "GitHub.svg",
"implies": [ "implies": "Ruby on Rails",
"Ruby on Rails"
],
"url": "^https?://[^/]+\\.github\\.io/", "url": "^https?://[^/]+\\.github\\.io/",
"website": "https://pages.github.com/" "website": "https://pages.github.com/"
}, },
@ -4294,9 +4284,7 @@
"Server": "GlassFish(?: Server)?(?: Open Source Edition)?(?: ?/?([\\d.]+))?\\;version:\\1" "Server": "GlassFish(?: Server)?(?: Open Source Edition)?(?: ?/?([\\d.]+))?\\;version:\\1"
}, },
"icon": "GlassFish.png", "icon": "GlassFish.png",
"implies": [ "implies": "Java",
"Java"
],
"website": "http://glassfish.java.net" "website": "http://glassfish.java.net"
}, },
"Glyphicons": { "Glyphicons": {
@ -4666,6 +4654,16 @@
"script": "assets\\.growingio\\.com/([\\d.]+)/gio.js\\;version:\\1", "script": "assets\\.growingio\\.com/([\\d.]+)/gio.js\\;version:\\1",
"website": "https://www.growingio.com/" "website": "https://www.growingio.com/"
}, },
"gRPC": {
"cats": [
18
],
"icon": "gRPC.png",
"headers": {
"Content-Type": "^application\\/grpc.+$"
},
"website": "https://grpc.io"
},
"HERE": { "HERE": {
"cats": [ "cats": [
35 35
@ -4814,6 +4812,19 @@
"script": "hellobar\\.js", "script": "hellobar\\.js",
"website": "http://hellobar.com" "website": "http://hellobar.com"
}, },
"Heroku": {
"cats": [
62
],
"headers": {
"Via": "/[\\d.-]+ vegur$"
},
"implies": [
"Amazon Web Services"
],
"icon": "heroku.svg",
"website": "https://www.heroku.com/"
},
"Hexo": { "Hexo": {
"cats": [ "cats": [
57 57
@ -4861,9 +4872,7 @@
"script": "bokeh.*\\.js", "script": "bokeh.*\\.js",
"website": "https://bokeh.org", "website": "https://bokeh.org",
"icon": "bokeh.png", "icon": "bokeh.png",
"implies": [ "implies": "Python"
"Python"
]
}, },
"Highlight.js": { "Highlight.js": {
"cats": [ "cats": [
@ -5394,6 +5403,14 @@
"icon": "JET Enterprise.svg", "icon": "JET Enterprise.svg",
"website": "http://www.jetecommerce.com.br/" "website": "http://www.jetecommerce.com.br/"
}, },
"Jitsi": {
"cats": [
52
],
"script": "lib-jitsi-meet.*\\.js",
"icon": "Jitsi.png",
"website": "https://jitsi.org"
},
"JS Charts": { "JS Charts": {
"cats": [ "cats": [
25 25
@ -5747,9 +5764,7 @@
"headers": { "headers": {
"Server": "^Kestrel" "Server": "^Kestrel"
}, },
"implies": [ "implies": "Microsoft ASP.NET",
"Microsoft ASP.NET"
],
"icon": "kestrel.svg", "icon": "kestrel.svg",
"website": "https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel" "website": "https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel"
}, },
@ -6245,6 +6260,19 @@
"script": "/js/al/common\\.js\\?[0-9_]+", "script": "/js/al/common\\.js\\?[0-9_]+",
"website": "http://liveinternet.ru/rating/" "website": "http://liveinternet.ru/rating/"
}, },
"Livewire": {
"cats": [
18, 19
],
"html": "<[^>]+wire:[^<]+",
"icon": "Livewire.png",
"implies": "Laravel",
"js": {
"livewire": ""
},
"script": "livewire(?:\\.min)?\\.js",
"website": "https://laravel-livewire.com"
},
"LocalFocus": { "LocalFocus": {
"cats": [ "cats": [
61 61
@ -7535,9 +7563,7 @@
"<!-- <meta name=\"NextGEN\" version=\"([\\d.]+)\" /> -->\\;version:\\1" "<!-- <meta name=\"NextGEN\" version=\"([\\d.]+)\" /> -->\\;version:\\1"
], ],
"icon": "NextGEN Gallery.png", "icon": "NextGEN Gallery.png",
"implies": [ "implies": "WordPress",
"WordPress"
],
"script": "/nextgen-gallery/js/", "script": "/nextgen-gallery/js/",
"website": "https://www.imagely.com/wordpress-gallery-plugin" "website": "https://www.imagely.com/wordpress-gallery-plugin"
}, },
@ -7588,6 +7614,15 @@
"icon": "vercel.svg", "icon": "vercel.svg",
"website": "https://vercel.com" "website": "https://vercel.com"
}, },
"Nuvemshop": {
"cats": [
6
],
"script": "(N|n)uvem",
"html": "<a target=\"_blank\" title=\"Nuvemshop\"",
"icon": "nuvem.png",
"website": "https://www.nuvemshop.com.br/"
},
"OWL Carousel": { "OWL Carousel": {
"cats": [ "cats": [
5 5
@ -8304,9 +8339,7 @@
}, },
"html": "<[^>]+(?:class|id)=\"phabricator-", "html": "<[^>]+(?:class|id)=\"phabricator-",
"icon": "Phabricator.png", "icon": "Phabricator.png",
"implies": [ "implies": "PHP",
"PHP"
],
"script": "/phabricator/[a-f0-9]{8}/rsrc/js/phui/[a-z-]+\\.js$", "script": "/phabricator/[a-f0-9]{8}/rsrc/js/phui/[a-z-]+\\.js$",
"website": "http://phacility.com" "website": "http://phacility.com"
}, },
@ -8329,9 +8362,7 @@
"<[^>]+id=\"phenomic(?:root)?\"" "<[^>]+id=\"phenomic(?:root)?\""
], ],
"icon": "Phenomic.svg", "icon": "Phenomic.svg",
"implies": [ "implies": "React",
"React"
],
"script": "/phenomic\\.browser\\.[a-f0-9]+\\.js", "script": "/phenomic\\.browser\\.[a-f0-9]+\\.js",
"website": "https://phenomic.io/" "website": "https://phenomic.io/"
}, },
@ -8539,6 +8570,14 @@
"icon": "PostgreSQL.png", "icon": "PostgreSQL.png",
"website": "http://www.postgresql.org/" "website": "http://www.postgresql.org/"
}, },
"Powerboutique": {
"cats": [
6
],
"script": "powerboutique",
"icon": "powerboutique.jpg",
"website": "https://www.powerboutique.com/"
},
"Powergap": { "Powergap": {
"cats": [ "cats": [
6 6
@ -9418,9 +9457,7 @@
9 9
], ],
"icon": "SlickStack.png", "icon": "SlickStack.png",
"implies": [ "implies": "WordPress",
"WordPress"
],
"headers": { "headers": {
"x-powered-by": "SlickStack" "x-powered-by": "SlickStack"
}, },
@ -9473,6 +9510,14 @@
}, },
"website": "http://www.spip.net" "website": "http://www.spip.net"
}, },
"Splitbee": {
"cats": [
10
],
"icon": "splitbee.svg",
"script": "^https:\\/\\/cdn\\.splitbee\\.io\\/sb\\.js",
"website": "https://splitbee.io"
},
"SQL Buddy": { "SQL Buddy": {
"cats": [ "cats": [
3 3
@ -9699,9 +9744,7 @@
"headers": { "headers": {
"x-powered-by": "^Seravo" "x-powered-by": "^Seravo"
}, },
"implies": [ "implies": "WordPress",
"WordPress"
],
"icon": "seravo.svg", "icon": "seravo.svg",
"website": "https://seravo.com" "website": "https://seravo.com"
}, },
@ -10274,6 +10317,16 @@
"script": "^/js/bundles/sonar\\.js?v=([\\d.]+)$\\;version:\\1", "script": "^/js/bundles/sonar\\.js?v=([\\d.]+)$\\;version:\\1",
"website": "https://www.sonarqube.org/" "website": "https://www.sonarqube.org/"
}, },
"Sotel": {
"cats": [
1
],
"icon": "Sotel.png",
"meta": {
"generator": "sotel"
},
"website": "https://www.soteledu.com/en/"
},
"SoundManager": { "SoundManager": {
"cats": [ "cats": [
59 59
@ -10747,7 +10800,10 @@
"cats": [ "cats": [
66 66
], ],
"html": "<link[^>]+?href=\"[^\"]+tailwindcss(?:\\.min)?\\.css", "html": [
"<link[^>]+?href=\"[^\"]+tailwindcss(?:\\.min)?\\.css",
"[^>]*class=\"[^\"]*(?:sm:|md:|lg:|xl:)"
],
"icon": "tailwindcss.svg", "icon": "tailwindcss.svg",
"website": "https://tailwindcss.com/" "website": "https://tailwindcss.com/"
}, },
@ -10864,9 +10920,7 @@
"cookies": { "cookies": {
"TNEW": "" "TNEW": ""
}, },
"implies": [ "implies": "Tessitura",
"Tessitura"
],
"icon": "tessitura.svg", "icon": "tessitura.svg",
"website": "https://www.tessituranetwork.com" "website": "https://www.tessituranetwork.com"
}, },
@ -11661,7 +11715,7 @@
"vue[.-]([\\d.]*\\d)[^/]*\\.js\\;version:\\1", "vue[.-]([\\d.]*\\d)[^/]*\\.js\\;version:\\1",
"(?:/([\\d.]+))?/vue(?:\\.min)?\\.js\\;version:\\1" "(?:/([\\d.]+))?/vue(?:\\.min)?\\.js\\;version:\\1"
], ],
"website": "http://vuejs.org" "website": "https://vuejs.org"
}, },
"Nuxt.js": { "Nuxt.js": {
"cats": [ "cats": [
@ -12098,9 +12152,7 @@
"X-Wix-Server-Artifact-Id": "" "X-Wix-Server-Artifact-Id": ""
}, },
"icon": "Wix.png", "icon": "Wix.png",
"implies": [ "implies": "React",
"React"
],
"js": { "js": {
"wixBiSession": "" "wixBiSession": ""
}, },
@ -12202,9 +12254,7 @@
"headers": { "headers": {
"x-powered-by": "^WordPress\\.com VIP" "x-powered-by": "^WordPress\\.com VIP"
}, },
"implies": [ "implies": "WordPress",
"WordPress"
],
"icon": "wpvip.svg", "icon": "wpvip.svg",
"website": "https://wpvip.com" "website": "https://wpvip.com"
}, },
@ -12373,9 +12423,7 @@
"X-Powered-By": "XeoraCube" "X-Powered-By": "XeoraCube"
}, },
"html": "<input type=\"hidden\" name=\"_sys_bind_\\d+\" id=\"_sys_bind_\\d+\" />", "html": "<input type=\"hidden\" name=\"_sys_bind_\\d+\" id=\"_sys_bind_\\d+\" />",
"implies": [ "implies": "Microsoft ASP.NET",
"Microsoft ASP.NET"
],
"icon": "xeora.png", "icon": "xeora.png",
"script": "/_bi_sps_v.+\\.js", "script": "/_bi_sps_v.+\\.js",
"website": "http://www.xeora.org" "website": "http://www.xeora.org"
@ -12526,9 +12574,7 @@
"Server": "Yaws(?: ([\\d.]+))?\\;version:\\1" "Server": "Yaws(?: ([\\d.]+))?\\;version:\\1"
}, },
"icon": "Yaws.png", "icon": "Yaws.png",
"implies": [ "implies": "Erlang",
"Erlang"
],
"website": "http://yaws.hyber.org" "website": "http://yaws.hyber.org"
}, },
"Yieldlab": { "Yieldlab": {
@ -12552,9 +12598,7 @@
"<!\\[CDATA\\[YII-BLOCK-(?:HEAD|BODY-BEGIN|BODY-END)\\]" "<!\\[CDATA\\[YII-BLOCK-(?:HEAD|BODY-BEGIN|BODY-END)\\]"
], ],
"icon": "Yii.png", "icon": "Yii.png",
"implies": [ "implies": "PHP",
"PHP"
],
"script": [ "script": [
"/assets/[a-zA-Z0-9]{8}\\/yii\\.js$", "/assets/[a-zA-Z0-9]{8}\\/yii\\.js$",
"/yii\\.(?:validation|activeForm)\\.js" "/yii\\.(?:validation|activeForm)\\.js"
@ -13037,9 +13081,7 @@
1 1
], ],
"icon": "govCMS.svg", "icon": "govCMS.svg",
"implies": [ "implies": "Drupal",
"Drupal"
],
"meta": { "meta": {
"generator": "Drupal ([\\d]+) \\(http:\\/\\/drupal\\.org\\) \\+ govCMS\\;version:\\1" "generator": "Drupal ([\\d]+) \\(http:\\/\\/drupal\\.org\\) \\+ govCMS\\;version:\\1"
}, },
@ -13237,9 +13279,7 @@
"headers": { "headers": {
"x-kinsta-cache": "" "x-kinsta-cache": ""
}, },
"implies": [ "implies": "WordPress",
"WordPress"
],
"icon": "kinsta.svg", "icon": "kinsta.svg",
"website": "https://kinsta.com" "website": "https://kinsta.com"
}, },
@ -13468,6 +13508,9 @@
"cats": [ "cats": [
13 13
], ],
"headers": {
"X-Powered-By": "OTRS ([\\d.]+)\\;version:\\1"
},
"html": "<!--\\s+OTRS: Copyright", "html": "<!--\\s+OTRS: Copyright",
"icon": "otrs.png", "icon": "otrs.png",
"implies": "Perl", "implies": "Perl",
@ -13710,9 +13753,7 @@
"icon": "Siteglide.svg", "icon": "Siteglide.svg",
"script": "siteglide\\.js", "script": "siteglide\\.js",
"website": "https://www.siteglide.com", "website": "https://www.siteglide.com",
"implies": [ "implies": "PlatformOS"
"PlatformOS"
]
}, },
"sNews": { "sNews": {
"cats": [ "cats": [
@ -13768,9 +13809,7 @@
"<[^>]+sc-component-id: sc-" "<[^>]+sc-component-id: sc-"
], ],
"icon": "styled-components.png", "icon": "styled-components.png",
"implies": [ "implies": "React",
"React"
],
"js": { "js": {
"styled": "" "styled": ""
}, },

@ -1,218 +1,245 @@
const { const { AWS_LAMBDA_FUNCTION_NAME, CHROME_BIN } = process.env
AWS_LAMBDA_FUNCTION_NAME,
CHROME_BIN,
} = process.env;
let chromium; let chromium
let puppeteer; let puppeteer
if (AWS_LAMBDA_FUNCTION_NAME) { if (AWS_LAMBDA_FUNCTION_NAME) {
// eslint-disable-next-line global-require, import/no-unresolved // eslint-disable-next-line global-require, import/no-unresolved
chromium = require('chrome-aws-lambda'); chromium = require('chrome-aws-lambda')
;({ puppeteer } = chromium)
({ puppeteer } = chromium);
} else { } else {
// eslint-disable-next-line global-require // eslint-disable-next-line global-require
puppeteer = require('puppeteer'); puppeteer = require('puppeteer')
} }
const Browser = require('../browser'); const Browser = require('../browser')
function getJs() { function getJs() {
const dereference = (obj, level = 0) => { const dereference = (obj, level = 0) => {
try { try {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
if (level > 5 || (level && obj === window)) { if (level > 5 || (level && obj === window)) {
return '[Removed]'; return '[Removed]'
} }
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
obj = obj.map(item => dereference(item, level + 1)); obj = obj.map((item) => dereference(item, level + 1))
} }
if (typeof obj === 'function' || (typeof obj === 'object' && obj !== null)) { if (
const newObj = {}; typeof obj === 'function' ||
(typeof obj === 'object' && obj !== null)
) {
const newObj = {}
Object.keys(obj).forEach((key) => { Object.keys(obj).forEach((key) => {
newObj[key] = dereference(obj[key], level + 1); newObj[key] = dereference(obj[key], level + 1)
}); })
return newObj; return newObj
} }
return obj; return obj
} catch (error) { } catch (error) {
return undefined; return undefined
} }
}; }
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
return dereference(window); return dereference(window)
} }
class PuppeteerBrowser extends Browser { class PuppeteerBrowser extends Browser {
constructor(options) { constructor(options) {
options.maxWait = options.maxWait || 60; options.maxWait = options.maxWait || 60
super(options); super(options)
} }
async visit(url) { async visit(url) {
let done = false; let done = false
let browser; let browser
try { try {
await new Promise(async (resolve, reject) => { await new Promise(async (resolve, reject) => {
try { try {
browser = await puppeteer.launch(chromium ? { browser = await puppeteer.launch(
args: [...chromium.args, '--ignore-certificate-errors'], chromium
defaultViewport: chromium.defaultViewport, ? {
executablePath: await chromium.executablePath, args: [...chromium.args, '--ignore-certificate-errors'],
headless: chromium.headless, defaultViewport: chromium.defaultViewport,
} : { executablePath: await chromium.executablePath,
args: ['--no-sandbox', '--headless', '--disable-gpu', '--ignore-certificate-errors'], headless: chromium.headless
executablePath: CHROME_BIN, }
}); : {
args: [
'--no-sandbox',
'--headless',
'--disable-gpu',
'--ignore-certificate-errors'
],
executablePath: CHROME_BIN
}
)
browser.on('disconnected', () => { browser.on('disconnected', () => {
if (!done) { if (!done) {
reject(new Error('browser: disconnected')); reject(new Error('browser: disconnected'))
} }
}); })
const page = await browser.newPage(); const page = await browser.newPage()
page.setDefaultTimeout(this.options.maxWait * 1.1); page.setDefaultTimeout(this.options.maxWait * 1.1)
await page.setRequestInterception(true); await page.setRequestInterception(true)
page.on('error', error => reject(new Error(`page error: ${error.message || error}`))); page.on('error', (error) =>
reject(new Error(`page error: ${error.message || error}`))
)
let responseReceived = false; let responseReceived = false
page.on('request', (request) => { page.on('request', (request) => {
try { try {
if ( if (
responseReceived responseReceived &&
&& request.isNavigationRequest() request.isNavigationRequest() &&
&& request.frame() === page.mainFrame() request.frame() === page.mainFrame() &&
&& request.url() !== url request.url() !== url
) { ) {
this.log(`abort navigation to ${request.url()}`); this.log(`abort navigation to ${request.url()}`)
request.abort('aborted'); request.abort('aborted')
} else if (!done) { } else if (!done) {
if (!['document', 'script'].includes(request.resourceType())) { if (!['document', 'script'].includes(request.resourceType())) {
request.abort(); request.abort()
} else { } else {
request.continue(); request.continue()
} }
} }
} catch (error) { } catch (error) {
reject(new Error(`page error: ${error.message || error}`)); reject(new Error(`page error: ${error.message || error}`))
} }
}); })
page.on('response', (response) => { page.on('response', (response) => {
try { try {
if (!this.statusCode) { if (!this.statusCode) {
this.statusCode = response.status(); this.statusCode = response.status()
this.headers = {}; this.headers = {}
const headers = response.headers(); const headers = response.headers()
Object.keys(headers).forEach((key) => { Object.keys(headers).forEach((key) => {
this.headers[key] = Array.isArray(headers[key]) ? headers[key] : [headers[key]]; this.headers[key] = Array.isArray(headers[key])
}); ? headers[key]
: [headers[key]]
})
this.contentType = headers['content-type'] || null; this.contentType = headers['content-type'] || null
} }
if (response.status() < 300 || response.status() > 399) { if (response.status() < 300 || response.status() > 399) {
responseReceived = true; responseReceived = true
} }
} catch (error) { } catch (error) {
reject(new Error(`page error: ${error.message || error}`)); reject(new Error(`page error: ${error.message || error}`))
} }
}); })
page.on('console', ({ _type, _text, _location }) => { page.on('console', ({ _type, _text, _location }) => {
if (!/Failed to load resource: net::ERR_FAILED/.test(_text)) { if (!/Failed to load resource: net::ERR_FAILED/.test(_text)) {
this.log(`${_text} (${_location.url}: ${_location.lineNumber})`, _type); this.log(
`${_text} (${_location.url}: ${_location.lineNumber})`,
_type
)
} }
}); })
if (this.options.userAgent) { if (this.options.userAgent) {
await page.setUserAgent(this.options.userAgent); await page.setUserAgent(this.options.userAgent)
} }
try { try {
await Promise.race([ await Promise.race([
page.goto(url, { waitUntil: 'domcontentloaded' }), page.goto(url, { waitUntil: 'domcontentloaded' }),
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
new Promise((resolve, reject) => setTimeout(() => reject(new Error('timeout')), this.options.maxWait)), new Promise((resolve, reject) =>
]); setTimeout(
() => reject(new Error('timeout')),
this.options.maxWait
)
)
])
} catch (error) { } catch (error) {
throw new Error(error.message || error.toString()); throw new Error(error.message || error.toString())
} }
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const links = await page.evaluateHandle(() => Array.from(document.getElementsByTagName('a')).map(({ const links = await page.evaluateHandle(() =>
hash, hostname, href, pathname, protocol, rel, Array.from(document.getElementsByTagName('a')).map(
}) => ({ ({ hash, hostname, href, pathname, protocol, rel }) => ({
hash, hash,
hostname, hostname,
href, href,
pathname, pathname,
protocol, protocol,
rel, rel
}))); })
)
this.links = await links.jsonValue(); )
this.links = await links.jsonValue()
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const scripts = await page.evaluateHandle(() => Array.from(document.getElementsByTagName('script')).map(({ const scripts = await page.evaluateHandle(() =>
src, Array.from(document.getElementsByTagName('script')).map(
}) => src)); ({ src }) => src
)
)
this.scripts = (await scripts.jsonValue()).filter(script => script); this.scripts = (await scripts.jsonValue()).filter((script) => script)
this.js = await page.evaluate(getJs); this.js = await page.evaluate(getJs)
this.cookies = (await page.cookies()).map(({ this.cookies = (await page.cookies()).map(
name, value, domain, path, ({ name, value, domain, path }) => ({
}) => ({ name,
name, value, domain, path, value,
})); domain,
path
})
)
this.html = await page.content(); this.html = await page.content()
resolve(); resolve()
} catch (error) { } catch (error) {
reject(new Error(`visit error: ${error.message || error}`)); reject(new Error(`visit error: ${error.message || error}`))
} }
}); })
} catch (error) { } catch (error) {
this.log(`visit error: ${error.message || error} (${url})`, 'error'); this.log(`visit error: ${error.message || error} (${url})`, 'error')
throw new Error(error.message || error.toString()); throw new Error(error.message || error.toString())
} finally { } finally {
done = true; done = true
if (browser) { if (browser) {
try { try {
await browser.close(); await browser.close()
this.log('browser close ok'); this.log('browser close ok')
} catch (error) { } catch (error) {
this.log(`browser close error: ${error.message || error}`, 'error'); this.log(`browser close error: ${error.message || error}`, 'error')
} }
} }
} }
this.log(`visit ok (${url})`); this.log(`visit ok (${url})`)
} }
} }
module.exports = PuppeteerBrowser; module.exports = PuppeteerBrowser

@ -1,6 +1,6 @@
{ {
"name": "wappalyzer", "name": "wappalyzer",
"version": "5.9.33", "version": "5.9.34",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

@ -0,0 +1,3 @@
/apps.json
/wappalyzer.js
/node_modules

@ -0,0 +1,34 @@
FROM node:12-alpine
MAINTAINER Wappalyzer <info@wappalyzer.com>
ENV WAPPALYZER_ROOT /opt/wappalyzer
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
ENV CHROME_BIN /usr/bin/chromium-browser
RUN apk update && apk add --no-cache \
nodejs \
nodejs-npm \
udev \
chromium \
ttf-freefont
RUN mkdir -p "$WAPPALYZER_ROOT/browsers"
WORKDIR "$WAPPALYZER_ROOT"
ADD apps.json .
ADD browser.js .
ADD browsers/zombie.js ./browsers
ADD browsers/puppeteer.js ./browsers
ADD cli.js .
ADD driver.js .
ADD index.js .
ADD package.json .
ADD wappalyzer.js .
RUN npm i && npm i puppeteer
RUN /usr/bin/chromium-browser --version
ENTRYPOINT ["node", "cli.js"]

@ -0,0 +1,94 @@
# Wappalyzer
[Wappalyzer](https://www.wappalyzer.com/) is a
[cross-platform](https://www.wappalyzer.com/nodejs) utility that uncovers the
technologies used on websites. It detects
[content management systems](https://www.wappalyzer.com/technologies/cms), [ecommerce platforms](https://www.wappalyzer.com/technologies/ecommerce), [web servers](https://www.wappalyzer.com/technologies/web-servers), [JavaScript frameworks](https://www.wappalyzer.com/technologies/javascript-frameworks),
[analytics tools](https://www.wappalyzer.com/technologies/analytics) and
[many more](https://www.wappalyzer.com/technologies).
## Installation
```shell
$ npm i -g wappalyzer # Globally
$ npm i wappalyzer --save # As a dependency
```
To use Puppeteer (headless Chrome browser), you must install the NPM package manually:
```shell
$ npm i puppeteer@^2.0.0
```
## Run from the command line
```
wappalyzer <url> [options]
```
### Options
```
-b, --batch-size=... Process links in batches
-d, --debug Output debug messages
-t, --delay=ms Wait for ms milliseconds between requests
-h, --help This text
--html-max-cols=... Limit the number of HTML characters per line processed
--html-max-rows=... Limit the number of HTML lines processed
-D, --max-depth=... Don't analyse pages more than num levels deep
-m, --max-urls=... Exit when num URLs have been analysed
-w, --max-wait=... Wait no more than ms milliseconds for page resources to load
-P, --pretty Pretty-print JSON output
-r, --recursive Follow links on pages (crawler)
-a, --user-agent=... Set the user agent string
```
## Run from a script
```javascript
const Wappalyzer = require('wappalyzer');
const url = 'https://www.wappalyzer.com';
const options = {
debug: false,
delay: 500,
maxDepth: 3,
maxUrls: 10,
maxWait: 5000,
recursive: true,
userAgent: 'Wappalyzer',
htmlMaxCols: 2000,
htmlMaxRows: 2000,
};
;(async function() {
const wappalyzer = await new Wappalyzer(options)
try {
await wappalyzer.init()
const site = await wappalyzer.open(url)
site.on('error', (error) => {
process.stderr.write(`error: ${error}\n`)
})
const results = await site.analyze()
process.stdout.write(`${JSON.stringify(results, null, 2)}\n`)
await wappalyzer.destroy()
process.exit(0)
} catch (error) {
process.stderr.write(error.toString())
await wappalyzer.destroy()
process.exit(1)
}
})()

@ -0,0 +1,20 @@
class Browser {
constructor(options) {
this.options = options;
this.window = null;
this.document = null;
this.statusCode = null;
this.contentType = null;
this.headers = null;
this.statusCode = null;
this.contentType = null;
this.html = null;
this.js = null;
this.links = null;
this.scripts = null;
this.cookies = null;
}
}
module.exports = Browser;

@ -0,0 +1,107 @@
#!/usr/bin/env node
const Wappalyzer = require('./driver')
const args = process.argv.slice(2)
const options = {}
let url
let arg
const aliases = {
a: 'userAgent',
b: 'batchSize',
d: 'debug',
t: 'delay',
h: 'help',
D: 'maxDepth',
m: 'maxUrls',
P: 'pretty',
r: 'recursive',
w: 'maxWait'
}
while (true) {
// eslint-disable-line no-constant-condition
arg = args.shift()
if (!arg) {
break
}
const matches = /^-?-([^=]+)(?:=(.+)?)?/.exec(arg)
if (matches) {
const key =
aliases[matches[1]] ||
matches[1].replace(/-\w/g, (_matches) => _matches[1].toUpperCase())
// eslint-disable-next-line no-nested-ternary
const value = matches[2]
? matches[2]
: args[0] && !args[0].startsWith('-')
? args.shift()
: true
options[key] = value
} else {
url = arg
}
}
if (!url || options.help) {
process.stdout.write(`Usage:
wappalyzer <url> [options]
Examples:
wappalyzer https://www.example.com
node cli.js https://www.example.com -r -D 3 -m 50
docker wappalyzer/cli https://www.example.com --pretty
Options:
-b, --batch-size=... Process links in batches
-d, --debug Output debug messages
-t, --delay=ms Wait for ms milliseconds between requests
-h, --help This text
--html-max-cols=... Limit the number of HTML characters per line processed
--html-max-rows=... Limit the number of HTML lines processed
-D, --max-depth=... Don't analyse pages more than num levels deep
-m, --max-urls=... Exit when num URLs have been analysed
-w, --max-wait=... Wait no more than ms milliseconds for page resources to load
-P, --pretty Pretty-print JSON output
-r, --recursive Follow links on pages (crawler)
-a, --user-agent=... Set the user agent string
`)
process.exit(1)
}
;(async function() {
const wappalyzer = await new Wappalyzer(options)
try {
await wappalyzer.init()
const site = await wappalyzer.open(url)
site.on('error', (error) => {
process.stderr.write(`page error: ${error}\n`)
})
const results = await site.analyze()
process.stdout.write(
`${JSON.stringify(results, null, options.pretty ? 2 : null)}\n`
)
await wappalyzer.destroy()
process.exit(0)
} catch (error) {
process.stderr.write(error.toString())
await wappalyzer.destroy()
process.exit(1)
}
})()

@ -0,0 +1,540 @@
const { URL } = require('url')
const fs = require('fs')
const LanguageDetect = require('languagedetect')
const Wappalyzer = require('./wappalyzer')
const { AWS_LAMBDA_FUNCTION_NAME } = process.env
let puppeteer
if (AWS_LAMBDA_FUNCTION_NAME) {
// eslint-disable-next-line global-require, import/no-unresolved
;({
chromium: { puppeteer }
} = require('chrome-aws-lambda'))
} else {
// eslint-disable-next-line global-require
puppeteer = require('puppeteer')
}
const languageDetect = new LanguageDetect()
languageDetect.setLanguageType('iso2')
const json = JSON.parse(fs.readFileSync('./apps.json'))
const extensions = /^([^.]+$|\.(asp|aspx|cgi|htm|html|jsp|php)$)/
const errorTypes = {
RESPONSE_NOT_OK: 'Response was not ok',
NO_RESPONSE: 'No response from server',
NO_HTML_DOCUMENT: 'No HTML document'
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function getJs() {
const dereference = (obj, level = 0) => {
try {
// eslint-disable-next-line no-undef
if (level > 5 || (level && obj === window)) {
return '[Removed]'
}
if (Array.isArray(obj)) {
obj = obj.map((item) => dereference(item, level + 1))
}
if (
typeof obj === 'function' ||
(typeof obj === 'object' && obj !== null)
) {
const newObj = {}
Object.keys(obj).forEach((key) => {
newObj[key] = dereference(obj[key], level + 1)
})
return newObj
}
return obj
} catch (error) {
return undefined
}
}
// eslint-disable-next-line no-undef
return dereference(window)
}
function processJs(window, patterns) {
const js = {}
Object.keys(patterns).forEach((appName) => {
js[appName] = {}
Object.keys(patterns[appName]).forEach((chain) => {
js[appName][chain] = {}
patterns[appName][chain].forEach((pattern, index) => {
const properties = chain.split('.')
let value = properties.reduce(
(parent, property) =>
parent && parent[property] ? parent[property] : null,
window
)
value =
typeof value === 'string' || typeof value === 'number'
? value
: !!value
if (value) {
js[appName][chain][index] = value
}
})
})
})
return js
}
function processHtml(html, maxCols, maxRows) {
if (maxCols || maxRows) {
const batchs = []
const rows = html.length / maxCols
for (let i = 0; i < rows; i += 1) {
if (i < maxRows / 2 || i > rows - maxRows / 2) {
batchs.push(html.slice(i * maxCols, (i + 1) * maxCols))
}
}
html = batchs.join('\n')
}
return html
}
class Driver {
constructor(options = {}) {
this.options = {
batchSize: 5,
debug: false,
delay: 500,
htmlMaxCols: 2000,
htmlMaxRows: 3000,
maxDepth: 3,
maxUrls: 10,
maxWait: 5000,
recursive: false,
...options
}
this.options.debug = Boolean(+this.options.debug)
this.options.recursive = Boolean(+this.options.recursive)
this.options.delay = this.options.recursive
? parseInt(this.options.delay, 10)
: 0
this.options.maxDepth = parseInt(this.options.maxDepth, 10)
this.options.maxUrls = parseInt(this.options.maxUrls, 10)
this.options.maxWait = parseInt(this.options.maxWait, 10)
this.options.htmlMaxCols = parseInt(this.options.htmlMaxCols, 10)
this.options.htmlMaxRows = parseInt(this.options.htmlMaxRows, 10)
this.destroyed = false
}
async init() {
this.log('Launching browser...')
try {
this.browser = await puppeteer.launch({
args: [
'--no-sandbox',
'--headless',
'--disable-gpu',
'--ignore-certificate-errors'
]
})
this.browser.on('disconnected', async () => {
this.log('Browser disconnected')
if (!this.destroyed) {
await this.init()
}
})
} catch (error) {
throw new Error(error.toString())
}
}
async destroy() {
this.destroyed = true
if (this.browser) {
try {
await sleep(1)
await this.browser.close()
this.log('Done')
} catch (error) {
throw new Error(error.toString())
}
}
}
open(url) {
return new Site(url, this)
}
log(message, source = 'driver', type = 'debug') {
if (this.options.debug) {
// eslint-disable-next-line no-console
console.log(`${type.toUpperCase()} | ${source} | ${message}`)
}
}
}
class Site {
constructor(url, driver) {
;({ options: this.options, browser: this.browser } = driver)
this.driver = driver
try {
this.originalUrl = new URL(url)
} catch (error) {
throw new Error(error.message || error.toString())
}
this.wappalyzer = new Wappalyzer()
this.wappalyzer.apps = json.apps
this.wappalyzer.categories = json.categories
this.wappalyzer.parseJsPatterns()
this.wappalyzer.driver.log = (message, source, type) =>
this.log(message, source, type)
this.wappalyzer.driver.displayApps = (detected, meta, context) =>
this.displayApps(detected, meta, context)
this.analyzedUrls = {}
this.technologies = []
this.meta = {}
this.listeners = {}
this.headers = {}
}
async init() {}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = []
}
this.listeners[event].push(callback)
}
emit(event, params) {
if (this.listeners[event]) {
this.listeners[event].forEach((listener) => listener(params))
}
}
log(...args) {
this.emit('log', ...args)
this.driver.log(...args)
}
async fetch(url, index, depth) {}
async goto(url) {
// Return when the URL is a duplicate or maxUrls has been reached
if (
this.analyzedUrls[url.href] ||
Object.keys(this.analyzedUrls).length >= this.options.maxUrls
) {
return
}
this.log(`Navigate to ${url}`, 'page')
this.analyzedUrls[url.href] = {
status: 0
}
if (!this.browser) {
throw new Error('Browser closed')
}
const page = await this.browser.newPage()
page.setDefaultTimeout(this.options.maxWait)
await page.setRequestInterception(true)
page.on('error', (error) => this.emit('error', error))
let responseReceived = false
page.on('request', (request) => {
try {
if (
(responseReceived && request.isNavigationRequest()) ||
request.frame() !== page.mainFrame() ||
!['document', 'script'].includes(request.resourceType())
) {
request.abort('blockedbyclient')
} else {
request.continue()
}
} catch (error) {
this.emit('error', error)
}
})
page.on('response', (response) => {
try {
if (response.url() === url.href) {
this.analyzedUrls[url.href] = {
status: response.status()
}
const headers = response.headers()
Object.keys(headers).forEach((key) => {
this.headers[key] = [
...(this.headers[key] || []),
...(Array.isArray(headers[key]) ? headers[key] : [headers[key]])
]
})
this.contentType = headers['content-type'] || null
if (response.status() >= 300 && response.status() < 400) {
if (this.headers.location) {
url = new URL(this.headers.location.slice(-1))
}
} else {
responseReceived = true
}
}
} catch (error) {
this.emit('error', error)
}
})
if (this.options.userAgent) {
await page.setUserAgent(this.options.userAgent)
}
try {
await Promise.race([
page.goto(url.href, { waitUntil: 'domcontentloaded' }),
new Promise((resolve, reject) =>
setTimeout(() => reject(new Error('Timeout')), this.options.maxWait)
)
])
} catch (error) {
this.emit('error', error)
}
await sleep(1000)
const links = await (
await page.evaluateHandle(() =>
Array.from(document.getElementsByTagName('a')).map(
({ hash, hostname, href, pathname, protocol, rel }) => ({
hash,
hostname,
href,
pathname,
protocol,
rel
})
)
)
).jsonValue()
// eslint-disable-next-line no-undef
const scripts = (
await (
await page.evaluateHandle(() =>
Array.from(document.getElementsByTagName('script')).map(
({ src }) => src
)
)
).jsonValue()
).filter((script) => script)
const js = processJs(await page.evaluate(getJs), this.wappalyzer.jsPatterns)
const cookies = (await page.cookies()).map(
({ name, value, domain, path }) => ({
name,
value,
domain,
path
})
)
const html = processHtml(
await page.content(),
this.options.htmlMaxCols,
this.options.htmlMaxRows
)
// Validate response
if (!this.analyzedUrls[url.href].status) {
throw new Error('NO_RESPONSE')
}
let language = null
try {
const [attrs] = languageDetect.detect(
html.replace(/<\/?[^>]+(>|$)/g, ' '),
1
)
if (attrs) {
;[language] = attrs
}
} catch (error) {
this.log(`${error} (${url.href})`, 'driver', 'error')
}
await this.wappalyzer.analyze(url, {
cookies,
headers: this.headers,
html,
js,
scripts,
language
})
const reducedLinks = Array.prototype.reduce.call(
links,
(results, link) => {
if (
results &&
Object.prototype.hasOwnProperty.call(
Object.getPrototypeOf(results),
'push'
) &&
link.protocol &&
link.protocol.match(/https?:/) &&
link.rel !== 'nofollow' &&
link.hostname === url.hostname &&
extensions.test(link.pathname)
) {
results.push(new URL(link.href.split('#')[0]))
}
return results
},
[]
)
this.emit('goto', url)
return reducedLinks
}
async analyze(url = this.originalUrl, index = 1, depth = 1) {
try {
await sleep(this.options.delay * index)
const links = await this.goto(url)
if (links && this.options.recursive && depth < this.options.maxDepth) {
await this.batch(links.slice(0, this.options.maxUrls), depth + 1)
}
} catch (error) {
const type =
error.message && errorTypes[error.message]
? error.message
: 'UNKNOWN_ERROR'
const message =
error.message && errorTypes[error.message]
? errorTypes[error.message]
: 'Unknown error'
this.analyzedUrls[url.href] = {
status: 0,
error: {
type,
message
}
}
this.log(`${message} (${url.href})`, 'driver', 'error')
}
return {
urls: this.analyzedUrls,
applications: this.technologies,
meta: this.meta
}
}
async batch(links, depth, batch = 0) {
if (links.length === 0) {
return
}
const batched = links.splice(0, this.options.batchSize)
await Promise.all(
batched.map((link, index) => this.analyze(link, index, depth))
)
await this.batch(links, depth, batch + 1)
}
displayApps(technologies, meta) {
this.meta = meta
Object.keys(technologies).forEach((name) => {
const {
confidenceTotal: confidence,
version,
props: { cats, icon, website, cpe }
} = technologies[name]
const categories = cats.reduce((categories, id) => {
categories[id] = json.categories[id].name
return categories
}, {})
if (!this.technologies.some(({ name: _name }) => name === _name)) {
this.technologies.push({
name,
confidence,
version: version || null,
icon: icon || 'default.svg',
website,
cpe: cpe || null,
categories
})
}
})
}
}
module.exports = Driver
module.exports.processJs = processJs
module.exports.processHtml = processHtml

@ -0,0 +1,12 @@
const Driver = require('./driver');
class Wappalyzer {
constructor(pageUrl, options) {
// eslint-disable-next-line import/no-dynamic-require, global-require
const Browser = require(`./browsers/${options.browser || 'zombie'}`);
return new Driver(Browser, pageUrl, options);
}
}
module.exports = Wappalyzer;

@ -0,0 +1,30 @@
{
"name": "wappalyzer",
"description": "Identify technology on websites",
"homepage": "https://www.wappalyzer.com",
"version": "6.0.0",
"author": "Wappalyzer",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/aliasio/wappalyzer"
},
"funding": {
"url": "https://github.com/sponsors/aliasio"
},
"main": "index.js",
"files": [
"apps.json",
"cli.js",
"driver.js",
"index.js",
"wappalyzer.js"
],
"bin": {
"wappalyzer": "./cli.js"
},
"dependencies": {
"languagedetect": "^2.0.0",
"puppeteer": "^2.0.0"
}
}

@ -0,0 +1,296 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/mime-types@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.0.tgz#9ca52cda363f699c69466c2a6ccdaad913ea7a73"
integrity sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=
agent-base@5:
version "5.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==
async-limiter@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
concat-stream@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
dependencies:
buffer-from "^1.0.0"
inherits "^2.0.3"
readable-stream "^2.2.2"
typedarray "^0.0.6"
core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
debug@4, debug@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
dependencies:
ms "^2.1.1"
debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
extract-zip@^1.6.6:
version "1.7.0"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
dependencies:
concat-stream "^1.6.2"
debug "^2.6.9"
mkdirp "^0.5.4"
yauzl "^2.10.0"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
dependencies:
pend "~1.2.0"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
glob@^7.1.3:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
https-proxy-agent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b"
integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==
dependencies:
agent-base "5"
debug "4"
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@^2.0.3, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
languagedetect@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/languagedetect/-/languagedetect-2.0.0.tgz#4b8fa2b7593b2a3a02fb1100891041c53238936c"
integrity sha512-AZb/liiQ+6ZoTj4f1J0aE6OkzhCo8fyH+tuSaPfSo8YHCWLFJrdSixhtO2TYdIkjcDQNaR4RmGaV2A5FJklDMQ==
mime-db@1.44.0:
version "1.44.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
mime-types@^2.1.25:
version "2.1.27"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
dependencies:
mime-db "1.44.0"
mime@^2.0.3:
version "2.4.5"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.5.tgz#d8de2ecb92982dedbb6541c9b6841d7f218ea009"
integrity sha512-3hQhEUF027BuxZjQA3s7rIv/7VCQPa27hN9u9g87sEkWaKwQPuXOkVKtOeiyUrnWqTDiOs8Ed2rwg733mB0R5w==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
mkdirp@^0.5.4:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
dependencies:
minimist "^1.2.5"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
progress@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
proxy-from-env@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
puppeteer@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-2.1.1.tgz#ccde47c2a688f131883b50f2d697bd25189da27e"
integrity sha512-LWzaDVQkk1EPiuYeTOj+CZRIjda4k2s5w4MK4xoH2+kgWV/SDlkYHmxatDdtYrciHUKSXTsGgPgPP8ILVdBsxg==
dependencies:
"@types/mime-types" "^2.1.0"
debug "^4.1.0"
extract-zip "^1.6.6"
https-proxy-agent "^4.0.0"
mime "^2.0.3"
mime-types "^2.1.25"
progress "^2.0.1"
proxy-from-env "^1.0.0"
rimraf "^2.6.1"
ws "^6.1.0"
readable-stream@^2.2.2:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
rimraf@^2.6.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
dependencies:
glob "^7.1.3"
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
dependencies:
safe-buffer "~5.1.0"
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
ws@^6.1.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
dependencies:
async-limiter "~1.0.0"
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"

@ -15,6 +15,7 @@
"termsAccept": { "message": "Acceptar" }, "termsAccept": { "message": "Acceptar" },
"termsContent": { "message": "Aquesta extensió envia informació anònima sobre els llocs web que visiteu, inclosos el nom de domini i les tecnologies identificades a <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Això pot desactivar-se a Opcions." }, "termsContent": { "message": "Aquesta extensió envia informació anònima sobre els llocs web que visiteu, inclosos el nom de domini i les tecnologies identificades a <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Això pot desactivar-se a Opcions." },
"privacyPolicy": { "message": "Política de privadesa" }, "privacyPolicy": { "message": "Política de privadesa" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Taulers de missatgeria" }, "categoryName2": { "message": "Taulers de missatgeria" },
"categoryName3": { "message": "Gestor de bases de dades" }, "categoryName3": { "message": "Gestor de bases de dades" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Immer Icon anzeigen" }, "categoryPin": { "message": "Immer Icon anzeigen" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Nachrichten Board" }, "categoryName2": { "message": "Nachrichten Board" },
"categoryName3": { "message": "Datenbankverwaltung" }, "categoryName3": { "message": "Datenbankverwaltung" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Διαδικτυακό Φόρουμ" }, "categoryName2": { "message": "Διαδικτυακό Φόρουμ" },
"categoryName3": { "message": "Διαχειριστής Βάσης Δεδομένων" }, "categoryName3": { "message": "Διαχειριστής Βάσης Δεδομένων" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" }, "privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Message boards" }, "categoryName2": { "message": "Message boards" },
"categoryName3": { "message": "Database managers" }, "categoryName3": { "message": "Database managers" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "Gestor de Contenido" }, "categoryName1": { "message": "Gestor de Contenido" },
"categoryName2": { "message": "Foro" }, "categoryName2": { "message": "Foro" },
"categoryName3": { "message": "Gestor de Bases de Datos" }, "categoryName3": { "message": "Gestor de Bases de Datos" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "همیشه نماد را نشان بده" }, "categoryPin": { "message": "همیشه نماد را نشان بده" },
"termsAccept": { "message": "قبول" }, "termsAccept": { "message": "قبول" },
"termsContent": { "message": "این افزونه اطلاعات وب‌سایت‌های بازدید شده توسط شما را به صورت ناشناس ارسال می‌کند، مانند آدرس سایت و تکنولوژی‌های استفاده شده در آن سایت را ارسال می‌کند. اطلاعات بیشتر در <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. شما می‌توانید این افزونه را غیرفعال کنید." }, "termsContent": { "message": "این افزونه اطلاعات وب‌سایت‌های بازدید شده توسط شما را به صورت ناشناس ارسال می‌کند، مانند آدرس سایت و تکنولوژی‌های استفاده شده در آن سایت را ارسال می‌کند. اطلاعات بیشتر در <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. شما می‌توانید این افزونه را غیرفعال کنید." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "سیستم مدیریت محتوا" }, "categoryName1": { "message": "سیستم مدیریت محتوا" },
"categoryName2": { "message": "انجمن پیام" }, "categoryName2": { "message": "انجمن پیام" },
"categoryName3": { "message": "مدیریت پایگاه داده" }, "categoryName3": { "message": "مدیریت پایگاه داده" },

@ -14,6 +14,8 @@
"categoryPin": { "message": " Toujours afficher l'icône" }, "categoryPin": { "message": " Toujours afficher l'icône" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Forum" }, "categoryName2": { "message": "Forum" },
"categoryName3": { "message": "Gestionnaire de base de données" }, "categoryName3": { "message": "Gestionnaire de base de données" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "Aceptar" }, "termsAccept": { "message": "Aceptar" },
"termsContent": { "message": "Esta extensión envía anonimamente información acerca das webs que visitas, incluindo dominio e aplicativos identificados, a <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Isto pode ser desactivado nas preferencias." }, "termsContent": { "message": "Esta extensión envía anonimamente información acerca das webs que visitas, incluindo dominio e aplicativos identificados, a <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Isto pode ser desactivado nas preferencias." },
"privacyPolicy": { "message": "Política de privacidade" }, "privacyPolicy": { "message": "Política de privacidade" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Taboleiro de mensaxes" }, "categoryName2": { "message": "Taboleiro de mensaxes" },
"categoryName3": { "message": "Xestor de base de datos" }, "categoryName3": { "message": "Xestor de base de datos" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Διαδικτυακό Φόρουμ" }, "categoryName2": { "message": "Διαδικτυακό Φόρουμ" },
"categoryName3": { "message": "Διαχειριστής Βάσης Δεδομένων" }, "categoryName3": { "message": "Διαχειριστής Βάσης Δεδομένων" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "Sistem Pengelola Konten" }, "categoryName1": { "message": "Sistem Pengelola Konten" },
"categoryName2": { "message": "Papan Pesan" }, "categoryName2": { "message": "Papan Pesan" },
"categoryName3": { "message": "Pengelola Basis Data" }, "categoryName3": { "message": "Pengelola Basis Data" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Forum" }, "categoryName2": { "message": "Forum" },
"categoryName3": { "message": "Gestore di Database" }, "categoryName3": { "message": "Gestore di Database" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "受諾する" }, "termsAccept": { "message": "受諾する" },
"termsContent": { "message": "この拡張機能は、ドメイン名や特定された技術など、アクセスしたWebサイトに関する匿名情報を<a href='https://www.wappalyzer.com'>wappalyzer.com</a>に送信します。これは設定で無効にできます。" }, "termsContent": { "message": "この拡張機能は、ドメイン名や特定された技術など、アクセスしたWebサイトに関する匿名情報を<a href='https://www.wappalyzer.com'>wappalyzer.com</a>に送信します。これは設定で無効にできます。" },
"privacyPolicy": { "message": "プライバシーポリシー" }, "privacyPolicy": { "message": "プライバシーポリシー" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "メッセージボード" }, "categoryName2": { "message": "メッセージボード" },
"categoryName3": { "message": "データベースマネージャー" }, "categoryName3": { "message": "データベースマネージャー" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Zawsze pokazuj tą ikonę" }, "categoryPin": { "message": "Zawsze pokazuj tą ikonę" },
"termsAccept": { "message": "Akceptuj" }, "termsAccept": { "message": "Akceptuj" },
"termsContent": { "message": "To rozszerzenie wysyła anonimowe informacje o stronach, które odwiedzasz, uwzględniając nazwy domen i zidentyfikowane technologie do <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Opcja może zostać wyłączona w ustawieniach." }, "termsContent": { "message": "To rozszerzenie wysyła anonimowe informacje o stronach, które odwiedzasz, uwzględniając nazwy domen i zidentyfikowane technologie do <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Opcja może zostać wyłączona w ustawieniach." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "System zarządzania treścią" }, "categoryName1": { "message": "System zarządzania treścią" },
"categoryName2": { "message": "Forum" }, "categoryName2": { "message": "Forum" },
"categoryName3": { "message": "Menedżer baz danych" }, "categoryName3": { "message": "Menedżer baz danych" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "Aceitar" }, "termsAccept": { "message": "Aceitar" },
"termsContent": { "message": "Esta extensão envia informações anónimas sobre os sites que visitas, incluindo o nome de domínio e as tecnologias identificadas, para o <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Isso pode ser desativado nas configurações." }, "termsContent": { "message": "Esta extensão envia informações anónimas sobre os sites que visitas, incluindo o nome de domínio e as tecnologias identificadas, para o <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Isso pode ser desativado nas configurações." },
"privacyPolicy": { "message": "Políticas de Privacidade" }, "privacyPolicy": { "message": "Políticas de Privacidade" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Fórum" }, "categoryName2": { "message": "Fórum" },
"categoryName3": { "message": "Gestor de Base de Dados" }, "categoryName3": { "message": "Gestor de Base de Dados" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Sempre mostrar ícone" }, "categoryPin": { "message": "Sempre mostrar ícone" },
"termsAccept": { "message": "Aceitar" }, "termsAccept": { "message": "Aceitar" },
"termsContent": { "message": "Esta extensão envia informações anônimas sobre os sites que você visita, incluindo domínio e tecnologias identificadas para <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Este comportamento pode ser desativado nas configurações." }, "termsContent": { "message": "Esta extensão envia informações anônimas sobre os sites que você visita, incluindo domínio e tecnologias identificadas para <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Este comportamento pode ser desativado nas configurações." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Fórum" }, "categoryName2": { "message": "Fórum" },
"categoryName3": { "message": "Gestão de Banco de Dados" }, "categoryName3": { "message": "Gestão de Banco de Dados" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Afișează icon tot timpul" }, "categoryPin": { "message": "Afișează icon tot timpul" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Forum de discuții" }, "categoryName2": { "message": "Forum de discuții" },
"categoryName3": { "message": "Manager baze de date" }, "categoryName3": { "message": "Manager baze de date" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "Принять" }, "termsAccept": { "message": "Принять" },
"termsContent": { "message": "Расширение отправляет обезличенную статистику посещенных сайтов, включая доменное имя и распознанные технологии на <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Это можно отключить в настройках." }, "termsContent": { "message": "Расширение отправляет обезличенную статистику посещенных сайтов, включая доменное имя и распознанные технологии на <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. Это можно отключить в настройках." },
"privacyPolicy": { "message": "Политика конфиденциальности" }, "privacyPolicy": { "message": "Политика конфиденциальности" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Форум" }, "categoryName2": { "message": "Форум" },
"categoryName3": { "message": "Менеджер БД" }, "categoryName3": { "message": "Менеджер БД" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Message Board" }, "categoryName2": { "message": "Message Board" },
"categoryName3": { "message": "Správca databáz" }, "categoryName3": { "message": "Správca databáz" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Her zaman bu kategorinin ikonunu kullan" }, "categoryPin": { "message": "Her zaman bu kategorinin ikonunu kullan" },
"termsAccept": { "message": "Kabul Ediyorum" }, "termsAccept": { "message": "Kabul Ediyorum" },
"termsContent": { "message": "Bu eklenti, ziyaret ettiğiniz web site bilgilerini, alan adları ve tespit edilen teknolojiler ile beraber anonim olarak <a href='https://www.wappalyzer.com'>wappalyzer.com</a>'a gönderir. Bunu, eklenti ayarlarından değiştirebilirsiniz." }, "termsContent": { "message": "Bu eklenti, ziyaret ettiğiniz web site bilgilerini, alan adları ve tespit edilen teknolojiler ile beraber anonim olarak <a href='https://www.wappalyzer.com'>wappalyzer.com</a>'a gönderir. Bunu, eklenti ayarlarından değiştirebilirsiniz." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "İçerik Yönetim Sistemi" }, "categoryName1": { "message": "İçerik Yönetim Sistemi" },
"categoryName2": { "message": "Mesaj Tahtası" }, "categoryName2": { "message": "Mesaj Tahtası" },
"categoryName3": { "message": "Veritabanı Yöneticisi" }, "categoryName3": { "message": "Veritabanı Yöneticisi" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS" }, "categoryName1": { "message": "CMS" },
"categoryName2": { "message": "Форум" }, "categoryName2": { "message": "Форум" },
"categoryName3": { "message": "Менеджер БД" }, "categoryName3": { "message": "Менеджер БД" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "Always show icon" }, "categoryPin": { "message": "Always show icon" },
"termsAccept": { "message": "Accept" }, "termsAccept": { "message": "Accept" },
"termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." }, "termsContent": { "message": "This extension sends anonymous information about websites you visit, including domain name and identified technologies, to <a href='https://www.wappalyzer.com'>wappalyzer.com</a>. This can be disabled in the settings." },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "CMS (KBT)" }, "categoryName1": { "message": "CMS (KBT)" },
"categoryName2": { "message": "Forum" }, "categoryName2": { "message": "Forum" },
"categoryName3": { "message": "MB boshqaruvi" }, "categoryName3": { "message": "MB boshqaruvi" },

@ -15,6 +15,7 @@
"termsAccept": { "message": "接受" }, "termsAccept": { "message": "接受" },
"termsContent": { "message": "此扩展程序发送关于您访问的网站的匿名信息至 <a href='https://www.wappalyzer.com'>wappalyzer.com</a>,包含域名和检测到的技术。这可以在设置中禁用。" }, "termsContent": { "message": "此扩展程序发送关于您访问的网站的匿名信息至 <a href='https://www.wappalyzer.com'>wappalyzer.com</a>,包含域名和检测到的技术。这可以在设置中禁用。" },
"privacyPolicy": { "message": "隐私政策" }, "privacyPolicy": { "message": "隐私政策" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "内容管理系统CMS" }, "categoryName1": { "message": "内容管理系统CMS" },
"categoryName2": { "message": "消息板" }, "categoryName2": { "message": "消息板" },
"categoryName3": { "message": "数据库管理器" }, "categoryName3": { "message": "数据库管理器" },

@ -14,6 +14,8 @@
"categoryPin": { "message": "永遠顯示圖示" }, "categoryPin": { "message": "永遠顯示圖示" },
"termsAccept": { "message": "接受" }, "termsAccept": { "message": "接受" },
"termsContent": { "message": "這個擴充功能將你所造訪網站的網域名稱和識別到的技術等資訊,匿名傳送至 <a href='https://www.wappalyzer.com'>wappalyzer.com</a>。你可以在選項中停用。" }, "termsContent": { "message": "這個擴充功能將你所造訪網站的網域名稱和識別到的技術等資訊,匿名傳送至 <a href='https://www.wappalyzer.com'>wappalyzer.com</a>。你可以在選項中停用。" },
"privacyPolicy": { "message": "Privacy policy" },
"createAlert": { "message": "Create an alert for this website" },
"categoryName1": { "message": "內容管理系統CMS" }, "categoryName1": { "message": "內容管理系統CMS" },
"categoryName2": { "message": "留言板/討論區" }, "categoryName2": { "message": "留言板/討論區" },
"categoryName3": { "message": "資料庫管理" }, "categoryName3": { "message": "資料庫管理" },

@ -31,6 +31,24 @@ body {
display: none; display: none;
} }
.footer {
align-items: center;
border-top: 1px solid #dbdbdb;
height: 3rem;
display: flex;
padding: 0 1.5rem;
}
.footer__link {
color: #4608ad;
text-decoration: none;
}
.footer__link:hover, .footer__link:active {
color: #4608ad;
text-decoration: underline;
}
.container { .container {
min-height: 5rem; min-height: 5rem;
padding: 1rem 1.5rem 0rem 1.5rem; padding: 1rem 1.5rem 0rem 1.5rem;
@ -202,50 +220,68 @@ body {
margin-top: 1rem; margin-top: 1rem;
} }
/* Add alternative color palette for Dark mode theme. */ @media (prefers-color-scheme: dark) {
body.theme-mode-sync { /* Add alternative color palette for Dark mode theme. */
background: linear-gradient(160deg, #32067c, #150233); body.theme-mode-sync {
} background: linear-gradient(160deg, #32067c, #150233);
}
.theme-mode-sync .header { .theme-mode-sync .header {
border-bottom: 1px solid #000; border-bottom: 1px solid rgba(255, 255, 255, .2);
} }
.theme-mode-sync .header__logo--dark { .theme-mode-sync .header__logo--dark {
display: inline-block; display: inline-block;
} }
.theme-mode-sync .header__logo--light { .theme-mode-sync .header__logo--light {
display: none; display: none;
} }
.theme-mode-sync .container { .theme-mode-sync .footer {
color: white; border-top: 1px solid rgba(255, 255, 255, .2);
} }
.theme-mode-sync .detected__category-link, .theme-mode-sync .detected__app { .theme-mode-sync .footer__link {
color: white; color: rgba(255, 255, 255, .8);
} }
.theme-mode-sync .detected__category-link:hover { .theme-mode-sync .footer__link:hover, .theme-mode-sync .footer__link:active {
color: white; color: rgba(255, 255, 255, .8);
border-bottom: 1px solid white; }
}
.theme-mode-sync .detected__app-version, .theme-mode-sync .detected__app-confidence { .theme-mode-sync .container {
background-color: #4608ad; color: white;
} }
.theme-mode-sync .detected__app:hover .detected__app-name { .theme-mode-sync .detected__category-link {
border-bottom: 1px solid white; color: #fff;
} }
.theme-mode-sync .detected__app:hover .theme-mode-sync .detected__app-version, .theme-mode-sync .detected__app {
.theme-mode-sync .detected__app:hover .theme-mode-sync .detected__app-confidence { color: rgba(255, 255, 255, .8);
border-bottom: none; }
}
.theme-mode-sync .terms__accept, .theme-mode-sync .detected__category-link:hover {
.theme-mode-sync .terms__privacy { color: white;
color: white; border-bottom: 1px solid white;
}
.theme-mode-sync .detected__app-version, .theme-mode-sync .detected__app-confidence {
background-color: #4608ad;
}
.theme-mode-sync .detected__app:hover .detected__app-name {
border-bottom: 1px solid white;
}
.theme-mode-sync .detected__app:hover .theme-mode-sync .detected__app-version,
.theme-mode-sync .detected__app:hover .theme-mode-sync .detected__app-confidence {
border-bottom: none;
}
.theme-mode-sync .terms__accept,
.theme-mode-sync .terms__privacy {
color: white;
}
} }

@ -14,8 +14,8 @@
<body> <body>
<div class="header"> <div class="header">
<a href="https://www.wappalyzer.com/" class="header__link" target="_blank"> <a href="https://www.wappalyzer.com/" class="header__link" target="_blank">
<img class="header__logo header__logo--light" src="../images/logo-purple.svg"> <img alt="" class="header__logo header__logo--light" src="../images/logo-purple.svg">
<img class="header__logo header__logo--dark" src="../images/logo-white.svg"> <img alt="" class="header__logo header__logo--dark" src="../images/logo-white.svg">
</a> </a>
</div> </div>
@ -24,11 +24,15 @@
<div class="terms"> <div class="terms">
<div class="terms__content" data-i18n="termsContent"></div> <div class="terms__content" data-i18n="termsContent"></div>
<button class="terms__accept" data-i18n="termsAccept"></button> <button class="terms__accept" data-i18n="termsAccept" />
<a class="terms__privacy" href="https://www.wappalyzer.com/privacy" data-i18n="privacyPolicy"></a> <a class="terms__privacy" href="https://www.wappalyzer.com/privacy" data-i18n="privacyPolicy"></a>
</div> </div>
</div> </div>
</div> </div>
<div class="footer">
<a class="footer__link" href="https://www.wappalyzer.com/alerts/manage" data-i18n="createAlert"></a>
</div>
</body> </body>
</html> </html>

@ -10,29 +10,29 @@
/** global: fetch */ /** global: fetch */
/** global: Wappalyzer */ /** global: Wappalyzer */
const wappalyzer = new Wappalyzer(); const wappalyzer = new Wappalyzer()
const tabCache = {}; const tabCache = {}
const robotsTxtQueue = {}; const robotsTxtQueue = {}
let categoryOrder = []; let categoryOrder = []
browser.tabs.onRemoved.addListener((tabId) => { browser.tabs.onRemoved.addListener((tabId) => {
tabCache[tabId] = null; tabCache[tabId] = null
}); })
function userAgent() { function userAgent() {
const url = chrome.extension.getURL('/'); const url = chrome.extension.getURL('/')
if (url.match(/^moz-/)) { if (url.match(/^moz-/)) {
return 'firefox'; return 'firefox'
} }
if (url.match(/^ms-browser-/)) { if (url.match(/^ms-browser-/)) {
return 'edge'; return 'edge'
} }
return 'chrome'; return 'chrome'
} }
/** /**
@ -40,22 +40,22 @@ function userAgent() {
*/ */
function getOption(name, defaultValue = null) { function getOption(name, defaultValue = null) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let value = defaultValue; let value = defaultValue
try { try {
const option = await browser.storage.local.get(name); const option = await browser.storage.local.get(name)
if (option[name] !== undefined) { if (option[name] !== undefined) {
value = option[name]; value = option[name]
} }
} catch (error) { } catch (error) {
wappalyzer.log(error.message, 'driver', 'error'); wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message); return reject(error.message)
} }
return resolve(value); return resolve(value)
}); })
} }
/** /**
@ -64,15 +64,15 @@ function getOption(name, defaultValue = null) {
function setOption(name, value) { function setOption(name, value) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
await browser.storage.local.set({ [name]: value }); await browser.storage.local.set({ [name]: value })
} catch (error) { } catch (error) {
wappalyzer.log(error.message, 'driver', 'error'); wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message); return reject(error.message)
} }
return resolve(); return resolve()
}); })
} }
/** /**
@ -81,8 +81,8 @@ function setOption(name, value) {
function openTab(args) { function openTab(args) {
browser.tabs.create({ browser.tabs.create({
url: args.url, url: args.url,
active: args.background === undefined || !args.background, active: args.background === undefined || !args.background
}); })
} }
/** /**
@ -92,321 +92,346 @@ async function post(url, body) {
try { try {
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
body: JSON.stringify(body), body: JSON.stringify(body)
}); })
wappalyzer.log(`POST ${url}: ${response.status}`, 'driver'); wappalyzer.log(`POST ${url}: ${response.status}`, 'driver')
} catch (error) { } catch (error) {
wappalyzer.log(`POST ${url}: ${error}`, 'driver', 'error'); wappalyzer.log(`POST ${url}: ${error}`, 'driver', 'error')
} }
} }
// Capture response headers // Capture response headers
browser.webRequest.onCompleted.addListener(async (request) => { browser.webRequest.onCompleted.addListener(
const headers = {}; async (request) => {
const headers = {}
if (request.responseHeaders) { if (request.responseHeaders) {
const url = wappalyzer.parseUrl(request.url); const url = wappalyzer.parseUrl(request.url)
let tab; let tab
try { try {
[tab] = await browser.tabs.query({ url: [url.href] }); ;[tab] = await browser.tabs.query({ url: [url.href] })
} catch (error) { } catch (error) {
wappalyzer.log(error, 'driver', 'error'); wappalyzer.log(error, 'driver', 'error')
} }
if (tab) { if (tab) {
request.responseHeaders.forEach((header) => { request.responseHeaders.forEach((header) => {
const name = header.name.toLowerCase(); const name = header.name.toLowerCase()
headers[name] = headers[name] || []; headers[name] = headers[name] || []
headers[name].push((header.value || header.binaryValue || '').toString()); headers[name].push(
}); (header.value || header.binaryValue || '').toString()
)
})
if (headers['content-type'] && /\/x?html/.test(headers['content-type'][0])) { if (
wappalyzer.analyze(url, { headers }, { tab }); headers['content-type'] &&
/\/x?html/.test(headers['content-type'][0])
) {
wappalyzer.analyze(url, { headers }, { tab })
}
} }
} }
} },
}, { urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] }, ['responseHeaders']); { urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] },
['responseHeaders']
)
browser.runtime.onConnect.addListener((port) => { browser.runtime.onConnect.addListener((port) => {
port.onMessage.addListener(async (message) => { port.onMessage.addListener(async (message) => {
if (message.id === undefined) { if (message.id === undefined) {
return; return
} }
if (message.id !== 'log') { if (message.id !== 'log') {
wappalyzer.log(`Message from ${port.name}: ${message.id}`, 'driver'); wappalyzer.log(`Message from ${port.name}: ${message.id}`, 'driver')
} }
const pinnedCategory = await getOption('pinnedCategory'); const pinnedCategory = await getOption('pinnedCategory')
const url = wappalyzer.parseUrl(port.sender.tab ? port.sender.tab.url : ''); const url = wappalyzer.parseUrl(port.sender.tab ? port.sender.tab.url : '')
const cookies = await browser.cookies.getAll({ domain: `.${url.hostname}`, firstPartyDomain: null }); const cookies = await browser.cookies.getAll({
domain: `.${url.hostname}`,
firstPartyDomain: null
})
let response; let response
switch (message.id) { switch (message.id) {
case 'log': case 'log':
wappalyzer.log(message.subject, message.source); wappalyzer.log(message.subject, message.source)
break; break
case 'init': case 'init':
wappalyzer.analyze(url, { cookies }, { tab: port.sender.tab }); wappalyzer.analyze(url, { cookies }, { tab: port.sender.tab })
break; break
case 'analyze': case 'analyze':
if (message.subject.html) { if (message.subject.html) {
browser.i18n.detectLanguage(message.subject.html) browser.i18n
.detectLanguage(message.subject.html)
.then(({ languages }) => { .then(({ languages }) => {
const language = languages const language = languages
.filter(({ percentage }) => percentage >= 75) .filter(({ percentage }) => percentage >= 75)
.map(({ language: lang }) => lang)[0]; .map(({ language: lang }) => lang)[0]
message.subject.language = language; message.subject.language = language
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab }); wappalyzer.analyze(url, message.subject, { tab: port.sender.tab })
}); })
} else { } else {
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab }); wappalyzer.analyze(url, message.subject, { tab: port.sender.tab })
} }
await setOption('hostnameCache', wappalyzer.hostnameCache); await setOption('hostnameCache', wappalyzer.hostnameCache)
break; break
case 'ad_log': case 'ad_log':
wappalyzer.cacheDetectedAds(message.subject); wappalyzer.cacheDetectedAds(message.subject)
break; break
case 'get_apps': case 'get_apps':
response = { response = {
tabCache: tabCache[message.tab.id], tabCache: tabCache[message.tab.id],
apps: wappalyzer.apps, apps: wappalyzer.apps,
categories: wappalyzer.categories, categories: wappalyzer.categories,
pinnedCategory, pinnedCategory,
termsAccepted: userAgent() === 'chrome' || await getOption('termsAccepted', false), termsAccepted:
}; userAgent() === 'chrome' ||
(await getOption('termsAccepted', false))
}
break; break
case 'set_option': case 'set_option':
await setOption(message.key, message.value); await setOption(message.key, message.value)
break; break
case 'get_js_patterns': case 'get_js_patterns':
response = { response = {
patterns: wappalyzer.jsPatterns, patterns: wappalyzer.jsPatterns
}; }
break; break
case 'update_theme_mode': case 'update_theme_mode':
// Sync theme mode to popup. // Sync theme mode to popup.
response = { response = {
themeMode: await getOption('themeMode', false), themeMode: await getOption('themeMode', false)
}; }
break; break
default: default:
// Do nothing // Do nothing
} }
if (response) { if (response) {
port.postMessage({ port.postMessage({
id: message.id, id: message.id,
response, response
}); })
} }
}); })
}); })
wappalyzer.driver.document = document; wappalyzer.driver.document = document
/** /**
* Log messages to console * Log messages to console
*/ */
wappalyzer.driver.log = (message, source, type) => { wappalyzer.driver.log = (message, source, type) => {
const log = ['warn', 'error'].indexOf(type) !== -1 ? type : 'log'; const log = ['warn', 'error'].includes(type) ? type : 'log'
console[log](`[wappalyzer ${type}]`, `[${source}]`, message); // eslint-disable-line no-console console[log](`[wappalyzer ${type}]`, `[${source}]`, message) // eslint-disable-line no-console
}; }
/** /**
* Display apps * Display apps
*/ */
wappalyzer.driver.displayApps = async (detected, meta, context) => { wappalyzer.driver.displayApps = async (detected, meta, context) => {
const { tab } = context; const { tab } = context
if (tab === undefined) { if (tab === undefined) {
return; return
} }
tabCache[tab.id] = tabCache[tab.id] || { tabCache[tab.id] = tabCache[tab.id] || {
detected: [], detected: []
}; }
tabCache[tab.id].detected = detected; tabCache[tab.id].detected = detected
const pinnedCategory = await getOption('pinnedCategory'); const pinnedCategory = await getOption('pinnedCategory')
const dynamicIcon = await getOption('dynamicIcon', true); const dynamicIcon = await getOption('dynamicIcon', true)
let found = false; let found = false
// Find the main application to display // Find the main application to display
[pinnedCategory].concat(categoryOrder).forEach((match) => { ;[pinnedCategory].concat(categoryOrder).forEach((match) => {
Object.keys(detected).forEach((appName) => { Object.keys(detected).forEach((appName) => {
const app = detected[appName]; const app = detected[appName]
app.props.cats.forEach((category) => { app.props.cats.forEach((category) => {
if (category === match && !found) { if (category === match && !found) {
let icon = app.props.icon && dynamicIcon ? app.props.icon : 'default.svg'; let icon =
app.props.icon && dynamicIcon ? app.props.icon : 'default.svg'
if (/\.svg$/i.test(icon)) { if (/\.svg$/i.test(icon)) {
icon = `converted/${icon.replace(/\.svg$/, '.png')}`; icon = `converted/${icon.replace(/\.svg$/, '.png')}`
} }
try { try {
browser.pageAction.setIcon({ browser.pageAction.setIcon({
tabId: tab.id, tabId: tab.id,
path: `../images/icons/${icon}`, path: `../images/icons/${icon}`
}); })
} catch (e) { } catch (e) {
// Firefox for Android does not support setIcon see https://bugzilla.mozilla.org/show_bug.cgi?id=1331746 // Firefox for Android does not support setIcon see https://bugzilla.mozilla.org/show_bug.cgi?id=1331746
} }
found = true; found = true
} }
}); })
}); })
}); })
browser.pageAction.show(tab.id); browser.pageAction.show(tab.id)
}; }
/** /**
* Fetch and cache robots.txt for host * Fetch and cache robots.txt for host
*/ */
wappalyzer.driver.getRobotsTxt = async (host, secure = false) => { wappalyzer.driver.getRobotsTxt = async (host, secure = false) => {
if (robotsTxtQueue[host]) { if (robotsTxtQueue[host]) {
return robotsTxtQueue[host]; return robotsTxtQueue[host]
} }
const tracking = await getOption('tracking', true); const tracking = await getOption('tracking', true)
const robotsTxtCache = await getOption('robotsTxtCache', {}); const robotsTxtCache = await getOption('robotsTxtCache', {})
robotsTxtQueue[host] = new Promise(async (resolve) => { robotsTxtQueue[host] = new Promise(async (resolve) => {
if (!tracking) { if (!tracking) {
return resolve([]); return resolve([])
} }
if (host in robotsTxtCache) { if (host in robotsTxtCache) {
return resolve(robotsTxtCache[host]); return resolve(robotsTxtCache[host])
} }
const timeout = setTimeout(() => resolve([]), 3000); const timeout = setTimeout(() => resolve([]), 3000)
let response; let response
try { try {
response = await fetch(`http${secure ? 's' : ''}://${host}/robots.txt`, { redirect: 'follow', mode: 'no-cors' }); response = await fetch(`http${secure ? 's' : ''}://${host}/robots.txt`, {
redirect: 'follow',
mode: 'no-cors'
})
} catch (error) { } catch (error) {
wappalyzer.log(error, 'driver', 'error'); wappalyzer.log(error, 'driver', 'error')
return resolve([]); return resolve([])
} }
clearTimeout(timeout); clearTimeout(timeout)
const robotsTxt = response.ok ? await response.text() : ''; const robotsTxt = response.ok ? await response.text() : ''
robotsTxtCache[host] = Wappalyzer.parseRobotsTxt(robotsTxt); robotsTxtCache[host] = Wappalyzer.parseRobotsTxt(robotsTxt)
await setOption('robotsTxtCache', robotsTxtCache); await setOption('robotsTxtCache', robotsTxtCache)
delete robotsTxtQueue[host]; delete robotsTxtQueue[host]
return resolve(robotsTxtCache[host]); return resolve(robotsTxtCache[host])
}); })
return robotsTxtQueue[host]; return robotsTxtQueue[host]
}; }
/** /**
* Anonymously track detected applications for research purposes * Anonymously track detected applications for research purposes
*/ */
wappalyzer.driver.ping = async (hostnameCache = {}, adCache = []) => { wappalyzer.driver.ping = async (hostnameCache = {}, adCache = []) => {
const tracking = await getOption('tracking', true); const tracking = await getOption('tracking', true)
const termsAccepted = userAgent() === 'chrome' || await getOption('termsAccepted', false); const termsAccepted =
userAgent() === 'chrome' || (await getOption('termsAccepted', false))
if (tracking && termsAccepted) { if (tracking && termsAccepted) {
if (Object.keys(hostnameCache).length) { if (Object.keys(hostnameCache).length) {
post('https://api.wappalyzer.com/ping/v1/', hostnameCache); post('https://api.wappalyzer.com/ping/v1/', hostnameCache)
} }
if (adCache.length) { if (adCache.length) {
post('https://ad.wappalyzer.com/log/wp/', adCache); post('https://ad.wappalyzer.com/log/wp/', adCache)
} }
await setOption('robotsTxtCache', {}); await setOption('robotsTxtCache', {})
} }
}; }
// Init // Init
(async () => { ;(async () => {
// Technologies // Technologies
try { try {
const response = await fetch('../apps.json'); const response = await fetch('../apps.json')
const json = await response.json(); const json = await response.json()
wappalyzer.apps = json.apps; wappalyzer.apps = json.apps
wappalyzer.categories = json.categories; wappalyzer.categories = json.categories
} catch (error) { } catch (error) {
wappalyzer.log(`GET apps.json: ${error.message}`, 'driver', 'error'); wappalyzer.log(`GET apps.json: ${error.message}`, 'driver', 'error')
} }
wappalyzer.parseJsPatterns(); wappalyzer.parseJsPatterns()
categoryOrder = Object.keys(wappalyzer.categories) categoryOrder = Object.keys(wappalyzer.categories)
.map(categoryId => parseInt(categoryId, 10)) .map((categoryId) => parseInt(categoryId, 10))
.sort((a, b) => wappalyzer.categories[a].priority - wappalyzer.categories[b].priority); .sort(
(a, b) =>
wappalyzer.categories[a].priority - wappalyzer.categories[b].priority
)
// Version check // Version check
const { version } = browser.runtime.getManifest(); const { version } = browser.runtime.getManifest()
const previousVersion = await getOption('version'); const previousVersion = await getOption('version')
const upgradeMessage = await getOption('upgradeMessage', true); const upgradeMessage = await getOption('upgradeMessage', true)
if (previousVersion === null) { if (previousVersion === null) {
openTab({ openTab({
url: `${wappalyzer.config.websiteURL}installed`, url: `${wappalyzer.config.websiteURL}installed`
}); })
} else if (version !== previousVersion && upgradeMessage) { } else if (version !== previousVersion && upgradeMessage) {
openTab({ openTab({
url: `${wappalyzer.config.websiteURL}upgraded?v${version}`, url: `${wappalyzer.config.websiteURL}upgraded?v${version}`,
background: true, background: true
}); })
} }
await setOption('version', version); await setOption('version', version)
// Hostname cache // Hostname cache
wappalyzer.hostnameCache = await getOption('hostnameCache', {}); wappalyzer.hostnameCache = await getOption('hostnameCache', {})
// Run content script on all tabs // Run content script on all tabs
try { try {
const tabs = await browser.tabs.query({ url: ['http://*/*', 'https://*/*'] }); const tabs = await browser.tabs.query({
url: ['http://*/*', 'https://*/*']
})
tabs.forEach(async (tab) => { tabs.forEach(async (tab) => {
try { try {
await browser.tabs.executeScript(tab.id, { await browser.tabs.executeScript(tab.id, {
file: '../js/content.js', file: '../js/content.js'
}); })
} catch (error) { } catch (error) {
// //
} }
}); })
} catch (error) { } catch (error) {
wappalyzer.log(error, 'driver', 'error'); wappalyzer.log(error, 'driver', 'error')
} }
})(); })()

@ -3,29 +3,29 @@
/* globals browser Wappalyzer */ /* globals browser Wappalyzer */
/* eslint-env browser */ /* eslint-env browser */
const wappalyzer = new Wappalyzer(); const wappalyzer = new Wappalyzer()
/** /**
* Get a value from localStorage * Get a value from localStorage
*/ */
function getOption(name, defaultValue = null) { function getOption(name, defaultValue = null) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let value = defaultValue; let value = defaultValue
try { try {
const option = await browser.storage.local.get(name); const option = await browser.storage.local.get(name)
if (option[name] !== undefined) { if (option[name] !== undefined) {
value = option[name]; value = option[name]
} }
} catch (error) { } catch (error) {
wappalyzer.log(error.message, 'driver', 'error'); wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message); return reject(error.message)
} }
return resolve(value); return resolve(value)
}); })
} }
/** /**
@ -34,72 +34,76 @@ function getOption(name, defaultValue = null) {
function setOption(name, value) { function setOption(name, value) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
await browser.storage.local.set({ [name]: value }); await browser.storage.local.set({ [name]: value })
} catch (error) { } catch (error) {
wappalyzer.log(error.message, 'driver', 'error'); wappalyzer.log(error.message, 'driver', 'error')
return reject(error.message); return reject(error.message)
} }
return resolve(); return resolve()
}); })
} }
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
const nodes = document.querySelectorAll('[data-i18n]'); const nodes = document.querySelectorAll('[data-i18n]')
Array.prototype.forEach.call(nodes, (node) => { Array.prototype.forEach.call(nodes, (node) => {
node.childNodes[0].nodeValue = browser.i18n.getMessage(node.dataset.i18n); node.childNodes[0].nodeValue = browser.i18n.getMessage(node.dataset.i18n)
}); })
document.querySelector('#github').addEventListener('click', () => { document.querySelector('#github').addEventListener('click', () => {
window.open(wappalyzer.config.githubURL); window.open(wappalyzer.config.githubURL)
}); })
document.querySelector('#twitter').addEventListener('click', () => { document.querySelector('#twitter').addEventListener('click', () => {
window.open(wappalyzer.config.twitterURL); window.open(wappalyzer.config.twitterURL)
}); })
document.querySelector('#wappalyzer').addEventListener('click', () => { document.querySelector('#wappalyzer').addEventListener('click', () => {
window.open(wappalyzer.config.websiteURL); window.open(wappalyzer.config.websiteURL)
}); })
let el; let el
let value; let value
// Upgrade message // Upgrade message
value = await getOption('upgradeMessage', true); value = await getOption('upgradeMessage', true)
el = document.querySelector('#option-upgrade-message'); el = document.querySelector('#option-upgrade-message')
el.checked = value; el.checked = value
el.addEventListener('change', e => setOption('upgradeMessage', e.target.checked)); el.addEventListener('change', (e) =>
setOption('upgradeMessage', e.target.checked)
)
// Dynamic icon // Dynamic icon
value = await getOption('dynamicIcon', true); value = await getOption('dynamicIcon', true)
el = document.querySelector('#option-dynamic-icon'); el = document.querySelector('#option-dynamic-icon')
el.checked = value; el.checked = value
el.addEventListener('change', e => setOption('dynamicIcon', e.target.checked)); el.addEventListener('change', (e) =>
setOption('dynamicIcon', e.target.checked)
)
// Tracking // Tracking
value = await getOption('tracking', true); value = await getOption('tracking', true)
el = document.querySelector('#option-tracking'); el = document.querySelector('#option-tracking')
el.checked = value; el.checked = value
el.addEventListener('change', e => setOption('tracking', e.target.checked)); el.addEventListener('change', (e) => setOption('tracking', e.target.checked))
// Theme Mode // Theme Mode
value = await getOption('themeMode', false); value = await getOption('themeMode', false)
el = document.querySelector('#option-theme-mode'); el = document.querySelector('#option-theme-mode')
el.checked = value; el.checked = value
el.addEventListener('change', e => setOption('themeMode', e.target.checked)); el.addEventListener('change', (e) => setOption('themeMode', e.target.checked))
}); })

@ -4,217 +4,267 @@
/** global: browser */ /** global: browser */
/** global: jsonToDOM */ /** global: jsonToDOM */
let pinnedCategory = null; let pinnedCategory = null
let termsAccepted = false; let termsAccepted = false
const port = browser.runtime.connect({ const port = browser.runtime.connect({
name: 'popup.js', name: 'popup.js'
}); })
function slugify(string) { function slugify(string) {
return string.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/--+/g, '-').replace(/(?:^-|-$)/, ''); return string
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/--+/g, '-')
.replace(/(?:^-|-$)/, '')
} }
function i18n() { function i18n() {
const nodes = document.querySelectorAll('[data-i18n]'); const nodes = document.querySelectorAll('[data-i18n]')
Array.prototype.forEach.call(nodes, (node) => { Array.prototype.forEach.call(nodes, (node) => {
node.innerHTML = browser.i18n.getMessage(node.dataset.i18n); node.innerHTML = browser.i18n.getMessage(node.dataset.i18n)
}); })
} }
function replaceDom(domTemplate) { function replaceDom(domTemplate) {
const container = document.getElementsByClassName('container')[0]; const container = document.getElementsByClassName('container')[0]
while (container.firstChild) { while (container.firstChild) {
container.removeChild(container.firstChild); container.removeChild(container.firstChild)
} }
container.appendChild(jsonToDOM(domTemplate, document, {})); container.appendChild(jsonToDOM(domTemplate, document, {}))
i18n(); i18n()
Array.from(document.querySelectorAll('.detected__category-pin-wrapper')).forEach((pin) => { Array.from(
document.querySelectorAll('.detected__category-pin-wrapper')
).forEach((pin) => {
pin.addEventListener('click', () => { pin.addEventListener('click', () => {
const categoryId = parseInt(pin.dataset.categoryId, 10); const categoryId = parseInt(pin.dataset.categoryId, 10)
if (categoryId === pinnedCategory) { if (categoryId === pinnedCategory) {
pin.className = 'detected__category-pin-wrapper'; pin.className = 'detected__category-pin-wrapper'
pinnedCategory = null; pinnedCategory = null
} else { } else {
const active = document.querySelector('.detected__category-pin-wrapper--active'); const active = document.querySelector(
'.detected__category-pin-wrapper--active'
)
if (active) { if (active) {
active.className = 'detected__category-pin-wrapper'; active.className = 'detected__category-pin-wrapper'
} }
pin.className = 'detected__category-pin-wrapper detected__category-pin-wrapper--active'; pin.className =
'detected__category-pin-wrapper detected__category-pin-wrapper--active'
pinnedCategory = categoryId; pinnedCategory = categoryId
} }
port.postMessage({ port.postMessage({
id: 'set_option', id: 'set_option',
key: 'pinnedCategory', key: 'pinnedCategory',
value: pinnedCategory, value: pinnedCategory
}); })
}); })
}); })
} }
function replaceDomWhenReady(dom) { function replaceDomWhenReady(dom) {
if (/complete|interactive|loaded/.test(document.readyState)) { if (/complete|interactive|loaded/.test(document.readyState)) {
replaceDom(dom); replaceDom(dom)
} else { } else {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
replaceDom(dom); replaceDom(dom)
}); })
} }
} }
function appsToDomTemplate(response) { function appsToDomTemplate(response) {
let template = []; let template = []
if (response.tabCache && Object.keys(response.tabCache.detected).length > 0) { if (response.tabCache && Object.keys(response.tabCache.detected).length > 0) {
const categories = {}; const categories = {}
// Group apps by category // Group apps by category
for (const appName in response.tabCache.detected) { for (const appName in response.tabCache.detected) {
response.apps[appName].cats.forEach((cat) => { response.apps[appName].cats.forEach((cat) => {
categories[cat] = categories[cat] || { apps: [] }; categories[cat] = categories[cat] || { apps: [] }
categories[cat].apps[appName] = appName; categories[cat].apps[appName] = appName
}); })
} }
for (const cat in categories) { for (const cat in categories) {
const apps = []; const apps = []
for (const appName in categories[cat].apps) { for (const appName in categories[cat].apps) {
const { confidence, version } = response.tabCache.detected[appName]; const { confidence, version } = response.tabCache.detected[appName]
apps.push( apps.push([
'a',
{
class: 'detected__app',
target: '_blank',
href: `https://www.wappalyzer.com/technologies/${slugify(appName)}`
},
[ [
'a', { 'img',
class: 'detected__app', {
target: '_blank', class: 'detected__app-icon',
href: `https://www.wappalyzer.com/technologies/${slugify(appName)}`, src: `../images/icons/${response.apps[appName].icon ||
}, [ 'default.svg'}`
'img', { }
class: 'detected__app-icon', ],
src: `../images/icons/${response.apps[appName].icon || 'default.svg'}`, [
}, 'span',
], [ {
'span', { class: 'detected__app-name'
class: 'detected__app-name', },
}, appName
appName,
], version ? [
'span', {
class: 'detected__app-version',
},
version,
] : null, confidence < 100 ? [
'span', {
class: 'detected__app-confidence',
},
`${confidence}% sure`,
] : null,
], ],
); version
? [
'span',
{
class: 'detected__app-version'
},
version
]
: null,
confidence < 100
? [
'span',
{
class: 'detected__app-confidence'
},
`${confidence}% sure`
]
: null
])
} }
template.push( template.push([
'div',
{
class: 'detected__category'
},
[ [
'div', { 'div',
class: 'detected__category', {
}, [ class: 'detected__category-name'
'div', { },
class: 'detected__category-name', [
}, [ 'a',
'a', { {
class: 'detected__category-link', class: 'detected__category-link',
target: '_blank', target: '_blank',
href: `https://www.wappalyzer.com/categories/${slugify(response.categories[cat].name)}`, href: `https://www.wappalyzer.com/categories/${slugify(
}, response.categories[cat].name
browser.i18n.getMessage(`categoryName${cat}`), )}`
], [
'span', {
class: `detected__category-pin-wrapper${pinnedCategory == cat ? ' detected__category-pin-wrapper--active' : ''}`,
'data-category-id': cat,
title: browser.i18n.getMessage('categoryPin'),
}, [
'img', {
class: 'detected__category-pin detected__category-pin--active',
src: '../images/pin-active.svg',
},
], [
'img', {
class: 'detected__category-pin detected__category-pin--inactive',
src: '../images/pin.svg',
},
],
],
], [
'div', {
class: 'detected__apps',
}, },
apps, browser.i18n.getMessage(`categoryName${cat}`)
], ],
[
'span',
{
class: `detected__category-pin-wrapper${
pinnedCategory == cat
? ' detected__category-pin-wrapper--active'
: ''
}`,
'data-category-id': cat,
title: browser.i18n.getMessage('categoryPin')
},
[
'img',
{
class: 'detected__category-pin detected__category-pin--active',
src: '../images/pin-active.svg'
}
],
[
'img',
{
class:
'detected__category-pin detected__category-pin--inactive',
src: '../images/pin.svg'
}
]
]
], ],
); [
'div',
{
class: 'detected__apps'
},
apps
]
])
} }
template = [ template = [
'div', { 'div',
class: 'detected', {
class: 'detected'
}, },
template, template
]; ]
} else { } else {
template = [ template = [
'div', { 'div',
class: 'empty', {
class: 'empty'
}, },
[ [
'span', { 'span',
class: 'empty__text', {
class: 'empty__text'
}, },
browser.i18n.getMessage('noAppsDetected'), browser.i18n.getMessage('noAppsDetected')
], ]
]; ]
} }
return template; return template
} }
async function getApps() { async function getApps() {
try { try {
const tabs = await browser.tabs.query({ const tabs = await browser.tabs.query({
active: true, active: true,
currentWindow: true, currentWindow: true
}); })
const url = new URL(tabs[0].url)
document.querySelector(
'.footer__link'
).href = `https://www.wappalyzer.com/alerts/manage?url=${encodeURIComponent(
`${url.protocol}//${url.hostname}`
)}`
port.postMessage({ port.postMessage({
id: 'get_apps', id: 'get_apps',
tab: tabs[0], tab: tabs[0]
}); })
} catch (error) { } catch (error) {
console.error(error); // eslint-disable-line no-console console.error(error) // eslint-disable-line no-console
} }
} }
/** /**
* Async function to update body class based on option. * Async function to update body class based on option.
*/ */
async function getThemeMode() { function getThemeMode() {
try { try {
port.postMessage({ port.postMessage({
id: 'update_theme_mode', id: 'update_theme_mode'
}); })
} catch (error) { } catch (error) {
console.error(error); // eslint-disable-line no-console console.error(error) // eslint-disable-line no-console
} }
} }
@ -224,50 +274,51 @@ async function getThemeMode() {
*/ */
function updateThemeMode(res) { function updateThemeMode(res) {
if (res.hasOwnProperty('themeMode') && res.themeMode !== false) { if (res.hasOwnProperty('themeMode') && res.themeMode !== false) {
document.body.classList.add('theme-mode-sync'); document.body.classList.add('theme-mode-sync')
} }
} }
function displayApps(response) { function displayApps(response) {
pinnedCategory = response.pinnedCategory; // eslint-disable-line prefer-destructuring pinnedCategory = response.pinnedCategory // eslint-disable-line prefer-destructuring
termsAccepted = response.termsAccepted; // eslint-disable-line prefer-destructuring termsAccepted = response.termsAccepted // eslint-disable-line prefer-destructuring
if (termsAccepted) { if (termsAccepted) {
replaceDomWhenReady(appsToDomTemplate(response)); replaceDomWhenReady(appsToDomTemplate(response))
} else { } else {
i18n(); i18n()
const wrapper = document.querySelector('.terms__wrapper'); const wrapper = document.querySelector('.terms__wrapper')
document.querySelector('.terms__accept').addEventListener('click', () => { document.querySelector('.terms__accept').addEventListener('click', () => {
port.postMessage({ port.postMessage({
id: 'set_option', id: 'set_option',
key: 'termsAccepted', key: 'termsAccepted',
value: true, value: true
}); })
wrapper.classList.remove('terms__wrapper--active'); wrapper.classList.remove('terms__wrapper--active')
getApps(); getApps()
}); })
wrapper.classList.add('terms__wrapper--active'); wrapper.classList.add('terms__wrapper--active')
} }
} }
port.onMessage.addListener((message) => { port.onMessage.addListener((message) => {
switch (message.id) { switch (message.id) {
case 'get_apps': case 'get_apps':
displayApps(message.response); displayApps(message.response)
break; break
case 'update_theme_mode': case 'update_theme_mode':
updateThemeMode(message.response); updateThemeMode(message.response)
break; break
default: default:
// Do nothing // Do nothing
} }
}); })
getThemeMode(); getThemeMode()
getApps(); getApps()

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

@ -0,0 +1,67 @@
‰PNG

IHDR @ @ ھiqق gAMA ±ژ|ûQ“ cHRM ‡ Œ R پ@ }y é‹ <ه جs<…w
9iCCPPhotoshop ICC profile HاwTTׇد½wz،ح0R†ق»ہ ز{“^Eaک`(34±!¢EDڑ"HPؤ€رP$VD±$(1ET,oFض®¬¼÷ٍٍûم¬oي³÷¹ûى½دZ ’§/——Kگتًƒ<œéQtى €`€) LVF؛_°{ةثح…‍!r_ًzX¼pسذ3€Nےں¤Yé|پèک ³9,ˆ8%Kگ.¶دٹک—,f%f¾(Aث‰9a
>û,²£کظ©<¶ˆإ9§³Sظbîٌ¶L!Gؤˆ¯ˆ 3¹œ,ك±Fٹ0•+â7âطT3 IlpX‰"61‰ن"âه àH _qـW,àd ؤ—rIKدلst.فشعڑA÷نd¥pأ &+™ةgس]زRس™¼ ïüY2âعزEE¶4µ¶´4432ھPےuَoJـغEzّ¹g­ے‹ي¯üز `ج‰j³َ
€خ- بفûbس8 €¤¨o×؟؛M</‰A؛چ±qVV—أ2ôO‡؟،¯¾g$>îڈٍذ]9ٌLaٹ€.®+-%Mب§g¤3Y؛لں‡ّuAœxںأE„‰¦ŒثKµ›او
¸i<:—÷ںڑّأ‏¤إ¹‰زّPcŒ€شu*@~ي(
 رûإ]ے£o¾ّ0 ~yل*“sےï7gء¥â%ƒً9خ%(„خٍ3÷ؤد Hت@è C`¬€-pnہّƒ VH©€²@ط
A1ط ِ€jPA3hاA'8خƒKà¸nƒû`L€g`¼ a!2Dپن!Hز‡ج d¹A¾P إB Byذf¨ھ،z¨ْ: ‌‡®@ƒذ]h ڑ†~‡قءL©°¬أ ط ِپCàUp¼خ… àp%ـ …;àًَ5ّ6<
?ƒç¢ٹ" ؤٌG¢گx„ڈ¬Gٹگ
¤iE؛>ن&2ٹج oQEG¢lQ¨P µµUھFFu zQ7Qc¨YشG4­ˆضGغ ½ذèt؛]پnB·£/¢o£'ذ¯1
£چ±آxb"1Iکµکج>Lوf3ژ™أb±ٍX}¬ضثؤ
°…ط*ىQىYىvûGؤ©àجpî¸(—ڈ«ہء‌ء
ل&q x)¼&قïڈgمsً¥ّF|7:~؟@گ&hى!„$آ&B%،•pً€ًH$ھ­‰پD.q#±xŒx™8F|K!é‘\Hر$!iééé.é%™Lض";’£بٍr3ùùùچEآHآK-±A¢F¢CbHâ¹$^RSزIrµd®d…ن ةë3Rx)-))¦شz©©“R#RsزiSiéTéé#زW¤§d°2Z2n2l™™ƒ2dئ)EâBaQ6S))T UêEM¢S؟£Pgeed—ة†ةfثضب¥!4-ڑ-…VJ;N¦½[¢´ؤi gةِ%­K†جث-•s”مبةµةف–{'O—w“Oك%ك)ےP¥ §¨گ¥°_ل¢آجRêRغ¥¬¥EKڈ/½§+ê))®U<¨ط¯8§¤¬ن،”®T¥tAiF™¦ى¨œ¤\®|FyZ…¢b¯آU)W9«ٍ”.Kw¢§ذ+é½ôYUEUOU،j½ê€êڑ¶Z¨Z¾ZعCu:C=^½\½G}VCEأO#O£Eم&^“،™¨¹W³Os^K[+\k«V§ض”¶œ¶—v®vِ²ژƒخ]†n²î>ف…^¢^چقu}XكRں«؟Oذ m`mہ3h01$:fژرŒ|چٍچ:چ‍kGï2î3hbabزhrكTئشغ4ك´غôw3=3Yچظ-s²¹»ùَ.َثô—qي_vاbلg±ص¢ا⃥•%ك²صrعJأ*ضھضj„Ae0J—­رضخض¬OY؟µ±´ط·ùحضذ6ظِˆيشrيهœهچثايشىکvُv£ِtûXûِ£ھL‡اژêژlا&اI']§$§£NدMœùخيخَ.6.ë\خ¹"®®E®n2n،nصnڈـصــ[ـg=,<ضzœَD{ْxîٌٍRٍby5{حz[y¯َîُ!ùûTû<ِصَهûvûء~ق~»‎¬ذ\ء[رéü½üwû? ذXًc &0 °&ًIگiP^P_0%8&ّHًëçگزگû،:،آذ0ة°è°و°ùp×ً²ًرمˆu×""¹‘]Qط¨°¨¦¨¹•n+÷¬œˆ¶ˆ.Œ^¥½*{ص•ص
«SVںژŒaئœˆEاا‰}دôg60çâ¼âjمfY.¬½¬glGv9{ڑcا)مLئغإ—إO%ط%ىNکNtH¬Hœل؛p«¹/<“ê’و“‎“%J OiKإ¥ئ¦نةًy½iتiظiƒéْé…é£klضىY3ث÷ل7e@«2؛TردT؟PG¸E8iںY“ù&+,ëD¶t6/»?G/g{خd®{î·kQkYk{ٍTَ6هچ­sZW؟Z·¾gƒْ†
=6قDط”¼é§|“ü²üWأ7w(l,كâ±¥¥P¢گ_8²صvkف6ش6î¶پيوغ«¶,b]-6)®(~_آ*¹ْچé7•ك|ع؟c ش²tےNجNقخل]»—I—هچïِغفQN//*µ'fد•ٹeu{ {…{G+}+»ھ4ھvV½¯N¬¾]م\سV«X»½v~{كذ~ا­uJuإuïpـ©÷¨ïhذj¨8ˆ9کyًIcXcك·Œoڑٹ›>â=t¸·ظھ¹ùˆâز¸Eط2}4ْèچï\؟ëj5l­o£µا„ا‍~ûًqںم=''Zذü،¶ز^شuنtجv&vژvEv
‍ô>ظسmغف£رڈ‡N©ھ9-{؛ô لLء™OgsدخK?7s>لüxOLد ِ\ô¹xùû¥ }N}g/غ]>uإوتة«Œ«×,¯uô[ô·ےdٌSû€ه@اu«ë]7¬ot.<3ن0t¦ëحK·¼n]»½âِàpèًèر;ى;SwS—yoلئèE¥V<R|شً³îدm££§ا\اْ?¾?خِKئ/ï'
گںTLھL6O™Mڑvں¾ٌtهس‰géدf
•‏µِ¹خَ~sü­6bvâےإ§كK^ت؟<ôjظ«¹€¹G¯S_/ج½sّ-مmك»ًw“ Yï±ï+?è~èèٌَء§شOںکَü؛ؤèس pHYs ؤ ؤ•+ عIDATx^ح[ |ُµ‏î‍››ـى I » ˆ
طâB]QiںKفjë£ض®V­è³j،د­tء}أbفµ( dب¾çوîû¼ïجL ‰ $¶ïم7خ½ے™¹َ?çخw¾3
پے \l([‰¢Œqکگw>ْïاہ9@~ئ`@\‰أh0êƒفcëرڈًèعaTŒh
شمت ?ء-سے€K¢~Fg(üMَ@VŒخ™ھڈ N<س“Aٌ§pGع°rامّpےَhُص#
ˆx±؟n3جü—2 #³خؤضٹڈ±§fƒ~††X<Fgئًقe¸uùéXüر5=U!‎„آہ8`ح;@rœ–,ک|Rmé¸ْإA¸éُQtبcً[‰i3£"ٌ0ŒF“êqه^s”î
6م¹MwcîR3mü%»bFک «üطص L
\sًآj:ء©h¸ےK±£z5,¦\6n!®ڑّ 8­bXwl¦D:&ˆٌù³pëŒGگbدآ‡{ںإٍ/# `pêHüٌڑ/ô_k‡أغ5P®ب×؟÷¦ َ©aف{ہRࢫ Mشp¨kQي.C¯eچ;±‏ذ
ن;G`\î9بJ*@Uغ حœ€›¦=„Œؤ<ص)—¼ ‡56O_U ³ةھےڑ†Xـ€7jـسس;;ô?~=ہ/ûڑعqد;sQï­„‰!‰‡pُؤ_âزqے­Wغ‡ا×€ـPŒشؤ,4yھٌسYئŒa—ëgtئOِ¸°ô€ٌ«‡è#§ژq@cذعؤ_ں1hcفàاç=CBôءfNdطغٌِîgًC]"Q.غôk2üں
_طچ±ƒfâŒءكRڈw‡";ù#اٍڑ€>rêèں6¯صتںغإطŒéƒ_ا çP\4و×±ش%qDء'%/©ج؟¾ôïـ¯‡أJH€.\>لاH´تyفأبr+œ¹¬ج§ڈœ:ْو Yeaْ÷—صGôAâànحêXد™d1ظ0kؤ÷T}epXSà µàحO¢ّب»je°­Œ?
سNG~تتîQ¤³چTy¢¬!ع}}ر8V2"ٍbKkك«Dïhkقz ّë#ہَK¸¨,×ژµ4jb(J:œ ©ِLL.کKàQ§œhu¢¦­ص®R~NQدٌ…ـک^4ة ™ê÷‍ً…+B¯لژا±¦Q3ِù£>|k ~½£7r_ـش7ءذ»ِî >yHb‰ث ى,ضژإإz 9ّى_عXpع30½ًU´أFه'DڈüRگ0,s"¹"A=قé ًFٌذA/‌ثپ4‡ٌü?<±‍£²½; ب< p¦§xپظ„9پh”yةGgسمoعù'@vr!چs¨!كqٹ،$F…è„ل0چe¼«)bْU…bh£لح~¦•وp\¢£ڑم
 w  ;
¨«$0ن…ٌs è³ے²YèI•â5ْEفأNqس®
;Bشa¶³ˆ$éذG؛اے”z:\:^nي
+dئجL.گ‡G8Hما$1ـء9ِقPDـْ+à¼yہک‰ہآûپة3ُƒ9Tf<±¦rp…EےGba&ژQeِ½DEzâ µT¯•¸وœ6ينeش¤pإçؤE‰Hم±;ئ§àîرةع½ wHgwْ™ہOGEَ؟ہ·¯àىع±Œي8ة6®@€dّآcع±n`6Y،&T´P °ٹ[Eë~n%8ز²ڈüذآUصع
~ص¦¥œھ;xK~b×>Oد°لط5+ دLHEژDIذ»”\—ذ.
ٌ†*¼^(نcءpض££@+'¢:B~ھٹ%ٌ™•ZعtAT‰ ؛ُ <•j4 fHع+HS$)’éب‡ةط}è^مo;™nâ pO»÷4'غŒطجتàچ*H4گاïg¥Y1‡Né
=; ٍ0ًٍ“شùù…Rèh§rمق`¦‡9y•tdXVFf~»ةغ"K®چ
>¨…OH­}eeüç4(ش
 œ8ç ³°pB£6~ں”iإ¦ؤّdخµtم ~½çf»دةZ$$!;ظغ™RنôvGبيT»uم²:¢¦حِ366cs­ںsèê4ن?Dûd!¤S¢0R QJ!™aإ¾¹ظب²v^”خّّ-زىmˆںŒ#ئ"‍گ£¯
ٌ²0î£گٍ—™K6;؟T…ــح<µ²K{ٌ}°ےک]ـˆُet أ؛ë-ڈA¦#FK9ن.…+i
#&ùٹ=ق:ىاeEIxç,êفcا°ڑئèل9—ھ_»E9سaلN†wf¶îˆإAآ ,خù¼ں—÷àYq/ه4¤¥Sزp[Aد•¤>إ*fèُ¹f5{Uxث]H*^é#Dِ.¾¨(£#X $M"”§‡¾>¥^ّ†0a]=ِشرةةذ !ؤ§¦¥مخآ‍¨Nض°’ه©صزôiµْ¥دب¸ég@”¶~® سن4^°{Nئ âêٹ”¸ؤ0پU!¼  ïئ ژ¬>خ®ُJظںvتا“Gظ~E¹`„¢ôژH4¤„"uژ¹Qش*”Bqيxoµ؛^ءëG•9إMْبI`ثo¥y·EQژqہع©ث1ëَ0Y»0mمJ¹کpRن.é™طك°د}~7%¯ءˆV“•D­`ءّQ™x%¬0c\²‘ٹذ€$¤_أي{\ّثّ“Hغv<ثٹvc-‰Z{~yجû»î’L}ُ"{¸éI!ؤ\}ç5ë?5r¥€آص?ؤڑqVl<ô.ح]†·ـڈ™أوcsùûبIH@rل]¸ًمF
ب¢ژ؟$غ†[sيکب6 ّˆ/JَB™“.’ؤ×Feأù+”w®Sچ~%ïc<v€؟ع£„Zƒع—%w+ت#؟ذ> jeل|إ³ï ¥1ع¬4ûj•—ٹïW¶W®VZ
J ش¬x¢ٹ2tUٍfچ_y·ض¯¼Xه´Z
9ةRيَة"âW”ح÷*تsE)}Cشpl©c3à™†`_j×V"شHrë|‡غ@âبs{Pو!mً‰G)¨×> $ه‰TS5îDK wSك$"ءڑژ+¼Hg){¸ج‡[¶µâ¦üD<y™ئض¨"q}<ں•çco©6ضّHًإw±ڈ¦<·°lذpى±¸پ"ئeGسْ*ک“¬h¼ق2ٹ&¯"ƒEسک`Rدëˆ5ïذhbثq pؤچŒsَa(ك®5G:ٌ(uءî/¨Nک³ç]Œْ¶rl>ٍ|،Vُ`vRŒ¶ءx؛ـ عڈ;¸<كژµµAـXگJ{¶àç_ـ'eصaخ=G@Oً°O©فہkط½6s.1¦نکy³iثqë¤إذƒڈoCغِ&Xط_اXnbق¬tŒsl:ىNXسlى8#:$cz.ک2بڑ5ِ\b،¨
ٹحژ،×¼ƒؤ—¥à¤ھٌ=CA%‎ئ–QDxbں›و$bz¢r-‰êYàJJô0ù¤ëCS*
نڑ*ھ‰کْbj’‍ô,Qےخ*6¥wr€ ـؤ¾ٹn ہڑڑ N>‰#Nءم&§LFr‰3چئ5•*¯ؤad_X„عwث, ؟c§ژ6D1?أGپEزعHلu چت™¦%ؤ¨*vھق
.ُˆ§7سIRپؤ)“îe[»~لq¨ًW¸لعق Kھ
gç1ô]8ü§]7aIOàIعةع“;
ژ#êcىâ³4,5ïزمtŒ%ظدءV »}vء@ءu 8ّ*!تoy`jثàFSFXZaç¸ص­5k9/Tه|½ن€آث€sںZKک»ƒةGعCc,إ،'w ٍچ8ْز>T.? çک ½m<œtآٌ7¼عîصژKBCF¤-q¦P_چwùآّ×—صhlcnِ†TzضbîGq•¹عIjعثvJq‰2پ©&«ق‏*-Ni.ئçںدv„\اtّb»ـ‡¹ے
ûِ>„é¶I7<ط¸®ِA(4ت_îFٍکt8Oد€s|yAپ¯¼Mحûژaنœ?ضL;G¦"}Z.÷iْر!خ|cأüîچفx}}9ض&IOڈگ÷YS¸ْ)ہگ 8 S€]§ڈعقـللGœ،< z%p&چَ7œé±[مژ9هèl.~
£ے¨iءB#¹IJج ¢\فمfëà€8Fد ک-}O+7ءه×ل—¾TW؟<³zg
½²Cy5S©7ب#َQك×؟¾6;Fgئ‏ˆآبGبµFع*)ؤJ$0و]6¾ز6•à"ma5Œsو
لہ¶¢mO¯1wZ}ùD&ذ¾ôn«آE¬ئ]/ذًي5ꓬT;’ىd¥طو=×ىھأ¢Wwà[÷}‚هںض¯ى
2“n/i a ث¦Woذؤ!R=b,ح>bِ™ê°Jµ£qul ç!×ژFB~2J~؟قR‰حتhزŒWלى/هR4آدجFت8فأفà³½ُxàµ](op£µ³د°wy"س،HŒع ٹLéگL~5,وں]¨يءfٍء=$ءح my»شaQâىC$E
€َ_،Sىjîاkضأک2کF. ·+ƒءz ش سٹٹ×JPûقaX3Xh¼pCشدqI´F8
شëf`¼ْ¹+J*غpثسPرàC«7چمû
qD C
S0;%ڈق<Nîو"ü\هدXعj×Y!ج4ز(a®G…شçگ2w80w95Lsٍ@b®zت1´أ_هإî;ضآت¾[آ>ـRW<mع ¦FىŒ#yىشجج}÷ي¨gN/X²{ڈ؛hDV³‰]ديگ>'q„l6 ™N^هLLظ!⢬ صëXقôع/%®e¯v?)™¢¤†ûط‎چ؛کLِoottr€ً@écغà9à"ëبôن\P€ü«F02l\‎îأ׈à{ڈnہ†¼J+چ¶v©پpD8¬½ .جIئكï:§êGVYفنY K_”ط¸…زû):d§¶زٍ²إحلٍM {†tr@ˆ5ëُے¢ض`è3Fف=U-q=پüs¬طXخfئ¢و·…1P†wE4Gگژ8,
+ï™…¢A'x$Iتں4Dآش(³)§¥)زqجى~Qُت^T­= 6
'ہفŒgق)پحfb®عXأ؟9أ»B}ٹہم cو„¼uïVو~OطBjE¸ژMءq„cطٍفTasو³sصƒفAJصïںكئ#Ae8`îہےnبج]ق ن©‹خ+آٍ_ںƒ4.F·ط»”Uàv„·±üké|ج!6Aإكyç®؟†حس×suضو—×”!•­r”U،¸¤ATدUےdه?¹³pC½+€8qح¼Qَّآiبpvز—,:C ­:@<ذ°ê(<û[0âgا»8!ں—>9„;‏¼V?ئژثءشQ™ppُح,56àPµG•° }Mفçذكôث¥C­a%ûŒغوڈءƒ×MB^z—w/³o¸ڑـàذتھو Nèً²¯0üGمصءvl9ط„—|†²7)6ؤ(حl…'چHGAf"RV4P*o¤÷“œنïy;¦DŒn‰َ÷إfyَ«½رس آھ¸D>‰#ؤظ‹‏:Ynûù8“ة/('uèQê)ڑZِ §1ن>ي´Ys¨ڑ؛ءژأ-X¸t Jھ]œ”ق`…Y2q؛ڑNêپي‡Z°·آ…™ZقثoZX
¥*XLLîé;و¨™t¦“Q$¯تهOc،8فA´ùBŒ:حiBtF طf†8W BD†÷-جq`ة-Spل$qZWىcˆ³5َؤü–ث‍ں¾ْ¨nxü3|؛§‍قŒھBQ‰S†g 7فث¦pأِ2تS.g:چœ79“y| ;½‚¬$ص9m”F‰پrg‰ ±ةLgظD8±”6{B(­ٹ6¬ظQƒüىçu­ق0¼شVٍژˆ#3ï¯U„¨*‏ٍس¸مزر¸b¶د'B ٌX“tŒ{C“;„ï.^i§eaTA*'7ىçٹ7·0ڑ!6oت`ŒبMئl†] H&هٍE°«¼_ھ6T4ْک£Axl؛ôصRمگ¢+Nحوuy‰8mp
ئ¥`وiظHI²1ڈي8\ëءgûêٌو¦J2G¼ً3­
™ژW°g¸ءxص9'ƒ>; ،چ2÷ء58oB.؛Yë¤‏ûa¾=1sh¸ƒ“©lٍمق—·£xں<ں سسfµIlل
[¸jF}kدl!/y¾"a,Q"،e…W[¢©hx:.b/pي¬"dبS§@om:ٹOwصâL.ب¢ش(<œ„ّîo×0²ٌ‡vںî{ae/?ydrط¼¼ٍI)rطحI¥P­نْtھcع¯مô$ü%ô£>fv§×حٹ—~رثSل>¢{FéRëµ|m_·مXْ^‰6- H`'d)F{OئxA§ëxكDFPNھùCRشٌe¦كZ®@ ïà™Fوj´€±3Omœنٌr§@بOت&Uہة4H wک}vہعµ8Pçإغھًلض*صطمّ†¬îيژ(ôة¥Bùè êY ِ²4ُ£ƒ¨néغk³vHâˆ9¶écQ]اے‌è“ن©ژ”Auv#BJQ
 ©89YQغ×^_C‡U×{Q]مAuµ[?·،J>ث£«؛رOM/
yîط³S&ْTd2O½»÷½؛SUvY8×خھ نق°ê0†ëf‘°,xâ»9Jكئb(ٹ‰Cس0ap2Fن$©df¥hùثهêح97إOe³‡è¤½ى/¶—µ •%•7„چJ3•”ر•c€÷Zُذ·1%°؟èstsU¾¢ًْ}…ڈ‌¤×ژ¼ےپfFب¼©y¬é5b~<w8Fسّ,§V*جî8ھمh
ؤص؟ï]گl5"[¶ح€‡ £)·ùm¬÷غ5ل½يux«¸؛ ‹سنwà¬ر'‏³ْ¾ د8¬³ں…آٍ·êپ9کآFIzƒصچ¼yط‡]^ê}ژأMپمcꄤ_èpW3CءئبJ¢XJ±گ!ï(‰§B;¾•mEuS eµn<üv V± ®Zz. شî/ْي€P$ٹدvصaئˆ4¸ىّù¦zى)u£پB®‰\،‏ ؤ؛تvrEû¾ d2ظ«فOcةث´™گةk‡:ًبظ9ںbD;½DFU.esرoبهBzù¯،fںü$WO^èث‹~ꃈهNtژœ&>—°ˆٍCˆژdة_~Zکyے¼e.زم6éض|Q¼خص_Qوإî£>ہإتA'¨NظK4¨؟س.5d“h،­ê^ rPوظ1¯( × Kئ¹yٍ™°¯گë و€î، ؤAqچ%-!”{#ـ³“s‡لVے¤•·fî'3ZF¦XQؤmX²£س¬کک•€)ظlpئخٌ
;àے;€ےز؟؛ُO— IEND®B`

@ -1,14 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Primary_Art" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 240 280" style="enable-background:new 0 0 240 280;" xml:space="preserve"> <svg version="1.1" id="Primary_Art" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
<style type="text/css"> y="0px" viewBox="0 0 438.7 512" style="enable-background:new 0 0 438.7 512;" xml:space="preserve">
.st0{fill:#550088;} <style type="text/css">
</style> .st0{fill:url(#SVGID_1_);}
<path class="st0" d="M146.4,18.7c45.3,0,69.4,21,69.4,36.2c0,1.1-0.4,1.7-0.7,2l-0.3,0.3l-0.3,0.3l-29.9,30.1l-19.9,20l26.1,10.5 </style>
c22.1,8.9,30.7,20.8,30.7,42.4c0,9.3-3.6,17.6-10.8,24.7l-54.4,54.6l-0.2,0.2l-0.2,0.2c-12.7,13.5-36.1,21.2-64,21.2 <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="219.3559" y1="514" x2="219.3559" y2="2" gradientTransform="matrix(1 0 0 -1 0 514)">
c-47.2,0-73.3-20.4-73.3-34.4c0-1.8,0.4-2.9,0.7-3.2l0.4-0.3L20,223l32.1-32.2l0.4-0.4l0.1-0.1L70,172.1l-22.8-11.3 <stop offset="0" style="stop-color:#550088"/>
c-17.3-8.5-24.3-20.1-24.3-39.9c0-10.3,4-19.5,12-27.4l54.4-54.6l0.1-0.1l0.1-0.1C101.7,26,122.5,18.7,146.4,18.7 M146.4,0 <stop offset="1" style="stop-color:#FF7782"/>
c-30,0-55,9.8-70.3,25.7L21.7,80.3C10.7,91.2,4.3,105.2,4.3,120.9c0,31.2,15.1,47,34.7,56.7c0,0,0,0,0,0L6.9,209.8 </linearGradient>
c-4.4,4-6.9,9.8-6.9,17.1C0,253,34.4,280,91.9,280c34,0,61.6-10.2,77.5-27.1l54.4-54.6c10.4-10.3,16.2-23.2,16.2-37.9 <path class="st0" d="M267.5,34.2c82.8,0,127,38.4,127,66.2c0,2-0.8,3.1-1.4,3.6l-0.6,0.5l-0.6,0.6l-54.7,55l-36.3,36.5l47.7,19.2
c0-34.8-19.1-50.3-42.3-59.7l29.9-30.1c4.3-3.9,6.9-9.4,6.9-15.8C234.5,27.9,201.5,0,146.4,0L146.4,0z" /> c40.3,16.3,56.1,38,56.1,77.5c0,17-6.6,32.2-19.7,45.2l-99.6,99.9l-0.4,0.4l-0.3,0.4c-23.3,24.7-65.9,38.8-116.9,38.8
c-86.3,0-134-37.2-134-63c0-3.2,0.6-5.3,1.3-5.9l0.6-0.6l0.6-0.6l58.7-58.9l0.8-0.8l0.3-0.3l31.7-33.2L86,294.2
c-31.6-15.5-44.4-36.6-44.4-73c0-18.8,7.3-35.6,22-50l99.5-99.9l0.3-0.3l0.3-0.3C185.8,47.5,223.6,34.2,267.5,34.2 M267.5,0
c-54.7,0-100.5,17.9-128.4,47.1L39.6,147c-20.2,19.8-31.9,45.4-31.9,74.2c0,57.1,27.5,86,63.5,103.7l0,0l-58.7,58.9
C4.5,391,0,401.7,0,414.9C0,462.6,62.9,512,168,512c62.2,0,112.6-18.7,141.6-49.5l99.5-99.9c19-18.8,29.6-42.5,29.6-69.3
c0-63.6-35-92-77.4-109.2l54.7-55c7.8-7.1,12.5-17.2,12.5-28.9C428.3,51,368.1,0,267.5,0L267.5,0z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 5.12 5.12" preserveAspectRatio="xMinYMin meet"><path d="M4.12 5.12H.968a.49.49 0 0 1-.488-.488V.488A.49.49 0 0 1 .968 0H4.12a.49.49 0 0 1 .488.488v4.144a.49.49 0 0 1-.488.488z" fill="#6762a6"/><path d="M3.068 4.415V2.382s.132-.487-1.63.2C1.436 2.6 1.436.7 1.436.7L2.01.697v1.2s1.61-.635 1.61.48v2.026h-.555zm.328-2.986h-.6c.22-.27.42-.73.42-.73h.63s-.108.3-.44.73zm-1.95 2.982V3.254l.58.58-.58.58z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@ -0,0 +1,23 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M10.9428 27.0767V16.6626L2.6115 23.1714L10.9428 27.0767Z" fill="white"/>
<path d="M10.9428 16.6626L10.3017 15.8419C10.6154 15.5968 11.0415 15.552 11.3993 15.7266C11.7572 15.9011 11.9842 16.2644 11.9842 16.6626H10.9428ZM10.9428 27.0767H11.9842C11.9842 27.433 11.8021 27.7645 11.5015 27.9556C11.2008 28.1467 10.8234 28.1709 10.5008 28.0197L10.9428 27.0767ZM2.6115 23.1714L2.16949 24.1144C1.84174 23.9608 1.61728 23.648 1.57667 23.2883C1.53605 22.9286 1.68512 22.5736 1.97036 22.3508L2.6115 23.1714ZM11.9842 16.6626V27.0767H9.9014V16.6626H11.9842ZM10.5008 28.0197L2.16949 24.1144L3.05351 22.2285L11.3848 26.1338L10.5008 28.0197ZM1.97036 22.3508L10.3017 15.8419L11.5839 17.4833L3.25264 23.9921L1.97036 22.3508Z" fill="#121212"/>
<path d="M16.7012 2.85818C17.4919 2.51833 18.5477 2.64976 19.6967 3.41652C20.8358 4.17674 21.9421 5.4954 22.6821 7.21699C23.422 8.93858 23.6175 10.6487 23.3852 11.9984C23.151 13.3597 22.5198 14.2162 21.7291 14.556C20.9384 14.8959 19.8825 14.7645 18.7336 13.9977C17.5944 13.2375 16.4882 11.9188 15.7482 10.1972C15.0082 8.47565 14.8127 6.76553 15.045 5.41585C15.2793 4.05456 15.9105 3.19804 16.7012 2.85818Z" fill="white" stroke="#121212" stroke-width="2.08283"/>
<path d="M7.79719 22.7012C8.92729 27.6627 14.6963 31.0028 21.5595 29.5902C28.4704 28.1678 32.6485 22.7322 31.5271 17.8091C30.4083 12.8972 24.2647 9.59512 17.3728 10.9929C9.48547 13.4136 6.83734 18.4871 7.79719 22.7012Z" fill="#FFC700"/>
<path d="M7.79719 22.7012C8.92729 27.6627 14.6963 31.0028 21.5595 29.5902C28.4704 28.1678 32.6485 22.7322 31.5271 17.8091C30.4083 12.8972 24.2647 9.59512 17.3728 10.9929C9.48547 13.4136 6.83734 18.4871 7.79719 22.7012Z" fill="url(#paint0_linear)"/>
<path d="M7.79719 22.7012C8.92729 27.6627 14.6963 31.0028 21.5595 29.5902C28.4704 28.1678 32.6485 22.7322 31.5271 17.8091C30.4083 12.8972 24.2647 9.59512 17.3728 10.9929C9.48547 13.4136 6.83734 18.4871 7.79719 22.7012Z" stroke="#121212" stroke-width="2.08283"/>
<path d="M21.2196 28.5301C21.7912 28.4661 22.316 28.8765 22.3919 29.4467C22.4678 30.0168 22.0659 30.5308 21.4943 30.5947L21.2196 28.5301ZM18.8098 10.4233C19.3813 10.3593 19.9062 10.7697 19.9821 11.3398C20.058 11.91 19.6561 12.424 19.0845 12.4879L18.8098 10.4233ZM13.0192 21.2333C13.3245 23.5277 14.3052 25.4821 15.7219 26.7876C17.1229 28.0784 18.9935 28.7789 21.2196 28.5301L21.4943 30.5947C18.656 30.912 16.173 30.0059 14.32 28.2985C12.4828 26.6057 11.3093 24.1703 10.9493 21.4647L13.0192 21.2333ZM19.0845 12.4879C16.8458 12.7382 15.21 13.8043 14.1894 15.3278C13.1578 16.8677 12.714 18.9401 13.0192 21.2333L10.9493 21.4647C10.589 18.7579 11.0945 16.1667 12.4446 14.1514C13.8057 12.1198 15.984 10.7392 18.8098 10.4233L19.0845 12.4879Z" fill="#121212"/>
<path d="M25.6649 26.9586C26.2039 26.7629 26.8093 27.0405 27.017 27.5787C27.2248 28.1169 26.9562 28.7118 26.4172 28.9075L25.6649 26.9586ZM21.8331 9.74C22.3964 9.62405 22.9566 9.98467 23.0844 10.5455C23.2121 11.1063 22.859 11.6549 22.2956 11.7708L21.8331 9.74ZM17.868 20.8515C18.209 22.3485 18.898 24.4201 20.1465 25.8308C20.755 26.5183 21.4707 27.02 22.325 27.2504C23.1764 27.48 24.2647 27.467 25.6649 26.9586L26.4172 28.9075C24.6943 29.5331 23.1599 29.6143 21.8119 29.2507C20.4669 28.888 19.4067 28.1093 18.5856 27.1815C16.975 25.3617 16.1906 22.8635 15.828 21.2714L17.868 20.8515ZM22.2956 11.7708C19.0532 12.4382 16.8401 16.3384 17.868 20.8515L15.828 21.2714C14.6372 16.0434 17.0949 10.7152 21.8331 9.74L22.2956 11.7708Z" fill="#121212"/>
<path d="M7.33097 6.6153C6.83401 7.31796 6.74283 8.37802 7.25369 9.66138C7.7602 10.9338 8.81976 12.2903 10.3497 13.3723C11.8796 14.4544 13.5116 15.0015 14.88 15.0552C16.2603 15.1093 17.2294 14.6702 17.7264 13.9675C18.2233 13.2649 18.3145 12.2048 17.8036 10.9214C17.2971 9.64902 16.2376 8.29253 14.7077 7.21049C13.1778 6.12845 11.5458 5.58133 10.1773 5.52765C8.79707 5.47351 7.82793 5.91264 7.33097 6.6153Z" fill="white" stroke="#121212" stroke-width="2.08283"/>
<circle r="1.04141" transform="matrix(-0.977353 0.211615 0.211615 0.977353 24.7067 17.4785)" fill="#121212" stroke="#121212" stroke-width="1.04141"/>
</g>
<defs>
<linearGradient id="paint0_linear" x1="17.0944" y1="9.9871" x2="23.0133" y2="30.3251" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFE073"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
<clipPath id="clip0">
<path d="M34.8953 0H0.499989V34.3953H34.8953V0Z" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

@ -8,130 +8,137 @@
const validation = { const validation = {
hostname: /(www.)?((.+?)\.(([a-z]{2,3}\.)?[a-z]{2,6}))$/, hostname: /(www.)?((.+?)\.(([a-z]{2,3}\.)?[a-z]{2,6}))$/,
hostnameBlacklist: /((local|dev(elopment)?|stag(e|ing)?|test(ing)?|demo(shop)?|admin|google|cache)\.|\/admin|\.local)/, hostnameBlacklist: /((local|dev(elopment)?|stag(e|ing)?|test(ing)?|demo(shop)?|admin|google|cache)\.|\/admin|\.local)/
}; }
/** /**
* Enclose string in array * Enclose string in array
*/ */
function asArray(value) { function asArray(value) {
return value instanceof Array ? value : [value]; return Array.isArray(value) ? value : [value]
} }
/** /**
* *
*/ */
function asyncForEach(iterable, iterator) { function asyncForEach(iterable, iterator) {
return Promise.all((iterable || []) return Promise.all(
.map(item => new Promise(resolve => setTimeout(() => resolve(iterator(item)), 1)))); (iterable || []).map(
(item) =>
new Promise((resolve) => setTimeout(() => resolve(iterator(item)), 1))
)
)
} }
/** /**
* Mark application as detected, set confidence and version * Mark application as detected, set confidence and version
*/ */
function addDetected(app, pattern, type, value, key) { function addDetected(app, pattern, type, value, key) {
app.detected = true; app.detected = true
// Set confidence level // Set confidence level
app.confidence[`${type} ${key ? `${key} ` : ''}${pattern.regex}`] = pattern.confidence === undefined ? 100 : parseInt(pattern.confidence, 10); app.confidence[`${type} ${key ? `${key} ` : ''}${pattern.regex}`] =
pattern.confidence === undefined ? 100 : parseInt(pattern.confidence, 10)
// Detect version number // Detect version number
if (pattern.version) { if (pattern.version) {
const versions = []; const versions = []
const matches = pattern.regex.exec(value); const matches = pattern.regex.exec(value)
let { version } = pattern; let { version } = pattern
if (matches) { if (matches) {
matches.forEach((match, i) => { matches.forEach((match, i) => {
// Parse ternary operator // Parse ternary operator
const ternary = new RegExp(`\\\\${i}\\?([^:]+):(.*)$`).exec(version); const ternary = new RegExp(`\\\\${i}\\?([^:]+):(.*)$`).exec(version)
if (ternary && ternary.length === 3) { if (ternary && ternary.length === 3) {
version = version.replace(ternary[0], match ? ternary[1] : ternary[2]); version = version.replace(ternary[0], match ? ternary[1] : ternary[2])
} }
// Replace back references // Replace back references
version = version.trim().replace(new RegExp(`\\\\${i}`, 'g'), match || ''); version = version
}); .trim()
.replace(new RegExp(`\\\\${i}`, 'g'), match || '')
})
if (version && versions.indexOf(version) === -1) { if (version && !versions.includes(version)) {
versions.push(version); versions.push(version)
} }
if (versions.length) { if (versions.length) {
// Use the longest detected version number // Use the longest detected version number
app.version = versions.reduce((a, b) => (a.length > b.length ? a : b)); app.version = versions.reduce((a, b) => (a.length > b.length ? a : b))
} }
} }
} }
} }
function resolveExcludes(apps, detected) { function resolveExcludes(apps, detected) {
const excludes = []; const excludes = []
const detectedApps = Object.assign({}, apps, detected); const detectedApps = Object.assign({}, apps, detected)
// Exclude app in detected apps only // Exclude app in detected apps only
Object.keys(detectedApps).forEach((appName) => { Object.keys(detectedApps).forEach((appName) => {
const app = detectedApps[appName]; const app = detectedApps[appName]
if (app.props.excludes) { if (app.props.excludes) {
asArray(app.props.excludes).forEach((excluded) => { asArray(app.props.excludes).forEach((excluded) => {
excludes.push(excluded); excludes.push(excluded)
}); })
} }
}); })
// Remove excluded applications // Remove excluded applications
Object.keys(apps).forEach((appName) => { Object.keys(apps).forEach((appName) => {
if (excludes.indexOf(appName) > -1) { if (excludes.includes(appName)) {
delete apps[appName]; delete apps[appName]
} }
}); })
} }
class Application { class Application {
constructor(name, props, detected) { constructor(name, props, detected) {
this.confidence = {}; this.confidence = {}
this.confidenceTotal = 0; this.confidenceTotal = 0
this.detected = Boolean(detected); this.detected = Boolean(detected)
this.excludes = []; this.excludes = []
this.name = name; this.name = name
this.props = props; this.props = props
this.version = ''; this.version = ''
} }
/** /**
* Calculate confidence total * Calculate confidence total
*/ */
getConfidence() { getConfidence() {
let total = 0; let total = 0
Object.keys(this.confidence).forEach((id) => { Object.keys(this.confidence).forEach((id) => {
total += this.confidence[id]; total += this.confidence[id]
}); })
this.confidenceTotal = Math.min(total, 100); this.confidenceTotal = Math.min(total, 100)
return this.confidenceTotal; return this.confidenceTotal
} }
} }
class Wappalyzer { class Wappalyzer {
constructor() { constructor() {
this.apps = {}; this.apps = {}
this.categories = {}; this.categories = {}
this.driver = {}; this.driver = {}
this.jsPatterns = {}; this.jsPatterns = {}
this.detected = {}; this.detected = {}
this.hostnameCache = {}; this.hostnameCache = {}
this.adCache = []; this.adCache = []
this.config = { this.config = {
websiteURL: 'https://www.wappalyzer.com/', websiteURL: 'https://www.wappalyzer.com/',
twitterURL: 'https://twitter.com/Wappalyzer', twitterURL: 'https://twitter.com/Wappalyzer',
githubURL: 'https://github.com/AliasIO/Wappalyzer', githubURL: 'https://github.com/AliasIO/Wappalyzer'
}; }
} }
/** /**
@ -139,124 +146,135 @@ class Wappalyzer {
*/ */
log(message, source, type) { log(message, source, type) {
if (this.driver.log) { if (this.driver.log) {
this.driver.log(message, source || '', type || 'debug'); this.driver.log(message, source || '', type || 'debug')
} }
} }
analyze(url, data, context) { analyze(url, data, context) {
const apps = {}; const apps = {}
const promises = []; const promises = []
const startTime = new Date(); const startTime = new Date()
const { const { scripts, cookies, headers, js } = data
scripts,
cookies, let { html } = data
headers,
js,
} = data;
let { html } = data;
if (this.detected[url.canonical] === undefined) { if (this.detected[url.canonical] === undefined) {
this.detected[url.canonical] = {}; this.detected[url.canonical] = {}
} }
const metaTags = []; const metaTags = []
// Additional information // Additional information
let language = null; let language = null
if (html) { if (html) {
if (typeof html !== 'string') { if (typeof html !== 'string') {
html = ''; html = ''
} }
let matches = data.html.match(new RegExp('<html[^>]*[: ]lang="([a-z]{2}((-|_)[A-Z]{2})?)"', 'i')); let matches = data.html.match(
new RegExp('<html[^>]*[: ]lang="([a-z]{2}((-|_)[A-Z]{2})?)"', 'i')
)
language = matches && matches.length ? matches[1] : data.language || null; language = matches && matches.length ? matches[1] : data.language || null
// Meta tags // Meta tags
const regex = /<meta[^>]+>/ig; const regex = /<meta[^>]+>/gi
do { do {
matches = regex.exec(html); matches = regex.exec(html)
if (!matches) { if (!matches) {
break; break
} }
metaTags.push(matches[0]); metaTags.push(matches[0])
} while (matches); } while (matches)
} }
Object.keys(this.apps).forEach((appName) => { Object.keys(this.apps).forEach((appName) => {
apps[appName] = this.detected[url.canonical] && this.detected[url.canonical][appName] apps[appName] =
? this.detected[url.canonical][appName] this.detected[url.canonical] && this.detected[url.canonical][appName]
: new Application(appName, this.apps[appName]); ? this.detected[url.canonical][appName]
: new Application(appName, this.apps[appName])
const app = apps[appName]; const app = apps[appName]
promises.push(this.analyzeUrl(app, url)); promises.push(this.analyzeUrl(app, url))
if (html) { if (html) {
promises.push(this.analyzeHtml(app, html)); promises.push(this.analyzeHtml(app, html))
promises.push(this.analyzeMeta(app, metaTags)); promises.push(this.analyzeMeta(app, metaTags))
} }
if (scripts) { if (scripts) {
promises.push(this.analyzeScripts(app, scripts)); promises.push(this.analyzeScripts(app, scripts))
} }
if (cookies) { if (cookies) {
promises.push(this.analyzeCookies(app, cookies)); promises.push(this.analyzeCookies(app, cookies))
} }
if (headers) { if (headers) {
promises.push(this.analyzeHeaders(app, headers)); promises.push(this.analyzeHeaders(app, headers))
} }
}); })
if (js) { if (js) {
Object.keys(js).forEach((appName) => { Object.keys(js).forEach((appName) => {
if (typeof js[appName] !== 'function') { if (typeof js[appName] !== 'function') {
promises.push(this.analyzeJs(apps[appName], js[appName])); promises.push(this.analyzeJs(apps[appName], js[appName]))
} }
}); })
} }
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
await Promise.all(promises); await Promise.all(promises)
Object.keys(apps).forEach((appName) => { Object.keys(apps).forEach((appName) => {
const app = apps[appName]; const app = apps[appName]
if (!app.detected || !app.getConfidence()) { if (!app.detected || !app.getConfidence()) {
delete apps[app.name]; delete apps[app.name]
} }
}); })
resolveExcludes(apps, this.detected[url]); resolveExcludes(apps, this.detected[url])
this.resolveImplies(apps, url.canonical); this.resolveImplies(apps, url.canonical)
this.cacheDetectedApps(apps, url.canonical); this.cacheDetectedApps(apps, url.canonical)
this.trackDetectedApps(apps, url, language); this.trackDetectedApps(apps, url, language)
this.log(`Processing ${Object.keys(data).join(', ')} took ${((new Date() - startTime) / 1000).toFixed(2)}s (${url.hostname})`, 'core'); this.log(
`Processing ${Object.keys(data).join(', ')} took ${(
(new Date() - startTime) /
1000
).toFixed(2)}s (${url.hostname})`,
'core'
)
if (Object.keys(apps).length) { if (Object.keys(apps).length) {
this.log(`Identified ${Object.keys(apps).join(', ')} (${url.hostname})`, 'core'); this.log(
`Identified ${Object.keys(apps).join(', ')} (${url.hostname})`,
'core'
)
} }
this.driver.displayApps(this.detected[url.canonical], { language }, context); this.driver.displayApps(
this.detected[url.canonical],
{ language },
context
)
return resolve(); return resolve()
}); })
} }
/** /**
* Cache detected ads * Cache detected ads
*/ */
cacheDetectedAds(ad) { cacheDetectedAds(ad) {
this.adCache.push(ad); this.adCache.push(ad)
} }
/** /**
@ -264,58 +282,65 @@ class Wappalyzer {
*/ */
robotsTxtAllows(url) { robotsTxtAllows(url) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const parsed = this.parseUrl(url); const parsed = this.parseUrl(url)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return reject(); return reject()
} }
const robotsTxt = await this.driver.getRobotsTxt(parsed.host, parsed.protocol === 'https:'); const robotsTxt = await this.driver.getRobotsTxt(
parsed.host,
if (robotsTxt.some(disallowedPath => parsed.pathname.indexOf(disallowedPath) === 0)) { parsed.protocol === 'https:'
return reject(); )
if (
robotsTxt.some(
(disallowedPath) => parsed.pathname.indexOf(disallowedPath) === 0
)
) {
return reject()
} }
return resolve(); return resolve()
}); })
} }
/** /**
* Parse a URL * Parse a URL
*/ */
parseUrl(url) { parseUrl(url) {
const a = this.driver.document.createElement('a'); const a = this.driver.document.createElement('a')
a.href = url; a.href = url
a.canonical = `${a.protocol}//${a.host}${a.pathname}`; a.canonical = `${a.protocol}//${a.host}${a.pathname}`
return a; return a
} }
/** /**
* *
*/ */
static parseRobotsTxt(robotsTxt) { static parseRobotsTxt(robotsTxt) {
const disallow = []; const disallow = []
let userAgent; let userAgent
robotsTxt.split('\n').forEach((line) => { robotsTxt.split('\n').forEach((line) => {
let matches = /^User-agent:\s*(.+)$/i.exec(line.trim()); let matches = /^User-agent:\s*(.+)$/i.exec(line.trim())
if (matches) { if (matches) {
userAgent = matches[1].toLowerCase(); userAgent = matches[1].toLowerCase()
} else if (userAgent === '*' || userAgent === 'wappalyzer') { } else if (userAgent === '*' || userAgent === 'wappalyzer') {
matches = /^Disallow:\s*(.+)$/i.exec(line.trim()); matches = /^Disallow:\s*(.+)$/i.exec(line.trim())
if (matches) { if (matches) {
disallow.push(matches[1]); disallow.push(matches[1])
} }
} }
}); })
return disallow; return disallow
} }
/** /**
@ -323,15 +348,15 @@ class Wappalyzer {
*/ */
ping() { ping() {
if (Object.keys(this.hostnameCache).length > 50) { if (Object.keys(this.hostnameCache).length > 50) {
this.driver.ping(this.hostnameCache); this.driver.ping(this.hostnameCache)
this.hostnameCache = {}; this.hostnameCache = {}
} }
if (this.adCache.length > 50) { if (this.adCache.length > 50) {
this.driver.ping({}, this.adCache); this.driver.ping({}, this.adCache)
this.adCache = []; this.adCache = []
} }
} }
@ -340,55 +365,55 @@ class Wappalyzer {
*/ */
parsePatterns(patterns) { parsePatterns(patterns) {
if (!patterns) { if (!patterns) {
return []; return []
} }
let parsed = {}; let parsed = {}
// Convert string to object containing array containing string // Convert string to object containing array containing string
if (typeof patterns === 'string' || patterns instanceof Array) { if (typeof patterns === 'string' || Array.isArray(patterns)) {
patterns = { patterns = {
main: asArray(patterns), main: asArray(patterns)
}; }
} }
Object.keys(patterns).forEach((key) => { Object.keys(patterns).forEach((key) => {
parsed[key] = []; parsed[key] = []
asArray(patterns[key]).forEach((pattern) => { asArray(patterns[key]).forEach((pattern) => {
const attrs = {}; const attrs = {}
pattern.split('\\;').forEach((attr, i) => { pattern.split('\\;').forEach((attr, i) => {
if (i) { if (i) {
// Key value pairs // Key value pairs
attr = attr.split(':'); attr = attr.split(':')
if (attr.length > 1) { if (attr.length > 1) {
attrs[attr.shift()] = attr.join(':'); attrs[attr.shift()] = attr.join(':')
} }
} else { } else {
attrs.string = attr; attrs.string = attr
try { try {
attrs.regex = new RegExp(attr.replace('/', '\/'), 'i'); // Escape slashes in regular expression attrs.regex = new RegExp(attr.replace('/', '/'), 'i') // Escape slashes in regular expression
} catch (error) { } catch (error) {
attrs.regex = new RegExp(); attrs.regex = new RegExp()
this.log(`${error.message}: ${attr}`, 'error', 'core'); this.log(`${error.message}: ${attr}`, 'error', 'core')
} }
} }
}); })
parsed[key].push(attrs); parsed[key].push(attrs)
}); })
}); })
// Convert back to array if the original pattern list was an array (or string) // Convert back to array if the original pattern list was an array (or string)
if ('main' in parsed) { if ('main' in parsed) {
parsed = parsed.main; parsed = parsed.main
} }
return parsed; return parsed
} }
/** /**
@ -397,49 +422,60 @@ class Wappalyzer {
parseJsPatterns() { parseJsPatterns() {
Object.keys(this.apps).forEach((appName) => { Object.keys(this.apps).forEach((appName) => {
if (this.apps[appName].js) { if (this.apps[appName].js) {
this.jsPatterns[appName] = this.parsePatterns(this.apps[appName].js); this.jsPatterns[appName] = this.parsePatterns(this.apps[appName].js)
} }
}); })
} }
resolveImplies(apps, url) { resolveImplies(apps, url) {
let checkImplies = true; let checkImplies = true
const resolve = (appName) => { const resolve = (appName) => {
const app = apps[appName]; const app = apps[appName]
if (app && app.props.implies) { if (app && app.props.implies) {
asArray(app.props.implies).forEach((implied) => { asArray(app.props.implies).forEach((implied) => {
[implied] = this.parsePatterns(implied); ;[implied] = this.parsePatterns(implied)
if (!this.apps[implied.string]) { if (!this.apps[implied.string]) {
this.log(`Implied application ${implied.string} does not exist`, 'core', 'warn'); this.log(
`Implied application ${implied.string} does not exist`,
'core',
'warn'
)
return; return
} }
if (!(implied.string in apps)) { if (!(implied.string in apps)) {
apps[implied.string] = this.detected[url] && this.detected[url][implied.string] apps[implied.string] =
? this.detected[url][implied.string] this.detected[url] && this.detected[url][implied.string]
: new Application(implied.string, this.apps[implied.string], true); ? this.detected[url][implied.string]
: new Application(
checkImplies = true; implied.string,
this.apps[implied.string],
true
)
checkImplies = true
} }
// Apply app confidence to implied app // Apply app confidence to implied app
Object.keys(app.confidence).forEach((id) => { Object.keys(app.confidence).forEach((id) => {
apps[implied.string].confidence[`${id} implied by ${appName}`] = app.confidence[id] * (implied.confidence === undefined ? 1 : implied.confidence / 100); apps[implied.string].confidence[`${id} implied by ${appName}`] =
}); app.confidence[id] *
}); (implied.confidence === undefined ? 1 : implied.confidence / 100)
})
})
} }
}; }
// Implied applications // Implied applications
// Run several passes as implied apps may imply other apps // Run several passes as implied apps may imply other apps
while (checkImplies) { while (checkImplies) {
checkImplies = false; checkImplies = false
Object.keys(apps).forEach(resolve); Object.keys(apps).forEach(resolve)
} }
} }
@ -448,19 +484,18 @@ class Wappalyzer {
*/ */
cacheDetectedApps(apps, url) { cacheDetectedApps(apps, url) {
Object.keys(apps).forEach((appName) => { Object.keys(apps).forEach((appName) => {
const app = apps[appName]; const app = apps[appName]
// Per URL // Per URL
this.detected[url][appName] = app; this.detected[url][appName] = app
Object.keys(app.confidence) Object.keys(app.confidence).forEach((id) => {
.forEach((id) => { this.detected[url][appName].confidence[id] = app.confidence[id]
this.detected[url][appName].confidence[id] = app.confidence[id]; })
}); })
});
if (this.driver.ping instanceof Function) { if (this.driver.ping instanceof Function) {
this.ping(); this.ping()
} }
} }
@ -469,204 +504,219 @@ class Wappalyzer {
*/ */
trackDetectedApps(apps, url, language) { trackDetectedApps(apps, url, language) {
if (!(this.driver.ping instanceof Function)) { if (!(this.driver.ping instanceof Function)) {
return; return
} }
const hostname = `${url.protocol}//${url.hostname}`; const hostname = `${url.protocol}//${url.hostname}`
Object.keys(apps).forEach((appName) => { Object.keys(apps).forEach((appName) => {
const app = apps[appName]; const app = apps[appName]
if (this.detected[url.canonical][appName].getConfidence() >= 100) { if (this.detected[url.canonical][appName].getConfidence() >= 100) {
if ( if (
validation.hostname.test(url.hostname) validation.hostname.test(url.hostname) &&
&& !validation.hostnameBlacklist.test(url.hostname) !validation.hostnameBlacklist.test(url.hostname)
) { ) {
if (!(hostname in this.hostnameCache)) { if (!(hostname in this.hostnameCache)) {
this.hostnameCache[hostname] = { this.hostnameCache[hostname] = {
applications: {}, applications: {},
meta: {}, meta: {}
}; }
} }
if (!(appName in this.hostnameCache[hostname].applications)) { if (!(appName in this.hostnameCache[hostname].applications)) {
this.hostnameCache[hostname].applications[appName] = { this.hostnameCache[hostname].applications[appName] = {
hits: 0, hits: 0
}; }
} }
this.hostnameCache[hostname].applications[appName].hits += 1; this.hostnameCache[hostname].applications[appName].hits += 1
if (apps[appName].version) { if (apps[appName].version) {
this.hostnameCache[hostname].applications[appName].version = app.version; this.hostnameCache[hostname].applications[appName].version =
app.version
} }
} }
} }
}); })
if (hostname in this.hostnameCache) { if (hostname in this.hostnameCache) {
this.hostnameCache[hostname].meta.language = language; this.hostnameCache[hostname].meta.language = language
} }
this.ping(); this.ping()
} }
/** /**
* Analyze URL * Analyze URL
*/ */
analyzeUrl(app, url) { analyzeUrl(app, url) {
const patterns = this.parsePatterns(app.props.url); const patterns = this.parsePatterns(app.props.url)
if (!patterns.length) { if (!patterns.length) {
return Promise.resolve(); return Promise.resolve()
} }
return asyncForEach(patterns, (pattern) => { return asyncForEach(patterns, (pattern) => {
if (pattern.regex.test(url.canonical)) { if (pattern.regex.test(url.canonical)) {
addDetected(app, pattern, 'url', url.canonical); addDetected(app, pattern, 'url', url.canonical)
} }
}); })
} }
/** /**
* Analyze HTML * Analyze HTML
*/ */
analyzeHtml(app, html) { analyzeHtml(app, html) {
const patterns = this.parsePatterns(app.props.html); const patterns = this.parsePatterns(app.props.html)
if (!patterns.length) { if (!patterns.length) {
return Promise.resolve(); return Promise.resolve()
} }
return asyncForEach(patterns, (pattern) => { return asyncForEach(patterns, (pattern) => {
if (pattern.regex.test(html)) { if (pattern.regex.test(html)) {
addDetected(app, pattern, 'html', html); addDetected(app, pattern, 'html', html)
} }
}); })
} }
/** /**
* Analyze script tag * Analyze script tag
*/ */
analyzeScripts(app, scripts) { analyzeScripts(app, scripts) {
const patterns = this.parsePatterns(app.props.script); const patterns = this.parsePatterns(app.props.script)
if (!patterns.length) { if (!patterns.length) {
return Promise.resolve(); return Promise.resolve()
} }
return asyncForEach(patterns, (pattern) => { return asyncForEach(patterns, (pattern) => {
scripts.forEach((uri) => { scripts.forEach((uri) => {
if (pattern.regex.test(uri)) { if (pattern.regex.test(uri)) {
addDetected(app, pattern, 'script', uri); addDetected(app, pattern, 'script', uri)
} }
}); })
}); })
} }
/** /**
* Analyze meta tag * Analyze meta tag
*/ */
analyzeMeta(app, metaTags) { analyzeMeta(app, metaTags) {
const patterns = this.parsePatterns(app.props.meta); const patterns = this.parsePatterns(app.props.meta)
const promises = []; const promises = []
if (!app.props.meta) { if (!app.props.meta) {
return Promise.resolve(); return Promise.resolve()
} }
metaTags.forEach((match) => { metaTags.forEach((match) => {
Object.keys(patterns).forEach((meta) => { Object.keys(patterns).forEach((meta) => {
const r = new RegExp(`(?:name|property)=["']${meta}["']`, 'i'); const r = new RegExp(`(?:name|property)=["']${meta}["']`, 'i')
if (r.test(match)) { if (r.test(match)) {
const content = match.match(/content=("|')([^"']+)("|')/i); const content = match.match(/content=("|')([^"']+)("|')/i)
promises.push(asyncForEach(patterns[meta], (pattern) => { promises.push(
if (content && content.length === 4 && pattern.regex.test(content[2])) { asyncForEach(patterns[meta], (pattern) => {
addDetected(app, pattern, 'meta', content[2], meta); if (
} content &&
})); content.length === 4 &&
pattern.regex.test(content[2])
) {
addDetected(app, pattern, 'meta', content[2], meta)
}
})
)
} }
}); })
}); })
return Promise.all(promises); return Promise.all(promises)
} }
/** /**
* Analyze response headers * Analyze response headers
*/ */
analyzeHeaders(app, headers) { analyzeHeaders(app, headers) {
const patterns = this.parsePatterns(app.props.headers); const patterns = this.parsePatterns(app.props.headers)
const promises = []; const promises = []
Object.keys(patterns).forEach((headerName) => { Object.keys(patterns).forEach((headerName) => {
if (typeof patterns[headerName] !== 'function') { if (typeof patterns[headerName] !== 'function') {
promises.push(asyncForEach(patterns[headerName], (pattern) => { promises.push(
headerName = headerName.toLowerCase(); asyncForEach(patterns[headerName], (pattern) => {
headerName = headerName.toLowerCase()
if (headerName in headers) {
headers[headerName].forEach((headerValue) => { if (headerName in headers) {
if (pattern.regex.test(headerValue)) { headers[headerName].forEach((headerValue) => {
addDetected(app, pattern, 'headers', headerValue, headerName); if (pattern.regex.test(headerValue)) {
} addDetected(app, pattern, 'headers', headerValue, headerName)
}); }
} })
})); }
})
)
} }
}); })
return promises ? Promise.all(promises) : Promise.resolve(); return promises ? Promise.all(promises) : Promise.resolve()
} }
/** /**
* Analyze cookies * Analyze cookies
*/ */
analyzeCookies(app, cookies) { analyzeCookies(app, cookies) {
const patterns = this.parsePatterns(app.props.cookies); const patterns = this.parsePatterns(app.props.cookies)
const promises = []; const promises = []
Object.keys(patterns).forEach((cookieName) => { Object.keys(patterns).forEach((cookieName) => {
if (typeof patterns[cookieName] !== 'function') { if (typeof patterns[cookieName] !== 'function') {
const cookieNameLower = cookieName.toLowerCase(); const cookieNameLower = cookieName.toLowerCase()
promises.push(asyncForEach(patterns[cookieName], (pattern) => { promises.push(
const cookie = cookies.find(_cookie => _cookie.name.toLowerCase() === cookieNameLower); asyncForEach(patterns[cookieName], (pattern) => {
const cookie = cookies.find(
(_cookie) => _cookie.name.toLowerCase() === cookieNameLower
)
if (cookie && pattern.regex.test(cookie.value)) { if (cookie && pattern.regex.test(cookie.value)) {
addDetected(app, pattern, 'cookies', cookie.value, cookieName); addDetected(app, pattern, 'cookies', cookie.value, cookieName)
} }
})); })
)
} }
}); })
return promises ? Promise.all(promises) : Promise.resolve(); return promises ? Promise.all(promises) : Promise.resolve()
} }
/** /**
* Analyze JavaScript variables * Analyze JavaScript variables
*/ */
analyzeJs(app, results) { analyzeJs(app, results) {
const promises = []; const promises = []
Object.keys(results).forEach((string) => { Object.keys(results).forEach((string) => {
if (typeof results[string] !== 'function') { if (typeof results[string] !== 'function') {
promises.push(asyncForEach(Object.keys(results[string]), (index) => { promises.push(
const pattern = this.jsPatterns[app.name][string][index]; asyncForEach(Object.keys(results[string]), (index) => {
const value = results[string][index]; const pattern = this.jsPatterns[app.name][string][index]
const value = results[string][index]
if (pattern && pattern.regex.test(value)) { if (pattern && pattern.regex.test(value)) {
addDetected(app, pattern, 'js', value, string); addDetected(app, pattern, 'js', value, string)
} }
})); })
)
} }
}); })
return promises ? Promise.all(promises) : Promise.resolve(); return promises ? Promise.all(promises) : Promise.resolve()
} }
} }
if (typeof module === 'object') { if (typeof module === 'object') {
module.exports = Wappalyzer; module.exports = Wappalyzer
} }

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save