commit
e2c507b2d1
@ -1,108 +0,0 @@
|
|||||||
body {
|
|
||||||
color: #303942;
|
|
||||||
cursor: default;
|
|
||||||
direction: __MSG_@@bidi_dir__;
|
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
|
||||||
font-size: .8rem;
|
|
||||||
line-height: 1.4rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3 {
|
|
||||||
font-weight: normal;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
border-bottom: 1px solid #dbdbdb;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin: 0 0 1.5rem 0;
|
|
||||||
padding: 1rem 0 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.3em;
|
|
||||||
margin-bottom: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
color: black;
|
|
||||||
font-size: 1.2em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: rgb(17, 85, 204);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background: #4608ad;
|
|
||||||
border: none;
|
|
||||||
border-radius: .2rem;
|
|
||||||
color: white;
|
|
||||||
font-size: inherit;
|
|
||||||
padding: 0 .6rem;
|
|
||||||
line-height: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:active {
|
|
||||||
color: rgb(5, 37, 119);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
background: linear-gradient(160deg, #32067c, #150233);
|
|
||||||
padding: 1.5rem 1.5rem 1rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero img {
|
|
||||||
height: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#options-saved {
|
|
||||||
display: none;
|
|
||||||
margin-left: .5rem;
|
|
||||||
-webkit-animation: fadeout 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#about {
|
|
||||||
border-top: 1px solid #dbdbdb;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
padding: 1.5rem 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#about img {
|
|
||||||
margin-right: .2rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
#about button {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #dbdbdb;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #303942;
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
margin-inline-end: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes fadeout {
|
|
||||||
from { opacity: 1; }
|
|
||||||
to { opacity: 0; }
|
|
||||||
}
|
|
@ -1,287 +0,0 @@
|
|||||||
body {
|
|
||||||
background: #fff;
|
|
||||||
direction: __MSG_@@bidi_dir__;
|
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
|
||||||
font-size: .8rem;
|
|
||||||
margin: 0;
|
|
||||||
min-width: 30rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid #dbdbdb;
|
|
||||||
height: 4rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header__link:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header__logo {
|
|
||||||
display: inline-block;
|
|
||||||
margin: .2rem 1.5rem 0 1.5rem;
|
|
||||||
-webkit-backface-visibility: hidden;
|
|
||||||
-webkit-transform: translateZ(0) scale(1.0, 1.0);
|
|
||||||
transform: translateZ(0);
|
|
||||||
height: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header__logo--dark {
|
|
||||||
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 {
|
|
||||||
min-height: 5rem;
|
|
||||||
padding: 1rem 1.5rem 0rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected {
|
|
||||||
columns: 2;
|
|
||||||
column-gap: 1.5rem;
|
|
||||||
line-height: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__category {
|
|
||||||
page-break-inside: avoid;
|
|
||||||
break-inside: avoid-column;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__category-name {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__category-link {
|
|
||||||
color: #4608ad;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 2rem;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__category-link:hover {
|
|
||||||
color: #4a4a4a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__category-pin-wrapper {
|
|
||||||
margin-left: .2rem;
|
|
||||||
margin-right: .2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__category-pin {
|
|
||||||
cursor: pointer;
|
|
||||||
display: none;
|
|
||||||
height: 16px;
|
|
||||||
margin-left: .2rem;
|
|
||||||
width: 16px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__category:hover .detected__category-pin--inactive {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__category-pin-wrapper--active .detected__category-pin--inactive,
|
|
||||||
.detected__category-pin-wrapper:hover .detected__category-pin--inactive {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__category-pin-wrapper--active .detected__category-pin--active,
|
|
||||||
.detected__category-pin-wrapper:hover .detected__category-pin--active {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__app {
|
|
||||||
color: #4a4a4a;
|
|
||||||
display: block;
|
|
||||||
line-height: 1.7rem;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__app:focus {
|
|
||||||
display: block;
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__app-icon {
|
|
||||||
display: inline-block;
|
|
||||||
height: 16px;
|
|
||||||
margin-inline-end: .5rem;
|
|
||||||
vertical-align: -.2rem;
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__app-name {
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__app-version, .detected__app-confidence {
|
|
||||||
background: #eee;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: .7rem;
|
|
||||||
margin-left: .3rem;
|
|
||||||
padding: .1rem .2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__app:hover .detected__app-name {
|
|
||||||
border-bottom: 1px solid #4a4a4a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected__app:hover .detected__app-version,
|
|
||||||
.detected__app:hover .detected__app-confidence {
|
|
||||||
border-bottom: 1px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected-app {
|
|
||||||
padding: 7px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected-app:first-child {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected-app:last-child {
|
|
||||||
border: none;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
display: flex;
|
|
||||||
height: 5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty__text {
|
|
||||||
}
|
|
||||||
|
|
||||||
.terms {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
height: 12rem;
|
|
||||||
width: 36rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terms__wrapper {
|
|
||||||
display: none;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terms__wrapper--active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terms__content {
|
|
||||||
font-size: .9rem;
|
|
||||||
line-height: 150%;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terms__accept {
|
|
||||||
background-color: #4608ad;
|
|
||||||
border: none;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: .9rem;
|
|
||||||
padding: .8rem 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terms__accept:hover {
|
|
||||||
background-color: #4107a1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terms__privacy {
|
|
||||||
color: #4608ad;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
/* Add alternative color palette for Dark mode theme. */
|
|
||||||
body.theme-mode-sync {
|
|
||||||
background: linear-gradient(160deg, #32067c, #150233);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-mode-sync .header {
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, .2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-mode-sync .header__logo--dark {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-mode-sync .header__logo--light {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-mode-sync .footer {
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, .2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-mode-sync .footer__link {
|
|
||||||
color: rgba(255, 255, 255, .8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-mode-sync .footer__link:hover, .theme-mode-sync .footer__link:active {
|
|
||||||
color: rgba(255, 255, 255, .8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-mode-sync .container {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-mode-sync .detected__category-link {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-mode-sync .detected__app {
|
|
||||||
color: rgba(255, 255, 255, .8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-mode-sync .detected__category-link:hover {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,271 @@
|
|||||||
|
:root {
|
||||||
|
--color-primary: #4608ad;
|
||||||
|
--color-primary-darken: #32067c;
|
||||||
|
--color-secondary: #e0e0e0;
|
||||||
|
--color-secondary-dark: rgba(255, 255, 255, .2);
|
||||||
|
--color-text: #4a4a4a;
|
||||||
|
--color-text-dark: rgba(255, 255, 255, .8);
|
||||||
|
--color-light-grey: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #fff;
|
||||||
|
color: var(--color-text);
|
||||||
|
direction: __MSG_@@bidi_dir__;
|
||||||
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
|
font-size: .9rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
min-width: 35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-primary);
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--color-secondary);
|
||||||
|
display: flex;
|
||||||
|
height: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__logo {
|
||||||
|
height: 2.5rem;
|
||||||
|
margin: .5rem 1.5rem 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__logo--dark {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-top: 1px solid var(--color-secondary);
|
||||||
|
height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts__icon {
|
||||||
|
color: var(--color-primary);
|
||||||
|
height: 1.1rem;
|
||||||
|
margin-right: .5rem;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
width: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__settings {
|
||||||
|
color: var(--color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
height: 1.1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detections {
|
||||||
|
columns: 2;
|
||||||
|
column-gap: 1.5rem;
|
||||||
|
padding: 1.5rem 1.5rem .5rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
opacity: .3;
|
||||||
|
padding: 3rem 1.5rem .5rem 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
break-inside: avoid-column;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category__link {
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 2rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category__pin {
|
||||||
|
color: var(--color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: none;
|
||||||
|
visibility: hidden;
|
||||||
|
height: 1.1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category__pin--outline {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category__pin--active {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category__pin.category__pin--active {
|
||||||
|
display: inline
|
||||||
|
}
|
||||||
|
|
||||||
|
.category__pin--outline.category__pin--active {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
.category__heading:hover .category__pin {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.technology {
|
||||||
|
display: block;
|
||||||
|
line-height: 1.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.technology__heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: .2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.technology__icon {
|
||||||
|
height: 16px;
|
||||||
|
margin-right: .5rem;
|
||||||
|
width: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.technology__link {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.technology__confidence {
|
||||||
|
opacity: .5;
|
||||||
|
font-size: .7rem;
|
||||||
|
margin-left: .2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.technology__version {
|
||||||
|
background: var(--color-secondary);
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: .7rem;
|
||||||
|
padding: .1rem .3rem;
|
||||||
|
margin-left: .4rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
height: 12rem;
|
||||||
|
width: 36rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms__wrapper {
|
||||||
|
display: none;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms__wrapper--active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms__content {
|
||||||
|
font-size: .9rem;
|
||||||
|
line-height: 150%;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms__accept {
|
||||||
|
background-color: #4608ad;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: .9rem;
|
||||||
|
padding: .8rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms__accept:hover {
|
||||||
|
background-color: #4107a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms__privacy {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
padding: 1.5rem 1.5rem 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options__label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body.theme-mode {
|
||||||
|
background: var(--color-primary-darken);
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-mode a {
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-mode .header {
|
||||||
|
border-color: var(--color-secondary-dark)
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-mode .header__logo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-mode .header__logo--dark {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-mode .category__link {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-mode .category__pin {
|
||||||
|
color: #fff
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-mode .technology__confidence {
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-mode .technology__version {
|
||||||
|
background: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-mode .footer {
|
||||||
|
border-color: var(--color-secondary-dark)
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-mode .footer__settings {
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-mode .alerts__icon {
|
||||||
|
color:var(--color-text-dark);
|
||||||
|
}
|
||||||
|
}
|
@ -1,69 +1,41 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<title data-i18n="options">Wappalyzer options</title>
|
<title data-i18n="options"></title>
|
||||||
|
|
||||||
<link rel="icon" href="../images/icon_32.png">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="../css/options.css">
|
<link rel="stylesheet" href="../css/styles.css">
|
||||||
|
|
||||||
<script src="../node_modules/webextension-polyfill/dist/browser-polyfill.js"></script>
|
<script src="../js/utils.js"></script>
|
||||||
<script src="../js/wappalyzer.js"></script>
|
|
||||||
<script src="../js/options.js"></script>
|
<script src="../js/options.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="hero">
|
<div class="options">
|
||||||
<div class="container">
|
<label class="options__label">
|
||||||
<img src="../images/logo-white.svg">
|
<input class="options__checkbox" type="checkbox">
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
<span data-i18n="optionUpgradeMessage"> </span>
|
||||||
<div class="container">
|
</label>
|
||||||
<h1 data-i18n="options">Options</h1>
|
|
||||||
|
|
||||||
<p>
|
<label class="options__label">
|
||||||
<label for="option-upgrade-message">
|
<input class="options__checkbox" type="checkbox">
|
||||||
<input id="option-upgrade-message" type="checkbox">
|
|
||||||
<span data-i18n="optionUpgradeMessage">Tell me about upgrades</span>
|
|
||||||
</label>
|
|
||||||
<label for="option-dynamic-icon">
|
|
||||||
<input id="option-dynamic-icon" type="checkbox">
|
|
||||||
<span data-i18n="optionDynamicIcon">Use application icon instead of Wappalyzer logo</span>
|
|
||||||
</label>
|
|
||||||
<label for="option-tracking">
|
|
||||||
<input id="option-tracking" type="checkbox">
|
|
||||||
<span data-i18n="optionTracking">Anonymously send reports on detected applications to wappalyzer.com for research</span>
|
|
||||||
</label>
|
|
||||||
<label for="option-theme-mode">
|
|
||||||
<input id="option-theme-mode" type="checkbox">
|
|
||||||
<span data-i18n="optionThemeMode">Enable dark mode compatibility</span>
|
|
||||||
</label>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div id="about">
|
<span data-i18n="optionDynamicIcon"> </span>
|
||||||
<p>
|
</label>
|
||||||
<button id="github">
|
|
||||||
<img src="../images/github.png" width="16" height="16" alt="GitHub icon" />
|
<label class="options__label">
|
||||||
<span data-i18n="github">Fork Wappalyzer on GitHub!</span>
|
<input class="options__checkbox" type="checkbox">
|
||||||
</button>
|
|
||||||
<button id="twitter">
|
<span data-i18n="optionTracking"> </span>
|
||||||
<img src="../images/twitter.png" width="16" height="16" alt="Twitter icon" />
|
</label>
|
||||||
<span data-i18n="twitter">Follow Wappalyzer on Twitter</span>
|
|
||||||
</button>
|
<label class="options__label">
|
||||||
<button id="wappalyzer">
|
<input class="options__checkbox" type="checkbox">
|
||||||
<img src="../images/icon_16.png" width="16" height="16" alt="Wappalyzer icon" />
|
|
||||||
<span data-i18n="website">Go to wappalyzer.com</span>
|
<span data-i18n="optionThemeMode"> </span>
|
||||||
</button>
|
</label>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,38 +1,80 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<link rel="stylesheet" href="../css/popup.css">
|
<title data-i18n="options"></title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="../css/styles.css">
|
||||||
|
|
||||||
<script src="../node_modules/webextension-polyfill/dist/browser-polyfill.js"></script>
|
<script src="../js/utils.js"></script>
|
||||||
<script src="../js/lib/jsontodom.js"></script>
|
|
||||||
<script src="../js/popup.js"></script>
|
<script src="../js/popup.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<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">
|
||||||
<img alt="" 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 alt="" 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>
|
||||||
|
|
||||||
<div class="container">
|
<div class="terms">
|
||||||
<div class="terms__wrapper">
|
<div class="terms__content" data-i18n="termsContent"></div>
|
||||||
<div class="terms">
|
|
||||||
<div class="terms__content" data-i18n="termsContent"></div>
|
<button class="terms__accept" data-i18n="termsAccept"> </button>
|
||||||
|
|
||||||
|
<a class="terms__privacy" href="https://www.wappalyzer.com/privacy" data-i18n="privacyPolicy"></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="empty" data-i18n="noAppsDetected"></div>
|
||||||
|
|
||||||
|
<div class="detections"></div>
|
||||||
|
|
||||||
|
<div data-template="category" class="category">
|
||||||
|
<div class="category__heading">
|
||||||
|
<a class="category__link" href="#"></a>
|
||||||
|
|
||||||
|
<svg class="category__pin category__pin--outline" viewBox="0 0 24 24">
|
||||||
|
<title data-i18n="categoryPin"></title>
|
||||||
|
<path fill="currentColor" d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12M8.8,14L10,12.8V4H14V12.8L15.2,14H8.8Z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg class="category__pin" viewBox="0 0 24 24">
|
||||||
|
<title data-i18n="categoryPin"></title>
|
||||||
|
<path fill="currentColor" d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="technologies"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-template="technology" class="technology">
|
||||||
|
<div class="technology__heading">
|
||||||
|
<img class="technology__icon" alt="" src="../images/icons/default.svg" />
|
||||||
|
|
||||||
<button class="terms__accept" data-i18n="termsAccept" />
|
<a class="technology__link" href="#"></a>
|
||||||
|
|
||||||
<a class="terms__privacy" href="https://www.wappalyzer.com/privacy" data-i18n="privacyPolicy"></a>
|
<span>
|
||||||
</div>
|
<span class="technology__version"> </span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="technology__confidence"> </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<a class="footer__link" href="https://www.wappalyzer.com/alerts/manage" data-i18n="createAlert"></a>
|
<div class="alerts">
|
||||||
|
<svg class="alerts__icon" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M12,8H4A2,2 0 0,0 2,10V14A2,2 0 0,0 4,16H5V20A1,1 0 0,0 6,21H8A1,1 0 0,0 9,20V16H12L17,20V4L12,8M21.5,12C21.5,13.71 20.54,15.26 19,16V8C20.53,8.75 21.5,10.3 21.5,12Z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<a class="alerts__link" href="https://www.wappalyzer.com/alerts/manage" data-i18n="createAlert"></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svg class="footer__settings" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,445 +1,483 @@
|
|||||||
/**
|
'use strict'
|
||||||
* WebExtension driver
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
/* global browser, chrome, Wappalyzer */
|
/* globals chrome, Wappalyzer, Utils */
|
||||||
|
|
||||||
/** global: browser */
|
const {
|
||||||
/** global: chrome */
|
setTechnologies,
|
||||||
/** global: fetch */
|
setCategories,
|
||||||
/** global: Wappalyzer */
|
analyze,
|
||||||
|
analyzeManyToMany,
|
||||||
const wappalyzer = new Wappalyzer()
|
resolve
|
||||||
|
} = Wappalyzer
|
||||||
const tabCache = {}
|
const { agent, promisify, getOption, setOption } = Utils
|
||||||
const robotsTxtQueue = {}
|
|
||||||
|
const expiry = 1000 * 60 * 60 * 24
|
||||||
let categoryOrder = []
|
|
||||||
|
const Driver = {
|
||||||
browser.tabs.onRemoved.addListener((tabId) => {
|
lastPing: Date.now(),
|
||||||
tabCache[tabId] = null
|
|
||||||
})
|
async init() {
|
||||||
|
chrome.runtime.onConnect.addListener(Driver.onRuntimeConnect)
|
||||||
|
|
||||||
|
await Driver.loadTechnologies()
|
||||||
|
|
||||||
|
const hostnameCache = (await getOption('hostnames')) || {}
|
||||||
|
|
||||||
|
Driver.cache = {
|
||||||
|
hostnames: Object.keys(hostnameCache).reduce(
|
||||||
|
(cache, hostname) => ({
|
||||||
|
...cache,
|
||||||
|
[hostname]: {
|
||||||
|
...hostnameCache[hostname],
|
||||||
|
detections: hostnameCache[hostname].detections.map(
|
||||||
|
({
|
||||||
|
pattern: { regex, confidence, version },
|
||||||
|
match,
|
||||||
|
technology: name
|
||||||
|
}) => ({
|
||||||
|
pattern: {
|
||||||
|
regex: new RegExp(regex, 'i'),
|
||||||
|
confidence,
|
||||||
|
version
|
||||||
|
},
|
||||||
|
match,
|
||||||
|
technology: Wappalyzer.technologies.find(
|
||||||
|
({ name: _name }) => name === _name
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
tabs: {},
|
||||||
|
robots: (await getOption('robots')) || {},
|
||||||
|
ads: (await getOption('ads')) || []
|
||||||
|
}
|
||||||
|
|
||||||
function userAgent() {
|
chrome.webRequest.onCompleted.addListener(
|
||||||
const url = chrome.extension.getURL('/')
|
Driver.onWebRequestComplete,
|
||||||
|
{ urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] },
|
||||||
|
['responseHeaders']
|
||||||
|
)
|
||||||
|
chrome.tabs.onRemoved.addListener((id) => (Driver.cache.tabs[id] = null))
|
||||||
|
},
|
||||||
|
|
||||||
if (url.startsWith('moz-')) {
|
log(message, source = 'driver', type = 'log') {
|
||||||
return 'firefox'
|
// eslint-disable-next-line no-console
|
||||||
}
|
console[type](`wappalyzer | ${source} |`, message)
|
||||||
|
},
|
||||||
|
|
||||||
if (url.startsWith('ms-browser')) {
|
warn(message, source = 'driver') {
|
||||||
return 'edge'
|
Driver.log(message, source, 'warn')
|
||||||
}
|
},
|
||||||
|
|
||||||
return 'chrome'
|
error(error, source = 'driver') {
|
||||||
}
|
Driver.log(error, source, 'error')
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
open(url, active = true) {
|
||||||
* Get a value from localStorage
|
chrome.tabs.create({ url, active })
|
||||||
*/
|
},
|
||||||
function getOption(name, defaultValue = null) {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
let value = defaultValue
|
|
||||||
|
|
||||||
|
async loadTechnologies() {
|
||||||
try {
|
try {
|
||||||
const option = await browser.storage.local.get(name)
|
const { apps: technologies, categories } = await (
|
||||||
|
await fetch(chrome.extension.getURL('apps.json'))
|
||||||
|
).json()
|
||||||
|
|
||||||
if (option[name] !== undefined) {
|
setTechnologies(technologies)
|
||||||
value = option[name]
|
setCategories(categories)
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
wappalyzer.log(error.message, 'driver', 'error')
|
Driver.error(error)
|
||||||
|
|
||||||
return reject(error.message)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
return resolve(value)
|
post(url, body) {
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a value in localStorage
|
|
||||||
*/
|
|
||||||
function setOption(name, value) {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
try {
|
try {
|
||||||
await browser.storage.local.set({ [name]: value })
|
return fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
wappalyzer.log(error.message, 'driver', 'error')
|
throw new Error(error.message || error.toString())
|
||||||
|
|
||||||
return reject(error.message)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
return resolve()
|
async analyzeJs(href, js) {
|
||||||
})
|
const url = new URL(href)
|
||||||
}
|
|
||||||
|
await Driver.onDetect(
|
||||||
|
url,
|
||||||
|
Array.prototype.concat.apply(
|
||||||
|
[],
|
||||||
|
await Promise.all(
|
||||||
|
js.map(({ name, chain, value }) =>
|
||||||
|
analyzeManyToMany(
|
||||||
|
Wappalyzer.technologies.find(({ name: _name }) => name === _name),
|
||||||
|
'js',
|
||||||
|
{ [chain]: [value] }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
onRuntimeConnect(port) {
|
||||||
* Open a tab
|
Driver.log(`Connected to ${port.name}`)
|
||||||
*/
|
|
||||||
function openTab(args) {
|
|
||||||
browser.tabs.create({
|
|
||||||
url: args.url,
|
|
||||||
active: args.background === undefined || !args.background
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
port.onMessage.addListener(async ({ func, args }) => {
|
||||||
* Make a POST request
|
if (!func) {
|
||||||
*/
|
return
|
||||||
async function post(url, body) {
|
}
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
})
|
|
||||||
|
|
||||||
wappalyzer.log(`POST ${url}: ${response.status}`, 'driver')
|
Driver.log({ port: port.name, func, args })
|
||||||
} catch (error) {
|
|
||||||
wappalyzer.log(`POST ${url}: ${error}`, 'driver', 'error')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture response headers
|
if (!Driver[func]) {
|
||||||
browser.webRequest.onCompleted.addListener(
|
Driver.error(new Error(`Method does not exist: Driver.${func}`))
|
||||||
async (request) => {
|
|
||||||
const headers = {}
|
|
||||||
|
|
||||||
if (request.responseHeaders) {
|
return
|
||||||
const url = wappalyzer.parseUrl(request.url)
|
}
|
||||||
|
|
||||||
|
port.postMessage({
|
||||||
|
func,
|
||||||
|
args: await Driver[func].call(port.sender, ...(args || []))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
let tab
|
async onWebRequestComplete(request) {
|
||||||
|
if (request.responseHeaders) {
|
||||||
|
const headers = {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
;[tab] = await browser.tabs.query({ url: [url.href] })
|
const url = new URL(request.url)
|
||||||
} catch (error) {
|
|
||||||
wappalyzer.log(error, 'driver', 'error')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tab) {
|
const [tab] = await promisify(chrome.tabs, 'query', { url: [url.href] })
|
||||||
request.responseHeaders.forEach((header) => {
|
|
||||||
const name = header.name.toLowerCase()
|
|
||||||
|
|
||||||
headers[name] = headers[name] || []
|
if (tab) {
|
||||||
|
request.responseHeaders.forEach((header) => {
|
||||||
|
const name = header.name.toLowerCase()
|
||||||
|
|
||||||
headers[name].push(
|
headers[name] = headers[name] || []
|
||||||
(header.value || header.binaryValue || '').toString()
|
|
||||||
)
|
headers[name].push(
|
||||||
})
|
(header.value || header.binaryValue || '').toString()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
if (
|
if (
|
||||||
headers['content-type'] &&
|
headers['content-type'] &&
|
||||||
/\/x?html/.test(headers['content-type'][0])
|
/\/x?html/.test(headers['content-type'][0])
|
||||||
) {
|
) {
|
||||||
wappalyzer.analyze(url, { headers }, { tab })
|
await Driver.onDetect(
|
||||||
|
url,
|
||||||
|
await analyze(url.href, { headers }, { tab })
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Driver.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] },
|
|
||||||
['responseHeaders']
|
|
||||||
)
|
|
||||||
|
|
||||||
browser.runtime.onConnect.addListener((port) => {
|
async onContentLoad(href, items, language) {
|
||||||
port.onMessage.addListener(async (message) => {
|
try {
|
||||||
if (message.id === undefined) {
|
const url = new URL(href)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.id !== 'log') {
|
items.cookies = await promisify(chrome.cookies, 'getAll', {
|
||||||
wappalyzer.log(`Message from ${port.name}: ${message.id}`, 'driver')
|
domain: `.${url.hostname}`
|
||||||
}
|
})
|
||||||
|
|
||||||
const pinnedCategory = await getOption('pinnedCategory')
|
await Driver.onDetect(url, await analyze(href, items), language, true)
|
||||||
|
} catch (error) {
|
||||||
|
Driver.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
const url = wappalyzer.parseUrl(port.sender.tab ? port.sender.tab.url : '')
|
getTechnologies() {
|
||||||
|
return Wappalyzer.technologies
|
||||||
|
},
|
||||||
|
|
||||||
const cookies = await browser.cookies.getAll({
|
async onDetect(url, detections = [], language, incrementHits = false) {
|
||||||
domain: `.${url.hostname}`
|
if (!detections.length) {
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let response
|
const { hostname, href } = url
|
||||||
|
|
||||||
switch (message.id) {
|
// Cache detections
|
||||||
case 'log':
|
const cache = (Driver.cache.hostnames[hostname] = {
|
||||||
wappalyzer.log(message.subject, message.source)
|
...(Driver.cache.hostnames[hostname] || {
|
||||||
|
detections: [],
|
||||||
|
hits: 0
|
||||||
|
}),
|
||||||
|
dateTime: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
break
|
// Remove duplicates
|
||||||
case 'init':
|
cache.detections = cache.detections = cache.detections.concat(detections)
|
||||||
wappalyzer.analyze(url, { cookies }, { tab: port.sender.tab })
|
|
||||||
|
|
||||||
break
|
cache.detections.filter(
|
||||||
case 'analyze':
|
({ technology: { name }, pattern: { regex } }, index) =>
|
||||||
if (message.subject.html) {
|
cache.detections.findIndex(
|
||||||
browser.i18n
|
({ technology: { name: _name }, pattern: { regex: _regex } }) =>
|
||||||
.detectLanguage(message.subject.html)
|
name === _name && (!regex || regex.toString() === _regex.toString())
|
||||||
.then(({ languages }) => {
|
) === index
|
||||||
const language = languages
|
)
|
||||||
.filter(({ percentage }) => percentage >= 75)
|
|
||||||
.map(({ language: lang }) => lang)[0]
|
|
||||||
|
|
||||||
message.subject.language = language
|
cache.hits += incrementHits ? 1 : 0
|
||||||
|
cache.language = cache.language || language
|
||||||
|
|
||||||
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab })
|
// Expire cache
|
||||||
})
|
Driver.cache.hostnames = Object.keys(Driver.cache.hostnames).reduce(
|
||||||
} else {
|
(hostnames, hostname) => {
|
||||||
wappalyzer.analyze(url, message.subject, { tab: port.sender.tab })
|
const cache = Driver.cache.hostnames[hostname]
|
||||||
}
|
|
||||||
|
|
||||||
await setOption('hostnameCache', wappalyzer.hostnameCache)
|
if (cache.dateTime > Date.now() - expiry) {
|
||||||
|
hostnames[hostname] = cache
|
||||||
break
|
|
||||||
case 'ad_log':
|
|
||||||
wappalyzer.cacheDetectedAds(message.subject)
|
|
||||||
|
|
||||||
break
|
|
||||||
case 'get_apps':
|
|
||||||
response = {
|
|
||||||
tabCache: tabCache[message.tab.id],
|
|
||||||
apps: wappalyzer.apps,
|
|
||||||
categories: wappalyzer.categories,
|
|
||||||
pinnedCategory,
|
|
||||||
termsAccepted:
|
|
||||||
userAgent() === 'chrome' ||
|
|
||||||
(await getOption('termsAccepted', false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
return hostnames
|
||||||
case 'set_option':
|
},
|
||||||
await setOption(message.key, message.value)
|
{}
|
||||||
|
)
|
||||||
break
|
|
||||||
case 'get_js_patterns':
|
|
||||||
response = {
|
|
||||||
patterns: wappalyzer.jsPatterns
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
await setOption(
|
||||||
case 'update_theme_mode':
|
'hostnames',
|
||||||
// Sync theme mode to popup.
|
Object.keys(Driver.cache.hostnames).reduce(
|
||||||
response = {
|
(cache, hostname) => ({
|
||||||
themeMode: await getOption('themeMode', false)
|
...cache,
|
||||||
}
|
[hostname]: {
|
||||||
|
...Driver.cache.hostnames[hostname],
|
||||||
|
detections: Driver.cache.hostnames[hostname].detections.map(
|
||||||
|
({
|
||||||
|
pattern: { regex, confidence, version },
|
||||||
|
match,
|
||||||
|
technology: { name: technology }
|
||||||
|
}) => ({
|
||||||
|
technology,
|
||||||
|
pattern: {
|
||||||
|
regex: regex.source,
|
||||||
|
confidence,
|
||||||
|
version
|
||||||
|
},
|
||||||
|
match
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
break
|
const resolved = resolve(Driver.cache.hostnames[hostname].detections)
|
||||||
default:
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response) {
|
await Driver.setIcon(url, resolved)
|
||||||
port.postMessage({
|
|
||||||
id: message.id,
|
|
||||||
response
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
wappalyzer.driver.document = document
|
const tabs = await promisify(chrome.tabs, 'query', { url: [href] })
|
||||||
|
|
||||||
/**
|
tabs.forEach(({ id }) => (Driver.cache.tabs[id] = resolved))
|
||||||
* Log messages to console
|
|
||||||
*/
|
|
||||||
wappalyzer.driver.log = (message, source, type) => {
|
|
||||||
const log = ['warn', 'error'].includes(type) ? type : 'log'
|
|
||||||
|
|
||||||
console[log](`[wappalyzer ${type}]`, `[${source}]`, message) // eslint-disable-line no-console
|
Driver.log({ hostname, technologies: resolved })
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
await Driver.ping()
|
||||||
* Display apps
|
},
|
||||||
*/
|
|
||||||
wappalyzer.driver.displayApps = async (detected, meta, context) => {
|
|
||||||
const { tab } = context
|
|
||||||
|
|
||||||
if (tab === undefined) {
|
async onAd(ad) {
|
||||||
return
|
Driver.cache.ads.push(ad)
|
||||||
}
|
|
||||||
|
|
||||||
tabCache[tab.id] = tabCache[tab.id] || {
|
await setOption('ads', Driver.cache.ads)
|
||||||
detected: []
|
},
|
||||||
}
|
|
||||||
|
|
||||||
tabCache[tab.id].detected = detected
|
async setIcon(url, technologies) {
|
||||||
|
const dynamicIcon = await getOption('dynamicIcon', true)
|
||||||
|
|
||||||
const pinnedCategory = await getOption('pinnedCategory')
|
let icon = 'default.svg'
|
||||||
const dynamicIcon = await getOption('dynamicIcon', true)
|
|
||||||
|
|
||||||
let found = false
|
if (dynamicIcon) {
|
||||||
|
const pinnedCategory = parseInt(await getOption('pinnedCategory'), 10)
|
||||||
|
|
||||||
// Find the main application to display
|
const pinned = technologies.find(({ categories }) =>
|
||||||
;[pinnedCategory].concat(categoryOrder).forEach((match) => {
|
categories.some(({ id }) => id === pinnedCategory)
|
||||||
Object.keys(detected).forEach((appName) => {
|
)
|
||||||
const app = detected[appName]
|
|
||||||
|
|
||||||
app.props.cats.forEach((category) => {
|
;({ icon } = pinned ||
|
||||||
if (category === match && !found) {
|
technologies.sort(({ categories: a }, { categories: b }) => {
|
||||||
let icon =
|
const max = (value) =>
|
||||||
app.props.icon && dynamicIcon ? app.props.icon : 'default.svg'
|
value.reduce((max, { priority }) => Math.max(max, priority))
|
||||||
|
|
||||||
if (/\.svg$/i.test(icon)) {
|
return max(a) > max(b) ? -1 : 1
|
||||||
icon = `converted/${icon.replace(/\.svg$/, '.png')}`
|
})[0] || { icon })
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const tabs = await promisify(chrome.tabs, 'query', { url: [url.href] })
|
||||||
browser.pageAction.setIcon({
|
|
||||||
tabId: tab.id,
|
await Promise.all(
|
||||||
path: `../images/icons/${icon}`
|
tabs.map(async ({ id: tabId }) => {
|
||||||
})
|
await promisify(chrome.pageAction, 'setIcon', {
|
||||||
} catch (e) {
|
tabId,
|
||||||
// Firefox for Android does not support setIcon see https://bugzilla.mozilla.org/show_bug.cgi?id=1331746
|
path: chrome.extension.getURL(
|
||||||
}
|
`../images/icons/${
|
||||||
|
/\.svg$/i.test(icon)
|
||||||
|
? `converted/${icon.replace(/\.svg$/, '.png')}`
|
||||||
|
: icon
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
found = true
|
chrome.pageAction.show(tabId)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
)
|
||||||
})
|
},
|
||||||
|
|
||||||
browser.pageAction.show(tab.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
async getDetections() {
|
||||||
* Fetch and cache robots.txt for host
|
const [{ id }] = await promisify(chrome.tabs, 'query', {
|
||||||
*/
|
active: true,
|
||||||
wappalyzer.driver.getRobotsTxt = async (host, secure = false) => {
|
currentWindow: true
|
||||||
if (robotsTxtQueue[host]) {
|
})
|
||||||
return robotsTxtQueue[host]
|
|
||||||
}
|
|
||||||
|
|
||||||
const tracking = await getOption('tracking', true)
|
return Driver.cache.tabs[id]
|
||||||
const robotsTxtCache = await getOption('robotsTxtCache', {})
|
},
|
||||||
|
|
||||||
robotsTxtQueue[host] = new Promise(async (resolve) => {
|
async getRobots(hostname, secure = false) {
|
||||||
if (!tracking) {
|
if (!(await getOption('tracking', true))) {
|
||||||
return resolve([])
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host in robotsTxtCache) {
|
if (typeof Driver.cache.robots[hostname] !== 'undefined') {
|
||||||
return resolve(robotsTxtCache[host])
|
return Driver.cache.robots[hostname]
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeout = setTimeout(() => resolve([]), 3000)
|
|
||||||
|
|
||||||
let response
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await fetch(`http${secure ? 's' : ''}://${host}/robots.txt`, {
|
Driver.cache.robots[hostname] = await Promise.race([
|
||||||
redirect: 'follow',
|
new Promise(async (resolve) => {
|
||||||
mode: 'no-cors'
|
const response = await fetch(
|
||||||
})
|
`http${secure ? 's' : ''}://${hostname}/robots.txt`,
|
||||||
} catch (error) {
|
{
|
||||||
wappalyzer.log(error, 'driver', 'error')
|
redirect: 'follow',
|
||||||
|
mode: 'no-cors'
|
||||||
return resolve([])
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
clearTimeout(timeout)
|
|
||||||
|
|
||||||
const robotsTxt = response.ok ? await response.text() : ''
|
|
||||||
|
|
||||||
robotsTxtCache[host] = Wappalyzer.parseRobotsTxt(robotsTxt)
|
if (!response.ok) {
|
||||||
|
Driver.error(new Error(response.statusText))
|
||||||
|
|
||||||
await setOption('robotsTxtCache', robotsTxtCache)
|
resolve('')
|
||||||
|
}
|
||||||
|
|
||||||
delete robotsTxtQueue[host]
|
let agent
|
||||||
|
|
||||||
return resolve(robotsTxtCache[host])
|
resolve(
|
||||||
})
|
(await response.text()).split('\n').reduce((disallows, line) => {
|
||||||
|
let matches = /^User-agent:\s*(.+)$/i.exec(line.trim())
|
||||||
|
|
||||||
return robotsTxtQueue[host]
|
if (matches) {
|
||||||
}
|
agent = matches[1].toLowerCase()
|
||||||
|
} else if (agent === '*' || agent === 'wappalyzer') {
|
||||||
|
matches = /^Disallow:\s*(.+)$/i.exec(line.trim())
|
||||||
|
|
||||||
/**
|
if (matches) {
|
||||||
* Anonymously track detected applications for research purposes
|
disallows.push(matches[1])
|
||||||
*/
|
}
|
||||||
wappalyzer.driver.ping = async (
|
}
|
||||||
hostnameCache = { expires: 0, hostnames: {} },
|
|
||||||
adCache = []
|
|
||||||
) => {
|
|
||||||
const tracking = await getOption('tracking', true)
|
|
||||||
const termsAccepted =
|
|
||||||
userAgent() === 'chrome' || (await getOption('termsAccepted', false))
|
|
||||||
|
|
||||||
if (tracking && termsAccepted) {
|
|
||||||
if (
|
|
||||||
hostnameCache.hostnames &&
|
|
||||||
Object.keys(hostnameCache.hostnames).length
|
|
||||||
) {
|
|
||||||
post('https://api.wappalyzer.com/ping/v1/', hostnameCache.hostnames)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (adCache.length) {
|
return disallows
|
||||||
post('https://ad.wappalyzer.com/log/wp/', adCache)
|
}, [])
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
new Promise((resolve) => setTimeout(() => resolve(''), 5000))
|
||||||
|
])
|
||||||
|
|
||||||
|
Driver.cache.robots = Object.keys(Driver.cache.robots)
|
||||||
|
.slice(-50)
|
||||||
|
.reduce(
|
||||||
|
(cache, hostname) => ({
|
||||||
|
...cache,
|
||||||
|
[hostname]: Driver.cache.robots[hostname]
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
await setOption('robots', Driver.cache.robots)
|
||||||
|
|
||||||
|
return Driver.cache.robots[hostname]
|
||||||
|
} catch (error) {
|
||||||
|
Driver.error(error)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
await setOption('robotsTxtCache', {})
|
async checkRobots(href) {
|
||||||
}
|
const url = new URL(href)
|
||||||
}
|
|
||||||
|
|
||||||
// Init
|
|
||||||
;(async () => {
|
|
||||||
// Technologies
|
|
||||||
try {
|
|
||||||
const response = await fetch('../apps.json')
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
wappalyzer.apps = json.apps
|
|
||||||
wappalyzer.categories = json.categories
|
|
||||||
} catch (error) {
|
|
||||||
wappalyzer.log(`GET apps.json: ${error.message}`, 'driver', 'error')
|
|
||||||
}
|
|
||||||
|
|
||||||
wappalyzer.parseJsPatterns()
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||||
|
throw new Error('Invalid protocol')
|
||||||
|
}
|
||||||
|
|
||||||
categoryOrder = Object.keys(wappalyzer.categories)
|
const robots = await Driver.getRobots(
|
||||||
.map((categoryId) => parseInt(categoryId, 10))
|
url.hostname,
|
||||||
.sort(
|
url.protocol === 'https:'
|
||||||
(a, b) =>
|
|
||||||
wappalyzer.categories[a].priority - wappalyzer.categories[b].priority
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Version check
|
if (robots.some((disallowed) => url.pathname.indexOf(disallowed) === 0)) {
|
||||||
const { version } = browser.runtime.getManifest()
|
throw new Error('Disallowed')
|
||||||
const previousVersion = await getOption('version')
|
}
|
||||||
const upgradeMessage = await getOption('upgradeMessage', true)
|
},
|
||||||
|
|
||||||
if (previousVersion === null) {
|
|
||||||
openTab({
|
|
||||||
url: `${wappalyzer.config.websiteURL}installed`
|
|
||||||
})
|
|
||||||
} else if (version !== previousVersion && upgradeMessage) {
|
|
||||||
// openTab({
|
|
||||||
// url: `${wappalyzer.config.websiteURL}upgraded?v${version}`,
|
|
||||||
// background: true
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
await setOption('version', version)
|
|
||||||
|
|
||||||
// Hostname cache
|
async ping() {
|
||||||
wappalyzer.hostnameCache = await getOption('hostnameCache', {
|
const tracking = await getOption('tracking', true)
|
||||||
expires: Date.now() + 1000 * 60 * 60 * 24,
|
const termsAccepted =
|
||||||
hostnames: {}
|
agent === 'chrome' || (await getOption('termsAccepted', false))
|
||||||
})
|
|
||||||
|
if (tracking && termsAccepted) {
|
||||||
|
const count = Object.keys(Driver.cache.hostnames).length
|
||||||
|
|
||||||
|
if (count && (count >= 50 || Driver.lastPing < Date.now() - expiry)) {
|
||||||
|
await Driver.post(
|
||||||
|
'https://api.wappalyzer.com/ping/v1/',
|
||||||
|
Object.keys(Driver.cache.hostnames).reduce((hostnames, hostname) => {
|
||||||
|
// eslint-disable-next-line standard/computed-property-even-spacing
|
||||||
|
const { language, detections, hits } = Driver.cache.hostnames[
|
||||||
|
hostname
|
||||||
|
]
|
||||||
|
|
||||||
|
hostnames[hostname] = hostnames[hostname] || {
|
||||||
|
applications: resolve(detections).reduce(
|
||||||
|
(technologies, { name, confidence, version }) => {
|
||||||
|
if (confidence === 100) {
|
||||||
|
technologies[name] = {
|
||||||
|
version,
|
||||||
|
hits
|
||||||
|
}
|
||||||
|
|
||||||
|
return technologies
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostnames
|
||||||
|
}, {})
|
||||||
|
)
|
||||||
|
|
||||||
|
await setOption('hostnames', (Driver.cache.hostnames = {}))
|
||||||
|
|
||||||
|
Driver.lastPing = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
// Run content script on all tabs
|
if (Driver.cache.ads.length > 50) {
|
||||||
try {
|
await Driver.post('https://ad.wappalyzer.com/log/wp/', Driver.cache.ads)
|
||||||
const tabs = await browser.tabs.query({
|
|
||||||
url: ['http://*/*', 'https://*/*']
|
|
||||||
})
|
|
||||||
|
|
||||||
tabs.forEach(async (tab) => {
|
await setOption('ads', (Driver.cache.ads = []))
|
||||||
try {
|
|
||||||
await browser.tabs.executeScript(tab.id, {
|
|
||||||
file: '../js/content.js'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
//
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
} catch (error) {
|
|
||||||
wappalyzer.log(error, 'driver', 'error')
|
|
||||||
}
|
}
|
||||||
})()
|
}
|
||||||
|
|
||||||
|
Driver.init()
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
jsonToDOM.namespaces = {
|
|
||||||
html: 'http://www.w3.org/1999/xhtml',
|
|
||||||
xul: 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul',
|
|
||||||
};
|
|
||||||
|
|
||||||
jsonToDOM.defaultNamespace = jsonToDOM.namespaces.html;
|
|
||||||
|
|
||||||
function jsonToDOM(jsonTemplate, doc, nodes) {
|
|
||||||
function namespace(name) {
|
|
||||||
const reElemNameParts = /^(?:(.*):)?(.*)$/.exec(name);
|
|
||||||
return { namespace: jsonToDOM.namespaces[reElemNameParts[1]], shortName: reElemNameParts[2] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that 'elemNameOrArray' is: either the full element name (eg. [html:]div) or an array of elements in JSON notation
|
|
||||||
function tag(elemNameOrArray, elemAttr) {
|
|
||||||
// Array of elements? Parse each one...
|
|
||||||
if (Array.isArray(elemNameOrArray)) {
|
|
||||||
const frag = doc.createDocumentFragment();
|
|
||||||
Array.prototype.forEach.call(arguments, (thisElem) => {
|
|
||||||
frag.appendChild(tag(...thisElem));
|
|
||||||
});
|
|
||||||
return frag;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single element? Parse element namespace prefix (if none exists, default to defaultNamespace), and create element
|
|
||||||
const elemNs = namespace(elemNameOrArray);
|
|
||||||
const elem = doc.createElementNS(elemNs.namespace || jsonToDOM.defaultNamespace, elemNs.shortName);
|
|
||||||
|
|
||||||
// Set element's attributes and/or callback functions (eg. onclick)
|
|
||||||
for (const key in elemAttr) {
|
|
||||||
const val = elemAttr[key];
|
|
||||||
if (nodes && key == 'key') {
|
|
||||||
nodes[val] = elem;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const attrNs = namespace(key);
|
|
||||||
if (typeof val === 'function') {
|
|
||||||
// Special case for function attributes; don't just add them as 'on...' attributes, but as events, using addEventListener
|
|
||||||
elem.addEventListener(key.replace(/^on/, ''), val, false);
|
|
||||||
} else {
|
|
||||||
// Note that the default namespace for XML attributes is, and should be, blank (ie. they're not in any namespace)
|
|
||||||
elem.setAttributeNS(attrNs.namespace || '', attrNs.shortName, val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and append this element's children
|
|
||||||
const childElems = Array.prototype.slice.call(arguments, 2);
|
|
||||||
childElems.forEach((childElem) => {
|
|
||||||
if (childElem != null) {
|
|
||||||
elem.appendChild(
|
|
||||||
childElem instanceof doc.defaultView.Node ? childElem
|
|
||||||
: Array.isArray(childElem) ? tag(...childElem)
|
|
||||||
: doc.createTextNode(childElem),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return elem;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tag(...jsonTemplate);
|
|
||||||
}
|
|
@ -1,335 +1,188 @@
|
|||||||
|
'use strict'
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
/* global browser, jsonToDOM */
|
/* globals chrome, Utils */
|
||||||
|
|
||||||
/** global: browser */
|
const { agent, i18n, getOption, setOption, promisify } = Utils
|
||||||
/** global: jsonToDOM */
|
|
||||||
|
|
||||||
let pinnedCategory = null
|
const Popup = {
|
||||||
let termsAccepted = false
|
port: chrome.runtime.connect({ name: 'popup.js' }),
|
||||||
|
|
||||||
const port = browser.runtime.connect({
|
async init() {
|
||||||
name: 'popup.js'
|
// Templates
|
||||||
})
|
Popup.templates = Array.from(
|
||||||
|
document.querySelectorAll('[data-template]')
|
||||||
function slugify(string) {
|
).reduce((templates, template) => {
|
||||||
return string
|
templates[template.dataset.template] = template.cloneNode(true)
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9-]/g, '-')
|
|
||||||
.replace(/--+/g, '-')
|
|
||||||
.replace(/(?:^-|-$)/, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
function i18n() {
|
|
||||||
const nodes = document.querySelectorAll('[data-i18n]')
|
|
||||||
|
|
||||||
Array.prototype.forEach.call(nodes, (node) => {
|
template.remove()
|
||||||
node.innerHTML = browser.i18n.getMessage(node.dataset.i18n)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceDom(domTemplate) {
|
|
||||||
const container = document.getElementsByClassName('container')[0]
|
|
||||||
|
|
||||||
while (container.firstChild) {
|
|
||||||
container.removeChild(container.firstChild)
|
|
||||||
}
|
|
||||||
|
|
||||||
container.appendChild(jsonToDOM(domTemplate, document, {}))
|
return templates
|
||||||
|
}, {})
|
||||||
|
|
||||||
i18n()
|
// Theme mode
|
||||||
|
const themeMode = await getOption('themeMode', false)
|
||||||
|
|
||||||
Array.from(
|
if (themeMode) {
|
||||||
document.querySelectorAll('.detected__category-pin-wrapper')
|
document.querySelector('body').classList.add('theme-mode')
|
||||||
).forEach((pin) => {
|
}
|
||||||
pin.addEventListener('click', () => {
|
|
||||||
const categoryId = parseInt(pin.dataset.categoryId, 10)
|
|
||||||
|
|
||||||
if (categoryId === pinnedCategory) {
|
|
||||||
pin.className = 'detected__category-pin-wrapper'
|
|
||||||
|
|
||||||
pinnedCategory = null
|
// Terms
|
||||||
} else {
|
const termsAccepted =
|
||||||
const active = document.querySelector(
|
agent === 'chrome' || (await getOption('termsAccepted', false))
|
||||||
'.detected__category-pin-wrapper--active'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (active) {
|
if (termsAccepted) {
|
||||||
active.className = 'detected__category-pin-wrapper'
|
document.querySelector('.terms').style.display = 'none'
|
||||||
}
|
|
||||||
|
|
||||||
pin.className =
|
Popup.driver('getDetections')
|
||||||
'detected__category-pin-wrapper detected__category-pin-wrapper--active'
|
} else {
|
||||||
|
document.querySelector('.detections').style.display = 'none'
|
||||||
|
|
||||||
pinnedCategory = categoryId
|
i18n()
|
||||||
}
|
}
|
||||||
|
|
||||||
port.postMessage({
|
// Alert
|
||||||
id: 'set_option',
|
const [{ url }] = await promisify(chrome.tabs, 'query', {
|
||||||
key: 'pinnedCategory',
|
active: true,
|
||||||
value: pinnedCategory
|
currentWindow: true
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
Array.from(document.querySelectorAll('a')).forEach((link) => {
|
document.querySelector(
|
||||||
link.addEventListener('click', () => {
|
'.alerts__link'
|
||||||
browser.tabs.create({ url: link.href })
|
).href = `https://www.wappalyzer.com/alerts/manage?url=${encodeURIComponent(
|
||||||
|
`${url}`
|
||||||
|
)}`
|
||||||
|
|
||||||
return false
|
document
|
||||||
})
|
.querySelector('.footer__settings')
|
||||||
})
|
.addEventListener('click', () => chrome.runtime.openOptionsPage())
|
||||||
}
|
},
|
||||||
|
|
||||||
|
driver(func, ...args) {
|
||||||
|
Popup.port.postMessage({ func, args })
|
||||||
|
},
|
||||||
|
|
||||||
|
log(message) {
|
||||||
|
Popup.driver('log', message, 'popup.js')
|
||||||
|
},
|
||||||
|
|
||||||
|
categorise(technologies) {
|
||||||
|
return Object.values(
|
||||||
|
technologies.reduce((categories, technology) => {
|
||||||
|
technology.categories.forEach((category) => {
|
||||||
|
categories[category.id] = categories[category.id] || {
|
||||||
|
...category,
|
||||||
|
technologies: []
|
||||||
|
}
|
||||||
|
|
||||||
|
categories[category.id].technologies.push(technology)
|
||||||
|
})
|
||||||
|
|
||||||
|
return categories
|
||||||
|
}, {})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
async onGetDetections(detections) {
|
||||||
|
const pinnedCategory = await getOption('pinnedCategory')
|
||||||
|
|
||||||
|
if (detections.length) {
|
||||||
|
document.querySelector('.empty').remove()
|
||||||
|
}
|
||||||
|
|
||||||
function replaceDomWhenReady(dom) {
|
Popup.categorise(detections).forEach(
|
||||||
if (/complete|interactive|loaded/.test(document.readyState)) {
|
({ id, name, slug: categorySlug, technologies }) => {
|
||||||
replaceDom(dom)
|
const categoryNode = Popup.templates.category.cloneNode(true)
|
||||||
} else {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
const link = categoryNode.querySelector('.category__link')
|
||||||
replaceDom(dom)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function appsToDomTemplate(response) {
|
link.href = `https://www.wappalyzer.com/technologies/${categorySlug}`
|
||||||
let template = []
|
link.textContent = name
|
||||||
|
|
||||||
if (response.tabCache && Object.keys(response.tabCache.detected).length > 0) {
|
const pins = categoryNode.querySelectorAll('.category__pin')
|
||||||
const categories = {}
|
|
||||||
|
|
||||||
// Group apps by category
|
if (pinnedCategory === id) {
|
||||||
for (const appName in response.tabCache.detected) {
|
pins.forEach((pin) => pin.classList.add('category__pin--active'))
|
||||||
response.apps[appName].cats.forEach((cat) => {
|
|
||||||
categories[cat] = categories[cat] || {
|
|
||||||
name: response.categories[cat].name,
|
|
||||||
apps: []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
categories[cat].apps[appName] = appName
|
pins.forEach((pin) =>
|
||||||
})
|
pin.addEventListener('click', async () => {
|
||||||
}
|
const pinnedCategory = await getOption('pinnedCategory')
|
||||||
|
|
||||||
for (const cat in categories) {
|
Array.from(
|
||||||
const apps = []
|
document.querySelectorAll('.category__pin--active')
|
||||||
|
).forEach((pin) => pin.classList.remove('category__pin--active'))
|
||||||
for (const appName in categories[cat].apps) {
|
|
||||||
const { confidenceTotal, version } = response.tabCache.detected[appName]
|
|
||||||
|
|
||||||
apps.push([
|
|
||||||
'a',
|
|
||||||
{
|
|
||||||
class: 'detected__app',
|
|
||||||
href: `https://www.wappalyzer.com/technologies/${slugify(
|
|
||||||
categories[cat].name
|
|
||||||
)}/${slugify(appName)}`
|
|
||||||
},
|
|
||||||
[
|
|
||||||
'img',
|
|
||||||
{
|
|
||||||
class: 'detected__app-icon',
|
|
||||||
src: `../images/icons/${response.apps[appName].icon ||
|
|
||||||
'default.svg'}`
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'span',
|
|
||||||
{
|
|
||||||
class: 'detected__app-name'
|
|
||||||
},
|
|
||||||
appName
|
|
||||||
],
|
|
||||||
version
|
|
||||||
? [
|
|
||||||
'span',
|
|
||||||
{
|
|
||||||
class: 'detected__app-version'
|
|
||||||
},
|
|
||||||
version
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
confidenceTotal < 100
|
|
||||||
? [
|
|
||||||
'span',
|
|
||||||
{
|
|
||||||
class: 'detected__app-confidence'
|
|
||||||
},
|
|
||||||
`${confidenceTotal}% sure`
|
|
||||||
]
|
|
||||||
: null
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
template.push([
|
if (pinnedCategory === id) {
|
||||||
'div',
|
await setOption('pinnedCategory', null)
|
||||||
{
|
} else {
|
||||||
class: 'detected__category'
|
await setOption('pinnedCategory', id)
|
||||||
},
|
|
||||||
[
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
class: 'detected__category-name'
|
|
||||||
},
|
|
||||||
[
|
|
||||||
'a',
|
|
||||||
{
|
|
||||||
class: 'detected__category-link',
|
|
||||||
href: `https://www.wappalyzer.com/categories/${slugify(
|
|
||||||
response.categories[cat].name
|
|
||||||
)}`
|
|
||||||
},
|
|
||||||
browser.i18n.getMessage(`categoryName${cat}`)
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'span',
|
|
||||||
{
|
|
||||||
class: `detected__category-pin-wrapper${
|
|
||||||
parseInt(pinnedCategory, 10) === parseInt(cat, 10)
|
|
||||||
? ' 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 = [
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
class: 'detected'
|
|
||||||
},
|
|
||||||
template
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
template = [
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
class: 'empty'
|
|
||||||
},
|
|
||||||
[
|
|
||||||
'span',
|
|
||||||
{
|
|
||||||
class: 'empty__text'
|
|
||||||
},
|
|
||||||
browser.i18n.getMessage('noAppsDetected')
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return template
|
pins.forEach((pin) => pin.classList.add('category__pin--active'))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
async function getApps() {
|
technologies
|
||||||
try {
|
.filter(({ confidence }) => confidence)
|
||||||
const tabs = await browser.tabs.query({
|
.forEach(({ name, slug, confidence, version, icon, website }) => {
|
||||||
active: true,
|
const technologyNode = Popup.templates.technology.cloneNode(true)
|
||||||
currentWindow: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const url = new URL(tabs[0].url)
|
const image = technologyNode.querySelector('.technology__icon')
|
||||||
|
|
||||||
document.querySelector(
|
image.src = `../images/icons/${icon}`
|
||||||
'.footer__link'
|
|
||||||
).href = `https://www.wappalyzer.com/alerts/manage?url=${encodeURIComponent(
|
|
||||||
`${url.protocol}//${url.hostname}`
|
|
||||||
)}`
|
|
||||||
|
|
||||||
port.postMessage({
|
const link = technologyNode.querySelector('.technology__link')
|
||||||
id: 'get_apps',
|
|
||||||
tab: tabs[0]
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error) // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
link.href = `https://www.wappalyzer.com/technologies/${categorySlug}/${slug}`
|
||||||
* Async function to update body class based on option.
|
link.textContent = name
|
||||||
*/
|
|
||||||
function getThemeMode() {
|
|
||||||
try {
|
|
||||||
port.postMessage({
|
|
||||||
id: 'update_theme_mode'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error) // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const confidenceNode = technologyNode.querySelector(
|
||||||
* Update theme mode based on browser option.
|
'.technology__confidence'
|
||||||
* @param {object} res Response from port listener.
|
)
|
||||||
*/
|
|
||||||
function updateThemeMode(res) {
|
|
||||||
if (res.hasOwnProperty('themeMode') && res.themeMode !== false) {
|
|
||||||
document.body.classList.add('theme-mode-sync')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayApps(response) {
|
if (confidence < 100) {
|
||||||
pinnedCategory = response.pinnedCategory // eslint-disable-line prefer-destructuring
|
confidenceNode.textContent = `${confidence}% sure`
|
||||||
termsAccepted = response.termsAccepted // eslint-disable-line prefer-destructuring
|
} else {
|
||||||
|
confidenceNode.remove()
|
||||||
|
}
|
||||||
|
|
||||||
if (termsAccepted) {
|
const versionNode = technologyNode.querySelector(
|
||||||
replaceDomWhenReady(appsToDomTemplate(response))
|
'.technology__version'
|
||||||
} else {
|
)
|
||||||
i18n()
|
|
||||||
|
|
||||||
const wrapper = document.querySelector('.terms__wrapper')
|
if (version) {
|
||||||
|
versionNode.textContent = version
|
||||||
|
} else {
|
||||||
|
versionNode.remove()
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelector('.terms__accept').addEventListener('click', () => {
|
categoryNode
|
||||||
port.postMessage({
|
.querySelector('.technologies')
|
||||||
id: 'set_option',
|
.appendChild(technologyNode)
|
||||||
key: 'termsAccepted',
|
})
|
||||||
value: true
|
|
||||||
})
|
|
||||||
|
|
||||||
wrapper.classList.remove('terms__wrapper--active')
|
document.querySelector('.detections').appendChild(categoryNode)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
getApps()
|
Array.from(document.querySelectorAll('a')).forEach((a) =>
|
||||||
})
|
a.addEventListener('click', () => Popup.driver('open', a.href))
|
||||||
|
)
|
||||||
|
|
||||||
wrapper.classList.add('terms__wrapper--active')
|
i18n()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
port.onMessage.addListener((message) => {
|
Popup.port.onMessage.addListener(({ func, args }) => {
|
||||||
switch (message.id) {
|
const onFunc = `on${func.charAt(0).toUpperCase() + func.slice(1)}`
|
||||||
case 'get_apps':
|
|
||||||
displayApps(message.response)
|
|
||||||
|
|
||||||
break
|
|
||||||
case 'update_theme_mode':
|
|
||||||
updateThemeMode(message.response)
|
|
||||||
|
|
||||||
break
|
if (Popup[onFunc]) {
|
||||||
default:
|
Popup[onFunc](args)
|
||||||
// Do nothing
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
getThemeMode()
|
if (/complete|interactive|loaded/.test(document.readyState)) {
|
||||||
getApps()
|
Popup.init()
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', Popup.init)
|
||||||
|
}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
'use strict'
|
||||||
|
/* eslint-env browser */
|
||||||
|
/* globals chrome */
|
||||||
|
|
||||||
|
const Utils = {
|
||||||
|
agent: chrome.extension.getURL('/').startsWith('moz-') ? 'firefox' : 'chrome',
|
||||||
|
|
||||||
|
promisify(context, method, ...args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
context[method](...args, (...args) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
return reject(chrome.runtime.lastError)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(...args)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
open(url, active = true) {
|
||||||
|
chrome.tabs.create({ url, active })
|
||||||
|
},
|
||||||
|
|
||||||
|
async getOption(name, defaultValue = null) {
|
||||||
|
try {
|
||||||
|
const option = await Utils.promisify(chrome.storage.local, 'get', name)
|
||||||
|
|
||||||
|
if (option[name] !== undefined) {
|
||||||
|
return option[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.message || error.toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async setOption(name, value) {
|
||||||
|
try {
|
||||||
|
await Utils.promisify(chrome.storage.local, 'set', {
|
||||||
|
[name]: value
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.message || error.toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
i18n() {
|
||||||
|
Array.from(document.querySelectorAll('[data-i18n]')).forEach(
|
||||||
|
(node) => (node.innerHTML = chrome.i18n.getMessage(node.dataset.i18n))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue