Add a boilerplate for mv3 extensions
136
.gitignore
vendored
|
@ -1,3 +1,133 @@
|
||||||
node_modules
|
_generated
|
||||||
npm-debug.log
|
*.swp # Vim's unfinished files
|
||||||
.swp
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
695
LICENSE
|
@ -1,674 +1,21 @@
|
||||||
GNU GENERAL PUBLIC LICENSE
|
MIT License
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
Copyright (c) 2024 ilyaigpetrov
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this license document, but changing it is not allowed.
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
Preamble
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
The GNU General Public License is a free, copyleft license for
|
furnished to do so, subject to the following conditions:
|
||||||
software and other kinds of works.
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
The licenses for most software and other practical works are designed
|
copies or substantial portions of the Software.
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
share and change all versions of a program--to make sure it remains free
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
GNU General Public License for most of our software; it applies also to
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
any other work released this way by its authors. You can apply it to
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
your programs, too.
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
{one line to give the program's name and a brief idea of what it does.}
|
|
||||||
Copyright (C) {year} {name of author}
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
{project} Copyright (C) {year} {fullname}
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
|
||||||
|
|
72
README.md
|
@ -1,71 +1 @@
|
||||||
If you __unstar__, please, [leave us a note](https://github.com/anticensority/runet-censorship-bypass/issues) why you do so.
|
# webext-mv3-boilerplate
|
||||||
|
|
||||||
[d1]: https://img.shields.io/badge/Поддержать-❤-green.svg
|
|
||||||
[d2]: https://github.com/anticensority/runet-censorship-bypass/wiki/Поддержать
|
|
||||||
|
|
||||||
[![Поддержать][d1]][d2]
|
|
||||||
[![Backers on Open Collective](https://opencollective.com/anticensority/backers/badge.svg)](#backers)
|
|
||||||
[![Sponsors on Open Collective](https://opencollective.com/anticensority/sponsors/badge.svg)](#sponsors)
|
|
||||||
|
|
||||||
# Russian Anticensorship on PAC-Scripts
|
|
||||||
|
|
||||||
This repo contains an extension for Chromium and FireFox that helps to bypass censorship in Russia: [WebStore](https://chrome.google.com/webstore/detail/npgcnondjocldhldegnakemclmfkngch)
|
|
||||||
| [Sources](./extensions/chromium/runet-censorship-bypass).
|
|
||||||
This extension uses pac scripts, one of which (anticensority) is generated by this [pac-generator].
|
|
||||||
|
|
||||||
[pac-generator]: https://github.com/anticensority/pac-script-generator
|
|
||||||
|
|
||||||
## Install / Установка
|
|
||||||
|
|
||||||
1. [Chrome Web Store](https://chrome.google.com/webstore/detail/обход-блокировок-рунета/npgcnondjocldhldegnakemclmfkngch)
|
|
||||||
2. [Chrome Web Store (MINI)](https://chrome.google.com/webstore/detail/обход-блокировок-рунета-м/gnknjnebjldmkpmlhjipalimhjofpgho)
|
|
||||||
3. [Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/обход-блокировок-рунета/ajgpnodjpffiagcfmifildjpoaeiobfh)
|
|
||||||
4. [Microsoft Edge Add-ons (MINI)](https://microsoftedge.microsoft.com/addons/detail/обход-блокировок-рунета-м/cjppllmpmkpjfchbaoebeneghcbmlibj)
|
|
||||||
5. [FireFox Add-ons](https://addons.mozilla.org/ru/firefox/addon/обход-блокировок-рунета/).
|
|
||||||
6. Opera: сначала [установщик расширений из WebStore](https://addons.opera.com/ru/extensions/details/install-chrome-extensions/) (от команды Opera), затем см. пункты 1 и 2 выше.
|
|
||||||
7. Пакеты для автономной (offline) установки / Packages for offline installation: https://github.com/anticensority/runet-censorship-bypass/releases.
|
|
||||||
|
|
||||||
Из-за блокировок адресов Google расширение может не устанавливаться из WebStore. Подробности и способы установки см. https://github.com/anticensority/runet-censorship-bypass/wiki/Автономная-установка-расширения.
|
|
||||||
|
|
||||||
## Why I do This
|
|
||||||
|
|
||||||
See [my arguments against censorship (ru)](https://github.com/anticensority/runet-censorship-bypass/wiki/Почему-мы-это-делаем%3F-Аргументы-против-цензуры).
|
|
||||||
|
|
||||||
Looking at how Russian government [distorts TV](https://therussianreader.wordpress.com/2015/11/22/russian-truckers-strike-dagestan/) and blocks [critics of Putin](https://www.reuters.com/article/us-russia-internet-idUSBREA2C21L20140313),
|
|
||||||
I decided to write an anti-censorship extension for Chromium before they strike me first.
|
|
||||||
|
|
||||||
## How it Works
|
|
||||||
|
|
||||||
0. PAC script is a JavaScript file, triggered on every URL request, which tells the browser which proxy to use if any for this particular URL.
|
|
||||||
1. The Chrome Extension sets the PAC-script in browser settings and keeps it synced with the PAC script on the server. It offers Antizapret (hosted on a dedicated server) or Anticensority (hosted on GitHub) built-in PAC scripts for the user choice.
|
|
||||||
2. On every request the PAC script checks if the host is blocked or if its IP is blocked.
|
|
||||||
3. If an address is blocked, the PAC script returns the proxy server to the browser. The Antizapret PAC script uses its own proxy servers and the Anticensority PAC-script uses local Tor.
|
|
||||||
4. PAC scripts on servers are updated periodically from https://github.com/zapret-info/z-i.
|
|
||||||
|
|
||||||
## Contributors
|
|
||||||
|
|
||||||
This project exists thanks to all the people who contribute.
|
|
||||||
<a href="https://github.com/anticensority/runet-censorship-bypass/graphs/contributors"><img src="https://opencollective.com/anticensority/contributors.svg?width=890&button=false?force" /></a>
|
|
||||||
|
|
||||||
|
|
||||||
## Backers
|
|
||||||
|
|
||||||
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/anticensority#backer)]
|
|
||||||
|
|
||||||
<a href="https://opencollective.com/anticensority#backers" target="_blank"><img src="https://opencollective.com/anticensority/backers.svg?width=890"></a>
|
|
||||||
|
|
||||||
|
|
||||||
## Sponsors
|
|
||||||
|
|
||||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/anticensority#sponsor)]
|
|
||||||
|
|
||||||
<a href="https://opencollective.com/anticensority/sponsor/0/website" target="_blank"><img src="https://opencollective.com/anticensority/sponsor/0/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/anticensority/sponsor/1/website" target="_blank"><img src="https://opencollective.com/anticensority/sponsor/1/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/anticensority/sponsor/2/website" target="_blank"><img src="https://opencollective.com/anticensority/sponsor/2/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/anticensority/sponsor/3/website" target="_blank"><img src="https://opencollective.com/anticensority/sponsor/3/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/anticensority/sponsor/4/website" target="_blank"><img src="https://opencollective.com/anticensority/sponsor/4/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/anticensority/sponsor/5/website" target="_blank"><img src="https://opencollective.com/anticensority/sponsor/5/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/anticensority/sponsor/6/website" target="_blank"><img src="https://opencollective.com/anticensority/sponsor/6/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/anticensority/sponsor/7/website" target="_blank"><img src="https://opencollective.com/anticensority/sponsor/7/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/anticensority/sponsor/8/website" target="_blank"><img src="https://opencollective.com/anticensority/sponsor/8/avatar.svg"></a>
|
|
||||||
<a href="https://opencollective.com/anticensority/sponsor/9/website" target="_blank"><img src="https://opencollective.com/anticensority/sponsor/9/avatar.svg"></a>
|
|
45
_locales/en/messages.json
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"ExtensionName": {
|
||||||
|
"message": "Runet censorship bypass",
|
||||||
|
"description": "Name of the extension."
|
||||||
|
},
|
||||||
|
"ExtensionDescription": {
|
||||||
|
"message": "Copy Unicode URLs to clipboard without nasty percent-encodings or Punycode!",
|
||||||
|
"description": "Description of the extension."
|
||||||
|
},
|
||||||
|
"FOR_TESTING": {
|
||||||
|
"message": "FOR TESTING"
|
||||||
|
},
|
||||||
|
|
||||||
|
"IfToDecode": {
|
||||||
|
"message": "If to decode",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"IfToEncodeUrlTerminators": {
|
||||||
|
"message": "If to encode URL terminators",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"IfToDecodeMultipleTimes": {
|
||||||
|
"message": "If to decode multiple times",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"Donate": {
|
||||||
|
"message": "Donate ❤",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"CopyUnicodeUrl": {
|
||||||
|
"message": "Copy Unicode URL",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"IconHint": {
|
||||||
|
"message": "Click to copy unicode URL into the clipboard",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"CopyUnicodeLinkToHighlight": {
|
||||||
|
"message": "Copy Unicode link to highlight",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"CopyUrlFromTheAddressBar": {
|
||||||
|
"message": "Copy URL from the address bar"
|
||||||
|
}
|
||||||
|
}
|
34
_locales/ru/messages.json
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"ExtensionName": {
|
||||||
|
"message": "Обход блокировок Рунета",
|
||||||
|
"description": "Name of the extension."
|
||||||
|
},
|
||||||
|
"ExtensionDescription": {
|
||||||
|
"message": "Копируйте юникодные ссылки в буфер обмена без ужасных кодировок с процентами или Punycode!",
|
||||||
|
"description": "Description of the extension."
|
||||||
|
},
|
||||||
|
"FOR_TESTING": {
|
||||||
|
"message": "FOR TESTING"
|
||||||
|
},
|
||||||
|
|
||||||
|
"IfToDecode": {
|
||||||
|
"message": "Декодировать",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"IfToEncodeUrlTerminators": {
|
||||||
|
"message": "Кодировать знаки окончания URL",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"Donate": {
|
||||||
|
"message": "Поддержать ❤",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"CopyUnicodeUrl": {
|
||||||
|
"message": "Скопировать юникодную ссылку",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"IconHint": {
|
||||||
|
"message": "Кликните для копирования юникодной ссылки в буфер обмена",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
34
_locales/zh/messages.json
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"ExtensionName": {
|
||||||
|
"message": "Copy Unicode URLs",
|
||||||
|
"description": "Name of the extension."
|
||||||
|
},
|
||||||
|
"ExtensionDescription": {
|
||||||
|
"message": "复制 Unicode 网址时去掉令人讨厌的百分比编码或Punycode!",
|
||||||
|
"description": "Description of the extension."
|
||||||
|
},
|
||||||
|
"FOR_TESTING": {
|
||||||
|
"message": "FOR TESTING"
|
||||||
|
},
|
||||||
|
|
||||||
|
"IfToDecode": {
|
||||||
|
"message": "是否编码",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"IfToEncodeUrlTerminators": {
|
||||||
|
"message": "是否编码 URL 终止符",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"Donate": {
|
||||||
|
"message": "捐赠 ❤",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"CopyUnicodeUrl": {
|
||||||
|
"message": "复制 Unicode URL",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"IconHint": {
|
||||||
|
"message": "复制 unicode URL 到剪贴板",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
https://chromereleases.googleblog.com/search/label/Stable%20updates
|
|
||||||
https://www.chromestatus.com
|
|
||||||
https://www.youtube.com/user/ChromeDevelopers/videos
|
|
|
@ -1,33 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
extends: ['eslint:recommended', 'google'],
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
webextensions: true,
|
|
||||||
es6: true
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
chrome: true
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: 'script',
|
|
||||||
ecmaVersion: 2017,
|
|
||||||
ecmaFeatures: {
|
|
||||||
impliedStrict: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
strict: ['error', 'global'],
|
|
||||||
'no-console': 'off',
|
|
||||||
'padded-blocks': 'off',
|
|
||||||
'require-jsdoc': 'off',
|
|
||||||
|
|
||||||
// Taken from airbnb:
|
|
||||||
'max-len': ['error', 100, 2, {
|
|
||||||
ignoreUrls: true,
|
|
||||||
ignoreComments: false,
|
|
||||||
ignoreRegExpLiterals: true,
|
|
||||||
ignoreStrings: true,
|
|
||||||
ignoreTemplateLiterals: true,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,6 +0,0 @@
|
||||||
node_modules
|
|
||||||
node_modules_linux
|
|
||||||
node_modules_win
|
|
||||||
npm-debug.log
|
|
||||||
*.swp
|
|
||||||
build/
|
|
|
@ -1,33 +0,0 @@
|
||||||
# Install
|
|
||||||
|
|
||||||
Tested on:
|
|
||||||
|
|
||||||
NodeJS: v16.1.0.
|
|
||||||
NPM: 7.11.2.
|
|
||||||
OS: Linux Mint 20 Xfce Edition.
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
cd src/extension-common/pages/options/
|
|
||||||
npm install
|
|
||||||
cd -
|
|
||||||
|
|
||||||
# For debugging:
|
|
||||||
npm start
|
|
||||||
# Use your build/extension-beta
|
|
||||||
|
|
||||||
# For production:
|
|
||||||
npm start
|
|
||||||
# Use your build/extension-full or build/extension-mini
|
|
||||||
```
|
|
||||||
|
|
||||||
# For Reviewers
|
|
||||||
|
|
||||||
See ./src/extension-common/FOR_REVIEWERS.md.
|
|
||||||
|
|
||||||
# Release Instructions
|
|
||||||
|
|
||||||
1. `npm run release`
|
|
||||||
2. `vim src/templates-data.js` and bump version.
|
|
||||||
3. Commit bumped version.
|
|
||||||
4. Merge development to production (usually after deployment and testing and many patches).
|
|
|
@ -1,12 +0,0 @@
|
||||||
# Blue Ribbon Icon
|
|
||||||
|
|
||||||
URL: http://www.iconsdb.com/icon-sets/cardboard-blue-icons/ribbon-12-icon.html
|
|
||||||
|
|
||||||
# SVG Icons, emoji
|
|
||||||
|
|
||||||
URL: https://github.com/jslegers/emoji-icon-font
|
|
||||||
Author: John Slegers aka jslegers
|
|
||||||
|
|
||||||
# Pac-Man And Ghosts
|
|
||||||
|
|
||||||
http://photobucket.com/gallery/user/chukiyo/media/bWVkaWFJZDoyMTA2NDcxMw==/?ref=
|
|
Before Width: | Height: | Size: 756 KiB |
|
@ -1,61 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
version="1.1"
|
|
||||||
id="svg15"
|
|
||||||
sodipodi:docname="my-circled-information-circle - Copywwwwww.svg"
|
|
||||||
inkscape:version="0.92.1 r15371">
|
|
||||||
<metadata
|
|
||||||
id="metadata21">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs19" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1600"
|
|
||||||
inkscape:window-height="847"
|
|
||||||
id="namedview17"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="6.8"
|
|
||||||
inkscape:cx="50"
|
|
||||||
inkscape:cy="50"
|
|
||||||
inkscape:window-x="-8"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg15" />
|
|
||||||
<g
|
|
||||||
id="icon-info"
|
|
||||||
viewBox="0 0 100 100">
|
|
||||||
<path
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:currentColor;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:geometricPrecision;text-rendering:auto;enable-background:accumulate"
|
|
||||||
d="M 50,1.5 C 23.255646,1.5 1.5,23.255646 1.5,50 1.5,76.744354 23.255646,98.5 50,98.5 76.744354,98.5 98.5,76.744354 98.5,50 98.5,23.255646 76.744354,1.5 50,1.5 Z m 0,7 C 72.961273,8.5 91.5,27.038727 91.5,50 91.5,72.961273 72.961273,91.5 50,91.5 27.038727,91.5 8.5,72.961273 8.5,50 8.5,27.038727 27.038727,8.5 50,8.5 Z"
|
|
||||||
id="circle10"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
<path
|
|
||||||
shape-rendering="crispEdges"
|
|
||||||
d="M 55,40 V 80 H 45 V 40 z m 0,-20 V 35 H 45 V 20 Z"
|
|
||||||
id="path12" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 3.2 KiB |
|
@ -1,6 +0,0 @@
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<g id="icon-info" viewBox="0 0 100 100">
|
|
||||||
<circle shape-rendering="geometricPrecision" fill="none" stroke="currentColor" stroke-width="7" cx="50" cy="50" r="45"/>
|
|
||||||
<path shape-rendering="crispEdges" d="M 55,40 V 80 H 45 V 40 z m 0,-20 V 35 H 45 V 20 Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 379 B |
|
@ -1,6 +0,0 @@
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<g id="icon-info" viewBox="0 0 100 100">
|
|
||||||
<path d="M 50,1.5 C 23.255646,1.5 1.5,23.255646 1.5,50 1.5,76.744354 23.255646,98.5 50,98.5 76.744354,98.5 98.5,76.744354 98.5,50 98.5,23.255646 76.744354,1.5 50,1.5 Z m 0,7 C 72.961273,8.5 91.5,27.038727 91.5,50 91.5,72.961273 72.961273,91.5 50,91.5 27.038727,91.5 8.5,72.961273 8.5,50 8.5,27.038727 27.038727,8.5 50,8.5 Z"/>
|
|
||||||
<path shape-rendering="crispEdges" d="M 55,40 V 80 H 45 V 40 z m 0,-20 V 35 H 45 V 20 Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 585 B |
|
@ -1,7 +0,0 @@
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<g id="icon-info" viewBox="0 0 100 100">
|
|
||||||
<path fill="none" stroke="currentColor" stroke-width="7" stroke-miterlimit="10"
|
|
||||||
d="M95,50c0,25-20,45-45,45 c-25,0-45-20-45-45 S25,5,50,5C75,5,95,25,95,45z"/>
|
|
||||||
<path shape-rendering="crispEdges" d="M 55,40 V 80 H 45 V 40 z m 0,-20 V 35 H 45 V 20 Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 423 B |
|
@ -1,25 +0,0 @@
|
||||||
# О расширении
|
|
||||||
|
|
||||||
Обход интернет-цензуры в России пока что не является преступлением.
|
|
||||||
|
|
||||||
Расширение позволяет обходить блокировки РосКомНадзора, давая вам доступ
|
|
||||||
к библиотекам, энциклопедиям, сайтам оппозиционеров, а также к неповинным
|
|
||||||
сайтам, случайно заблокированным в силу разных причин.
|
|
||||||
|
|
||||||
Проксирует только заблокированные сайты, оставляя нетронутыми все остальные.
|
|
||||||
|
|
||||||
Устанавливает PAC-скрипт, работающий через сервера anticenz.org и antizapret.prostovpn.org.
|
|
||||||
|
|
||||||
Обновляет PAC-скрипт каждые 4 часа, что составляет примерно 7MB трафика в сутки.
|
|
||||||
Также расширение постоянно потребляет ~15MB памяти для информирования о блокировках через иконку.
|
|
||||||
|
|
||||||
Синяя лента – кампания фонда EFF в защиту свобод слова, прессы и союзов.
|
|
||||||
|
|
||||||
Если расширение не работает: https://git.io/vgDDj
|
|
||||||
|
|
||||||
Антицензура на Реддите: https://www.reddit.com/r/anticensorship_russia
|
|
||||||
История изменений: https://github.com/anticensority/runet-censorship-bypass/releases
|
|
||||||
|
|
||||||
# Дополнительно
|
|
||||||
|
|
||||||
Иконка синей ленты: http://www.iconsdb.com/icon-sets/cardboard-blue-icons/ribbon-12-icon.html
|
|
|
@ -1 +0,0 @@
|
||||||
grep -r "$@" ./*.js ./src --exclude-dir=vendor --exclude-dir=node_modules --exclude-dir=dist
|
|
|
@ -1,107 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const gulp = require('gulp');
|
|
||||||
const del = require('del');
|
|
||||||
const through = require('through2');
|
|
||||||
const PluginError = require('plugin-error');
|
|
||||||
const changed = require('gulp-changed');
|
|
||||||
|
|
||||||
const PluginName = 'Template literals';
|
|
||||||
|
|
||||||
const templatePlugin = (context) => through.obj(function(file, encoding, cb) {
|
|
||||||
|
|
||||||
const suffixes = ['.tmpl.json', 'tmpl.js'];
|
|
||||||
if ( suffixes.some( (suff) => file.path.endsWith(suff) ) ) {
|
|
||||||
|
|
||||||
const originalPath = file.path;
|
|
||||||
file.path = file.path.replace(new RegExp(`tmpl.([^.]+)$`), '$1');
|
|
||||||
|
|
||||||
if (file.isStream()) {
|
|
||||||
return cb(new PluginError(PluginName, 'Streams are not supported!'));
|
|
||||||
} else if (file.isBuffer()) {
|
|
||||||
|
|
||||||
const {keys, values} = Object.keys(context).reduce( (acc, key) => {
|
|
||||||
|
|
||||||
const value = context[key];
|
|
||||||
acc.keys.push(key);
|
|
||||||
acc.values.push(value);
|
|
||||||
return acc;
|
|
||||||
|
|
||||||
}, { keys: [], values: [] });
|
|
||||||
try {
|
|
||||||
file.contents = Buffer.from(
|
|
||||||
(new Function(...keys, 'return `' + String(file.contents) + '`;'))(...values)
|
|
||||||
);
|
|
||||||
} catch(e) {
|
|
||||||
e.message += '\nIN FILE: ' + originalPath;
|
|
||||||
return cb(new PluginError(PluginName, e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
cb(null, file);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const clean = function(cb) {
|
|
||||||
|
|
||||||
del.sync('./build');
|
|
||||||
return cb();
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const contexts = require('./src/templates-data').contexts;
|
|
||||||
|
|
||||||
const excFolder = (name) => [`!./src/**/${name}`, `!./src/**/${name}/**/*`];
|
|
||||||
const excluded = [ ...excFolder('test') , ...excFolder('node_modules'), ...excFolder('src') ];
|
|
||||||
|
|
||||||
const miniDst = './build/extension-mini';
|
|
||||||
const fullDst = './build/extension-full';
|
|
||||||
const betaDst = './build/extension-beta';
|
|
||||||
const firefoxDst = './build/extension-firefox';
|
|
||||||
|
|
||||||
const commonSrc = './src/extension-common/**/*';;
|
|
||||||
const miniSrc = './src/extension-mini/**/*';
|
|
||||||
const fullSrc = './src/extension-full/**/*';
|
|
||||||
const firefoxSrc = './src/extension-firefox/**/*';
|
|
||||||
|
|
||||||
const joinSrc = (...args) => [...args, ...excluded];
|
|
||||||
|
|
||||||
const copyMini = function(cb) {
|
|
||||||
|
|
||||||
gulp.src(joinSrc(commonSrc, miniSrc))
|
|
||||||
//.pipe(changed(miniDst))
|
|
||||||
.pipe(templatePlugin(contexts.mini))
|
|
||||||
.pipe(gulp.dest(miniDst))
|
|
||||||
.on('end', cb);
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyFull = function(cb) {
|
|
||||||
|
|
||||||
gulp.src(joinSrc(commonSrc, fullSrc))
|
|
||||||
//.pipe(changed(fullDst))
|
|
||||||
.pipe(templatePlugin(contexts.full))
|
|
||||||
.pipe(gulp.dest(fullDst))
|
|
||||||
.on('end', cb);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyBeta = function(cb) {
|
|
||||||
|
|
||||||
gulp.src(joinSrc(commonSrc, fullSrc))
|
|
||||||
//.pipe(changed(fullDst))
|
|
||||||
.pipe(templatePlugin(contexts.beta))
|
|
||||||
.pipe(gulp.dest(betaDst))
|
|
||||||
.on('end', cb);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildAll = gulp.series(clean, gulp.parallel(copyMini, copyFull, copyBeta));
|
|
||||||
const buildBeta = copyBeta;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
default: buildAll,
|
|
||||||
buildAll,
|
|
||||||
buildBeta,
|
|
||||||
};
|
|
10618
extensions/chromium/runet-censorship-bypass/package-lock.json
generated
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
"name": "runet-censorship-bypass",
|
|
||||||
"version": "0.0.19",
|
|
||||||
"description": "Development tools for chromium extension",
|
|
||||||
"scripts": {
|
|
||||||
"lint": "eslint ./src/**/*.js --ignore-pattern vendor",
|
|
||||||
"gulp": "gulp",
|
|
||||||
"test": "mocha --recursive ./src/**/test/*",
|
|
||||||
"subpages": "cd ./src/extension-common/pages/options/ && npm run build && cd -",
|
|
||||||
"subpages:dev": "cd ./src/extension-common/pages/options/ && npm run build:dev:nocomp && cd -",
|
|
||||||
"start": "npm run release",
|
|
||||||
"release": "npm run subpages && npx gulp -- buildAll"
|
|
||||||
},
|
|
||||||
"author": "Ilya Ig. Petrov",
|
|
||||||
"license": "GPLv3",
|
|
||||||
"devDependencies": {
|
|
||||||
"chai": "^4.3.0",
|
|
||||||
"eslint": "^7.19.0",
|
|
||||||
"eslint-config-google": "^0.14.0",
|
|
||||||
"gulp-changed": "^4.0.2",
|
|
||||||
"mocha": "^8.2.1",
|
|
||||||
"sinon-chrome": "^3.0.1",
|
|
||||||
"symlink-to": "^0.0.4"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"del": "^6.0.0",
|
|
||||||
"gulp": "^4.0.2",
|
|
||||||
"plugin-error": "^1.0.1",
|
|
||||||
"through2": "^4.0.2"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,306 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
console.log('Extension started.');
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
const IF_DEBUG = true;
|
|
||||||
|
|
||||||
if (!IF_DEBUG) {
|
|
||||||
// I believe logging objects precludes them from being GCed.
|
|
||||||
// I also don't remove logs for sake of client-side troubleshooting
|
|
||||||
// (though no one sent me logs so far).
|
|
||||||
['log', 'warn', 'error'].forEach( (meth) => {
|
|
||||||
const _meth = window.console[meth].bind(console);
|
|
||||||
window.console[meth] = function(...args) {
|
|
||||||
|
|
||||||
_meth(...args.map((a) => '' + a));
|
|
||||||
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const privates = {
|
|
||||||
requestToResponder: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const self = window.utils = {
|
|
||||||
|
|
||||||
mandatory() {
|
|
||||||
|
|
||||||
throw new TypeError('Missing required argument. ' +
|
|
||||||
'Be explicit if you swallow errors.');
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
throwIfError(err) {
|
|
||||||
|
|
||||||
if(err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
lastError: undefined,
|
|
||||||
|
|
||||||
checkChromeError() {
|
|
||||||
|
|
||||||
// Chrome API calls your cb in a context different from the point of API
|
|
||||||
// method invokation.
|
|
||||||
const err = chrome.runtime.lastError || chrome.extension.lastError || self.lastError;
|
|
||||||
if (!err) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.warn('API returned error:', err);
|
|
||||||
delete self.lastError;
|
|
||||||
return new Error(err.message); // Add stack.
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
timeouted(cb = self.mandatory) {
|
|
||||||
|
|
||||||
// setTimeout fixes error context, see bug #357568.
|
|
||||||
return (...args) => setTimeout(() => cb(...args), 0);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
chromified(cb = self.mandatory()) {
|
|
||||||
|
|
||||||
// Take error first callback and convert it to chrome API callback.
|
|
||||||
return function(...args) {
|
|
||||||
|
|
||||||
const err = self.checkChromeError();
|
|
||||||
self.timeouted(cb)(err, ...args);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
getOrDie(cb = self.mandatory()) {
|
|
||||||
|
|
||||||
return self.chromified((err, ...args) => {
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
cb(...args);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
getProp(obj, path = self.mandatory()) {
|
|
||||||
|
|
||||||
const props = path.split('.');
|
|
||||||
if (!props.length) {
|
|
||||||
throw new TypeError('Property must be supplied.');
|
|
||||||
}
|
|
||||||
const lastProp = props.pop();
|
|
||||||
for( const prop of props ) {
|
|
||||||
if (!(prop in obj)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
obj = obj[prop];
|
|
||||||
}
|
|
||||||
return obj[lastProp];
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
assert(value) {
|
|
||||||
|
|
||||||
if(!value) {
|
|
||||||
console.assert(value);
|
|
||||||
throw new Error('Assert failed for:' + value);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
addRequestResponder(requestType, responder) {
|
|
||||||
|
|
||||||
if( privates.requestToResponder[requestType] ) {
|
|
||||||
throw new TypeError(`Request ${requestType} already has responder!`);
|
|
||||||
}
|
|
||||||
privates.requestToResponder[requestType] = responder;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
fireRequest(requestType, ...args) {
|
|
||||||
|
|
||||||
const cb = args.slice(-1)[0];
|
|
||||||
self.assert(typeof(cb) === 'function');
|
|
||||||
const responder = privates.requestToResponder[requestType];
|
|
||||||
if(responder) {
|
|
||||||
responder(...args);
|
|
||||||
} else {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
createStorage(prefix) {
|
|
||||||
|
|
||||||
return function state(key, value) {
|
|
||||||
|
|
||||||
key = prefix + key;
|
|
||||||
if (value === null) {
|
|
||||||
return window.localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
if (value === undefined) {
|
|
||||||
const item = window.localStorage.getItem(key);
|
|
||||||
return item && JSON.parse(item);
|
|
||||||
}
|
|
||||||
if (value instanceof Date) {
|
|
||||||
throw new TypeError('Converting Date format to JSON is not supported.');
|
|
||||||
}
|
|
||||||
window.localStorage.setItem(key, JSON.stringify(value));
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
promisedLocalStorage: {
|
|
||||||
get(key) {
|
|
||||||
return new Promise((resolve) => (
|
|
||||||
chrome.storage.local.get(
|
|
||||||
key,
|
|
||||||
window.utils.getOrDie((storage) => resolve(key ? storage[key] : storage)),
|
|
||||||
)
|
|
||||||
));
|
|
||||||
},
|
|
||||||
set(items) {
|
|
||||||
return new Promise((resolve) => (
|
|
||||||
chrome.storage.local.set(items, resolve)
|
|
||||||
));
|
|
||||||
},
|
|
||||||
remove(keys) {
|
|
||||||
return new Promise((resolve) => (
|
|
||||||
chrome.storage.local.remove(keys, resolve)
|
|
||||||
));
|
|
||||||
},
|
|
||||||
clear() {
|
|
||||||
return new Promise((resolve) => (
|
|
||||||
chrome.storage.local.clear(resolve)
|
|
||||||
));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Possible values for levelOfControl:
|
|
||||||
*
|
|
||||||
* 1. "not_controllable"
|
|
||||||
* 2. "controlled_by_other_extensions"
|
|
||||||
* 3. "controllable_by_this_extension"
|
|
||||||
* 4. "controlled_by_this_extension"
|
|
||||||
*
|
|
||||||
* See: https://developer.chrome.com/extensions/proxy
|
|
||||||
* */
|
|
||||||
|
|
||||||
|
|
||||||
areSettingsControllableFor(details) {
|
|
||||||
|
|
||||||
return details.levelOfControl.endsWith('this_extension');
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
areSettingsControlledFor(details) {
|
|
||||||
|
|
||||||
return details.levelOfControl.startsWith('controlled_by_this');
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
messages: {
|
|
||||||
|
|
||||||
searchSettingsForUrl(niddle) {
|
|
||||||
|
|
||||||
return 'chrome://settings/?search=' + (chrome.i18n.getMessage(niddle) || niddle);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
whichExtensionHtml() {
|
|
||||||
|
|
||||||
return chrome.i18n.getMessage('noControl') +
|
|
||||||
` <a href="${ this.searchSettingsForUrl('proxy') }">
|
|
||||||
${ chrome.i18n.getMessage('WhichQ') }
|
|
||||||
</a>`;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
parseProxyScheme(proxyAsStringRaw) {
|
|
||||||
|
|
||||||
const proxyAsString = proxyAsStringRaw.trim();
|
|
||||||
const [type] = proxyAsString.split(/\s+/);
|
|
||||||
/*
|
|
||||||
if (!/^[a-zA-Z0-9]+$/.test(type)) {
|
|
||||||
throw new Error(`${type} is not a proxy type!`);
|
|
||||||
}
|
|
||||||
JS has no code blocks in RE, seems safe to omit this check.
|
|
||||||
*/
|
|
||||||
const typeRe = new RegExp(`^${type}\\s+`, 'g');
|
|
||||||
const crededAddr = proxyAsString.replace(typeRe, '');
|
|
||||||
|
|
||||||
let parts;
|
|
||||||
parts = crededAddr.split('@');
|
|
||||||
let [creds, addr] = [parts.slice(0, -1).join('@'), parts[parts.length - 1]];
|
|
||||||
|
|
||||||
const [hostname, port] = addr.split(':');
|
|
||||||
|
|
||||||
parts = creds.split(':')
|
|
||||||
const username = parts[0];
|
|
||||||
const password = parts.slice(1).join(':');
|
|
||||||
|
|
||||||
return {
|
|
||||||
type,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
hostname,
|
|
||||||
port,
|
|
||||||
creds,
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
openAndFocus(url) {
|
|
||||||
|
|
||||||
chrome.tabs.create(
|
|
||||||
{url: url},
|
|
||||||
(tab) => chrome.windows.update(tab.windowId, {focused: true})
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const max = 2**16;
|
|
||||||
const versionToArray = (v) => [ ...v.split('.'), 0, 0, 0].slice(0,4);
|
|
||||||
const versionToInt = (v) => versionToArray(v)
|
|
||||||
.reverse()
|
|
||||||
.reduce((acc, vv, i) => acc + parseInt(vv)*(max**i), 0);
|
|
||||||
|
|
||||||
const compareVersions = (a, b) => versionToInt(a) - versionToInt(b);
|
|
||||||
|
|
||||||
const ifFirefox = navigator.userAgent.toLowerCase().includes('firefox');
|
|
||||||
|
|
||||||
let give;
|
|
||||||
const promise = !ifFirefox ? Promise.resolve() : new Promise((resolve) => {
|
|
||||||
give = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.apis = {
|
|
||||||
consent: {
|
|
||||||
promise,
|
|
||||||
give,
|
|
||||||
},
|
|
||||||
platform: {
|
|
||||||
ifFirefox,
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
ifMini: false,
|
|
||||||
build: chrome.runtime.getManifest().version.replace(/\d+\.\d+\./g, ''),
|
|
||||||
|
|
||||||
isLeq: (a, b) => compareVersions(a, b) <= 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,294 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
{ // Private namespace
|
|
||||||
|
|
||||||
const timeouted = window.utils.timeouted;
|
|
||||||
const throwIfError = window.utils.throwIfError;
|
|
||||||
|
|
||||||
const errorJsonReplacer = function errorJsonReplacer(key, value) {
|
|
||||||
|
|
||||||
// fooWindow.ErrorEvent !== barWindow.ErrorEvent
|
|
||||||
if (value === window) {
|
|
||||||
return; // STUPID, because other window object may be passed.
|
|
||||||
}
|
|
||||||
if (!( value && value.constructor
|
|
||||||
&& ['Error', 'Event'].some(
|
|
||||||
(suff) => value.constructor.name.endsWith(suff)
|
|
||||||
)
|
|
||||||
)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
const alt = {};
|
|
||||||
|
|
||||||
Object.getOwnPropertyNames(value).forEach(function(key) {
|
|
||||||
|
|
||||||
alt[key] = value[key];
|
|
||||||
|
|
||||||
}, value);
|
|
||||||
|
|
||||||
for(const prop in value) {
|
|
||||||
if (/^[A-Z]/.test(prop)) {
|
|
||||||
// MOUSEMOVE, CLICK, KEYUP, NONE, etc.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
alt[prop] = value[prop];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.constructor.name === 'ErrorEvent') {
|
|
||||||
for(const circularProp of
|
|
||||||
[ // First line are circular props.
|
|
||||||
'target', 'srcElement', 'path', 'currentTarget',
|
|
||||||
'bubbles', 'cancelBubble', 'cancelable', 'composed',
|
|
||||||
'defaultPrevented', 'eventPhase', 'isTrusted', 'returnValue',
|
|
||||||
'timeStamp']) {
|
|
||||||
delete alt[circularProp];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.name) {
|
|
||||||
alt.name = value.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return alt;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const ifPrefix = 'if-on-';
|
|
||||||
const extName = chrome.runtime.getManifest().name;
|
|
||||||
const extVersion = window.apis.version.build;
|
|
||||||
|
|
||||||
window.apis.errorHandlers = {
|
|
||||||
|
|
||||||
state: window.utils.createStorage('handlers-'),
|
|
||||||
|
|
||||||
viewError(type = window.utils.mandatory(), err) {
|
|
||||||
|
|
||||||
const errors = err ? {[type]: err} : this.idToError;
|
|
||||||
const json = JSON.stringify(errors, errorJsonReplacer, 0);
|
|
||||||
|
|
||||||
window.utils.openAndFocus(
|
|
||||||
'https://anticensority.github.io/error/?json=' + encodeURIComponent(json) +
|
|
||||||
(type ? '&type=' + encodeURIComponent(type) : '') +
|
|
||||||
'&version=' + chrome.runtime.getManifest().version +
|
|
||||||
'&useragent=' + encodeURIComponent(navigator.userAgent) +
|
|
||||||
'&platform=' + encodeURIComponent(navigator.platform),
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
getEventsMap() {
|
|
||||||
|
|
||||||
return new Map([
|
|
||||||
['pac-error', 'ошибки PAC скриптов'],
|
|
||||||
['ext-error', 'ошибки расширения'],
|
|
||||||
['no-control', 'утеря контроля над настройками'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
switch(onOffStr, eventName) {
|
|
||||||
|
|
||||||
if (!['on', 'off'].includes(onOffStr)) {
|
|
||||||
throw new TypeError('First argument bust be "on" or "off".');
|
|
||||||
}
|
|
||||||
for(
|
|
||||||
const name of (eventName ? [eventName] : this.getEventsMap().keys() )
|
|
||||||
) {
|
|
||||||
this.state( ifPrefix + name, onOffStr === 'on' ? 'on' : null );
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
isOn(eventName) {
|
|
||||||
|
|
||||||
return this.state( ifPrefix + eventName );
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
ifControlled: null,
|
|
||||||
ifControllable: null,
|
|
||||||
|
|
||||||
isControllable(details) {
|
|
||||||
|
|
||||||
if (details) {
|
|
||||||
this.ifControllable = window.utils.areSettingsControllableFor(details);
|
|
||||||
|
|
||||||
if (this.ifControllable) {
|
|
||||||
this.ifControlled = window.utils.areSettingsControlledFor(details);
|
|
||||||
} else {
|
|
||||||
this.ifControlled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.ifControlled) {
|
|
||||||
chrome.browserAction.setIcon( {path: './icons/default-128.png'} );
|
|
||||||
} else {
|
|
||||||
chrome.browserAction.setIcon({
|
|
||||||
path: './icons/default-grayscale-128.png',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.ifControllable;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
isControlled(details) {
|
|
||||||
|
|
||||||
if (details) {
|
|
||||||
this.isControllable(details);
|
|
||||||
}
|
|
||||||
return this.ifControlled;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
updateControlState(cb = throwIfError) {
|
|
||||||
|
|
||||||
chrome.proxy.settings.get(
|
|
||||||
{},
|
|
||||||
timeouted(
|
|
||||||
(details) => {
|
|
||||||
|
|
||||||
if (details) {
|
|
||||||
this.isControllable(details);
|
|
||||||
}
|
|
||||||
cb();
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
idToError: {},
|
|
||||||
|
|
||||||
mayNotify(
|
|
||||||
id, title, errOrMessage,
|
|
||||||
{
|
|
||||||
icon = 'default-128.png',
|
|
||||||
context = extName + ' ' + extVersion,
|
|
||||||
ifSticky = true,
|
|
||||||
} = {}
|
|
||||||
) {
|
|
||||||
|
|
||||||
if ( !this.isOn(id) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.idToError[id] = errOrMessage;
|
|
||||||
const message = errOrMessage.message || errOrMessage.toString();
|
|
||||||
chrome.notifications.create(
|
|
||||||
id,
|
|
||||||
Object.assign({
|
|
||||||
title: title,
|
|
||||||
message: message,
|
|
||||||
contextMessage: context,
|
|
||||||
type: 'basic',
|
|
||||||
iconUrl: './icons/' + icon,
|
|
||||||
appIconMaskUrl: './icons/default-mask-128.png',
|
|
||||||
isClickable: true,
|
|
||||||
}, window.apis.platform.ifFirefox ? {} : { requireInteraction: ifSticky }),
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
installListenersOn(win, name, cb) {
|
|
||||||
|
|
||||||
win.addEventListener('error', (errEvent) => {
|
|
||||||
|
|
||||||
console.warn(name + ':GLOBAL ERROR', errEvent);
|
|
||||||
this.mayNotify('ext-error', 'Ошибка расширения', errEvent,
|
|
||||||
{icon: 'ext-error-128.png'});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
win.addEventListener('unhandledrejection', (event) => {
|
|
||||||
|
|
||||||
console.warn(name + ': Unhandled rejection. Throwing error.');
|
|
||||||
event.preventDefault();
|
|
||||||
console.log('ev', event);
|
|
||||||
throw event.reason;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cb) {
|
|
||||||
// In most cases getBackgroundPage( (bg) => installListenersOn
|
|
||||||
// Without setTimeout errors are swallowed, bug #357568
|
|
||||||
setTimeout(cb, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlers = window.apis.errorHandlers;
|
|
||||||
|
|
||||||
// Initialization
|
|
||||||
// ==============
|
|
||||||
|
|
||||||
chrome.proxy.settings.get(
|
|
||||||
{},
|
|
||||||
timeouted( handlers.isControllable.bind(handlers) )
|
|
||||||
);
|
|
||||||
|
|
||||||
chrome.notifications.onClicked.addListener( timeouted( (notId) => {
|
|
||||||
|
|
||||||
chrome.notifications.clear(notId);
|
|
||||||
if(notId === 'no-control') {
|
|
||||||
return window.utils.openAndFocus(
|
|
||||||
window.utils.messages.searchSettingsForUrl('proxy')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
handlers.viewError(notId);
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
handlers.installListenersOn(window, 'BG');
|
|
||||||
|
|
||||||
(chrome.proxy.onProxyError || chrome.proxy.onError).addListener( timeouted( (details) => {
|
|
||||||
|
|
||||||
if (!handlers.ifControlled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Example:
|
|
||||||
details: "line: 7: Uncaught Error: This is error, man.",
|
|
||||||
error: "net::ERR_PAC_SCRIPT_FAILED",
|
|
||||||
fatal: false,
|
|
||||||
*/
|
|
||||||
const ifConFail = [
|
|
||||||
'net::ERR_TUNNEL_CONNECTION_FAILED',
|
|
||||||
'net::ERR_PROXY_CONNECTION_FAILED',
|
|
||||||
].includes(details.error);
|
|
||||||
|
|
||||||
if (ifConFail) {
|
|
||||||
// Happens if you return neither prixies nor "DIRECT".
|
|
||||||
// Ignore it.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.warn('PAC ERROR', details);
|
|
||||||
// TOOD: add "view pac script at this line" button.
|
|
||||||
handlers.mayNotify('pac-error', 'Ошибка PAC!',
|
|
||||||
(details.error || details.message /* Firefox */) + '\n' + details.details,
|
|
||||||
{icon: 'pac-error-128.png'}
|
|
||||||
);
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
chrome.proxy.settings.onChange.addListener( timeouted( (details) => {
|
|
||||||
|
|
||||||
console.log('Proxy settings changed:', details.levelOfControl);
|
|
||||||
const noCon = 'no-control';
|
|
||||||
const ifWasControllable = handlers.ifControllable;
|
|
||||||
if ( !handlers.isControllable(details) && ifWasControllable ) {
|
|
||||||
handlers.mayNotify(
|
|
||||||
noCon,
|
|
||||||
chrome.i18n.getMessage('noControl'),
|
|
||||||
chrome.i18n.getMessage('WhichQ'),
|
|
||||||
{icon: 'no-control-128.png', ifSticky: false}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
chrome.notifications.clear( noCon );
|
|
||||||
}
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
'use strict';
|
|
||||||
/*
|
|
||||||
* Error Library
|
|
||||||
* PURPOSE 1:
|
|
||||||
* Allow wrapping errors with clarifications when they bubble up.
|
|
||||||
* Why:
|
|
||||||
* Sometimes low level errors may bubble up through a chain of callbacks.
|
|
||||||
* And when they reach top level of a user they loose context and convey
|
|
||||||
* nonsense like "Error 404: Can't find file".
|
|
||||||
* -- What file? WTH, I just hit update button?!
|
|
||||||
* PURPOSE 2:
|
|
||||||
* Supply separate class for warnings.
|
|
||||||
* Why:
|
|
||||||
* Some callbacks expect warnings which are like non-fatal errors.
|
|
||||||
* I want Warnings and FooError to be distinctable by code readers,
|
|
||||||
* so I create separate class for warnings.
|
|
||||||
**/
|
|
||||||
{
|
|
||||||
|
|
||||||
const mandatory = window.utils.mandatory;
|
|
||||||
|
|
||||||
const self = window.apis.errorsLib = {
|
|
||||||
|
|
||||||
// I don't use Error class, because we don't need stack here.
|
|
||||||
Warning: class {
|
|
||||||
|
|
||||||
constructor(message = mandatory()) {
|
|
||||||
|
|
||||||
this.message = message;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
clarify: function(err, message = mandatory(), {data} = {}) {
|
|
||||||
|
|
||||||
if (!err) {
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
const warn = new self.Warning(message);
|
|
||||||
warn.wrapped = err;
|
|
||||||
if (data) {
|
|
||||||
warn.data = data;
|
|
||||||
}
|
|
||||||
return warn;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
clarifyThen: function(message, cb = mandatory()) {
|
|
||||||
|
|
||||||
return (err, ...args) => cb( self.clarify(err, message), ...args );
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
const mandatory = window.utils.mandatory;
|
|
||||||
const errorsLib = window.apis.errorsLib;
|
|
||||||
|
|
||||||
const checkCon = 'Что-то не так с сетью, проверьте соединение.';
|
|
||||||
|
|
||||||
window.apis.httpLib = {
|
|
||||||
|
|
||||||
ifModifiedSince(
|
|
||||||
url,
|
|
||||||
lastModified,
|
|
||||||
cb = mandatory()
|
|
||||||
) {
|
|
||||||
|
|
||||||
if (url.startsWith('data:')) {
|
|
||||||
return cb(null, false);
|
|
||||||
}
|
|
||||||
const wasModifiedIn1970 = new Date(0).toUTCString();
|
|
||||||
const notModifiedCode = 304;
|
|
||||||
fetch(url, {
|
|
||||||
method: 'HEAD',
|
|
||||||
headers: new Headers({
|
|
||||||
'If-Modified-Since': lastModified,
|
|
||||||
}),
|
|
||||||
}).then(
|
|
||||||
(res) => {
|
|
||||||
cb(
|
|
||||||
null,
|
|
||||||
res.status === notModifiedCode ?
|
|
||||||
false :
|
|
||||||
(res.headers.get('Last-Modified') || wasModifiedIn1970)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
errorsLib.clarifyThen(checkCon, (err) => cb(err, wasModifiedIn1970))
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
get(url, cb = mandatory()) {
|
|
||||||
|
|
||||||
const start = Date.now();
|
|
||||||
fetch(url, {cache: 'no-store'}).then(
|
|
||||||
(res) => {
|
|
||||||
|
|
||||||
const textCb =
|
|
||||||
(err) => {
|
|
||||||
|
|
||||||
console.log('Reading response as text...');
|
|
||||||
res.text().then(
|
|
||||||
(text) => {
|
|
||||||
console.log('Calling CB...');
|
|
||||||
cb(err, text);
|
|
||||||
},
|
|
||||||
cb
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const status = res.status;
|
|
||||||
if ( !( status >= 200 && status < 300 || status === 304 ) ) {
|
|
||||||
return textCb(
|
|
||||||
errorsLib.clarify(
|
|
||||||
res,
|
|
||||||
'Получен ответ с неудачным HTTP-кодом ' + status + '.'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('GETed with success:', url.substr(0, 100), Date.now() - start);
|
|
||||||
textCb();
|
|
||||||
|
|
||||||
},
|
|
||||||
errorsLib.clarifyThen(checkCon, cb)
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
head(url, cb = mandatory()) {
|
|
||||||
|
|
||||||
const start = Date.now();
|
|
||||||
fetch(url, {cache: 'no-store', method: 'HEAD'}).then(
|
|
||||||
(res) => {
|
|
||||||
|
|
||||||
const status = res.status;
|
|
||||||
if ( !( status >= 200 && status < 300 || status === 304 ) ) {
|
|
||||||
return cb(
|
|
||||||
errorsLib.clarify(
|
|
||||||
res,
|
|
||||||
'Получен ответ с неудачным HTTP-кодом ' + status + '.'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('HEADed with success:', url, Date.now() - start);
|
|
||||||
cb();
|
|
||||||
|
|
||||||
},
|
|
||||||
errorsLib.clarifyThen(checkCon, cb)
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
if (window.apis.platform.ifFirefox) {
|
|
||||||
|
|
||||||
const prefix = 'firefox-only';
|
|
||||||
|
|
||||||
const originalSet = chrome.proxy.settings.set.bind( chrome.proxy.settings );
|
|
||||||
chrome.proxy.settings.set = function(details, cb) {
|
|
||||||
const pac = window.utils.getProp(details, 'value.pacScript') || {};
|
|
||||||
if (!(pac && pac.data)) {
|
|
||||||
return originalSet(details, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([pac.data], { type : 'application/x-ns-proxy-autoconfig' });
|
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
|
||||||
originalSet({
|
|
||||||
value: {
|
|
||||||
proxyType: 'autoConfig',
|
|
||||||
autoConfigUrl: blobUrl,
|
|
||||||
},
|
|
||||||
}, window.utils.chromified( async (err) => {
|
|
||||||
if (err) {
|
|
||||||
window.utils.lastError = err;
|
|
||||||
cb();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await window.utils.promisedLocalStorage.set({ [`${prefix}-pac-data`]: pac.data });
|
|
||||||
cb();
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const originalGet = chrome.proxy.settings.get.bind( chrome.proxy.settings );
|
|
||||||
chrome.proxy.settings.get = function(details, cb) {
|
|
||||||
originalGet(details, window.utils.chromified(async (err, originalDetails) => {
|
|
||||||
if (err) {
|
|
||||||
window.utils.lastError = err;
|
|
||||||
cb(originalDetails);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let pacData = await window.utils.promisedLocalStorage.get(`${prefix}-pac-data`);
|
|
||||||
if (!pacData || !Object.keys(pacData).length) {
|
|
||||||
cb(originalDetails);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cb(Object.assign(
|
|
||||||
originalDetails,
|
|
||||||
{
|
|
||||||
value: {
|
|
||||||
mode: 'pac_script',
|
|
||||||
pacScript: {
|
|
||||||
data: pacData,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const originalClear = chrome.proxy.settings.clear.bind( chrome.proxy.settings );
|
|
||||||
chrome.proxy.settings.clear = async function(details, cb) {
|
|
||||||
await window.utils.promisedLocalStorage.remove(`${prefix}-pac-data`);
|
|
||||||
originalClear(details, cb);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,693 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
{ // Private namespace starts.
|
|
||||||
|
|
||||||
const mandatory = window.utils.mandatory;
|
|
||||||
const throwIfError = window.utils.throwIfError;
|
|
||||||
const chromified = window.utils.chromified;
|
|
||||||
const timeouted = window.utils.timeouted;
|
|
||||||
|
|
||||||
const kitchenStartsMark = '\n\n//%#@@@@@@ PAC_KITCHEN_STARTS @@@@@@#%';
|
|
||||||
const kitchenState = window.utils.createStorage('pac-kitchen-');
|
|
||||||
const ifIncontinence = 'if-incontinence';
|
|
||||||
const modsKey = 'mods';
|
|
||||||
|
|
||||||
let proxyHostToCredsList = {};
|
|
||||||
const ifAuthSupported = chrome.webRequest && chrome.webRequest.onAuthRequired && !window.apis.version.ifMini;
|
|
||||||
if (ifAuthSupported) {
|
|
||||||
|
|
||||||
const requestIdToTries = {};
|
|
||||||
|
|
||||||
chrome.webRequest.onAuthRequired.addListener(
|
|
||||||
(details) => {
|
|
||||||
|
|
||||||
if (!details.isProxy) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const proxyHost = `${details.challenger.host}:${details.challenger.port}`;
|
|
||||||
const credsList = proxyHostToCredsList[proxyHost];
|
|
||||||
if (!credsList) {
|
|
||||||
return {}; // No creds found for this proxy.
|
|
||||||
}
|
|
||||||
const requestId = details.requestId;
|
|
||||||
const tries = requestIdToTries[requestId] || 0;
|
|
||||||
if (tries > credsList.length) {
|
|
||||||
return {}; // All creds for this proxy were tried already.
|
|
||||||
}
|
|
||||||
requestIdToTries[requestId] = tries + 1;
|
|
||||||
return {
|
|
||||||
authCredentials: credsList[tries],
|
|
||||||
};
|
|
||||||
|
|
||||||
},
|
|
||||||
{urls: ['<all_urls>']},
|
|
||||||
['blocking'],
|
|
||||||
);
|
|
||||||
|
|
||||||
const forgetRequestId = (details) => {
|
|
||||||
|
|
||||||
delete requestIdToTries[details.requestId];
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
chrome.webRequest.onCompleted.addListener(
|
|
||||||
forgetRequestId,
|
|
||||||
{urls: ['<all_urls>']},
|
|
||||||
);
|
|
||||||
|
|
||||||
chrome.webRequest.onErrorOccurred.addListener(
|
|
||||||
forgetRequestId,
|
|
||||||
{urls: ['<all_urls>']},
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDefaultConfigs = () => ({// Configs user may mutate them and we don't care!
|
|
||||||
|
|
||||||
ifProxyHttpsUrlsOnly: {
|
|
||||||
dflt: false,
|
|
||||||
label: 'проксировать только HTTP<em>S</em>-сайты',
|
|
||||||
desc: 'Проксировать только сайты, доступные по шифрованному протоколу HTTP<em>S</em>. Прокси и провайдер смогут видеть только адреса проксируемых HTTP<em>S</em>-сайтов, но не их содержимое. Используйте, если вы не доверяете прокси-серверам ваш HTTP-трафик. Разумеется, что с этой опцией разблокировка HTTP-сайтов работать не будет.',
|
|
||||||
order: 0,
|
|
||||||
},
|
|
||||||
ifUseSecureProxiesOnly: {
|
|
||||||
dflt: false,
|
|
||||||
label: 'только шифрованная связь с прокси',
|
|
||||||
desc: 'Шифровать соединение до прокси от провайдера, используя только прокси типа HTTPS или локальный Tor. Провайдер всё же сможет видеть адреса (но не содержимое) проксируемых ресурсов из протокола DNS (даже с Tor). Опция вряд ли может быть вам полезна, т.к. шифруется не весь трафик, а лишь разблокируемые ресурсы.',
|
|
||||||
order: 1,
|
|
||||||
},
|
|
||||||
ifProhibitDns: {
|
|
||||||
dflt: false,
|
|
||||||
label: 'запретить определение по IP/DNS',
|
|
||||||
desc: 'Пытается запретить скрипту использовать DNS, без которого определение блокировки по IP работать не будет (т.е. будет разблокироваться меньше сайтов). Используйте, чтобы получить прирост в производительности или если вам кажется, что мы проксируем слишком много сайтов. Запрет действует только для скрипта, браузер и др.программы продолжат использование DNS.',
|
|
||||||
order: 2,
|
|
||||||
},
|
|
||||||
ifProxyOrDie: {
|
|
||||||
dflt: true,
|
|
||||||
ifDfltMods: true,
|
|
||||||
label: 'проксируй или умри!',
|
|
||||||
desc: 'Запрещает соединение с сайтами напрямую без прокси в случаях, когда все прокси отказывают. Например, если все ВАШИ прокси вдруг недоступны, то добавленные вручную сайты открываться не будут совсем. Однако смысл опции в том, что она препятствует занесению прокси в чёрные списки Хрома. Рекомендуется не отключать.',
|
|
||||||
order: 3,
|
|
||||||
},
|
|
||||||
ifUsePacScriptProxies: {
|
|
||||||
dflt: true,
|
|
||||||
category: 'ownProxies',
|
|
||||||
label: 'использовать прокси PAC-скрипта',
|
|
||||||
desc: 'Использовать прокси-сервера от авторов PAC-скрипта.',
|
|
||||||
order: 4,
|
|
||||||
},
|
|
||||||
ifUseLocalTor: {
|
|
||||||
dflt: false,
|
|
||||||
category: 'ownProxies',
|
|
||||||
label: 'использовать СВОЙ локальный Tor',
|
|
||||||
desc: 'Установите <a href="https://github.com/anticensority/runet-censorship-bypass/wiki/Расширение-и-TOR">Tor</a> на свой компьютер и используйте его как прокси-сервер. <a href="https://github.com/anticensority/runet-censorship-bypass/wiki/Расширение-и-TOR#важно">ВАЖНО</a>.',
|
|
||||||
order: 5,
|
|
||||||
},
|
|
||||||
ifUseLocalWarp: {
|
|
||||||
dflt: false,
|
|
||||||
category: 'ownProxies',
|
|
||||||
label: 'использовать WARP как прокси',
|
|
||||||
desc: 'Использовать СВОЙ локальный CloudFlare WARP (<a href="https://one.one.one.one">https://one.one.one.one</a>) в качестве прокси.',
|
|
||||||
order: 5.5,
|
|
||||||
},
|
|
||||||
exceptions: {
|
|
||||||
dflt: null,
|
|
||||||
category: 'exceptions',
|
|
||||||
},
|
|
||||||
ifMindExceptions: {
|
|
||||||
dflt: true,
|
|
||||||
category: 'exceptions',
|
|
||||||
label: 'учитывать исключения',
|
|
||||||
desc: 'Учитывать сайты, добавленные вручную. Только для своих прокси-серверов! Без своих прокси работать не будет.',
|
|
||||||
order: 6,
|
|
||||||
},
|
|
||||||
whitelist: {
|
|
||||||
dflt: [],
|
|
||||||
category: 'exceptions',
|
|
||||||
},
|
|
||||||
ifMindWhitelist: {
|
|
||||||
dflt: false,
|
|
||||||
category: 'exceptions',
|
|
||||||
label: 'Ограничиться только <a href="../exceptions/index.html">белым списком</a>',
|
|
||||||
desc: 'Разрешить расширению работать только с адресами из белого списка.',
|
|
||||||
order: 6.5,
|
|
||||||
},
|
|
||||||
customProxyStringRaw: {
|
|
||||||
dflt: '',
|
|
||||||
category: 'ownProxies',
|
|
||||||
label: 'использовать СВОИ прокси',
|
|
||||||
url: 'https://github.com/anticensority/runet-censorship-bypass/wiki/Свои-прокси-в-расширении',
|
|
||||||
order: 7,
|
|
||||||
},
|
|
||||||
ifUseOwnProxiesOnlyForOwnSites: {
|
|
||||||
dflt: false,
|
|
||||||
category: 'ownProxies',
|
|
||||||
label: 'СВОИ прокси только для СВОИХ сайтов',
|
|
||||||
desc: 'Не использовать СВОИ прокси для всех сайтов из PAC-скрипта, а только для добавленных вручную исключений.',
|
|
||||||
order: 7.5,
|
|
||||||
},
|
|
||||||
ifProxyMoreDomains: {
|
|
||||||
ifDisabled: true,
|
|
||||||
dflt: false,
|
|
||||||
category: 'ownProxies',
|
|
||||||
label: 'проксировать .onion, .i2p и <a href="https://en.wikipedia.org/wiki/OpenNIC#OpenNIC_TLDs">OpenNIC</a>',
|
|
||||||
desc: 'Проксировать особые домены. Необходима поддержка со стороны СВОИХ прокси.',
|
|
||||||
order: 8,
|
|
||||||
},
|
|
||||||
replaceDirectWith: {
|
|
||||||
ifDisabled: true,
|
|
||||||
dflt: false,
|
|
||||||
category: 'ownProxies',
|
|
||||||
label: 'подменять DIRECT на',
|
|
||||||
desc: 'Использовать в PAC-скрипте указанную строку для запросов напрямую (вместо директивы DIRECT). Данная строка не проверяется на требования к шифрованию связи до прокси! Строка должна соответствовать формату возвращаемого значения PAC-скрипта, который подобен <a href="https://github.com/anticensority/runet-censorship-bypass/wiki/Свои-прокси-в-расширении#формат">формату своих прокси</a>.',
|
|
||||||
order: 9,
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
const getDefaults = function getDefaults() {
|
|
||||||
|
|
||||||
const configs = getDefaultConfigs();
|
|
||||||
return Object.keys(configs).reduce((acc, key) => {
|
|
||||||
|
|
||||||
acc[key] = configs[key].dflt;
|
|
||||||
return acc;
|
|
||||||
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCurrentConfigs = function getCurrentConfigs(ifRaw = false) {
|
|
||||||
|
|
||||||
const oldMods = kitchenState(modsKey);
|
|
||||||
if (ifRaw) {
|
|
||||||
// No migration!
|
|
||||||
return oldMods;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client may expect mods.included and mods.excluded!
|
|
||||||
// On first install they are not defined.
|
|
||||||
const [err, mods, ...warns] = createPacModifiers(oldMods);
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return mods;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOrderedConfigsForUser = function getOrderedConfigs(category) {
|
|
||||||
|
|
||||||
const pacMods = getCurrentConfigs();
|
|
||||||
const configs = getDefaultConfigs();
|
|
||||||
return Object.keys(configs)
|
|
||||||
.sort((keyA, keyB) => configs[keyA].order - configs[keyB].order)
|
|
||||||
.reduce((arr, key) => {
|
|
||||||
|
|
||||||
const conf = configs[key];
|
|
||||||
if(typeof(conf.order) === 'number') {
|
|
||||||
if(!category || category === (conf.category || 'general')) {
|
|
||||||
conf.value = pacMods[key];
|
|
||||||
conf.key = key;
|
|
||||||
conf.category = category || 'general';
|
|
||||||
arr.push(conf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const createPacModifiers = function createPacModifiers(mods = {}) {
|
|
||||||
|
|
||||||
mods = mods || {}; // null?
|
|
||||||
const configs = getDefaultConfigs();
|
|
||||||
const ifNoMods = Object.keys(configs)
|
|
||||||
.every((dProp) => {
|
|
||||||
|
|
||||||
const ifDflt = (
|
|
||||||
!(
|
|
||||||
dProp in mods &&
|
|
||||||
Boolean(configs[dProp].dflt) !== Boolean(mods[dProp])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const ifMods = configs[dProp].ifDfltMods; // If default value implies PAC-script modification.
|
|
||||||
return ifDflt ? !ifMods : ifMods;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
const self = {};
|
|
||||||
const gdft = getDefaults();
|
|
||||||
Object.assign(self, gdft, mods);
|
|
||||||
self.ifNoMods = ifNoMods;
|
|
||||||
|
|
||||||
let customProxyArray = [];
|
|
||||||
if (self.customProxyStringRaw) {
|
|
||||||
customProxyArray = self.customProxyStringRaw
|
|
||||||
.replace(/#.*$/mg, '') // Strip comments.
|
|
||||||
.split( /(?:\s*(?:;\r?\n)+\s*)+/g )
|
|
||||||
.map( (p) => p.trim() )
|
|
||||||
.filter( (p) => p && /\s+/g.test(p) ); // At least one space is required.
|
|
||||||
if (self.ifUseSecureProxiesOnly) {
|
|
||||||
customProxyArray = customProxyArray.filter( (pStr) => /^HTTPS\s/.test(pStr) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (self.ifUseLocalWarp) {
|
|
||||||
self.warpPoints = ['SOCKS5 localhost:40000', 'HTTPS localhost:40000'];
|
|
||||||
customProxyArray.push(...self.warpPoints);
|
|
||||||
}
|
|
||||||
if (self.ifUseLocalTor) {
|
|
||||||
self.torPoints = ['SOCKS5 localhost:9150', 'SOCKS5 localhost:9050'];
|
|
||||||
customProxyArray.push(...self.torPoints);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hanlde protected proxies in customProxyArray.
|
|
||||||
const protectedProxies = [];
|
|
||||||
customProxyArray = customProxyArray.map((proxyScheme) => {
|
|
||||||
|
|
||||||
if (proxyScheme.includes('@')) {
|
|
||||||
|
|
||||||
const proxy = window.utils.parseProxyScheme(proxyScheme);
|
|
||||||
protectedProxies.push(proxy);
|
|
||||||
return `${proxy.type} ${proxy.hostname}:${proxy.port}`;
|
|
||||||
|
|
||||||
}
|
|
||||||
return proxyScheme;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!ifAuthSupported && protectedProxies.length) {
|
|
||||||
return [new Error('Запароленные прокси не поддерживаются в данной версии/платформе!')];
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyHostToCredsList = {};
|
|
||||||
protectedProxies.forEach(({ hostname, port, username, password }) => {
|
|
||||||
|
|
||||||
proxyHostToCredsList[`${hostname}:${port}`] =
|
|
||||||
proxyHostToCredsList[`${hostname}:${port}`] || [];
|
|
||||||
const tries = proxyHostToCredsList[`${hostname}:${port}`];
|
|
||||||
tries.push({
|
|
||||||
username: username || '',
|
|
||||||
password: password || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
self.filteredCustomsString = '';
|
|
||||||
if (customProxyArray.length) {
|
|
||||||
self.customProxyArray = customProxyArray;
|
|
||||||
self.filteredCustomsString = customProxyArray.join('; ');
|
|
||||||
} else {
|
|
||||||
if (!self.ifUsePacScriptProxies) {
|
|
||||||
return [new TypeError('Нет ни одного прокси, удовлетворяющего вашим требованиям!')];
|
|
||||||
}
|
|
||||||
self.customProxyArray = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
[self.included, self.excluded] = [[], []];
|
|
||||||
if (self.ifProxyMoreDomains) {
|
|
||||||
self.moreDomains = [
|
|
||||||
/* Networks */
|
|
||||||
'onion', 'i2p',
|
|
||||||
/* OpenNIC */
|
|
||||||
'bbs', 'chan', 'dyn', 'free', 'geek', 'gopher', 'indy',
|
|
||||||
'libre', 'neo', 'null', 'o', 'oss', 'oz', 'parody', 'pirate',
|
|
||||||
/* OpenNIC Alternatives */
|
|
||||||
'bazar', 'bit', 'coin', 'emc', 'fur', 'ku', 'lib', 'te', 'ti', 'uu'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (self.ifMindExceptions && self.exceptions) {
|
|
||||||
self.included = [];
|
|
||||||
self.excluded = [];
|
|
||||||
for(const host of Object.keys(self.exceptions)) {
|
|
||||||
const ifProxy = self.exceptions[host] || false;
|
|
||||||
if (ifProxy) {
|
|
||||||
self.included.push(host);
|
|
||||||
} else {
|
|
||||||
self.excluded.push(host);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
['included', 'excluded'].forEach((who) => {
|
|
||||||
|
|
||||||
self[who] = self[who]
|
|
||||||
.map( (domain) => domain.split('').reverse() )
|
|
||||||
.sort()
|
|
||||||
.map( (rDomain) => rDomain.reverse().join('') );
|
|
||||||
|
|
||||||
});
|
|
||||||
if (self.included.length && !self.filteredCustomsString) {
|
|
||||||
return [null, self, new TypeError(
|
|
||||||
'Имеются сайты, добавленные вручную. Они проксироваться не будут, т.к. нет СВОИХ проски, удовлетворяющих вашим требованиям! Если прокси всё же имеются, то проверьте требования (модификаторы).'
|
|
||||||
)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [null, self];
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
window.apis.pacKitchen = {
|
|
||||||
|
|
||||||
getPacMods: getCurrentConfigs,
|
|
||||||
getPacModsRaw: () => getCurrentConfigs(true),
|
|
||||||
getOrderedConfigs: getOrderedConfigsForUser,
|
|
||||||
|
|
||||||
cook(pacData, pacMods = mandatory()) {
|
|
||||||
|
|
||||||
pacData = pacData.replace(
|
|
||||||
new RegExp(kitchenStartsMark + '[\\s\\S]*$', 'g'),
|
|
||||||
''
|
|
||||||
);
|
|
||||||
/a/.test('a'); // GC RegExp.input and friends.
|
|
||||||
|
|
||||||
return pacMods.ifNoMods ? pacData : pacData + `${ kitchenStartsMark }
|
|
||||||
/******/
|
|
||||||
/******/;(function(global) {
|
|
||||||
/******/ "use strict";
|
|
||||||
/******/
|
|
||||||
/******/ const originalFindProxyForURL = FindProxyForURL;
|
|
||||||
/******/ let tmp = function(url, host) {
|
|
||||||
/******/ const dotHost = '.' + host;
|
|
||||||
${
|
|
||||||
function() {
|
|
||||||
let generatedPac = `
|
|
||||||
/******/ if (${pacMods.ifMindWhitelist && pacMods.whitelist.length}) {
|
|
||||||
/******/ const ifWhitelisted =
|
|
||||||
/******/ ${JSON.stringify(pacMods.whitelist)}.some((whiteHost) => {
|
|
||||||
/******/ const ifWild = whiteHost.startsWith('*');
|
|
||||||
/******/ if (ifWild) {
|
|
||||||
/******/ return dotHost.endsWith(whiteHost.substr(1));
|
|
||||||
/******/ }
|
|
||||||
/******/ return host === whiteHost;
|
|
||||||
/******/ })
|
|
||||||
/******/ if (!ifWhitelisted) {
|
|
||||||
/******/ return 'DIRECT';
|
|
||||||
/******/ }
|
|
||||||
/******/ }`;
|
|
||||||
|
|
||||||
generatedPac += pacMods.ifProhibitDns ? `
|
|
||||||
/******/
|
|
||||||
/******/ global.dnsResolve = function(host) { return null; };
|
|
||||||
/******/
|
|
||||||
/******/` : '';
|
|
||||||
if (pacMods.ifProxyHttpsUrlsOnly) {
|
|
||||||
|
|
||||||
generatedPac += `
|
|
||||||
/******/
|
|
||||||
/******/ if (!url.startsWith("https")) {
|
|
||||||
/******/ return "DIRECT";
|
|
||||||
/******/ }
|
|
||||||
/******/
|
|
||||||
/******/ `;
|
|
||||||
}
|
|
||||||
if (pacMods.ifUseLocalTor) {
|
|
||||||
|
|
||||||
generatedPac += `
|
|
||||||
/******/
|
|
||||||
/******/ if (host.endsWith(".onion")) {
|
|
||||||
/******/ return "${pacMods.torPoints.join('; ')}";
|
|
||||||
/******/ }
|
|
||||||
/******/
|
|
||||||
/******/ `;
|
|
||||||
}
|
|
||||||
generatedPac += `
|
|
||||||
/******/
|
|
||||||
/******/ const directIfAllowed = ${pacMods.ifProxyOrDie ? '""/* Not allowed. */' : '"DIRECT"'};
|
|
||||||
/******/`;
|
|
||||||
if (pacMods.filteredCustomsString) {
|
|
||||||
generatedPac += `
|
|
||||||
/******/
|
|
||||||
/******/ const filteredCustomProxies = "${pacMods.filteredCustomsString}";
|
|
||||||
/******/`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ifIncluded = pacMods.included && pacMods.included.length;
|
|
||||||
const ifExcluded = pacMods.excluded && pacMods.excluded.length;
|
|
||||||
const ifManualExceptions = ifIncluded || ifExcluded;
|
|
||||||
let finalExceptions = {};
|
|
||||||
if (pacMods.ifProxyMoreDomains) {
|
|
||||||
finalExceptions = pacMods.moreDomains.reduce((acc, tld) => {
|
|
||||||
|
|
||||||
acc['*.' + tld] = true;
|
|
||||||
return acc;
|
|
||||||
|
|
||||||
}, finalExceptions);
|
|
||||||
}
|
|
||||||
if (pacMods.ifMindExceptions) {
|
|
||||||
Object.assign(finalExceptions, (pacMods.exceptions || {}));
|
|
||||||
}
|
|
||||||
const ifExceptions = Object.keys(finalExceptions).length;
|
|
||||||
|
|
||||||
if (ifExceptions) {
|
|
||||||
generatedPac += `
|
|
||||||
/******/
|
|
||||||
/******/ /* EXCEPTIONS START */
|
|
||||||
// TODO: handle wildcards.
|
|
||||||
/******/ const isHostInDomain = (domain, ifWild) => {
|
|
||||||
if (ifWild) {
|
|
||||||
return dotHost.endsWith(domain.substr(1));
|
|
||||||
}
|
|
||||||
return domain === host;
|
|
||||||
}
|
|
||||||
/******/ const domainReducer = (maxWeight, [domain, ifProxy]) => {
|
|
||||||
/******/
|
|
||||||
const ifWild = domain.startsWith('*.');
|
|
||||||
/******/ if (!isHostInDomain(domain, ifWild)) {
|
|
||||||
/******/ return maxWeight;
|
|
||||||
/******/ }
|
|
||||||
let len = domain.length;
|
|
||||||
if (ifWild) {
|
|
||||||
len = len === 0 ? len : (len - 2)*2 - 1;
|
|
||||||
} else {
|
|
||||||
len = len*2;
|
|
||||||
}
|
|
||||||
/******/ const newWeightAbs = len;
|
|
||||||
/******/ if (newWeightAbs < Math.abs(maxWeight)) {
|
|
||||||
/******/ return maxWeight;
|
|
||||||
/******/ }
|
|
||||||
/******/ return newWeightAbs*(ifProxy ? 1 : -1);
|
|
||||||
/******/
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ const excWeight = ${ JSON.stringify(Object.entries(finalExceptions)) }.reduce( domainReducer, 0 );
|
|
||||||
/******/ if (excWeight !== 0) {
|
|
||||||
/******/ if (excWeight < 0) {
|
|
||||||
/******/ // Never proxy it!
|
|
||||||
/******/ return "DIRECT";
|
|
||||||
/******/ }
|
|
||||||
/******/ // Always proxy it!
|
|
||||||
${ pacMods.filteredCustomsString
|
|
||||||
? `/******/ return filteredCustomProxies + "; " + directIfAllowed;`
|
|
||||||
: '/******/ /* No custom proxies -- continue. */'
|
|
||||||
}
|
|
||||||
/******/ }
|
|
||||||
/******/ /* EXCEPTIONS END */
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
generatedPac += `
|
|
||||||
/******/ const pacScriptProxies = originalFindProxyForURL(url, host)${
|
|
||||||
/******/ pacMods.ifProxyOrDie
|
|
||||||
? '.replace(/DIRECT/g, "")'
|
|
||||||
: ' + "; " + directIfAllowed'
|
|
||||||
};`;
|
|
||||||
if(
|
|
||||||
!pacMods.ifUseSecureProxiesOnly &&
|
|
||||||
!pacMods.filteredCustomsString &&
|
|
||||||
pacMods.ifUsePacScriptProxies
|
|
||||||
) {
|
|
||||||
return generatedPac + `
|
|
||||||
/******/ return [pacScriptProxies, directIfAllowed]
|
|
||||||
.filter((p) => p).join("; ") || "DIRECT";`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return generatedPac + `
|
|
||||||
/******/ let pacProxyArray = pacScriptProxies.split(/(?:\\s*;\\s*)+/g).filter( (p) => p );
|
|
||||||
/******/ const ifNoProxies = pacProxyArray${pacMods.ifProxyOrDie ? '.length === 0' : '.every( (p) => /^DIRECT$/i.test(p) )'};
|
|
||||||
/******/ if (ifNoProxies) {
|
|
||||||
/******/ // Directs only or null, no proxies.
|
|
||||||
/******/ return "DIRECT";
|
|
||||||
/******/ }
|
|
||||||
/******/ return ` +
|
|
||||||
((pacMods.filteredCustomsString && !pacMods.ifUseOwnProxiesOnlyForOwnSites)
|
|
||||||
? 'filteredCustomProxies + "; " + '
|
|
||||||
: ''
|
|
||||||
) +
|
|
||||||
function() {
|
|
||||||
|
|
||||||
if (!pacMods.ifUsePacScriptProxies) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
let filteredPacExp = 'pacScriptProxies';
|
|
||||||
if (pacMods.ifUseSecureProxiesOnly) {
|
|
||||||
filteredPacExp =
|
|
||||||
'pacProxyArray.filter( (pStr) => /^HTTPS\\s/.test(pStr) ).join("; ")';
|
|
||||||
}
|
|
||||||
return filteredPacExp + ' + "; " + ';
|
|
||||||
|
|
||||||
}() + 'directIfAllowed;'; // Without DIRECT you will get 'PROXY CONN FAILED' pac-error.
|
|
||||||
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
/******/ };
|
|
||||||
${
|
|
||||||
!pacMods.replaceDirectWith
|
|
||||||
? ''
|
|
||||||
: `
|
|
||||||
/******/ const oldTmp = tmp;
|
|
||||||
/******/ tmp = function(url, host) {
|
|
||||||
/******/ const ip = dnsResolve(host);
|
|
||||||
/******/ if (ip) {
|
|
||||||
/******/ const ipInt = convert_addr(ip);
|
|
||||||
/******/ if([
|
|
||||||
/******/ /* Reserved networks: https://en.wikipedia.org/wiki/Reserved_IP_addresses#IPv4 */
|
|
||||||
/******/ [-16777216, 0 ], // ['0.0.0.0' , '255.0.0.0' ],
|
|
||||||
/******/ [-16777216, 167772160 ], // ['10.0.0.0' , '255.0.0.0' ],
|
|
||||||
/******/ [-4194304, 1681915904 ], // ['100.64.0.0' , '255.192.0.0'],
|
|
||||||
/******/ [-16777216, 2130706432 ], // ['127.0.0.0' , '255.0.0.0' ],
|
|
||||||
/******/ [-65536, -1442971648], // ['169.254.0.0', '255.255.0.0'],
|
|
||||||
/******/ [-1048576, -1408237568], // ['172.16.0.0', '255.240.0.0'],
|
|
||||||
/******/ [-256, -1073741824], // ['192.0.0.0' , '255.255.255.0'],
|
|
||||||
/******/ [-256, -1073741312], // ['192.0.2.0' , '255.255.255.0'],
|
|
||||||
/******/ [-256, -1067949312], // ['192.88.99.0' , '255.255.255.0'],
|
|
||||||
/******/ [-65536, -1062731776], // ['192.168.0.0', '255.255.0.0'],
|
|
||||||
/******/ [-131072, -971898880 ], // ['198.18.0.0', '255.254.0.0'],
|
|
||||||
/******/ [-256, -969710592 ], // ['198.51.100.0', '255.255.255.0'],
|
|
||||||
/******/ [-256, -889163520 ], // ['203.0.113.0', '255.255.255.0'],
|
|
||||||
/******/ [-268435456, -536870912 ], // ['224.0.0.0', '240.0.0.0'],
|
|
||||||
/******/ [-268435456, -268435456 ], // ['240.0.0.0', '240.0.0.0'],
|
|
||||||
/******/ [-1, -1 ], // ['255.255.255.255' , '255.255.255.255'],
|
|
||||||
/******/ ].some(([netMask, maskedNet]) => (ipInt & netMask) === maskedNet)
|
|
||||||
/******/ ) {
|
|
||||||
/******/ return "DIRECT";
|
|
||||||
/******/ }
|
|
||||||
/******/ }
|
|
||||||
/******/ return oldTmp.call(this, url, host).replace(/(;|^)\\s*DIRECT\\s*(?=;|$)/g, "$1${pacMods.replaceDirectWith}");
|
|
||||||
/******/ };
|
|
||||||
`
|
|
||||||
}
|
|
||||||
/******/ if (global) {
|
|
||||||
/******/ global.FindProxyForURL = tmp;
|
|
||||||
/******/ } else {
|
|
||||||
/******/ FindProxyForURL = tmp;
|
|
||||||
/******/ }
|
|
||||||
|
|
||||||
/*****/})(this);`;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
setNowAsync(details, cb = throwIfError) {
|
|
||||||
|
|
||||||
if (typeof(details) === 'function') {
|
|
||||||
cb = details;
|
|
||||||
details = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
new Promise((resolve) =>
|
|
||||||
|
|
||||||
details
|
|
||||||
? resolve(details)
|
|
||||||
: chrome.proxy.settings.get({}, timeouted(resolve) ),
|
|
||||||
|
|
||||||
).then((details) => {
|
|
||||||
|
|
||||||
if (
|
|
||||||
details && details.levelOfControl === 'controlled_by_this_extension'
|
|
||||||
) {
|
|
||||||
const pac = window.utils.getProp(details, 'value.pacScript');
|
|
||||||
if (pac && pac.data) {
|
|
||||||
return chrome.proxy.settings.set(details, chromified(cb));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kitchenState(ifIncontinence, true);
|
|
||||||
cb(null, null, new TypeError(
|
|
||||||
'Не найдено активного PAC-скрипта! Изменения будут применены при возвращении контроля настроек прокси или установке нового PAC-скрипта.'
|
|
||||||
));
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
checkIncontinence(details) {
|
|
||||||
|
|
||||||
if ( kitchenState(ifIncontinence) ) {
|
|
||||||
this.setNowAsync(details, (err) => { if (err) { throw err; } }); // TODO: suppress?
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
keepCookedNowAsync(pacMods = mandatory(), cb = throwIfError) {
|
|
||||||
|
|
||||||
let ifProxiesChanged = false;
|
|
||||||
let modsWarns = [];
|
|
||||||
if (typeof(pacMods) === 'function') {
|
|
||||||
cb = pacMods;
|
|
||||||
pacMods = getCurrentConfigs();
|
|
||||||
} else {
|
|
||||||
let modsErr;
|
|
||||||
[modsErr, pacMods, ...modsWarns] = createPacModifiers(pacMods);
|
|
||||||
if (modsErr) {
|
|
||||||
return cb(modsErr, null, modsWarns);
|
|
||||||
}
|
|
||||||
const oldProxies = getCurrentConfigs().filteredCustomsString || '';
|
|
||||||
const newProxies = pacMods.filteredCustomsString || '';
|
|
||||||
ifProxiesChanged = oldProxies !== newProxies;
|
|
||||||
kitchenState(modsKey, pacMods);
|
|
||||||
}
|
|
||||||
this.setNowAsync(
|
|
||||||
(err, res, ...setWarns) => {
|
|
||||||
|
|
||||||
const accWarns = modsWarns.concat(setWarns); // Acc = accumulated.
|
|
||||||
if (err) {
|
|
||||||
return cb(err, res, ...accWarns);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ifProxiesChanged) {
|
|
||||||
return cb(null, res, ...accWarns);
|
|
||||||
}
|
|
||||||
const newHosts = (pacMods.customProxyArray || []).map( (ps) => ps.split(/\s+/)[1] );
|
|
||||||
window.utils.fireRequest(
|
|
||||||
'ip-to-host-replace-all',
|
|
||||||
newHosts,
|
|
||||||
(err, res, ...moreWarns) =>
|
|
||||||
cb(err, res, ...accWarns, ...moreWarns),
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
resetToDefaults() {
|
|
||||||
|
|
||||||
kitchenState(modsKey, null);
|
|
||||||
kitchenState(ifIncontinence, null);
|
|
||||||
this.keepCookedNowAsync(throwIfError);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const pacKitchen = window.apis.pacKitchen;
|
|
||||||
|
|
||||||
const originalSet = chrome.proxy.settings.set.bind( chrome.proxy.settings );
|
|
||||||
|
|
||||||
chrome.proxy.settings.set = function(details, cb) {
|
|
||||||
const pac = window.utils.getProp(details, 'value.pacScript');
|
|
||||||
if (!(pac && pac.data)) {
|
|
||||||
return originalSet(details, window.utils.timeouted(cb));
|
|
||||||
}
|
|
||||||
const pacMods = getCurrentConfigs();
|
|
||||||
pac.data = pacKitchen.cook( pac.data, pacMods );
|
|
||||||
originalSet({value: details.value}, window.utils.chromified((err) => {
|
|
||||||
|
|
||||||
if (!err) {
|
|
||||||
kitchenState(ifIncontinence, null);
|
|
||||||
}
|
|
||||||
window.utils.lastError = err;
|
|
||||||
cb && cb();
|
|
||||||
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
} // Private namespace ends.
|
|
|
@ -1,730 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/*
|
|
||||||
Task 1. Gets IPs for proxies of antizapret/anticenz via dns over https.
|
|
||||||
These IPs are used in block-informer to inform user when proxy is ON.
|
|
||||||
Task 2. Downloads PAC proxy script from antizapret/anticenz/
|
|
||||||
my Google Drive and sets it in Chromium settings.
|
|
||||||
Task 3. Schedules tasks 1 & 2 for every 4 hours.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
In background scripts use window.apis.antiCensorRu public variables.
|
|
||||||
In pages window.apis.antiCensorRu is not accessible,
|
|
||||||
use chrome.runtime.getBackgroundPage(..),
|
|
||||||
extension.getBackgroundPage is deprecated
|
|
||||||
|
|
||||||
If you want to catch errors, then call api from setTimeout!
|
|
||||||
See errorHandlers api for more.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
Due to History
|
|
||||||
|
|
||||||
- *Async suffix usually means that function requires a cb.
|
|
||||||
It may not be related to async (returning a Promise).
|
|
||||||
This naming is confusing and should be reconsidered.
|
|
||||||
*/
|
|
||||||
|
|
||||||
{ // Private namespace starts.
|
|
||||||
|
|
||||||
const ifRu = chrome.i18n.getMessage('@@ui_locale').startsWith('ru');
|
|
||||||
console.log('Russian?', ifRu);
|
|
||||||
const mandatory = window.utils.mandatory;
|
|
||||||
const throwIfError = window.utils.throwIfError;
|
|
||||||
const chromified = window.utils.chromified;
|
|
||||||
const timeouted = window.utils.timeouted;
|
|
||||||
|
|
||||||
const clarifyThen = window.apis.errorsLib.clarifyThen;
|
|
||||||
const clarify = window.apis.errorsLib.clarify;
|
|
||||||
const Warning = window.apis.errorsLib.Warning;
|
|
||||||
|
|
||||||
const httpLib = window.apis.httpLib;
|
|
||||||
const handlers = window.apis.errorHandlers;
|
|
||||||
|
|
||||||
const asyncLogGroup = function asyncLogGroup(...args) {
|
|
||||||
|
|
||||||
const cb = args.pop();
|
|
||||||
if(!(cb && typeof(cb) === 'function')) {
|
|
||||||
throw new TypeError('cb must be a function, but got: ' + cb);
|
|
||||||
}
|
|
||||||
console.group(...args);
|
|
||||||
return function(...cbArgs) {
|
|
||||||
|
|
||||||
console.groupEnd();
|
|
||||||
console.log('Group finished.');
|
|
||||||
cb(...cbArgs);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const tryPromiseSeveralTimesAsync = (createPromise, timeoutsInSec) =>
|
|
||||||
createPromise().then(
|
|
||||||
(res) => Promise.resolve(res),
|
|
||||||
(err) => {
|
|
||||||
console.log('Promise failed, are there any retries?');
|
|
||||||
const outSec = timeoutsInSec.shift();
|
|
||||||
if (outSec === undefined) {
|
|
||||||
console.log('No retries left.');
|
|
||||||
return Promise.reject(err);
|
|
||||||
}
|
|
||||||
console.log('Retrying in', outSec, 'sec');
|
|
||||||
/*
|
|
||||||
const alarmName = 'try-promise=several-times-async';
|
|
||||||
const res = new Promise((resolve) => {
|
|
||||||
chrome.alarms.onAlarm.addListener((alarmInfo) => {
|
|
||||||
if (alarmInfo.name === alarmName) {
|
|
||||||
console.log('Time to retry.');
|
|
||||||
resolve(tryPromiseSeveralTimesAsync(createPromise, timeoutsInSec));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
chrome.alarms.create(alarmName, { delayInMinutes: outSec/60 });
|
|
||||||
return res;
|
|
||||||
*/
|
|
||||||
return new Promise((resolve) =>
|
|
||||||
window.setTimeout(() => resolve(tryPromiseSeveralTimesAsync(createPromise, timeoutsInSec)), outSec*1000),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const doWithoutProxyAsync = (createPromise) => new Promise((resolve, reject) => {
|
|
||||||
console.log('Doing without proxy...');
|
|
||||||
chrome.proxy.settings.get({}, chromified((getErr, settings) => {
|
|
||||||
if (getErr) {
|
|
||||||
reject(getErr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ifWeAreInControl = window.utils.areSettingsControlledFor(settings);
|
|
||||||
if (!ifWeAreInControl) {
|
|
||||||
resolve(createPromise());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
delete settings.levelOfControl;
|
|
||||||
const setProxyAsync = () => new Promise((setResolve, setReject) => {
|
|
||||||
|
|
||||||
console.log('Restoring chrome proxy settings...');
|
|
||||||
chrome.proxy.settings.set(
|
|
||||||
settings,
|
|
||||||
chromified((err) => err ? setReject(err) : setResolve()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
console.log('Clearing chrome proxy settings...');
|
|
||||||
chrome.proxy.settings.clear({}, chromified((clearErr) => {
|
|
||||||
if (clearErr) {
|
|
||||||
reject(clearErr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
createPromise().then((actionResult) => setProxyAsync().then(() => resolve(actionResult)), reject);
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
const setPacAsync = function setPacAsync(
|
|
||||||
pacData = mandatory(), cb = throwIfError,
|
|
||||||
) {
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
mode: 'pac_script',
|
|
||||||
pacScript: {
|
|
||||||
mandatory: false,
|
|
||||||
data: pacData,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
console.log('Setting chrome proxy settings...');
|
|
||||||
chrome.proxy.settings.set( { value: config }, chromified((err) => {
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
if (err.message === 'proxy.settings requires private browsing permission.') {
|
|
||||||
clarifyThen(
|
|
||||||
chrome.i18n.getMessage('AllowExtensionToRunInPrivateWindows'),
|
|
||||||
cb,
|
|
||||||
)(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
handlers.updateControlState( () => {
|
|
||||||
|
|
||||||
if ( !handlers.ifControlled ) {
|
|
||||||
|
|
||||||
console.warn('Failed, other extension is in control.');
|
|
||||||
return cb(
|
|
||||||
new Error( window.utils.messages.whichExtensionHtml() ),
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
console.log('Successfuly set PAC in proxy settings.');
|
|
||||||
cb();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatePacProxyIps = function updatePacProxyIps(
|
|
||||||
cb = throwIfError
|
|
||||||
) {
|
|
||||||
|
|
||||||
cb = asyncLogGroup(
|
|
||||||
'Getting IPs for PAC hosts...',
|
|
||||||
cb,
|
|
||||||
);
|
|
||||||
window.utils.fireRequest('ip-to-host-update-all', cb);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const setPacScriptFromProviderAsync = function setPacScriptFromProviderAsync(
|
|
||||||
provider, lastModifiedStr, ifUnattended = mandatory(), cb = throwIfError,
|
|
||||||
) {
|
|
||||||
|
|
||||||
const pacUrl = provider.pacUrls[0];
|
|
||||||
cb = asyncLogGroup(
|
|
||||||
'Getting PAC script from provider...', pacUrl,
|
|
||||||
cb,
|
|
||||||
);
|
|
||||||
|
|
||||||
const warnings = [];
|
|
||||||
const originalCb = cb;
|
|
||||||
cb = (err, res, ...warns) => originalCb(err, res, ...warns, ...warnings);
|
|
||||||
const addWarning = (wText) => { warnings.push(new Warning(wText)) };
|
|
||||||
|
|
||||||
if (provider.distinctKey === 'Anticensority') {
|
|
||||||
|
|
||||||
const pacMods = window.apis.pacKitchen.getPacMods();
|
|
||||||
if (!pacMods.filteredCustomsString) {
|
|
||||||
addWarning(
|
|
||||||
ifRu
|
|
||||||
? \`
|
|
||||||
Не найдено СВОИХ прокси. Этот PAC-скрипт
|
|
||||||
работает только со <a href="https://git.io/ac-own-proxy">СВОИМИ прокси</a>
|
|
||||||
(по умолчанию будет использоваться локальный <a href="https://git.io/ac-tor">Tor</a>).
|
|
||||||
\`
|
|
||||||
: \`
|
|
||||||
Couldn't find OWN proxies. This PAC-script
|
|
||||||
works only with <a href="https://git.io/ac-own-proxy">OWN proxies</a>
|
|
||||||
(by default local <a href="https://git.io/ac-tor">Tor</a> will be used).
|
|
||||||
\`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
doWithoutProxyAsync(
|
|
||||||
// Employ all urls, the latter are fallbacks for the former.
|
|
||||||
() => {
|
|
||||||
const tryAllUrlsAsync = () => provider.pacUrls.reduce(
|
|
||||||
(promise, url) => promise.catch(
|
|
||||||
() => new Promise(
|
|
||||||
(resolve, reject) => httpLib.get(
|
|
||||||
url,
|
|
||||||
(newErr, pacData) =>
|
|
||||||
newErr ? reject(newErr) : resolve(pacData),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Promise.reject(),
|
|
||||||
);
|
|
||||||
return (ifUnattended
|
|
||||||
? tryPromiseSeveralTimesAsync(tryAllUrlsAsync, [20, 40, 60])
|
|
||||||
: tryAllUrlsAsync()
|
|
||||||
).catch(
|
|
||||||
(err) => Promise.reject(clarify(
|
|
||||||
err,
|
|
||||||
chrome.i18n.getMessage('FailedToDownloadPacScriptFromAddresses') + ': [ '
|
|
||||||
+ provider.pacUrls.join(' , ') + ' ].',
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).then(
|
|
||||||
(pacData) => {
|
|
||||||
setPacAsync(
|
|
||||||
pacData,
|
|
||||||
(err, res) => cb(
|
|
||||||
err,
|
|
||||||
Object.assign(res || {}, {lastModified: lastModifiedStr}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cb,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.apis.antiCensorRu = {
|
|
||||||
|
|
||||||
version: chrome.runtime.getManifest().version,
|
|
||||||
|
|
||||||
pacProviders: {
|
|
||||||
Антизапрет: {
|
|
||||||
// Distinct keys are needed if you want to check if a given
|
|
||||||
// provider is this or that (distinct it from others).
|
|
||||||
distinctKey: 'Antizapret',
|
|
||||||
label: chrome.i18n.getMessage('Antizapret'),
|
|
||||||
desc: ifRu
|
|
||||||
? \`Основной PAC-скрипт от автора проекта «Антизапрет».
|
|
||||||
Охватывет меньше сайтов.
|
|
||||||
Блокировка определяется по доменному имени и при необходимости по IP.
|
|
||||||
<br/> <a href="https://github.com/anticensority/runet-censorship-bypass/wiki/PAC-скрипты:-различия">Сравнение PAC-скриптов</a>.
|
|
||||||
\`
|
|
||||||
: \`The main PAC-script from the author of project "Antizapret"\.
|
|
||||||
Covers fewer sites.
|
|
||||||
Block is detected based on a domain name and, if necessary, on an IP.
|
|
||||||
<br/> <a href="https://github.com/anticensority/runet-censorship-bypass/wiki/PAC-скрипты:-различия">Comparison of PAC-scripts (ru)</a>.
|
|
||||||
\`,
|
|
||||||
order: 0,
|
|
||||||
pacUrls: [
|
|
||||||
'https://e.cen.rodeo:8443/proxy.pac',
|
|
||||||
'https://antizapret.prostovpn.org:8443/proxy.pac',
|
|
||||||
'https://antizapret.prostovpn.org:18443/proxy.pac',
|
|
||||||
'https://antizapret.prostovpn.org/proxy.pac',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
Антицензорити: {
|
|
||||||
distinctKey: 'Anticensority',
|
|
||||||
label: chrome.i18n.getMessage('Anticensority'),
|
|
||||||
desc: ifRu
|
|
||||||
? \`Альтернативный PAC-скрипт от автора расширения.
|
|
||||||
Охватывает больше сайтов.
|
|
||||||
Блокировка определятся по доменному имени или IP адресу.
|
|
||||||
Подходит для провайдеров, блокирующих все сайты на одном IP.
|
|
||||||
<br/> <a href="https://github.com/anticensority/runet-censorship-bypass/wiki/PAC-скрипты:-различия">Сравнение PAC-скриптов</a>.
|
|
||||||
\`
|
|
||||||
: \`Alternative PAC-script from the author of this extension.
|
|
||||||
Covers more sites.
|
|
||||||
Block is detected based on a domain name and on an IP address.
|
|
||||||
Better fits providers that block all sites on one IP.
|
|
||||||
<br/> <a href="https://github.com/anticensority/runet-censorship-bypass/wiki/PAC-скрипты:-различия">Comparison of PAC-scripts (ru)</a>.
|
|
||||||
\`,
|
|
||||||
order: 1,
|
|
||||||
|
|
||||||
/*
|
|
||||||
Don't use in system configs! Because Windows does poor caching.
|
|
||||||
Some urls are encoded to counter abuse.
|
|
||||||
Version: 0.17
|
|
||||||
*/
|
|
||||||
pacUrls: ${JSON.stringify(anticensorityPacUrls, null, 2)},
|
|
||||||
},
|
|
||||||
onlyOwnSites: {
|
|
||||||
distinctKey: 'onlyOwnSites',
|
|
||||||
label: chrome.i18n.getMessage('Only_own_sites_and_only_own_proxies'),
|
|
||||||
desc: ifRu
|
|
||||||
? 'Проксируются только добавленные вручную адреса через СВОИ вручную добавленные прокси или через локальный Tor.'
|
|
||||||
: 'Only added manually urls are proxied via your OWN manually added proxies or via Tor.',
|
|
||||||
order: 99,
|
|
||||||
pacUrls: [
|
|
||||||
'data:application/x-ns-proxy-autoconfig,' + escape('function FindProxyForURL(){ return "DIRECT"; }'),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getSortedEntriesForProviders() {
|
|
||||||
|
|
||||||
return Object.entries(this.pacProviders).sort((entryA, entryB) => entryA[1].order - entryB[1].order).map(([key, prov]) => Object.assign({key: key}, prov));
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
_currentPacProviderKey: 'Антизапрет',
|
|
||||||
|
|
||||||
/* Is it the first time extension installed?
|
|
||||||
Do something, e.g. initiate PAC sync.
|
|
||||||
*/
|
|
||||||
ifFirstInstall: false,
|
|
||||||
/* We have .lastPacUpdateStamp and ._currentPacProviderLastModified.
|
|
||||||
LastModified is received from a server, we kind of don't trust it,
|
|
||||||
just use it for cache and maybe show to the user.
|
|
||||||
UpdateStamp is got from client and we base our timers on it,
|
|
||||||
malicious server can't interfere with it.
|
|
||||||
*/
|
|
||||||
lastPacUpdateStamp: 0,
|
|
||||||
|
|
||||||
setTitle() {
|
|
||||||
|
|
||||||
const upDate = new Date(this.lastPacUpdateStamp).toLocaleString('ru-RU')
|
|
||||||
.replace(/:\\d+$/, '').replace(/\\.\\d{4}/, '');
|
|
||||||
chrome.browserAction.setTitle({
|
|
||||||
title: \`\${chrome.i18n.getMessage('Updated')} \${upDate} | \${chrome.i18n.getMessage('Version')} \${window.apis.version.build}\`,
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
_currentPacProviderLastModified: 0,
|
|
||||||
|
|
||||||
getLastModifiedForKey(key = mandatory()) {
|
|
||||||
|
|
||||||
if (this._currentPacProviderKey === key) {
|
|
||||||
return new Date(this._currentPacProviderLastModified).toUTCString();
|
|
||||||
}
|
|
||||||
return new Date(0).toUTCString();
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
setLastModified(newValue = mandatory()) {
|
|
||||||
|
|
||||||
this._currentPacProviderLastModified = newValue;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
mustBeKey(key = mandatory()) {
|
|
||||||
|
|
||||||
if ( !(key === null || this.pacProviders[key]) ) {
|
|
||||||
throw new TypeError('No provider for key:' + key);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
getCurrentPacProviderKey() {
|
|
||||||
|
|
||||||
return this._currentPacProviderKey;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
setCurrentPacProviderKey(
|
|
||||||
newKey = mandatory(),
|
|
||||||
lastModified = new Date().toUTCString(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
this.mustBeKey(newKey);
|
|
||||||
this._currentPacProviderKey = newKey;
|
|
||||||
this._currentPacProviderLastModified = lastModified;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
getPacProvider(key) {
|
|
||||||
|
|
||||||
if(key) {
|
|
||||||
this.mustBeKey(key);
|
|
||||||
} else {
|
|
||||||
key = this.getCurrentPacProviderKey();
|
|
||||||
}
|
|
||||||
return this.pacProviders[key];
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
_periodicUpdateAlarmReason: 'Периодичное обновление PAC-скрипта',
|
|
||||||
|
|
||||||
pushToStorageAsync(cb = throwIfError) {
|
|
||||||
|
|
||||||
console.log('Pushing to storage...');
|
|
||||||
|
|
||||||
// Copy only settable properties (except functions).
|
|
||||||
const onlySettable = {};
|
|
||||||
for(const key of Object.keys(this)) {
|
|
||||||
if (
|
|
||||||
Object.getOwnPropertyDescriptor(this, key).writable
|
|
||||||
&& typeof(this[key]) !== 'function'
|
|
||||||
) {
|
|
||||||
onlySettable[key] = this[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.storage.local.remove(
|
|
||||||
'antiCensorRu',
|
|
||||||
() => chrome.storage.local.set(
|
|
||||||
{ antiCensorRu: onlySettable },
|
|
||||||
chromified(cb),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
syncWithPacProviderAsync(opts = {}, cb = throwIfError) {
|
|
||||||
const optsDefaults = Object.freeze({ key: this.getCurrentPacProviderKey(), ifUnattended: false });
|
|
||||||
if( typeof(opts) === 'function' ) {
|
|
||||||
cb = opts;
|
|
||||||
opts = {};
|
|
||||||
}
|
|
||||||
let { key, ifUnattended } = { ...optsDefaults, ...opts };
|
|
||||||
cb = asyncLogGroup('Syncing with PAC provider ' + key + '...', cb);
|
|
||||||
|
|
||||||
if (key === null) {
|
|
||||||
// No pac provider set.
|
|
||||||
return clarifyThen(chrome.i18n.getMessage('ChoosePacProviderFirstD'), cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pacProvider = this.getPacProvider(key);
|
|
||||||
|
|
||||||
const pacSetPromise = new Promise(
|
|
||||||
(resolve, reject) => setPacScriptFromProviderAsync(
|
|
||||||
pacProvider,
|
|
||||||
this.getLastModifiedForKey(key),
|
|
||||||
ifUnattended,
|
|
||||||
(err, res, ...warns) => {
|
|
||||||
|
|
||||||
if (!err) {
|
|
||||||
this.setCurrentPacProviderKey(key, res.lastModified);
|
|
||||||
this.lastPacUpdateStamp = Date.now();
|
|
||||||
this.ifFirstInstall = false;
|
|
||||||
this.setAlarms();
|
|
||||||
this.setTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve([err, null, ...warns]);
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateIpsAsync = () => new Promise(
|
|
||||||
(resolve, reject) => updatePacProxyIps(
|
|
||||||
(err, res, ...warns) => {
|
|
||||||
if (err) {
|
|
||||||
reject([err, ...warns]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve([err, res, ...warns]);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const ipsPromise = !ifUnattended
|
|
||||||
? updateIpsAsync()
|
|
||||||
: tryPromiseSeveralTimesAsync(updateIpsAsync, [20, 40, 60]);
|
|
||||||
|
|
||||||
Promise.all([pacSetPromise, ipsPromise]).then(
|
|
||||||
([[pacErr, pacRes, ...pacWarns], [ipsErr, ipsRes, ...ipsWarns]]) => {
|
|
||||||
|
|
||||||
if (pacErr) {
|
|
||||||
return cb(pacErr, pacRes);
|
|
||||||
}
|
|
||||||
const warns = pacWarns;
|
|
||||||
if (ipsErr) {
|
|
||||||
warns.push(ipsErr);
|
|
||||||
}
|
|
||||||
if (ipsWarns.length) {
|
|
||||||
warns.push(...ipsWarns);
|
|
||||||
}
|
|
||||||
this.pushToStorageAsync(
|
|
||||||
(pushErr) => cb(pacErr || pushErr, null, ...warns),
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
cb
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
_pacUpdatePeriodInMinutes: 12*60,
|
|
||||||
get pacUpdatePeriodInMinutes() {
|
|
||||||
|
|
||||||
return this._pacUpdatePeriodInMinutes;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
setAlarms() {
|
|
||||||
|
|
||||||
let nextUpdateMoment = this.lastPacUpdateStamp
|
|
||||||
+ this._pacUpdatePeriodInMinutes*60*1000;
|
|
||||||
const now = Date.now();
|
|
||||||
if (nextUpdateMoment < now) {
|
|
||||||
nextUpdateMoment = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'Next PAC update is scheduled on',
|
|
||||||
new Date(nextUpdateMoment).toLocaleString('ru-RU'),
|
|
||||||
);
|
|
||||||
|
|
||||||
chrome.alarms.create(
|
|
||||||
this._periodicUpdateAlarmReason,
|
|
||||||
{
|
|
||||||
when: nextUpdateMoment,
|
|
||||||
periodInMinutes: this._pacUpdatePeriodInMinutes,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ifAlarmTriggered. May be changed in the future.
|
|
||||||
return nextUpdateMoment === now;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
installPacAsync(key, cb = throwIfError) {
|
|
||||||
|
|
||||||
console.log('Installing PAC...');
|
|
||||||
if (!key) {
|
|
||||||
throw new Error('Key must be defined.');
|
|
||||||
}
|
|
||||||
if (this.currentProviderKey !== key) {
|
|
||||||
return this.syncWithPacProviderAsync({ key }, cb);
|
|
||||||
}
|
|
||||||
console.log(key + ' already installed.');
|
|
||||||
cb();
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
clearPacAsync(cb = throwIfError) {
|
|
||||||
|
|
||||||
cb = asyncLogGroup('Cearing alarms and PAC...', cb);
|
|
||||||
chrome.alarms.clearAll(
|
|
||||||
() => chrome.proxy.settings.clear(
|
|
||||||
{},
|
|
||||||
chromified((err) => {
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
this.setCurrentPacProviderKey(null);
|
|
||||||
this.pushToStorageAsync(
|
|
||||||
() => handlers.updateControlState(cb),
|
|
||||||
);
|
|
||||||
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
// ON EACH LAUNCH, STARTUP, RELOAD, UPDATE, ENABLE
|
|
||||||
(async () => {
|
|
||||||
let ifConsentGiven = await window.utils.promisedLocalStorage.get('ifConsentGiven');
|
|
||||||
if (!ifConsentGiven) {
|
|
||||||
window.utils.openAndFocus('/pages/consent/index.html');
|
|
||||||
await window.apis.consent.promise;
|
|
||||||
ifConsentGiven = true
|
|
||||||
await window.utils.promisedLocalStorage.set({ ifConsentGiven });
|
|
||||||
}
|
|
||||||
|
|
||||||
let oldAntiCensorRu = await window.utils.promisedLocalStorage.get('antiCensorRu') || {};
|
|
||||||
|
|
||||||
const otherKeys = [
|
|
||||||
'pac-kitchen-if-incontinence',
|
|
||||||
'pac-kitchen-mods',
|
|
||||||
'ip-to-host',
|
|
||||||
'handlers-pac-error',
|
|
||||||
'handlers-ext-error',
|
|
||||||
'handlers-no-control',
|
|
||||||
];
|
|
||||||
/*
|
|
||||||
Event handlers that ALWAYS work (even if installation is not done
|
|
||||||
or failed).
|
|
||||||
E.g. install window may fail to open or be closed by user accidentally.
|
|
||||||
In such case extension _should_ try to work on default parameters.
|
|
||||||
*/
|
|
||||||
const antiCensorRu = window.apis.antiCensorRu;
|
|
||||||
|
|
||||||
chrome.alarms.onAlarm.addListener(
|
|
||||||
timeouted( (alarm) => {
|
|
||||||
|
|
||||||
if (alarm.name === antiCensorRu._periodicUpdateAlarmReason) {
|
|
||||||
console.log(
|
|
||||||
'Periodic PAC update triggered:',
|
|
||||||
new Date().toLocaleString('ru-RU'),
|
|
||||||
);
|
|
||||||
antiCensorRu.syncWithPacProviderAsync({ ifUnattended: true }, () => { /* Swallow. */ });
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
);
|
|
||||||
console.log('Alarm listener installed. We won\\'t miss any PAC update.');
|
|
||||||
|
|
||||||
window.addEventListener('online', () => {
|
|
||||||
|
|
||||||
console.log('We are online, checking periodic updates...');
|
|
||||||
antiCensorRu.setAlarms();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Keep cooked...');
|
|
||||||
await new Promise((resolve) => window.apis.pacKitchen.keepCookedNowAsync(resolve));
|
|
||||||
|
|
||||||
//console.log('Storage on init:', oldAntiCensorRu);
|
|
||||||
antiCensorRu.ifFirstInstall = Object.keys(oldAntiCensorRu).length === 0;
|
|
||||||
|
|
||||||
if (antiCensorRu.ifFirstInstall) {
|
|
||||||
// INSTALL
|
|
||||||
console.log('Installing...');
|
|
||||||
handlers.switch('on', 'ext-error');
|
|
||||||
chrome.runtime.openOptionsPage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// LAUNCH, RELOAD, UPDATE
|
|
||||||
// Use old or migrate to default.
|
|
||||||
antiCensorRu._currentPacProviderKey =
|
|
||||||
oldAntiCensorRu._currentPacProviderKey || null;
|
|
||||||
antiCensorRu.lastPacUpdateStamp =
|
|
||||||
oldAntiCensorRu.lastPacUpdateStamp || antiCensorRu.lastPacUpdateStamp;
|
|
||||||
antiCensorRu._currentPacProviderLastModified =
|
|
||||||
oldAntiCensorRu._currentPacProviderLastModified
|
|
||||||
|| antiCensorRu._currentPacProviderLastModified;
|
|
||||||
console.log(
|
|
||||||
'Last PAC update was on',
|
|
||||||
new Date(antiCensorRu.lastPacUpdateStamp).toLocaleString('ru-RU'),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
1. There is no way to check that chrome.runtime.onInstalled wasn't fired
|
|
||||||
except timeout.
|
|
||||||
Otherwise we could put storage migration code only there.
|
|
||||||
2. We have to check storage for migration before using it.
|
|
||||||
Better on each launch then on each pull.
|
|
||||||
*/
|
|
||||||
|
|
||||||
await new Promise(async (resolve) => {
|
|
||||||
|
|
||||||
const ifUpdating = antiCensorRu.version !== oldAntiCensorRu.version;
|
|
||||||
if (!ifUpdating) {
|
|
||||||
|
|
||||||
// LAUNCH, RELOAD, ENABLE
|
|
||||||
antiCensorRu.pacProviders = oldAntiCensorRu.pacProviders;
|
|
||||||
console.log('Extension launched, reloaded or enabled.');
|
|
||||||
return resolve();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// UPDATE & MIGRATION
|
|
||||||
|
|
||||||
const ifUpdatedCb = () => antiCensorRu.pushToStorageAsync(() => {
|
|
||||||
|
|
||||||
console.log('Extension updated.');
|
|
||||||
resolve();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Updating from', oldAntiCensorRu.version, 'to', antiCensorRu.version);
|
|
||||||
try {
|
|
||||||
if (window.apis.version.isLeq(oldAntiCensorRu.version, '0.0.1.62')) {
|
|
||||||
window.apis.antiCensorRu.pacProviders['Антизапрет'].pacUrls = [
|
|
||||||
'https://e.cen.rodeo:8443/proxy.pac',
|
|
||||||
'https://antizapret.prostovpn.org:8443/proxy.pac',
|
|
||||||
'https://antizapret.prostovpn.org:18443/proxy.pac',
|
|
||||||
'https://antizapret.prostovpn.org/proxy.pac',
|
|
||||||
];
|
|
||||||
console.log('Successfully updated to 0.0.1.63.');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Log update error.
|
|
||||||
console.log('UPDATE ERROR:');
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
ifUpdatedCb();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
if (antiCensorRu.getPacProvider()) {
|
|
||||||
antiCensorRu.setAlarms();
|
|
||||||
}
|
|
||||||
antiCensorRu.setTitle();
|
|
||||||
|
|
||||||
/*
|
|
||||||
History of Changes to Storage (Migration Guide)
|
|
||||||
-----------------------------------------------
|
|
||||||
Version 0.0.0.17:
|
|
||||||
* Remove "Антиценз".
|
|
||||||
* Rename "Оба_и_на_свитчах" to "Антицензорити"
|
|
||||||
* Add provider.label and provider.desc.
|
|
||||||
Version 0.0.0.10:
|
|
||||||
* Add this.version.
|
|
||||||
* Change PacProvider.proxyIps from {ip -> Boolean} to {ip -> hostname}.
|
|
||||||
Version 0.0.0.8-9:
|
|
||||||
* Change storage.ifNotInstalled to storage.ifFirstInstall.
|
|
||||||
* Add storage.lastPacUpdateStamp.
|
|
||||||
**/
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
window.apis.menus = {
|
|
||||||
|
|
||||||
getItemsAsObject: () => ({
|
|
||||||
|
|
||||||
googleTranslate: {
|
|
||||||
title: 'Через Google Translate',
|
|
||||||
getUrl: (blockedUrl) => (
|
|
||||||
'https://translate.google.com/translate?hl=&sl=en&tl=ru&anno=2&sandbox=1&u=' + blockedUrl),
|
|
||||||
order: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
googleCache: {
|
|
||||||
title: 'Из кэша Google',
|
|
||||||
getUrl: (blockedUrl) => 'http://webcache.googleusercontent.com/search?q=cache:' + blockedUrl,
|
|
||||||
order: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
archiveOrg: {
|
|
||||||
title: 'Из архива archive.org',
|
|
||||||
getUrl: (blockedUrl) => 'https://web.archive.org/web/*/' + blockedUrl,
|
|
||||||
order: 2,
|
|
||||||
},
|
|
||||||
|
|
||||||
otherUnblock: {
|
|
||||||
title: 'Разблокировать по-другому',
|
|
||||||
getUrl: (blockedUrl) => ('https://anticensority.github.io/unblock#' + blockedUrl),
|
|
||||||
order: 3,
|
|
||||||
},
|
|
||||||
|
|
||||||
antizapretInfo: {
|
|
||||||
title: 'Сайт в реестре блокировок?',
|
|
||||||
getUrl: (blockedUrl) => 'https://reestr.rublacklist.net/?q=' + new URL(blockedUrl).hostname,
|
|
||||||
order: 4,
|
|
||||||
},
|
|
||||||
|
|
||||||
support: {
|
|
||||||
title: 'Документация / Помощь / Поддержка',
|
|
||||||
getUrl: (blockedUrl) => 'https://github.com/anticensority/runet-censorship-bypass/wiki',
|
|
||||||
order: 99,
|
|
||||||
},
|
|
||||||
|
|
||||||
}),
|
|
||||||
|
|
||||||
getItemsAsArray: function() {
|
|
||||||
|
|
||||||
const itemsObj = this.getItemsAsObject();
|
|
||||||
return Object.keys(itemsObj).reduce((acc, key) => {
|
|
||||||
|
|
||||||
acc.push(itemsObj[key]);
|
|
||||||
return acc;
|
|
||||||
|
|
||||||
}, [])
|
|
||||||
.sort((a, b) => a.order - b.order);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
const chromified = window.utils.chromified;
|
|
||||||
|
|
||||||
let seqId = 0;
|
|
||||||
|
|
||||||
const createMenuLinkEntry = (title, tab2url) => {
|
|
||||||
|
|
||||||
const id = (++seqId).toString();
|
|
||||||
|
|
||||||
chrome.contextMenus.create({
|
|
||||||
id: id,
|
|
||||||
title: title,
|
|
||||||
contexts: ['browser_action'],
|
|
||||||
}, chromified((err) => {
|
|
||||||
|
|
||||||
if(err) {
|
|
||||||
console.warn('Context menu error ignored:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
|
||||||
|
|
||||||
if(info.menuItemId === id) {
|
|
||||||
Promise.resolve( tab2url( tab ) )
|
|
||||||
.then( (url) => chrome.tabs.create({url: url}) );
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
window.apis.menus.getItemsAsArray().forEach((item) => {
|
|
||||||
|
|
||||||
createMenuLinkEntry(
|
|
||||||
item.title,
|
|
||||||
(tab) => item.getUrl(tab.url)
|
|
||||||
);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
# For Reviewers
|
|
||||||
|
|
||||||
## Prerequirements
|
|
||||||
|
|
||||||
* You need a globally installed `gulp-cli@3.0.0`.
|
|
||||||
See https://gulpjs.com/docs/en/getting-started/quick-start#install-the-gulp-command-line-utility.
|
|
||||||
* Node v21.7.3
|
|
||||||
* NPM 10.5.0
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
Steps to reproduce the same zip:
|
|
||||||
```
|
|
||||||
npm ci
|
|
||||||
cd src/extension-common/pages/options/
|
|
||||||
npm ci
|
|
||||||
cd -
|
|
||||||
npm start
|
|
||||||
# See ./build/extension-full
|
|
||||||
cd ./build/extension-full
|
|
||||||
zip -r runet-censorship-bypass-full.zip ./*
|
|
||||||
```
|
|
||||||
|
|
||||||
## Minified Files
|
|
||||||
|
|
||||||
### Ace Editor
|
|
||||||
|
|
||||||
https://ace.c9.io -> https://github.com/ajaxorg/ace -> Building Ace -> "The ace-builds repository endeavours to maintain the latest build" -> https://github.com/ajaxorg/ace-builds/ -> Select tag of 1.2.5, open https://github.com/ajaxorg/ace-builds/tree/v1.2.5/src-min and download the files you want to check.
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
mkdir downloaded
|
|
||||||
cd downloaded
|
|
||||||
wget https://raw.githubusercontent.com/ajaxorg/ace-builds/v1.2.5/src-min/ace.js
|
|
||||||
wget https://raw.githubusercontent.com/ajaxorg/ace-builds/v1.2.5/src-min/ext-searchbox.js
|
|
||||||
wget https://raw.githubusercontent.com/ajaxorg/ace-builds/v1.2.5/src-min/mode-javascript.js
|
|
||||||
wget https://raw.githubusercontent.com/ajaxorg/ace-builds/v1.2.5/src-min/worker-javascript.js
|
|
||||||
|
|
||||||
downloaded$ for i in ./*; do md5sum "$i"; done
|
|
||||||
2b9a1157bb3ba711a0402b6751d9ac71 ./ace.js
|
|
||||||
1f73efaff2853571af0e701c5e9a15ee ./ext-searchbox.js
|
|
||||||
e5eebd85c4e66667c28f124e6a07e3ed ./mode-javascript.js
|
|
||||||
f0d1342102d16ab7abe319b2683d10ea ./worker-javascript.js
|
|
||||||
```
|
|
||||||
|
|
||||||
## PAC-Script AntiZapret
|
|
||||||
|
|
||||||
https://antizapret.prostovpn.org/proxy.pac (old) and https://e.cen.rodeo:8443/proxy.pac (new) are generated by https://bitbucket.org/anticensority/antizapret-pac-generator-light/, reviewers may find justifications related to this PAC-script in that repo.
|
|
|
@ -1,143 +0,0 @@
|
||||||
{
|
|
||||||
"extName": {
|
|
||||||
"message": "Runet Censorship Bypass${nameSuffixEn}"
|
|
||||||
},
|
|
||||||
"extDesc": {
|
|
||||||
"message": "Circumvent Russian Internet Censorship: https://git.io/ac-wiki"
|
|
||||||
},
|
|
||||||
"proxy": {
|
|
||||||
"message": "proxy"
|
|
||||||
},
|
|
||||||
"noControl": {
|
|
||||||
"message": "Other extension controls proxy!"
|
|
||||||
},
|
|
||||||
"WhichQ": {
|
|
||||||
"message": "Which?"
|
|
||||||
},
|
|
||||||
"update": {
|
|
||||||
"message": "update"
|
|
||||||
},
|
|
||||||
"UpdatingDDD": {
|
|
||||||
"message": "Updating..."
|
|
||||||
},
|
|
||||||
"UpdatedD": {
|
|
||||||
"message": "Updated."
|
|
||||||
},
|
|
||||||
"DisablingDDD": {
|
|
||||||
"message": "Disabling..."
|
|
||||||
},
|
|
||||||
"DisabledD": {
|
|
||||||
"message": "Disabled."
|
|
||||||
},
|
|
||||||
"InstallingDDD": {
|
|
||||||
"message": "Installing..."
|
|
||||||
},
|
|
||||||
"PacScriptWasInstalledD": {
|
|
||||||
"message": "PAC-script was installed."
|
|
||||||
},
|
|
||||||
"Version": {
|
|
||||||
"message": "Version"
|
|
||||||
},
|
|
||||||
"FullVersion": {
|
|
||||||
"message": "Full version"
|
|
||||||
},
|
|
||||||
"VersionForSlowMachines": {
|
|
||||||
"message": "Version for slow machines"
|
|
||||||
},
|
|
||||||
"FailedToDownloadPacScriptFromAddresses": {
|
|
||||||
"message": "Failed to download PAC-script from addresses"
|
|
||||||
},
|
|
||||||
"ChoosePacProviderFirstD": {
|
|
||||||
"message": "Choose PAC-provider first."
|
|
||||||
},
|
|
||||||
"ProblemsQ": {
|
|
||||||
"message": "Problems?"
|
|
||||||
},
|
|
||||||
"Finish": {
|
|
||||||
"message": "OK"
|
|
||||||
},
|
|
||||||
"Disable": {
|
|
||||||
"message": "Disable"
|
|
||||||
},
|
|
||||||
"Only_own_sites_and_only_own_proxies": {
|
|
||||||
"message": "Only own sites and only own proxies"
|
|
||||||
},
|
|
||||||
"Antizapret": {
|
|
||||||
"message": "Antizapret"
|
|
||||||
},
|
|
||||||
"Anticensority": {
|
|
||||||
"message": "Anticensority"
|
|
||||||
},
|
|
||||||
"PAC_script": {
|
|
||||||
"message": "PAC-script"
|
|
||||||
},
|
|
||||||
"Exceptions": {
|
|
||||||
"message": "Exceptions"
|
|
||||||
},
|
|
||||||
"Own_proxies": {
|
|
||||||
"message": "Own proxies"
|
|
||||||
},
|
|
||||||
"Modifiers": {
|
|
||||||
"message": "Modifiers"
|
|
||||||
},
|
|
||||||
"Notifications": {
|
|
||||||
"message": "Notifications"
|
|
||||||
},
|
|
||||||
"Error": {
|
|
||||||
"message": "Error"
|
|
||||||
},
|
|
||||||
"Non_critical_error": {
|
|
||||||
"message": "Non-critical error. Don't worry: it works"
|
|
||||||
},
|
|
||||||
"Donate": {
|
|
||||||
"message": "Donate"
|
|
||||||
},
|
|
||||||
"Updated": {
|
|
||||||
"message": "Updated"
|
|
||||||
},
|
|
||||||
"ago": {
|
|
||||||
"message": "ago"
|
|
||||||
},
|
|
||||||
"never": {
|
|
||||||
"message": "never"
|
|
||||||
},
|
|
||||||
"ms": {
|
|
||||||
"message": "ms"
|
|
||||||
},
|
|
||||||
"s": {
|
|
||||||
"message": "s"
|
|
||||||
},
|
|
||||||
"min": {
|
|
||||||
"message": "min"
|
|
||||||
},
|
|
||||||
"h": {
|
|
||||||
"message": "h"
|
|
||||||
},
|
|
||||||
"d": {
|
|
||||||
"message": "d"
|
|
||||||
},
|
|
||||||
"w": {
|
|
||||||
"message": "w"
|
|
||||||
},
|
|
||||||
"m": {
|
|
||||||
"message": "m"
|
|
||||||
},
|
|
||||||
"ProxyTheDomainNameBelowQ": {
|
|
||||||
"message": "Proxy the domain name below?"
|
|
||||||
},
|
|
||||||
"auto": {
|
|
||||||
"message": "auto"
|
|
||||||
},
|
|
||||||
"yes": {
|
|
||||||
"message": "yes"
|
|
||||||
},
|
|
||||||
"no": {
|
|
||||||
"message": "no"
|
|
||||||
},
|
|
||||||
"noOwnProxiesError": {
|
|
||||||
"message": "Proxying of OWN sites is possible only via OWN proxies. No own proxies found that satisfy your requirements."
|
|
||||||
},
|
|
||||||
"AllowExtensionToRunInPrivateWindows": {
|
|
||||||
"message": "For the extension to work it is required to allow it to run in private windows, see <a href='https://github.com/anticensority/runet-censorship-bypass/wiki/Как-разрешить-запуск-расширения-в-приватных-окнах-|-How-to-allow-extension-to-run-in-private-windows'>a HOWTO</a>."
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,143 +0,0 @@
|
||||||
{
|
|
||||||
"extName": {
|
|
||||||
"message": "Обход блокировок Рунета${nameSuffixRu}"
|
|
||||||
},
|
|
||||||
"extDesc": {
|
|
||||||
"message": "Обход интернет-цензуры в России: https://git.io/ac-wiki"
|
|
||||||
},
|
|
||||||
"proxy": {
|
|
||||||
"message": "прокси"
|
|
||||||
},
|
|
||||||
"noControl": {
|
|
||||||
"message": "Другое расширение контролирует настройки прокси!"
|
|
||||||
},
|
|
||||||
"WhichQ": {
|
|
||||||
"message": "Какое?"
|
|
||||||
},
|
|
||||||
"update": {
|
|
||||||
"message": "обновить"
|
|
||||||
},
|
|
||||||
"UpdatingDDD": {
|
|
||||||
"message": "Обновляем..."
|
|
||||||
},
|
|
||||||
"UpdatedD": {
|
|
||||||
"message": "Обновлено."
|
|
||||||
},
|
|
||||||
"DisablingDDD": {
|
|
||||||
"message": "Отключение..."
|
|
||||||
},
|
|
||||||
"DisabledD": {
|
|
||||||
"message": "Отключено."
|
|
||||||
},
|
|
||||||
"InstallingDDD": {
|
|
||||||
"message": "Установка..."
|
|
||||||
},
|
|
||||||
"PacScriptWasInstalledD": {
|
|
||||||
"message": "PAC-скрипт установлен."
|
|
||||||
},
|
|
||||||
"Version": {
|
|
||||||
"message": "Версия"
|
|
||||||
},
|
|
||||||
"FullVersion": {
|
|
||||||
"message": "Полная версия"
|
|
||||||
},
|
|
||||||
"VersionForSlowMachines": {
|
|
||||||
"message": "Версия для слабых машин"
|
|
||||||
},
|
|
||||||
"FailedToDownloadPacScriptFromAddresses": {
|
|
||||||
"message": "Не удалось скачать PAC-скрипт с адресов"
|
|
||||||
},
|
|
||||||
"ChoosePacProviderFirstD": {
|
|
||||||
"message": "Сперва выберите PAC-провайдера."
|
|
||||||
},
|
|
||||||
"ProblemsQ": {
|
|
||||||
"message": "Проблемы?"
|
|
||||||
},
|
|
||||||
"Finish": {
|
|
||||||
"message": "Готово"
|
|
||||||
},
|
|
||||||
"Disable": {
|
|
||||||
"message": "Отключить"
|
|
||||||
},
|
|
||||||
"Only_own_sites_and_only_own_proxies": {
|
|
||||||
"message": "Только свои сайты и свои прокси"
|
|
||||||
},
|
|
||||||
"Antizapret": {
|
|
||||||
"message": "Антизапрет"
|
|
||||||
},
|
|
||||||
"Anticensority": {
|
|
||||||
"message": "Антицензорити"
|
|
||||||
},
|
|
||||||
"PAC_script": {
|
|
||||||
"message": "PAC-скрипт"
|
|
||||||
},
|
|
||||||
"Exceptions": {
|
|
||||||
"message": "Исключения"
|
|
||||||
},
|
|
||||||
"Own_proxies": {
|
|
||||||
"message": "Свои прокси"
|
|
||||||
},
|
|
||||||
"Modifiers": {
|
|
||||||
"message": "Модификаторы"
|
|
||||||
},
|
|
||||||
"Notifications": {
|
|
||||||
"message": "Уведомления"
|
|
||||||
},
|
|
||||||
"Error": {
|
|
||||||
"message": "Ошибка"
|
|
||||||
},
|
|
||||||
"Non_critical_error": {
|
|
||||||
"message": "Некритичная ошибка. Всё хорошо, продолжаем работу"
|
|
||||||
},
|
|
||||||
"Donate": {
|
|
||||||
"message": "Поддержать"
|
|
||||||
},
|
|
||||||
"Updated": {
|
|
||||||
"message": "Обновлялись"
|
|
||||||
},
|
|
||||||
"ago": {
|
|
||||||
"message": "назад"
|
|
||||||
},
|
|
||||||
"never": {
|
|
||||||
"message": "никогда"
|
|
||||||
},
|
|
||||||
"ms": {
|
|
||||||
"message": "мс"
|
|
||||||
},
|
|
||||||
"s": {
|
|
||||||
"message": "с"
|
|
||||||
},
|
|
||||||
"min": {
|
|
||||||
"message": "мин"
|
|
||||||
},
|
|
||||||
"h": {
|
|
||||||
"message": "ч"
|
|
||||||
},
|
|
||||||
"d": {
|
|
||||||
"message": "дн"
|
|
||||||
},
|
|
||||||
"w": {
|
|
||||||
"message": " недель"
|
|
||||||
},
|
|
||||||
"m": {
|
|
||||||
"message": " месяцев"
|
|
||||||
},
|
|
||||||
"ProxyTheDomainNameBelowQ": {
|
|
||||||
"message": "Проксировать указанное доменное имя?"
|
|
||||||
},
|
|
||||||
"auto": {
|
|
||||||
"message": "авто"
|
|
||||||
},
|
|
||||||
"yes": {
|
|
||||||
"message": "да"
|
|
||||||
},
|
|
||||||
"no": {
|
|
||||||
"message": "нет"
|
|
||||||
},
|
|
||||||
"noOwnProxiesError": {
|
|
||||||
"message": "Проксировать СВОИ сайты можно только при наличии СВОИХ прокси. Нет своих прокси, удовлетворяющих вашим требованиям."
|
|
||||||
},
|
|
||||||
"AllowExtensionToRunInPrivateWindows": {
|
|
||||||
"message": "Для работы расширения необходимо разрешить запуск в приватных окнах, см. <a href='https://github.com/anticensority/runet-censorship-bypass/wiki/Как-разрешить-запуск-расширения-в-приватных-окнах-|-How-to-allow-extension-to-run-in-private-windows'>инструкции</a>."
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
{
|
|
||||||
"manifest_version": 2,
|
|
||||||
|
|
||||||
"name": "__MSG_extName__",
|
|
||||||
"default_locale": "ru",
|
|
||||||
"description": "__MSG_extDesc__",
|
|
||||||
"version": "0.0.${version}",
|
|
||||||
"icons": {
|
|
||||||
"128": "icons/default-128.png"
|
|
||||||
},
|
|
||||||
"author": "anticensority+owners@googlegroups.com",
|
|
||||||
|
|
||||||
"permissions": [
|
|
||||||
"proxy"
|
|
||||||
, "alarms"
|
|
||||||
, "storage"
|
|
||||||
, "unlimitedStorage"
|
|
||||||
, "<all_urls>"
|
|
||||||
, "tabs"
|
|
||||||
, "contextMenus"
|
|
||||||
, "notifications"
|
|
||||||
${extra_permissions}
|
|
||||||
],
|
|
||||||
"minimum_chrome_version": "55.0.0.0",
|
|
||||||
"browser_specific_settings": {
|
|
||||||
"gecko": {
|
|
||||||
"strict_min_version": "91.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"background": {
|
|
||||||
${persistent}
|
|
||||||
"scripts": [
|
|
||||||
"00-init-apis.js"
|
|
||||||
${scripts_0x}
|
|
||||||
, "11-error-handlers-api.js"
|
|
||||||
, "12-errors-lib.js"
|
|
||||||
, "13-http-lib.js"
|
|
||||||
, "15-firefox-proxy-settings.js"
|
|
||||||
${scripts_2x}
|
|
||||||
, "35-pac-kitchen-api.js"
|
|
||||||
, "37-sync-pac-script-with-pac-provider-api.js"
|
|
||||||
${scripts_8x}
|
|
||||||
, "70-menu-items.js"
|
|
||||||
, "75-context-menus.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
"browser_action": {
|
|
||||||
"default_title": "Этот сайт благословлён | Версия ${version + versionSuffix}",
|
|
||||||
"default_popup": "/pages/options/index.html"
|
|
||||||
},
|
|
||||||
"options_ui": {
|
|
||||||
"page": "/pages/options/index.html",
|
|
||||||
"chrome_style": false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="icon" type="image/x-icon" href="./ribbon-128.ico">
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Согласие на сбор данных / Data Collection Consent</title>
|
|
||||||
<style>
|
|
||||||
p, ol { font-size: 16px }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div style="margin: 0 auto; max-width: 35rem; text-align: justify; font-family: sans-serif">
|
|
||||||
<img src="./ribbon-128.png">
|
|
||||||
<h1>Согласие на сбор данных / Data Collection Consent</h1>
|
|
||||||
<p>
|
|
||||||
Этот документ написан на двух языках. Английский предпочтительней.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
This document is written in two languages. English is preferable.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Привет! На связи команда разработчиков браузерного расширения «Обход блокировок Рунета».
|
|
||||||
Вы либо только что установили расширение, либо только что обновились до новой версии.
|
|
||||||
По новым правилам мы <a href="https://github.com/anticensority/runet-censorship-bypass/wiki/Зачем-нам-согласие-со-сбором-данных%3F">должны</a>
|
|
||||||
получить от вас согласие на сбор ваших данных и передачу их
|
|
||||||
третьим лицам.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Hi! It's "Runet Censorship Bypass" browser extension developers team.
|
|
||||||
You have just installed the extension or just updated to a new version.
|
|
||||||
According to the new requirements we <a href="https://github.com/anticensority/runet-censorship-bypass/wiki/Зачем-нам-согласие-со-сбором-данных%3F">have to</a> get your consent before collecting your data and
|
|
||||||
sharing it with 3rd parties.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Сообщаем вам, что после первого запуска расширения и при дальнейшем его использовании
|
|
||||||
настроенные в нём PAC-скрипты и прокси-сервера могут собирать или уже* собирают
|
|
||||||
некоторые ваши данные. Какие именно, зависит от выбранного вами поставщика этих ресурсов,
|
|
||||||
так что рекомендуем ознакомиться с их соответствующей политикой конфиденциальности (Privacy
|
|
||||||
Policy).
|
|
||||||
<br>
|
|
||||||
* Если вы установили и пользовалсись расширением уже некоторое время. Этого экрана согласия не
|
|
||||||
было в старых версиях.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
We inform you that starting from the first launch of the extension and on further usage chosen
|
|
||||||
PAC-scripts and proxy-servers may collect or are already* collecting some of your data. Which
|
|
||||||
exactly depends on the chosen provider of these resources so we recommend you to get
|
|
||||||
acquainted with their corresponding Privacy Policy.
|
|
||||||
<br>
|
|
||||||
* If you have installed and have been using the extension for some time already. This consent
|
|
||||||
screen wasn't shown in the old versions.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
При первом запуске и по умолчанию будут использоваться PAC-скрипт и встроенные в него
|
|
||||||
прокси-сервера, предоставляемые <a href="https://antizapret.prostovpn.org:8443">проектом
|
|
||||||
"АнтиЗапрет"</a>, — политику конфиденциальности этого решения см. в
|
|
||||||
<a href="https://antizapret.prostovpn.org:8443/faq.html">FAQ</a> под заголовком "Какие данные
|
|
||||||
собирает сервис и каким образом они используются?".
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
On the first launch and by default the PAC-script and its built-in proxy-servers provided by
|
|
||||||
<a href="https://antizapret.prostovpn.org:8443">project "AntiZapret" (RU)</a> will be used, —
|
|
||||||
see its Privacy Policy in
|
|
||||||
<a href="https://antizapret.prostovpn.org:8443/faq.html">the FAQ (RU)</a> under the title
|
|
||||||
"Какие данные собирает сервис и каким образом они используются?". Its translation to EN is
|
|
||||||
present in the Privacy Policy of the extension.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Политику конфиденциальности самого расширения см.
|
|
||||||
<a
|
|
||||||
href="https://github.com/anticensority/runet-censorship-bypass/wiki/Privacy-Policy-|-Политика-конфиденциальности"
|
|
||||||
>здесь (EN)</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
See the Privacy Policy of this extension
|
|
||||||
<a
|
|
||||||
href="https://github.com/anticensority/runet-censorship-bypass/wiki/Privacy-Policy-|-Политика-конфиденциальности"
|
|
||||||
>here</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
В расширении представлены кнопки / There are these buttons in the extension:
|
|
||||||
</p>
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
"Через Google Translate" / "Via Google Translate"
|
|
||||||
</li><li>
|
|
||||||
"Из кэша Google" / "From Google Cache"
|
|
||||||
</li><li>
|
|
||||||
"Из архива archive.org" / "From archive.org archive"
|
|
||||||
</li><li>
|
|
||||||
"Разблокировать по-другому" / "Unblock another way"
|
|
||||||
</li><li>
|
|
||||||
"Сайт в реестре блокировок?" / "Is site in the registry of blockings?"
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<p>
|
|
||||||
Все эти кнопки передают URL-адрес текущей вкладки в соответствующие службы. /
|
|
||||||
All these buttons share URL-address of the current tab with corresponding services.
|
|
||||||
</p>
|
|
||||||
<button id="agreeBtn">Разрешаю собирать заявленные данные / Allow claimed data collection
|
|
||||||
</button>
|
|
||||||
<button id="rejectBtn">Нет, удалите расширение / No, delete this extension
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
<script src="../lib/keep-links-clickable.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,14 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
chrome.runtime.getBackgroundPage( (backgroundPage) =>
|
|
||||||
backgroundPage.apis.errorHandlers.installListenersOn(
|
|
||||||
window, 'CONSENT', () => {
|
|
||||||
agreeBtn.onclick = () => {
|
|
||||||
backgroundPage.apis.consent.give();
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
rejectBtn.onclick = () =>
|
|
||||||
chrome.management.uninstallSelf();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 10 KiB |
|
@ -1,40 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>PAC Sandbox</title>
|
|
||||||
<style type="text/css" media="screen">
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
#editor {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
nav {
|
|
||||||
padding: 0.2em;
|
|
||||||
}
|
|
||||||
#status {
|
|
||||||
padding-left: 0.5em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav>
|
|
||||||
<button id="read-button">READ</button>
|
|
||||||
<button id="save-button">SAVE</button>
|
|
||||||
<button id="clear-button">CLEAR</button>
|
|
||||||
<span id="status">Press READ button to read PAC from settings</span>
|
|
||||||
</nav>
|
|
||||||
<div id="editor"></div>
|
|
||||||
<script src="./vendor/ace-editor/ace.js" type="text/javascript" charset="utf-8"></script>
|
|
||||||
<script src="./index.js" charset="utf-8"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,62 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
chrome.runtime.getBackgroundPage((bgWin) => {
|
|
||||||
|
|
||||||
const setStatusTo = (msg) => document.getElementById('status').innerHTML = msg;
|
|
||||||
|
|
||||||
const red = (text) => '<span style="color: red">' + text + '</span>';
|
|
||||||
|
|
||||||
const editor = window.ace.edit('editor');
|
|
||||||
editor.getSession().setOptions({
|
|
||||||
mode: 'ace/mode/javascript',
|
|
||||||
useSoftTabs: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
bgWin.chrome.proxy.settings.onChange.addListener(
|
|
||||||
(details) => setStatusTo(red( details.levelOfControl + '!') )
|
|
||||||
);
|
|
||||||
|
|
||||||
function _read() {
|
|
||||||
|
|
||||||
bgWin.chrome.proxy.settings.get({}, (details) => {
|
|
||||||
|
|
||||||
let control = details.levelOfControl;
|
|
||||||
if (control.startsWith('controlled_by_other')) {
|
|
||||||
control = red(control);
|
|
||||||
}
|
|
||||||
setStatusTo(control);
|
|
||||||
const pac = details.value.pacScript;
|
|
||||||
const data = pac && pac.data || 'PAC скрипт не установлен.';
|
|
||||||
editor.setValue( data );
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelector('#read-button').onclick = _read;
|
|
||||||
|
|
||||||
document.querySelector('#save-button').onclick = () => {
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
mode: 'pac_script',
|
|
||||||
pacScript: {
|
|
||||||
mandatory: false,
|
|
||||||
data: editor.getValue(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
bgWin.chrome.proxy.settings.set( {value: config}, () => alert('Saved!') );
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
document.querySelector('#clear-button').onclick = () => {
|
|
||||||
|
|
||||||
bgWin.chrome.proxy.settings.clear({}, () => {
|
|
||||||
|
|
||||||
alert('Cleared! Reading...');
|
|
||||||
_read();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
})
|
|
|
@ -1,50 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html style="display: none; will-change: contents, display">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Импорт/экспорт исключений</title>
|
|
||||||
<style>
|
|
||||||
@font-face {
|
|
||||||
font-family: "emoji";
|
|
||||||
src:url("../lib/fonts/emoji.woff") format("woff");
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
.emoji {
|
|
||||||
font-family: "emoji";
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
textarea {
|
|
||||||
border-width: 1px 0 0 0;
|
|
||||||
padding: 1em;
|
|
||||||
height: 100%;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
nav {
|
|
||||||
padding: 0.2em;
|
|
||||||
}
|
|
||||||
#status {
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav>
|
|
||||||
<button id="load">Загрузить <span class="emoji">⬇</span></button>
|
|
||||||
<button id="save">Сохранить <span class="emoji">⬆</span></button>
|
|
||||||
<span id="status"></span>
|
|
||||||
</nav>
|
|
||||||
<textarea id="editor"></textarea>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,83 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
chrome.runtime.getBackgroundPage( (backgroundPage) =>
|
|
||||||
backgroundPage.apis.errorHandlers.installListenersOn(
|
|
||||||
window, 'EXC', () => {
|
|
||||||
|
|
||||||
const editor = document.getElementById('editor');
|
|
||||||
const loadBtn = document.getElementById('load');
|
|
||||||
const saveBtn = document.getElementById('save');
|
|
||||||
const status = document.getElementById('status');
|
|
||||||
|
|
||||||
loadBtn.onclick = function() {
|
|
||||||
|
|
||||||
const mods = backgroundPage.apis.pacKitchen.getPacMods();
|
|
||||||
editor.value = `# Комментарии начинаются с # и действуют до конца строки.
|
|
||||||
# Комментарии НЕ сохраняются!
|
|
||||||
# Сначала идёт список проксируемых сайтов,
|
|
||||||
# затем ==== на отдельной строке,
|
|
||||||
# затем исключённые сайты.
|
|
||||||
# После ещё одной строки с ==== идёт белый список.
|
|
||||||
# Сортировка — с конца строки.
|
|
||||||
# Адреса со звёздочками поддерживаются: *.kasparov.ru, например.
|
|
||||||
|
|
||||||
# ПРОКСИРОВАТЬ:
|
|
||||||
|
|
||||||
${(mods.included || []).join('\n')}
|
|
||||||
|
|
||||||
===============================
|
|
||||||
# НЕ ПРОКСИРОВАТЬ:
|
|
||||||
|
|
||||||
${(mods.excluded || []).join('\n')}
|
|
||||||
|
|
||||||
|
|
||||||
===============================
|
|
||||||
# БЕЛЫЙ СПИСОК
|
|
||||||
# Разрешить расширению работать только с этими адресами:
|
|
||||||
|
|
||||||
${(mods.whitelist || []).join('\n')}
|
|
||||||
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
status.innerText = 'Успешно загружено!';
|
|
||||||
|
|
||||||
};
|
|
||||||
loadBtn.click();
|
|
||||||
|
|
||||||
saveBtn.onclick = function() {
|
|
||||||
|
|
||||||
let [proxyList, dontProxyList, whitelist] = editor.value
|
|
||||||
.trim()
|
|
||||||
.replace(/#.*/g, '')
|
|
||||||
.split(/=+/g)
|
|
||||||
.map( (listStr) => listStr
|
|
||||||
.trim()
|
|
||||||
.split(/(?:\s*\r?\n\s*)+/g)
|
|
||||||
.filter((host) => host)
|
|
||||||
)
|
|
||||||
dontProxyList = dontProxyList || [];
|
|
||||||
whitelist = whitelist || [];
|
|
||||||
|
|
||||||
const exceptions = {};
|
|
||||||
proxyList.forEach((host) => (exceptions[host] = true));
|
|
||||||
dontProxyList.forEach((host) => (exceptions[host] = false));
|
|
||||||
const mods = backgroundPage.apis.pacKitchen.getPacMods();
|
|
||||||
mods.exceptions = exceptions;
|
|
||||||
mods.whitelist = whitelist;
|
|
||||||
backgroundPage.apis.pacKitchen.keepCookedNowAsync(mods, (err) => {
|
|
||||||
if (!err) {
|
|
||||||
status.innerText = 'Успешно сохранено!';
|
|
||||||
loadBtn.click();
|
|
||||||
} else {
|
|
||||||
status.innerText = '<em>ОШИБКА:</em>' + err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.oninput = () => (status.innerText = 'Вы держитесь там!');
|
|
||||||
|
|
||||||
document.documentElement.style.display = 'initial';
|
|
||||||
|
|
||||||
})
|
|
||||||
);
|
|
Before Width: | Height: | Size: 161 B |
|
@ -1,346 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2014 The Chromium Authors. All rights reserved.
|
|
||||||
* Use of this source code is governed by a BSD-style license that can be
|
|
||||||
* found in the LICENSE file.
|
|
||||||
*
|
|
||||||
* This stylesheet is used to apply Chrome styles to extension pages that opt in
|
|
||||||
* to using them.
|
|
||||||
*
|
|
||||||
* These styles have been copied from ui/webui/resources/css/chrome_shared.css
|
|
||||||
* and ui/webui/resources/css/widgets.css *with CSS class logic removed*, so
|
|
||||||
* that it's as close to a user-agent stylesheet as possible.
|
|
||||||
*
|
|
||||||
* For example, extensions shouldn't be able to set a .link-button class and
|
|
||||||
* have it do anything.
|
|
||||||
*
|
|
||||||
* Other than that, keep this file and chrome_shared.css/widgets.cc in sync as
|
|
||||||
* much as possible.
|
|
||||||
*/
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: #333;
|
|
||||||
cursor: default;
|
|
||||||
/* Note that the correct font-family and font-size are set in
|
|
||||||
* extension_fonts.css. */
|
|
||||||
/* This top margin of 14px matches the top padding on the h1 element on
|
|
||||||
* overlays (see the ".overlay .page h1" selector in overlay.css), which
|
|
||||||
* every dialogue has.
|
|
||||||
*
|
|
||||||
* Similarly, the bottom 14px margin matches the bottom padding of the area
|
|
||||||
* which hosts the buttons (see the ".overlay .page * .action-area" selector
|
|
||||||
* in overlay.css).
|
|
||||||
*
|
|
||||||
* Both have a padding left/right of 17px.
|
|
||||||
*
|
|
||||||
* Note that we're putting this here in the Extension content, rather than
|
|
||||||
* the WebUI element which contains the content, so that scrollbars in the
|
|
||||||
* Extension content don't get a 6px margin, which looks quite odd.
|
|
||||||
*/
|
|
||||||
margin: 14px 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
line-height: 1.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3 {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
font-weight: normal;
|
|
||||||
/* Makes the vertical size of the text the same for all fonts. */
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.3em;
|
|
||||||
margin-bottom: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
color: black;
|
|
||||||
font-size: 1.2em;
|
|
||||||
margin-bottom: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: rgb(17, 85, 204);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:active {
|
|
||||||
color: rgb(5, 37, 119);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Default state **************************************************************/
|
|
||||||
|
|
||||||
:-webkit-any(button,
|
|
||||||
input[type='button'],
|
|
||||||
input[type='submit']),
|
|
||||||
select,
|
|
||||||
input[type='checkbox'],
|
|
||||||
input[type='radio'] {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
background-image: linear-gradient(#ededed, #ededed 38%, #dedede);
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.25);
|
|
||||||
border-radius: 2px;
|
|
||||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08),
|
|
||||||
inset 0 1px 2px rgba(255, 255, 255, 0.75);
|
|
||||||
color: #444;
|
|
||||||
font: inherit;
|
|
||||||
margin: 0 1px 0 0;
|
|
||||||
outline: none;
|
|
||||||
text-shadow: 0 1px 0 rgb(240, 240, 240);
|
|
||||||
}
|
|
||||||
|
|
||||||
:-webkit-any(button,
|
|
||||||
input[type='button'],
|
|
||||||
input[type='submit']),
|
|
||||||
select {
|
|
||||||
min-height: 2em;
|
|
||||||
min-width: 4em;
|
|
||||||
/*<if expr="is_win">
|
|
||||||
/* The following platform-specific rule is necessary to get adjacent
|
|
||||||
* buttons, text inputs, and so forth to align on their borders while also
|
|
||||||
* aligning on the text's baselines. */
|
|
||||||
padding-bottom: 1px;
|
|
||||||
/*</if>*/
|
|
||||||
}
|
|
||||||
|
|
||||||
:-webkit-any(button,
|
|
||||||
input[type='button'],
|
|
||||||
input[type='submit']) {
|
|
||||||
-webkit-padding-end: 10px;
|
|
||||||
-webkit-padding-start: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-webkit-padding-end: 20px;
|
|
||||||
-webkit-padding-start: 6px;
|
|
||||||
/* OVERRIDE */
|
|
||||||
background-image: /*url(../../../ui/webui/resources/images/select.png),*/
|
|
||||||
linear-gradient(#ededed, #ededed 38%, #dedede);
|
|
||||||
background-position: right center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[dir='rtl'] select {
|
|
||||||
background-position: center left;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='checkbox'] {
|
|
||||||
height: 13px;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: middle;
|
|
||||||
width: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='radio'] {
|
|
||||||
/* OVERRIDE */
|
|
||||||
border-radius: 100%;
|
|
||||||
height: 15px;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: middle;
|
|
||||||
width: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TODO(estade): add more types here? */
|
|
||||||
input[type='number'],
|
|
||||||
input[type='password'],
|
|
||||||
input[type='search'],
|
|
||||||
input[type='text'],
|
|
||||||
input[type='url'],
|
|
||||||
input:not([type]),
|
|
||||||
textarea {
|
|
||||||
border: 1px solid #bfbfbf;
|
|
||||||
border-radius: 2px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: #444;
|
|
||||||
font: inherit;
|
|
||||||
margin: 0;
|
|
||||||
/* Use min-height to accommodate addditional padding for touch as needed. */
|
|
||||||
min-height: 2em;
|
|
||||||
padding: 3px;
|
|
||||||
outline: none;
|
|
||||||
/*<if expr="is_win or is_macosx or is_ios">*
|
|
||||||
/* For better alignment between adjacent buttons and inputs. */
|
|
||||||
padding-bottom: 4px;
|
|
||||||
/*</if>*/
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='search'] {
|
|
||||||
-webkit-appearance: textfield;
|
|
||||||
/* NOTE: Keep a relatively high min-width for this so we don't obscure the end
|
|
||||||
* of the default text in relatively spacious languages (i.e. German). */
|
|
||||||
min-width: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Checked ********************************************************************/
|
|
||||||
|
|
||||||
input[type='checkbox']:checked::before {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
background-image: url(./check.png);
|
|
||||||
background-size: 100% 100%;
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='radio']:checked::before {
|
|
||||||
background-color: #666;
|
|
||||||
border-radius: 100%;
|
|
||||||
bottom: 3px;
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
left: 3px;
|
|
||||||
position: absolute;
|
|
||||||
right: 3px;
|
|
||||||
top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover **********************************************************************/
|
|
||||||
|
|
||||||
:enabled:hover:-webkit-any(
|
|
||||||
select,
|
|
||||||
input[type='checkbox'],
|
|
||||||
input[type='radio'],
|
|
||||||
:-webkit-any(
|
|
||||||
button,
|
|
||||||
input[type='button'],
|
|
||||||
input[type='submit'])) {
|
|
||||||
background-image: linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
|
|
||||||
border-color: rgba(0, 0, 0, 0.3);
|
|
||||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12),
|
|
||||||
inset 0 1px 2px rgba(255, 255, 255, 0.95);
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled:hover:-webkit-any(select) {
|
|
||||||
/* OVERRIDE */
|
|
||||||
background-image: /*url(../../../ui/webui/resources/images/select.png)*/,
|
|
||||||
linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Active *********************************************************************/
|
|
||||||
|
|
||||||
:enabled:active:-webkit-any(
|
|
||||||
select,
|
|
||||||
input[type='checkbox'],
|
|
||||||
input[type='radio'],
|
|
||||||
:-webkit-any(
|
|
||||||
button,
|
|
||||||
input[type='button'],
|
|
||||||
input[type='submit'])) {
|
|
||||||
background-image: linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
|
|
||||||
box-shadow: none;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled:active:-webkit-any(select) {
|
|
||||||
/* OVERRIDE */
|
|
||||||
background-image: /*url(../../../ui/webui/resources/images/select.png),*/
|
|
||||||
linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Disabled *******************************************************************/
|
|
||||||
|
|
||||||
:disabled:-webkit-any(
|
|
||||||
button,
|
|
||||||
input[type='button'],
|
|
||||||
input[type='submit']),
|
|
||||||
select:disabled {
|
|
||||||
background-image: linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6);
|
|
||||||
border-color: rgba(80, 80, 80, 0.2);
|
|
||||||
box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08),
|
|
||||||
inset 0 1px 2px rgba(255, 255, 255, 0.75);
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
select:disabled {
|
|
||||||
/* OVERRIDE */
|
|
||||||
background-image: /*url(../../../ui/webui/resources/images/disabled_select.png),*/
|
|
||||||
linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6);
|
|
||||||
}
|
|
||||||
|
|
||||||
input:disabled:-webkit-any([type='checkbox'],
|
|
||||||
[type='radio']) {
|
|
||||||
opacity: .75;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:disabled:-webkit-any([type='password'],
|
|
||||||
[type='search'],
|
|
||||||
[type='text'],
|
|
||||||
[type='url'],
|
|
||||||
:not([type])) {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus **********************************************************************/
|
|
||||||
|
|
||||||
:enabled:focus:-webkit-any(
|
|
||||||
select,
|
|
||||||
input[type='checkbox'],
|
|
||||||
input[type='number'],
|
|
||||||
input[type='password'],
|
|
||||||
input[type='radio'],
|
|
||||||
input[type='search'],
|
|
||||||
input[type='text'],
|
|
||||||
input[type='url'],
|
|
||||||
input:not([type]),
|
|
||||||
:-webkit-any(
|
|
||||||
button,
|
|
||||||
input[type='button'],
|
|
||||||
input[type='submit'])) {
|
|
||||||
/* OVERRIDE */
|
|
||||||
-webkit-transition: border-color 200ms;
|
|
||||||
/* We use border color because it follows the border radius (unlike outline).
|
|
||||||
* This is particularly noticeable on mac. */
|
|
||||||
border-color: rgb(77, 144, 254);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Checkbox/radio helpers ******************************************************
|
|
||||||
*
|
|
||||||
* .checkbox and .radio classes wrap labels. Checkboxes and radios should use
|
|
||||||
* these classes with the markup structure:
|
|
||||||
*
|
|
||||||
* <div class="checkbox">
|
|
||||||
* <label>
|
|
||||||
* <input type="checkbox"></input>
|
|
||||||
* <span>
|
|
||||||
* </label>
|
|
||||||
* </div>
|
|
||||||
*/
|
|
||||||
|
|
||||||
:-webkit-any(.checkbox, .radio) label {
|
|
||||||
/* Don't expand horizontally: <http://crbug.com/112091>. */
|
|
||||||
align-items: center;
|
|
||||||
display: inline-flex;
|
|
||||||
padding-bottom: 7px;
|
|
||||||
padding-top: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:-webkit-any(.checkbox, .radio) label input {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:-webkit-any(.checkbox, .radio) label input ~ span {
|
|
||||||
-webkit-margin-start: 0.6em;
|
|
||||||
/* Make sure long spans wrap at the same horizontal position they start. */
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
:-webkit-any(.checkbox, .radio) label:hover {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
label > input:disabled:-webkit-any([type='checkbox'], [type='radio']) ~ span {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
'use strict';
|
|
||||||
/*
|
|
||||||
In popup links are not clickable at all, fix it.
|
|
||||||
On other pages "chrome://" links are not clickable, fix it.
|
|
||||||
Use only if really required because of performance penalty.
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
|
|
||||||
const target = document.documentElement;
|
|
||||||
|
|
||||||
const updateLinks = () => {
|
|
||||||
|
|
||||||
const links = document.querySelectorAll('a:not([href=""])');
|
|
||||||
for (let i = 0; i < links.length; i++) {
|
|
||||||
const ln = links[i];
|
|
||||||
const location = ln.href;
|
|
||||||
ln.onclick = function() {
|
|
||||||
|
|
||||||
chrome.tabs.create({active: this.dataset.inBg === "false", url: location});
|
|
||||||
return false;
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
new MutationObserver( updateLinks )
|
|
||||||
.observe(target, {
|
|
||||||
attributes: true,
|
|
||||||
subtree: true,
|
|
||||||
childList: true,
|
|
||||||
characterData: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', updateLinks);
|
|
||||||
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
ChromeStyle: https://cs.chromium.org/chromium/src/extensions/renderer/resources/extension.css
|
|
|
@ -1,8 +0,0 @@
|
||||||
[ignore]
|
|
||||||
.*/test/.*
|
|
||||||
|
|
||||||
[include]
|
|
||||||
|
|
||||||
[libs]
|
|
||||||
|
|
||||||
[options]
|
|
|
@ -1,4 +0,0 @@
|
||||||
node_modules
|
|
||||||
npm-debug.log
|
|
||||||
*.swp
|
|
||||||
dist
|
|
|
@ -1,11 +0,0 @@
|
||||||
# React + Flow + Webpack 2 Boilerplate with Babel
|
|
||||||
|
|
||||||
Switch branches for adding/removing Babel for JSX + Flowtype.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
`yarn install` or `npm install --dev`
|
|
||||||
|
|
||||||
## Run
|
|
||||||
|
|
||||||
`yarn/npm run build`
|
|
|
@ -1,39 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html style="visibility: hidden; will-change: contents, visibility">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Настройки</title>
|
|
||||||
<link rel="stylesheet" href="../lib/chrome-style/index.css">
|
|
||||||
<style><!-- Don't delete this mount point! --></style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app-root"></div>
|
|
||||||
<svg style="display: none" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<symbol id="iconInfo" viewBox="0 0 100 100">
|
|
||||||
<title>info</title>
|
|
||||||
<circle shape-rendering="geometricPrecision" fill="none" stroke="currentColor" stroke-width="7" cx="50" cy="50" r="45"/>
|
|
||||||
<path shape-rendering="crispEdges" d="M 55,40 V 80 H 45 V 40 z m 0,-20 V 35 H 45 V 20 Z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="iconLoopRound" viewBox="0 0 32 32">
|
|
||||||
<title>loop-round</title>
|
|
||||||
<path d="M27.802 5.197c-2.925-3.194-7.13-5.197-11.803-5.197-8.837 0-16 7.163-16 16h3c0-7.18 5.82-13 13-13 3.844 0 7.298 1.669 9.678 4.322l-4.678 4.678h11v-11l-4.198 4.197z"/>
|
|
||||||
<path d="M29 16c0 7.18-5.82 13-13 13-3.844 0-7.298-1.669-9.678-4.322l4.678-4.678h-11v11l4.197-4.197c2.925 3.194 7.13 5.197 11.803 5.197 8.837 0 16-7.163 16-16h-3z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="iconImportExport" viewBox="0 0 32 32">
|
|
||||||
<title>import-export</title>
|
|
||||||
<g transform="rotate(0 16 16)">
|
|
||||||
<path d="M7 22 h 25 v 4 h -25 v 5 l -7-7 7-7 v5 z"/>
|
|
||||||
<path d="M25 10 h-25 v-4 h 25 v -5 l 7 7 -7 7 z"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- With bars on peaks.
|
|
||||||
<path d="M30 0h2v16h-2v-16z"></path>
|
|
||||||
<path d="M0 16h2v16h-2v-16z"></path>
|
|
||||||
<path d="M10 22 h 22 v 4 h -22 v 5 l -7-7 7-7 v5 z"></path>
|
|
||||||
<path d="M22 10 h-22 v-4 h 22 v -5 l 7 7 -7 7 z"></path-->
|
|
||||||
</symbol>
|
|
||||||
</svg>
|
|
||||||
<script src="./dist/bundle.min.js"></script>
|
|
||||||
<script src="../lib/keep-links-clickable.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,24 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const loaderUtils = require('loader-utils');
|
|
||||||
const concat = require('concat-stream');
|
|
||||||
|
|
||||||
module.exports = function(content) {
|
|
||||||
|
|
||||||
const cb = this.async();
|
|
||||||
const Readable = require('stream').Readable;
|
|
||||||
const src = new Readable();
|
|
||||||
src._read = function noop() {};
|
|
||||||
src.push(content);
|
|
||||||
src.push(null);
|
|
||||||
|
|
||||||
const opts = loaderUtils.getOptions(this) || {};
|
|
||||||
const readme = Object.keys(opts).reduce((readable, moduleName) => {
|
|
||||||
|
|
||||||
const newStream = require(moduleName)(/* No filename. */);
|
|
||||||
return readable.pipe(newStream);
|
|
||||||
|
|
||||||
}, src);
|
|
||||||
readme.pipe(concat ((buf) => cb(null, buf.toString()) ));
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,34 +0,0 @@
|
||||||
{
|
|
||||||
"name": "options-page-builder",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.js",
|
|
||||||
"license": "GPLv3",
|
|
||||||
"devDependencies": {
|
|
||||||
"babel-cli": "^6.26.0",
|
|
||||||
"babel-loader": "^7.1.5",
|
|
||||||
"babel-plugin-dynamic-import-webpack": "^1.1.0",
|
|
||||||
"babel-preset-flow": "^6.23.0",
|
|
||||||
"babel-preset-react": "^6.24.1",
|
|
||||||
"concat-stream": "^1.6.2",
|
|
||||||
"csjs-inject": "^1.0.1",
|
|
||||||
"flow-bin": "^0.45.0",
|
|
||||||
"gulp": "^4.0.2",
|
|
||||||
"inferno": "~3.2.0",
|
|
||||||
"inferno-component": "^3.10.1",
|
|
||||||
"inferno-create-element": "^3.10.1",
|
|
||||||
"webpack": "^5.82.0",
|
|
||||||
"webpack-cli": "^5.0.2"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"check": "flow status",
|
|
||||||
"build:prod": "webpack --node-env=\"'production'\" --env=prod",
|
|
||||||
"build:dev:nocomp": "NODE_ENV=development webpack --define process.env.NODE_ENV=\"'development'\" --env=dev",
|
|
||||||
"build:dev": "NODE_ENV=development webpack --debug --define process.env.NODE_ENV=\"'development'\" --output-pathinfo --env=dev",
|
|
||||||
"gulp": "cd .. && npm run gulp",
|
|
||||||
"build": "npm run build:prod",
|
|
||||||
"start": "cd .. && npm start"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"babel-plugin-inferno": "^3.5.1"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,309 +0,0 @@
|
||||||
import Inferno from 'inferno';
|
|
||||||
import Component from 'inferno-component';
|
|
||||||
import createElement from 'inferno-create-element';
|
|
||||||
|
|
||||||
import getNotControlledWarning from './NotControlledWarning';
|
|
||||||
import getMain from './Main';
|
|
||||||
import getFooter from './Footer';
|
|
||||||
|
|
||||||
export default function getApp(theState) {
|
|
||||||
|
|
||||||
const NotControlledWarning = getNotControlledWarning(theState);
|
|
||||||
const Main = getMain(theState);
|
|
||||||
const Footer = getFooter(theState);
|
|
||||||
|
|
||||||
return class App extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const sanitizedUrl = theState.flags.ifOpenedUnsafely ? { hash: '', search: '' } : window.location;
|
|
||||||
const hashParams = new URLSearchParams(sanitizedUrl.hash.substr(1));
|
|
||||||
const searchParams = new URLSearchParams(sanitizedUrl.search.substr(1));
|
|
||||||
this.state = {
|
|
||||||
status: 'Загрузка...',
|
|
||||||
ifInputsDisabled: false,
|
|
||||||
hashParams,
|
|
||||||
searchParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setStatusTo = this.setStatusTo.bind(this);
|
|
||||||
this.conduct = this.conduct.bind(this);
|
|
||||||
this.showErrors = this.showErrors.bind(this);
|
|
||||||
this.showNews = this.showNews.bind(this);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatusTo(msg, cb) {
|
|
||||||
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
status: msg || 'Хорошего настроения Вам!',
|
|
||||||
},
|
|
||||||
cb
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
setNewsStatusTo(newsArr) {
|
|
||||||
|
|
||||||
this.setStatusTo(
|
|
||||||
<ol style="list-style-type: initial;">
|
|
||||||
{newsArr
|
|
||||||
.map(([title, url]) => (<li><a href={url}>{title}</a></li>))
|
|
||||||
.reverse() // News order.
|
|
||||||
}
|
|
||||||
</ol>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async showNews() {
|
|
||||||
|
|
||||||
const uiComDate = 'ui-last-comment-date';
|
|
||||||
const uiComEtag = 'ui-last-comments-etag';
|
|
||||||
const uiLastNewsArr = 'ui-last-news-arr';
|
|
||||||
|
|
||||||
const statusFromUrl = this.state.searchParams.get('status');
|
|
||||||
if (statusFromUrl) {
|
|
||||||
return this.setStatusTo(statusFromUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
const comDate = localStorage[uiComDate];
|
|
||||||
const query = comDate ? `?since=${comDate}` : '';
|
|
||||||
const oldEtag = localStorage[uiComEtag];
|
|
||||||
const headers = {
|
|
||||||
'User-Agent': 'https://github.com/anticensority/runet-censorship-bypass',
|
|
||||||
};
|
|
||||||
if (oldEtag) {
|
|
||||||
Object.assign(headers, {
|
|
||||||
'If-None-Match': oldEtag,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const params = {
|
|
||||||
headers: new Headers(headers),
|
|
||||||
};
|
|
||||||
|
|
||||||
// I comment and uncomment this variable manually before release or build:
|
|
||||||
const ghUrl = `https://api.github.com/repos/anticensority/chromium-extension/issues/10/comments${query}`;
|
|
||||||
// const ghUrl = `https://api.github.com/repos/anticensority/for-testing/issues/1/comments${query}`;
|
|
||||||
|
|
||||||
const [error, comments, etag] = await fetch(
|
|
||||||
ghUrl,
|
|
||||||
params
|
|
||||||
).then(
|
|
||||||
(res) => !( res.status >= 200 && res.status < 300 || res.status === 304 )
|
|
||||||
? Promise.reject({message: `Получен ответ с неудачным кодом ${res.status}.`, data: res})
|
|
||||||
: res
|
|
||||||
).then(
|
|
||||||
(res) => Promise.all([
|
|
||||||
null,
|
|
||||||
res.status !== 304 ? res.json() : false,
|
|
||||||
res.headers.get('ETag')
|
|
||||||
]),
|
|
||||||
(err) => {
|
|
||||||
|
|
||||||
const statusCode = err.data && err.data.status;
|
|
||||||
const ifCritical = null;
|
|
||||||
this.showErrors(ifCritical, {
|
|
||||||
message: statusCode === 403
|
|
||||||
? 'Слишком много запросов :-( Сообщите разработчику, как вам удалось всё истратить.'
|
|
||||||
: 'Не удалось достать новости: что-то не так с сетью.',
|
|
||||||
wrapped: err,
|
|
||||||
});
|
|
||||||
return [err, false, false];
|
|
||||||
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (etag) {
|
|
||||||
localStorage[uiComEtag] = etag;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return; // Let the user see the error message and contemplate.
|
|
||||||
}
|
|
||||||
|
|
||||||
const ifNewsWasSet = (() => {
|
|
||||||
|
|
||||||
if (comments === false) {
|
|
||||||
// 304
|
|
||||||
const json = localStorage[uiLastNewsArr];
|
|
||||||
const news = json && JSON.parse(json);
|
|
||||||
if (news && news.length) {
|
|
||||||
this.setNewsStatusTo(news);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let minDate; // Minimal date among all news-comments.
|
|
||||||
const news = comments.reduce((acc, comment) => {
|
|
||||||
|
|
||||||
const curDate = comment.updated_at || comment.created_at;
|
|
||||||
const newsTitle = this.getNewsHeadline( comment.body );
|
|
||||||
if (newsTitle) {
|
|
||||||
if (!minDate || curDate <= minDate) {
|
|
||||||
minDate = curDate;
|
|
||||||
}
|
|
||||||
acc.push([newsTitle, comment.html_url]);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
|
|
||||||
}, []);
|
|
||||||
// Response with empty news is cached too.
|
|
||||||
localStorage[uiLastNewsArr] = JSON.stringify(news);
|
|
||||||
if (news.length) {
|
|
||||||
if (minDate) {
|
|
||||||
localStorage[uiComDate] = minDate;
|
|
||||||
}
|
|
||||||
this.setNewsStatusTo(news);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
|
|
||||||
})();
|
|
||||||
if (!ifNewsWasSet) {
|
|
||||||
this.setStatusTo();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
|
|
||||||
if (!this.props.apis.antiCensorRu.ifFirstInstall) {
|
|
||||||
this.showNews();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getNewsHeadline(comBody) {
|
|
||||||
|
|
||||||
const headline = comBody.split(/\r?\n/)[0];
|
|
||||||
const ifOver = /#+\s*$/.test(headline);
|
|
||||||
if (ifOver) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return headline.replace(/^\s*#+\s*/g, '');
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
showErrors(err, ...args/* ...warns, cb */) {
|
|
||||||
|
|
||||||
const lastArg = args[args.length - 1];
|
|
||||||
const cb = (lastArg && typeof lastArg === 'function')
|
|
||||||
? args.pop()
|
|
||||||
: () => {};
|
|
||||||
const warns = args;
|
|
||||||
|
|
||||||
const errToHtmlMessage = (error) => {
|
|
||||||
|
|
||||||
let messageHtml = '';
|
|
||||||
let wrapped = error.wrapped;
|
|
||||||
messageHtml = error.message || '';
|
|
||||||
|
|
||||||
while( wrapped ) {
|
|
||||||
const deeperMsg = wrapped && wrapped.message;
|
|
||||||
if (deeperMsg) {
|
|
||||||
messageHtml = messageHtml + ' > ' + deeperMsg;
|
|
||||||
}
|
|
||||||
wrapped = wrapped.wrapped;
|
|
||||||
}
|
|
||||||
return messageHtml;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
let messageHtml = err ? errToHtmlMessage(err) : '';
|
|
||||||
|
|
||||||
const warningHtml = warns
|
|
||||||
.filter((w) => w)
|
|
||||||
.map(
|
|
||||||
(w) => errToHtmlMessage(w)
|
|
||||||
)
|
|
||||||
.map( (m) => '✘ ' + m )
|
|
||||||
.join('<br/>');
|
|
||||||
|
|
||||||
messageHtml = messageHtml.trim();
|
|
||||||
if (warningHtml) {
|
|
||||||
messageHtml = messageHtml ? messageHtml + '<br/>' + warningHtml : warningHtml;
|
|
||||||
}
|
|
||||||
this.setStatusTo(
|
|
||||||
(<span>
|
|
||||||
<span style="color:red">
|
|
||||||
{err
|
|
||||||
? <span><span class="emoji">🔥</span> {chrome.i18n.getMessage('Error')}!</span>
|
|
||||||
: `${chrome.i18n.getMessage('Non_critical_error')}.`
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
<br/>
|
|
||||||
<span style="font-size: 0.9em; color: darkred" dangerouslySetInnerHTML={{__html: messageHtml}}></span>
|
|
||||||
{' '}
|
|
||||||
{err && <a href="" onClick={(evt) => {
|
|
||||||
|
|
||||||
this.props.apis.errorHandlers.viewError('pup-ext-err', err);
|
|
||||||
evt.preventDefault();
|
|
||||||
|
|
||||||
}}>[Техн.детали]</a>}
|
|
||||||
</span>),
|
|
||||||
cb
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
switchInputs(val) {
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
ifInputsDisabled: val === 'off' ? true : false,
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
conduct(
|
|
||||||
beforeStatus, operation, afterStatus,
|
|
||||||
onSuccess = () => {}, onError = () => {}
|
|
||||||
) {
|
|
||||||
|
|
||||||
this.setStatusTo(beforeStatus);
|
|
||||||
this.switchInputs('off');
|
|
||||||
operation((err, res, ...warns) => {
|
|
||||||
warns = warns.filter( (w) => w );
|
|
||||||
if (err || warns.length) {
|
|
||||||
this.showErrors(err, ...warns);
|
|
||||||
} else {
|
|
||||||
this.setStatusTo(afterStatus);
|
|
||||||
}
|
|
||||||
this.switchInputs('on');
|
|
||||||
if (!err) {
|
|
||||||
onSuccess(res);
|
|
||||||
} else {
|
|
||||||
onError(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render(originalProps) {
|
|
||||||
|
|
||||||
const props = Object.assign({}, originalProps, {
|
|
||||||
funs: {
|
|
||||||
setStatusTo: this.setStatusTo,
|
|
||||||
conduct: this.conduct,
|
|
||||||
showErrors: this.showErrors,
|
|
||||||
showNews: this.showNews,
|
|
||||||
},
|
|
||||||
ifInputsDisabled: this.state.ifInputsDisabled,
|
|
||||||
hashParams: this.state.hashParams,
|
|
||||||
});
|
|
||||||
|
|
||||||
return createElement('div', null, [
|
|
||||||
...( props.flags.ifNotControlled ? [createElement(NotControlledWarning, props)] : [] ),
|
|
||||||
createElement(Main, props),
|
|
||||||
createElement(Footer, Object.assign({ status: this.state.status }, props)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,37 +0,0 @@
|
||||||
import Inferno, { linkEvent } from 'inferno';
|
|
||||||
|
|
||||||
export default function getApplyMods(theState) {
|
|
||||||
|
|
||||||
const resetMods = function resetMods(props) {
|
|
||||||
|
|
||||||
const ifSure = props.bgWindow.confirm('Сбросить все модификаторы и ИСКЛЮЧЕНИЯ?');
|
|
||||||
if (!ifSure) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
props.funs.conduct(
|
|
||||||
'Сбрасываем...',
|
|
||||||
(cb) => {
|
|
||||||
|
|
||||||
props.apis.pacKitchen.resetToDefaults();
|
|
||||||
props.bgWindow.utils.fireRequest('ip-to-host-reset-to-defaults', cb);
|
|
||||||
window.localStorage.clear();
|
|
||||||
|
|
||||||
},
|
|
||||||
'Откройте окно заново для отображения эффекта.',
|
|
||||||
() => window.close()
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return function ApplyMods(props) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section class="controlRow horFlex" style="margin-top: 1em">
|
|
||||||
<input type="button" value="Применить" disabled={props.ifInputsDisabled} onClick={props.onClick}/>
|
|
||||||
<a href="" onClick={linkEvent(props, resetMods)}>К изначальным!</a>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,359 +0,0 @@
|
||||||
import Inferno from 'inferno';
|
|
||||||
import Component from 'inferno-component';
|
|
||||||
import css from 'csjs-inject';
|
|
||||||
|
|
||||||
export default function getExcEditor(theState) {
|
|
||||||
|
|
||||||
const scopedCss = css`
|
|
||||||
|
|
||||||
#exc-address-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#exc-address-container > a {
|
|
||||||
border-bottom: 1px solid transparent;
|
|
||||||
margin-left: 0.2em;
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
#exc-address {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
--exc-hieght: 1.6em;
|
|
||||||
font-size: 1em;
|
|
||||||
border-bottom: 1px solid var(--ribbon-color) !important;
|
|
||||||
}
|
|
||||||
input#exc-editor {
|
|
||||||
border: none;
|
|
||||||
width: 100%;
|
|
||||||
background: inherit;
|
|
||||||
/* The two below align '.' (dot) vertically. */
|
|
||||||
max-height: var(--exc-hieght) !important;
|
|
||||||
min-height: var(--exc-hieght) !important;
|
|
||||||
}
|
|
||||||
#exc-radio {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
[name="if-proxy-this-site"]:checked + label {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
#exc-address.ifYes {
|
|
||||||
background-color: lightgreen;
|
|
||||||
}
|
|
||||||
#exc-address.ifNo {
|
|
||||||
background-color: pink;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const labelIfProxied = '✔';
|
|
||||||
const labelIfNotProxied = '✘';
|
|
||||||
const labelIfAuto = '↻';
|
|
||||||
|
|
||||||
/* Not used.
|
|
||||||
const sortOptions = (options) => {
|
|
||||||
|
|
||||||
const aWins = 1;
|
|
||||||
return options.sort(([aHost, aState], [bHost, bState]) => aState === undefined ? aWins : aHost.localeCompare(bHost))
|
|
||||||
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
return class ExcEditor extends Component {
|
|
||||||
|
|
||||||
modsToOpts(pacMods) {
|
|
||||||
|
|
||||||
return Object.keys(pacMods.exceptions || {}).sort().map(
|
|
||||||
(excHost) => [excHost, pacMods.exceptions[excHost]]
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
|
|
||||||
super(props);
|
|
||||||
const trimmedInputValueOrSpace =
|
|
||||||
props.currentTab &&
|
|
||||||
props.currentTab.url &&
|
|
||||||
!props.currentTab.url.startsWith('chrome')
|
|
||||||
? '*.' + (new URL(props.currentTab.url).hostname.replace(/^www\./g, ''))
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const pacMods = props.apis.pacKitchen.getPacMods();
|
|
||||||
this.state = {
|
|
||||||
trimmedInputValueOrSpace,
|
|
||||||
sortedListOfOptions: this.modsToOpts(pacMods),
|
|
||||||
hostToIfHidden: {},
|
|
||||||
};
|
|
||||||
this.handleRadioClick = this.handleRadioClick.bind(this);
|
|
||||||
this.handleInputOrClick = this.handleInputOrClick.bind(this);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
hideAllOptions() {
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
hostToIfHidden: this.state.sortedListOfOptions.reduce(
|
|
||||||
(hostToIfHidden, [excHost]) => {
|
|
||||||
|
|
||||||
hostToIfHidden[excHost] = true;
|
|
||||||
return hostToIfHidden;
|
|
||||||
|
|
||||||
},
|
|
||||||
{}),
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
isHostValid(host) {
|
|
||||||
|
|
||||||
const ValidHostnameRegex = /^(?:\*\.)?(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
|
|
||||||
if(!ValidHostnameRegex.test(host)) {
|
|
||||||
this.props.funs.showErrors(new TypeError('Должно быть только доменное имя, без протокола, порта и пути. Попробуйте ещё раз.'));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRadioClick(event) {
|
|
||||||
|
|
||||||
const host = this.state.trimmedInputValueOrSpace;
|
|
||||||
(() => { // `return` === `preventDefault`.
|
|
||||||
|
|
||||||
if(!this.isHostValid(host)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pacMods = this.props.apis.pacKitchen.getPacMods();
|
|
||||||
pacMods.exceptions = pacMods.exceptions || {};
|
|
||||||
|
|
||||||
let ifYesClicked = false;
|
|
||||||
switch(event.target.id) {
|
|
||||||
case 'this-auto':
|
|
||||||
delete pacMods.exceptions[host];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'this-yes':
|
|
||||||
ifYesClicked = true;
|
|
||||||
case 'this-no':
|
|
||||||
if (ifYesClicked && !pacMods.filteredCustomsString) {
|
|
||||||
this.props.funs.showErrors( new TypeError(
|
|
||||||
chrome.i18n.getMessage('noOwnProxiesError'),
|
|
||||||
));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pacMods.exceptions[host] = ifYesClicked;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error('Only yes/no/auto!');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.funs.conduct(
|
|
||||||
'Применяем исключения...',
|
|
||||||
(cb) => this.props.apis.pacKitchen.keepCookedNowAsync(pacMods, cb),
|
|
||||||
'Исключения применены. Не забывайте о кэше!',
|
|
||||||
() => this.setState({sortedListOfOptions: this.modsToOpts(pacMods)})
|
|
||||||
);
|
|
||||||
|
|
||||||
})();
|
|
||||||
// Don't check before operation is finished.
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown(event) {
|
|
||||||
|
|
||||||
if(event.key === 'Enter') {
|
|
||||||
this.hideAllOptions();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInputOrClick(event) {
|
|
||||||
|
|
||||||
// Episode 1.
|
|
||||||
|
|
||||||
const ifClick = event && event.type === 'click';
|
|
||||||
|
|
||||||
// If triangle button on right of datalist input clicked.
|
|
||||||
let ifTriangleClicked = false;
|
|
||||||
{
|
|
||||||
const maxIndentFromRightInPx = 15;
|
|
||||||
ifTriangleClicked = ifClick
|
|
||||||
&& !this.rawInput.selectionStart && !this.rawInput.selectionEnd
|
|
||||||
&& event.x > this.rawInput.getBoundingClientRect().right - maxIndentFromRightInPx;
|
|
||||||
}
|
|
||||||
|
|
||||||
const setInputValue = (newValue) => {
|
|
||||||
|
|
||||||
if (ifClick && !ifTriangleClicked) {
|
|
||||||
// Don't jerk cursor on simple clicks.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// See bug in my comment to http://stackoverflow.com/a/32394157/521957
|
|
||||||
// First click on empty input may be still ignored.
|
|
||||||
const newPos = this.rawInput.selectionStart + newValue.length - this.rawInput.value.length;
|
|
||||||
this.rawInput.value = newValue;
|
|
||||||
window.setTimeout(() => this.rawInput.setSelectionRange(newPos, newPos), 0);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const trimmedInput = event.target.value.trim();
|
|
||||||
const ifInputEmpty = !trimmedInput;
|
|
||||||
const ifInit = !event;
|
|
||||||
const currentHost = ifTriangleClicked ? '' : (trimmedInput || (ifInit ? '' : ' '));
|
|
||||||
setInputValue(currentHost);
|
|
||||||
this.setState({
|
|
||||||
trimmedInputValueOrSpace: currentHost,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Episode 2.
|
|
||||||
|
|
||||||
let exactHost, exactState; // Undefined.
|
|
||||||
let editedHost = false;
|
|
||||||
const hidden = this.state.sortedListOfOptions.reduce(
|
|
||||||
(hiddenAcc, [excHost, excState]) => {
|
|
||||||
|
|
||||||
if (excState === undefined) {
|
|
||||||
editedHost = excHost;
|
|
||||||
} else if (excHost === trimmedInput) {
|
|
||||||
// Exact match found for input.
|
|
||||||
[exactHost, exactState] = [excHost, excState];
|
|
||||||
}
|
|
||||||
hiddenAcc[excHost] = false;
|
|
||||||
return hiddenAcc;
|
|
||||||
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
let options = this.state.sortedListOfOptions;
|
|
||||||
const removeEditedHost = () => {
|
|
||||||
|
|
||||||
options = options.filter(([excHost, excState]) => editedHost !== excHost);
|
|
||||||
delete hidden[editedHost];
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
(() => {// `return` === setState
|
|
||||||
|
|
||||||
if (ifTriangleClicked || ifInputEmpty) {
|
|
||||||
// Show all opts.
|
|
||||||
if (editedHost) {
|
|
||||||
// Example of editedOpt.value: 'abcde ' <- Mind the space (see unhideOptAndAddSpace)!
|
|
||||||
const ifBackspacedOneChar = ifInputEmpty && editedHost.length < 3;
|
|
||||||
if (ifBackspacedOneChar) {
|
|
||||||
removeEditedHost();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editedHost) {
|
|
||||||
const ifUpdateNeeded = editedHost !== trimmedInput;
|
|
||||||
if(!ifUpdateNeeded) {
|
|
||||||
hidden[editedHost] = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Not exact! Update!
|
|
||||||
removeEditedHost();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!exactHost) {
|
|
||||||
editedHost = trimmedInput;
|
|
||||||
options.unshift([editedHost, undefined]);
|
|
||||||
if (!ifClick) {
|
|
||||||
// New value was typed -- don't show tooltip.
|
|
||||||
hidden[editedHost] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exact found!
|
|
||||||
// Do nothing.
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
hostToIfHidden: hidden,
|
|
||||||
sortedListOfOptions: options,
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render(props) {
|
|
||||||
|
|
||||||
const inputProxyingState = this.state.sortedListOfOptions.reduce((acc, [excHost, excState]) => {
|
|
||||||
|
|
||||||
if ( acc !== undefined ) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
return this.state.trimmedInputValueOrSpace === excHost ? excState : undefined;
|
|
||||||
|
|
||||||
}, undefined);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section style="padding-bottom: 1em;">
|
|
||||||
<div>{chrome.i18n.getMessage('ProxyTheDomainNameBelowQ')}</div>
|
|
||||||
<div id="exc-address-container">
|
|
||||||
<div id="exc-address" class={inputProxyingState !== undefined ? ( inputProxyingState === true ? scopedCss.ifYes : scopedCss.ifNo ) : ''}>
|
|
||||||
<input placeholder="*.navalny.com" list="exc-list" id="exc-editor"
|
|
||||||
value={this.state.trimmedInputValueOrSpace}
|
|
||||||
ref={(inputNode) => { this.rawInput = inputNode; }}
|
|
||||||
onKeyDown={this.handleKeyDown.bind(this)}
|
|
||||||
onInput={this.handleInputOrClick}
|
|
||||||
onClick={this.handleInputOrClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/*<a href class="emoji">⇄</a>*/}
|
|
||||||
<a href="../exceptions/index.html" title="импорт/экспорт"><svg
|
|
||||||
class="icon"
|
|
||||||
><use xlink:href="#iconImportExport"></use></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<datalist id="exc-list">
|
|
||||||
{
|
|
||||||
this.state.sortedListOfOptions.map(([excHost, excState]) => {
|
|
||||||
|
|
||||||
// 1. Option's value may be changed to hide it from the tooltip.
|
|
||||||
// 2. Space is used in matching so even an empty input (replaced with space) has tooltip with prompts.
|
|
||||||
const ifProxy = excState;
|
|
||||||
return <option
|
|
||||||
value={ this.state.hostToIfHidden[excHost] ? '\n' : excHost + ' ' }
|
|
||||||
label={ ifProxy === true ? labelIfProxied : (ifProxy === false ? labelIfNotProxied : labelIfAuto) }/>
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</datalist>
|
|
||||||
<ol class="horizontalList middledChildren" id="exc-radio">
|
|
||||||
<li><input id="this-auto" type="radio" checked name="if-proxy-this-site" onClick={this.handleRadioClick}/>{' '}
|
|
||||||
<label for="this-auto">{/*<span class="emoji">🔄(looks fat)</span>*/}<svg
|
|
||||||
class="icon"
|
|
||||||
style="position: relative; top: 0.15em;"><use xlink:href="#iconLoopRound"></use></svg> {chrome.i18n.getMessage('auto')}</label>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<input id="this-yes" type="radio" name="if-proxy-this-site" checked={inputProxyingState === true} onClick={this.handleRadioClick}/>
|
|
||||||
{' '}<label for="this-yes">
|
|
||||||
<span
|
|
||||||
class="emoji____buggy"
|
|
||||||
>✔</span> {chrome.i18n.getMessage('yes')}
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<input id="this-no" type="radio" name="if-proxy-this-site" checked={inputProxyingState === false} onClick={this.handleRadioClick}/>
|
|
||||||
{' '}<label for="this-no"><span class="emoji">✘</span> {chrome.i18n.getMessage('no')}</label></li>
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,90 +0,0 @@
|
||||||
import Inferno from 'inferno';
|
|
||||||
import createElement from 'inferno-create-element';
|
|
||||||
import css from 'csjs-inject';
|
|
||||||
|
|
||||||
import getInfoLi from './InfoLi';
|
|
||||||
import getExcEditor from './ExcEditor';
|
|
||||||
|
|
||||||
export default function getExceptions(theState) {
|
|
||||||
|
|
||||||
const scopedCss = css`
|
|
||||||
|
|
||||||
.excMods {
|
|
||||||
padding-top: 1em;
|
|
||||||
}
|
|
||||||
.excMods input#mods-ifMindExceptions:not(:checked) + * > label {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InfoLi = getInfoLi(theState);
|
|
||||||
const ExcEditor = getExcEditor(theState);
|
|
||||||
|
|
||||||
return function Exceptions(props) {
|
|
||||||
|
|
||||||
const applyMods = (newMods) => {
|
|
||||||
|
|
||||||
props.apis.pacKitchen.keepCookedNowAsync(newMods, (err, ...warns) =>
|
|
||||||
err
|
|
||||||
? props.funs.showErrors(err, ...warns)
|
|
||||||
: props.funs.setStatusTo('Применено.')
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
return props.flags.ifInsideOptionsPage
|
|
||||||
? (
|
|
||||||
<div class="nowrap">
|
|
||||||
Редактор исключений доступен только для <a href="chrome://newtab">вкладок</a>.
|
|
||||||
</div>)
|
|
||||||
:
|
|
||||||
(<div>
|
|
||||||
{createElement(ExcEditor, props)}
|
|
||||||
<ul class={scopedCss.excMods}>
|
|
||||||
{
|
|
||||||
props.apis.pacKitchen.getOrderedConfigs('exceptions').map((conf) => {
|
|
||||||
|
|
||||||
return <InfoLi
|
|
||||||
type="checkbox"
|
|
||||||
conf={conf}
|
|
||||||
idPrefix="mods-"
|
|
||||||
checked={conf.value}
|
|
||||||
disabled={props.ifInputsDisabled}
|
|
||||||
onClick={(evt) => {
|
|
||||||
|
|
||||||
const oldMods = props.apis.pacKitchen.getPacMods();
|
|
||||||
oldMods[conf.key] = !conf.value;
|
|
||||||
applyMods(oldMods);
|
|
||||||
|
|
||||||
}}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{
|
|
||||||
!props.flags.ifMini && (
|
|
||||||
<InfoLi
|
|
||||||
type="checkbox"
|
|
||||||
conf={{
|
|
||||||
label: '<span>Собирать <a data-in-bg="false" href="../errors-to-exc/index.html">последние ошибки</a> сайтов</span>',
|
|
||||||
key: 'lookupLastErrors',
|
|
||||||
desc: 'Собирать последние ошибки в запросах, чтобы вручную добавлять избранные из них в исключения.',
|
|
||||||
}}
|
|
||||||
checked={props.bgWindow.apis.lastNetErrors.ifCollecting}
|
|
||||||
onChange={(event) => {
|
|
||||||
|
|
||||||
props.bgWindow.apis.lastNetErrors.ifCollecting = event.target.checked;
|
|
||||||
props.funs.setStatusTo('Сделано.');
|
|
||||||
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,38 +0,0 @@
|
||||||
import Inferno from 'inferno';
|
|
||||||
import css from 'csjs-inject';
|
|
||||||
|
|
||||||
export default function getFooter(theState) {
|
|
||||||
|
|
||||||
const scopedCss = css`
|
|
||||||
|
|
||||||
.statusRow {
|
|
||||||
padding: 0 0.3em 1em;
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.controlRow {
|
|
||||||
margin: 1em 0 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
return (props) => (
|
|
||||||
<div class="horPadded">
|
|
||||||
<section class={scopedCss.statusRow}>
|
|
||||||
<div class={scopedCss.status} style="will-change: contents">
|
|
||||||
{typeof(props.status) === 'string' ? <div dangerouslySetInnerHTML={{ __html: props.status }}></div> : props.status}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<footer class={scopedCss.controlRow + ' horFlex nowrap'}>
|
|
||||||
<input type="button" value={chrome.i18n.getMessage('Finish')} disabled={props.ifInputsDisabled} style={{ display: theState.flags.ifInsideEdgeOptionsPage ? 'none' : 'initial' }} onClick={() => window.close()} />
|
|
||||||
<a href="https://github.com/anticensority/runet-censorship-bypass/wiki/Поддержать">{chrome.i18n.getMessage('Donate')}</a>
|
|
||||||
<a data-in-bg="false" href="../troubleshoot/index.html">
|
|
||||||
{chrome.i18n.getMessage('ProblemsQ')}
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,159 +0,0 @@
|
||||||
import Inferno from 'inferno';
|
|
||||||
import createElement from 'inferno-create-element';
|
|
||||||
import css from 'csjs-inject';
|
|
||||||
|
|
||||||
export default function getInfoLi() {
|
|
||||||
|
|
||||||
const scopedCss = css`
|
|
||||||
/* CONTROL RAW = BUTTON + LINK */
|
|
||||||
|
|
||||||
.labelContainer {
|
|
||||||
flex-grow: 9;
|
|
||||||
padding-left: 0.3em;
|
|
||||||
/* Vertical align to middle. */
|
|
||||||
/*align-self: flex-end;*/
|
|
||||||
line-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* INFO SIGNS */
|
|
||||||
|
|
||||||
input:disabled + .labelContainer label {
|
|
||||||
color: var(--default-grey);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoRow {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.infoRow a {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.infoRow > input[type="checkbox"] {
|
|
||||||
position: relative;
|
|
||||||
top: -0.08em;
|
|
||||||
}
|
|
||||||
.rightBottomIcon {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
.infoUrl, .infoUrl:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Source: https://jsfiddle.net/greypants/zgCb7/ */
|
|
||||||
.desc {
|
|
||||||
text-align: right;
|
|
||||||
color: var(--ribbon-color);
|
|
||||||
cursor: help;
|
|
||||||
padding-left: 0.3em;
|
|
||||||
}
|
|
||||||
.tooltip {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
white-space: initial;
|
|
||||||
word-break: initial;
|
|
||||||
/* top: 100%; Commented to get rid of bug when tooltip is placed below InfoLi children fields. */
|
|
||||||
left: 0;
|
|
||||||
right: 1em;
|
|
||||||
z-index: 1;
|
|
||||||
background-color: var(--ribbon-color);
|
|
||||||
padding: 1em;
|
|
||||||
color: white;
|
|
||||||
text-align: initial;
|
|
||||||
}
|
|
||||||
.desc:hover .tooltip {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.tooltip a,
|
|
||||||
.tooltip em {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.desc .tooltip:after {
|
|
||||||
border-left: solid transparent 0.5em;
|
|
||||||
border-bottom: solid var(--ribbon-color) 0.5em;
|
|
||||||
position: absolute;
|
|
||||||
top: -0.5em;
|
|
||||||
content: "";
|
|
||||||
width: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
/* This bridges the gap so you can mouse into the tooltip without it disappearing. */
|
|
||||||
.desc .tooltip:before {
|
|
||||||
position: absolute;
|
|
||||||
top: -1em;
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
height: 1.2em;
|
|
||||||
left: 75%;
|
|
||||||
width: calc(25% + 0.6em);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CHILDREN */
|
|
||||||
|
|
||||||
.children {
|
|
||||||
flex-grow: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const camelToDash = (name) => name.replace(/([A-Z])/g, (_, p1) => '-' + p1.toLowerCase());
|
|
||||||
// const dashToCamel = (name) => name.replace(/-(.)/g, (_, p1) => p1.toUpperCase());
|
|
||||||
|
|
||||||
const InfoIcon = function InfoIcon(props) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg class="icon" style="position: relative; top: 0.08em">$
|
|
||||||
<use xlink:href="#iconInfo"></use>$
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
return function InfoLi(originalProps) {
|
|
||||||
|
|
||||||
const props = Object.assign({}, {
|
|
||||||
idPrefix: '',
|
|
||||||
ifDashify: false,
|
|
||||||
}, originalProps);
|
|
||||||
|
|
||||||
const iddy = props.idPrefix + ( props.ifDashify ? camelToDash(props.conf.key) : props.conf.key );
|
|
||||||
|
|
||||||
const inputProps = {
|
|
||||||
id: iddy,
|
|
||||||
name: props.name,
|
|
||||||
type: props.type,
|
|
||||||
checked: props.checked,
|
|
||||||
onClick: props.onClick,
|
|
||||||
onChange: props.onChange,
|
|
||||||
class: props.class,
|
|
||||||
disabled: props.ifInputsDisabled,
|
|
||||||
};
|
|
||||||
delete inputProps.children;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li class={scopedCss.infoRow + ' horFlex'} style={ props.children && 'flex-wrap: wrap'}>
|
|
||||||
{createElement('input', inputProps)}
|
|
||||||
<div class={scopedCss.labelContainer}>
|
|
||||||
<label for={iddy} dangerouslySetInnerHTML={{__html: props.conf.label}}></label>
|
|
||||||
{props.nodeAfterLabel}
|
|
||||||
</div>
|
|
||||||
{props.conf.desc
|
|
||||||
? (
|
|
||||||
<div class={scopedCss.desc}>
|
|
||||||
<InfoIcon />
|
|
||||||
<div class={scopedCss.tooltip} dangerouslySetInnerHTML={{__html: props.conf.desc}}/>
|
|
||||||
</div>)
|
|
||||||
: (props.conf.url
|
|
||||||
? (<a href={props.conf.url} class={[scopedCss.rightBottomIcon, scopedCss.infoUrl].join(' ')} title="Открыть документацию"><InfoIcon /></a>)
|
|
||||||
: (<span> </span>) // Affects vertical align of flexbox items.
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{/* props.checked && props.children */}
|
|
||||||
{props.checked && props.children && (<div class={scopedCss.children}>{props.children}</div>)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,64 +0,0 @@
|
||||||
import Inferno from 'inferno';
|
|
||||||
import Component from 'inferno-component';
|
|
||||||
|
|
||||||
export default function getLastUpdateDate(theState) {
|
|
||||||
|
|
||||||
return class LastUpdateDate extends Component {
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
|
|
||||||
this.onStorageChangedHandler = (changes) => {
|
|
||||||
const ac = changes.antiCensorRu;
|
|
||||||
return ac && ac.newValue && ac.newValue.lastPacUpdateStamp && this.forceUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
chrome.storage.onChanged.addListener( this.onStorageChangedHandler );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
|
|
||||||
chrome.storage.onChanged.removeListener( this.onStorageChangedHandler );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getDate(antiCensorRu) {
|
|
||||||
|
|
||||||
let dateForUser = chrome.i18n.getMessage('never');
|
|
||||||
if( antiCensorRu.lastPacUpdateStamp ) {
|
|
||||||
let diff = Date.now() - antiCensorRu.lastPacUpdateStamp;
|
|
||||||
let units = chrome.i18n.getMessage('ms');
|
|
||||||
const gauges = [
|
|
||||||
[1000, chrome.i18n.getMessage('s')],
|
|
||||||
[60, chrome.i18n.getMessage('min')],
|
|
||||||
[60, chrome.i18n.getMessage('h')],
|
|
||||||
[24, chrome.i18n.getMessage('d')],
|
|
||||||
[7, chrome.i18n.getMessage('w')],
|
|
||||||
[4, chrome.i18n.getMessage('m')],
|
|
||||||
];
|
|
||||||
for(const g of gauges) {
|
|
||||||
const diffy = Math.floor(diff / g[0]);
|
|
||||||
if (!diffy)
|
|
||||||
break;
|
|
||||||
diff = diffy;
|
|
||||||
units = g[1];
|
|
||||||
}
|
|
||||||
dateForUser = diff + units + ' ' + chrome.i18n.getMessage('ago');
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
text: `${dateForUser} / ${antiCensorRu.pacUpdatePeriodInMinutes/60}${chrome.i18n.getMessage('h')}`,
|
|
||||||
title: new Date(antiCensorRu.lastPacUpdateStamp).toLocaleString('ru-RU'),
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render(props) {
|
|
||||||
|
|
||||||
const date = this.getDate(props.apis.antiCensorRu);
|
|
||||||
return (<div>{chrome.i18n.getMessage('Updated')}: <span class="updateDate" title={date.title}>{ date.text }</span></div>);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,221 +0,0 @@
|
||||||
import Inferno, {linkEvent} from 'inferno';
|
|
||||||
import Component from 'inferno-component';
|
|
||||||
import createElement from 'inferno-create-element';
|
|
||||||
import css from 'csjs-inject';
|
|
||||||
|
|
||||||
import getTabPanel from './TabPanel';
|
|
||||||
import getPacChooser from './PacChooser';
|
|
||||||
import getExceptions from './Exceptions';
|
|
||||||
import getModList from './ModList';
|
|
||||||
import getProxyEditor from './ProxyEditor';
|
|
||||||
import getApplyMods from './ApplyMods';
|
|
||||||
import getNotifications from './Notifications';
|
|
||||||
|
|
||||||
export default function getMain(theState) {
|
|
||||||
|
|
||||||
const scopedCss = css`
|
|
||||||
|
|
||||||
input#ifProxyHttpsUrlsOnly:checked + div {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabPanel = getTabPanel(theState);
|
|
||||||
|
|
||||||
const PacChooser = getPacChooser(theState);
|
|
||||||
const Exceptions = getExceptions(theState);
|
|
||||||
const ModList = getModList(theState);
|
|
||||||
const ProxyEditor = getProxyEditor(theState);
|
|
||||||
const ApplyMods = getApplyMods(theState);
|
|
||||||
const Notifications = getNotifications(theState);
|
|
||||||
|
|
||||||
const checksName = 'pacMods';
|
|
||||||
let selection = [0, 0]; // TODO: dirty hack but seems ok.
|
|
||||||
|
|
||||||
return class Main extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
ifModsChangesAreStashed: false,
|
|
||||||
ifModsChangesAreValid: true,
|
|
||||||
catToOrderedMods: {
|
|
||||||
'general': props.apis.pacKitchen.getOrderedConfigs('general'),
|
|
||||||
'ownProxies': props.apis.pacKitchen.getOrderedConfigs('ownProxies'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.handleModChange = this.handleModChange.bind(this);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllMods() {
|
|
||||||
|
|
||||||
return [].concat(...Object.keys(this.state.catToOrderedMods).map((cat) =>
|
|
||||||
this.state.catToOrderedMods[cat]
|
|
||||||
))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModApply(that) {
|
|
||||||
|
|
||||||
if (!that.state.ifModsChangesAreValid) {
|
|
||||||
// Error message must be already set by a config validator.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const modsMutated = that.props.apis.pacKitchen.getPacMods();
|
|
||||||
const newMods = that.getAllMods().reduce((_, conf) => {
|
|
||||||
|
|
||||||
modsMutated[conf.key] = conf.value;
|
|
||||||
return modsMutated;
|
|
||||||
|
|
||||||
}, modsMutated/* Needed for index 0*/);
|
|
||||||
that.props.funs.conduct(
|
|
||||||
'Применяем настройки...',
|
|
||||||
(cb) => that.props.apis.pacKitchen.keepCookedNowAsync(newMods, cb),
|
|
||||||
'Настройки применены.',
|
|
||||||
() => that.setState({
|
|
||||||
ifModsChangesAreStashed: false,
|
|
||||||
ifModsChangesAreValid: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModChange({ifValid, targetConf, targetIndex, newValue}) {
|
|
||||||
|
|
||||||
if (ifValid === undefined) {
|
|
||||||
// User input some data, but not validated yet.
|
|
||||||
this.setState({
|
|
||||||
// Make apply button clickable when user only starts writing.
|
|
||||||
ifModsChangesAreStashed: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ifValid === false) {
|
|
||||||
this.setState({
|
|
||||||
ifModsChangesAreValid: false,
|
|
||||||
ifModsChangesAreStashed: true,
|
|
||||||
})
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const oldCats = this.state.catToOrderedMods;
|
|
||||||
const newCats = Object.keys(this.state.catToOrderedMods).reduce((acc, cat) => {
|
|
||||||
|
|
||||||
if (cat !== targetConf.category) {
|
|
||||||
acc[cat] = oldCats[cat];
|
|
||||||
} else {
|
|
||||||
acc[cat] = oldCats[cat].map((conf, index) => {
|
|
||||||
|
|
||||||
if (targetIndex !== index) {
|
|
||||||
return conf;
|
|
||||||
}
|
|
||||||
return Object.assign({}, conf, {
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
catToOrderedMods: newCats,
|
|
||||||
ifModsChangesAreStashed: true,
|
|
||||||
ifModsChangesAreValid: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render(props) {
|
|
||||||
|
|
||||||
const applyModsEl = createElement(ApplyMods, Object.assign({}, props,
|
|
||||||
{
|
|
||||||
ifInputsDisabled: !this.state.ifModsChangesAreStashed || props.ifInputsDisabled,
|
|
||||||
onClick: linkEvent(this, this.handleModApply),
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
const modsHandlers = {
|
|
||||||
onConfChanged: this.handleModChange,
|
|
||||||
};
|
|
||||||
|
|
||||||
return createElement(TabPanel, Object.assign({}, props, {
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
label: chrome.i18n.getMessage('PAC_script'),
|
|
||||||
content: createElement(PacChooser, props),
|
|
||||||
key: 'pacScript',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: chrome.i18n.getMessage('Exceptions'),
|
|
||||||
content: createElement(Exceptions, props),
|
|
||||||
key: 'exceptions',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: chrome.i18n.getMessage('Own_proxies'),
|
|
||||||
content: createElement(
|
|
||||||
ModList,
|
|
||||||
Object.assign({}, props, {
|
|
||||||
orderedConfigs: this.state.catToOrderedMods['ownProxies'],
|
|
||||||
childrenOfMod: {
|
|
||||||
customProxyStringRaw: ProxyEditor,
|
|
||||||
replaceDirectWith: ({ conf, onNewValue, ifInputsDisabled }) =>
|
|
||||||
(<input
|
|
||||||
style="width: 100%; margin: 0.5em 0"
|
|
||||||
disabled={ifInputsDisabled}
|
|
||||||
value={conf.value || ''}
|
|
||||||
onInput={(event) => {
|
|
||||||
|
|
||||||
const t = event.target;
|
|
||||||
selection = [t.selectionStart, t.selectionEnd];
|
|
||||||
onNewValue(true, t.value);
|
|
||||||
}}
|
|
||||||
ref={(input) => {
|
|
||||||
|
|
||||||
if (input) {
|
|
||||||
input.focus();
|
|
||||||
input.selectionStart = selection[0];
|
|
||||||
input.selectionEnd = selection[1];
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>),
|
|
||||||
},
|
|
||||||
name: checksName,
|
|
||||||
}, modsHandlers)
|
|
||||||
),
|
|
||||||
key: 'ownProxies',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: chrome.i18n.getMessage('Modifiers'),
|
|
||||||
content: createElement(
|
|
||||||
ModList,
|
|
||||||
Object.assign({}, props, {
|
|
||||||
orderedConfigs: this.state.catToOrderedMods['general'],
|
|
||||||
name: checksName,
|
|
||||||
}, modsHandlers)
|
|
||||||
),
|
|
||||||
key: 'mods',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
content: applyModsEl,
|
|
||||||
key: 'applyMods',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: chrome.i18n.getMessage('Notifications'),
|
|
||||||
content: createElement(Notifications, props),
|
|
||||||
key: 'notifications',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
alwaysShownWith: {
|
|
||||||
'applyMods': ['ownProxies', 'mods'],
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,89 +0,0 @@
|
||||||
import Inferno, {linkEvent} from 'inferno';
|
|
||||||
import Component from 'inferno-component';
|
|
||||||
import createElement from 'inferno-create-element';
|
|
||||||
import getInfoLi from './InfoLi';
|
|
||||||
|
|
||||||
export default function getModList(theState) {
|
|
||||||
|
|
||||||
const InfoLi = getInfoLi(theState);
|
|
||||||
|
|
||||||
return class ModList extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
|
|
||||||
super(props);
|
|
||||||
this.state= {
|
|
||||||
checks: props.orderedConfigs.map((mod) => Boolean(mod.value)),
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCheck(confMeta, ifChecked) {
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
checks: this.state.checks.map(
|
|
||||||
(ch, i) => i === confMeta.index ? ifChecked : ch
|
|
||||||
)
|
|
||||||
});
|
|
||||||
if (ifChecked === false || !confMeta.ifChild) {
|
|
||||||
this.handleNewValue(true, confMeta, ifChecked);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewValue(ifValid, { conf, index }, newValue) {
|
|
||||||
|
|
||||||
this.props.onConfChanged({
|
|
||||||
targetConf: conf,
|
|
||||||
targetIndex: index,
|
|
||||||
newValue: newValue,
|
|
||||||
ifValid,
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render(props) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ol>
|
|
||||||
{
|
|
||||||
props.orderedConfigs.map((conf, index) => {
|
|
||||||
|
|
||||||
const ifMayHaveChild = props.childrenOfMod && props.childrenOfMod[conf.key];
|
|
||||||
const confMeta = { conf, index, ifChild: ifMayHaveChild };
|
|
||||||
|
|
||||||
const child = ifMayHaveChild && this.state.checks[index]
|
|
||||||
&& createElement(
|
|
||||||
props.childrenOfMod[conf.key],
|
|
||||||
Object.assign(
|
|
||||||
{},
|
|
||||||
props,
|
|
||||||
{
|
|
||||||
conf,
|
|
||||||
onNewValue: (ifValid, newValue) => this.handleNewValue(ifValid, confMeta, newValue),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (<InfoLi
|
|
||||||
conf={conf}
|
|
||||||
type='checkbox'
|
|
||||||
name={props.name}
|
|
||||||
checked={this.state.checks[index]}
|
|
||||||
key={index}
|
|
||||||
onChange={(event) => this.handleCheck(confMeta, event.target.checked)}
|
|
||||||
ifInputsDisabled={props.ifInputsDisabled}
|
|
||||||
>
|
|
||||||
{child}
|
|
||||||
</InfoLi>);
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ol>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,41 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import css from 'csjs-inject';
|
|
||||||
import Inferno from 'inferno';
|
|
||||||
|
|
||||||
export default function getNotControlledWarning({ flags }) {
|
|
||||||
|
|
||||||
const cssClasses = css`
|
|
||||||
|
|
||||||
.warningContainer {
|
|
||||||
background-color: red;
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
${ flags.ifInsideOptionsPage
|
|
||||||
? `
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
` : `
|
|
||||||
padding-top: 1em;
|
|
||||||
padding-bottom: 1em;
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bottom: 1px solid var(--default-grey);
|
|
||||||
}
|
|
||||||
.warningContainer a {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
return function NotControlledWarning(props) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section class={cssClasses.warningContainer + " horPadded"} dangerouslySetInnerHTML={{ __html: props.utils.messages.whichExtensionHtml() }} />
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
import Inferno from 'inferno';
|
|
||||||
import css from 'csjs-inject';
|
|
||||||
|
|
||||||
export default function getPacChooser(theState) {
|
|
||||||
|
|
||||||
const scopedCss = css`
|
|
||||||
|
|
||||||
.listOfNotifiers {
|
|
||||||
margin-left: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
return function Notifications(props) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<header>Я <span class="emoji" style="color: #f93a17">❤</span> yведомления:</header>
|
|
||||||
<ul class={scopedCss.listOfNotifiers + ' middledChildren'}>
|
|
||||||
{
|
|
||||||
Array.from(props.apis.errorHandlers.getEventsMap()).map(([ntfId, ntfName]) => {
|
|
||||||
|
|
||||||
const iddy = `if-on-${ntfId}`;
|
|
||||||
const ifChecked = props.apis.errorHandlers.isOn(ntfId);
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={iddy}
|
|
||||||
checked={ifChecked}
|
|
||||||
disabled={props.ifInputsDisabled}
|
|
||||||
onClick={() => {
|
|
||||||
|
|
||||||
props.apis.errorHandlers.switch(
|
|
||||||
ifChecked ? 'off' : 'on', // Reverse.
|
|
||||||
ntfId
|
|
||||||
);
|
|
||||||
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{' '}
|
|
||||||
<label for={iddy}>{ntfName}</label>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,170 +0,0 @@
|
||||||
import Inferno from 'inferno';
|
|
||||||
import Component from 'inferno-component';
|
|
||||||
import createElement from 'inferno-create-element';
|
|
||||||
import css from 'csjs-inject';
|
|
||||||
|
|
||||||
import getLastUpdateDate from './LastUpdateDate';
|
|
||||||
import getInfoLi from './InfoLi';
|
|
||||||
|
|
||||||
export default function getPacChooser(theState) {
|
|
||||||
|
|
||||||
const scopedCss = css`
|
|
||||||
/* OTHER VERSION */
|
|
||||||
|
|
||||||
.otherVersion {
|
|
||||||
font-size: 1.7em;
|
|
||||||
color: var(--ribbon-color);
|
|
||||||
margin-left: 0.1em;
|
|
||||||
}
|
|
||||||
.otherVersion:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.fullLineHeight,
|
|
||||||
.fullLineHeight * {
|
|
||||||
line-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TAB_1: PAC PROVIDER */
|
|
||||||
|
|
||||||
.updateButton {
|
|
||||||
visibility: hidden;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
input:checked + div .updateButton {
|
|
||||||
visibility: inherit;
|
|
||||||
}
|
|
||||||
label[for="onlyOwnSites"] + .updateButton,
|
|
||||||
label[for="none"] + .updateButton {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#none:checked + div label[for="none"] {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
#updateMessage {
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LastUpdateDate = getLastUpdateDate(theState);
|
|
||||||
const InfoLi = getInfoLi(theState);
|
|
||||||
|
|
||||||
return class PacChooser extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
chosenPacName: 'none',
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updatePac = function updatePac(onSuccess) {
|
|
||||||
props.funs.conduct(
|
|
||||||
chrome.i18n.getMessage('UpdatingDDD'),
|
|
||||||
(cb) => theState.apis.antiCensorRu.syncWithPacProviderAsync(cb),
|
|
||||||
chrome.i18n.getMessage('UpdatedD'),
|
|
||||||
onSuccess
|
|
||||||
);
|
|
||||||
};
|
|
||||||
this.radioClickHandler = this.radioClickHandler.bind(this);
|
|
||||||
this.updateClickHandler = this.updateClickHandler.bind(this);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentProviderId() {
|
|
||||||
|
|
||||||
return theState.apis.antiCensorRu.getCurrentPacProviderKey() || 'none';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
updateClickHandler(event) {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
this.updatePac();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
radioClickHandler(event) {
|
|
||||||
|
|
||||||
const checkChosenProvider = () =>
|
|
||||||
this.setState({ chosenPacName: this.getCurrentProviderId() });
|
|
||||||
|
|
||||||
const pacKey = event.target.id;
|
|
||||||
if (
|
|
||||||
pacKey === (
|
|
||||||
theState.apis.antiCensorRu.getCurrentPacProviderKey() || 'none'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (pacKey === 'none') {
|
|
||||||
this.props.funs.conduct(
|
|
||||||
chrome.i18n.getMessage('DisablingDDD'),
|
|
||||||
(cb) => theState.apis.antiCensorRu.clearPacAsync(cb),
|
|
||||||
chrome.i18n.getMessage('DisabledD'),
|
|
||||||
() => this.setState({ chosenPacName: 'none' }),
|
|
||||||
checkChosenProvider
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.props.funs.conduct(
|
|
||||||
chrome.i18n.getMessage('InstallingDDD'),
|
|
||||||
(cb) => theState.apis.antiCensorRu.installPacAsync(pacKey, cb),
|
|
||||||
chrome.i18n.getMessage('PacScriptWasInstalledD'),
|
|
||||||
checkChosenProvider
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render(props) {
|
|
||||||
|
|
||||||
const iddyToCheck = this.getCurrentProviderId();
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{props.flags.ifInsideOptionsPage && (<header>{chrome.i18n.getMessage('PAC_script')}:</header>)}
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
[...theState.apis.antiCensorRu.getSortedEntriesForProviders(), {key: 'none', label: chrome.i18n.getMessage('Disable')}].map((provConf) =>
|
|
||||||
(<InfoLi
|
|
||||||
onClick={this.radioClickHandler}
|
|
||||||
conf={provConf}
|
|
||||||
type="radio"
|
|
||||||
name="pacProvider"
|
|
||||||
checked={iddyToCheck === provConf.key}
|
|
||||||
ifInputsDisabled={props.ifInputsDisabled}
|
|
||||||
nodeAfterLabel={<a href="" class={scopedCss.updateButton} onClick={this.updateClickHandler}>[{chrome.i18n.getMessage('update')}]</a>}
|
|
||||||
/>)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
<div id="updateMessage" class="horFlex" style="align-items: center">
|
|
||||||
{ createElement(LastUpdateDate, props) }
|
|
||||||
<div class={scopedCss.fullLineHeight}>
|
|
||||||
{
|
|
||||||
props.flags.ifMini
|
|
||||||
? (<a class={scopedCss.otherVersion + ' emoji'} href="https://github.com/anticensority/runet-censorship-bypass/wiki/Различные-версии-расширения"
|
|
||||||
title={chrome.i18n.getMessage("FullVersion")}>🏋</a>)
|
|
||||||
: (<a class={scopedCss.otherVersion + ' emoji'} href="https://github.com/anticensority/runet-censorship-bypass/wiki/Различные-версии-расширения"
|
|
||||||
title={chrome.i18n.getMessage("VersionForSlowMachines")}>🐌</a>)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
|
|
||||||
if (theState.apis.antiCensorRu.ifFirstInstall) {
|
|
||||||
this.updatePac();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,621 +0,0 @@
|
||||||
import Inferno, {linkEvent} from 'inferno';
|
|
||||||
import Component from 'inferno-component';
|
|
||||||
import createElement from 'inferno-create-element';
|
|
||||||
import css from 'csjs-inject';
|
|
||||||
|
|
||||||
export default function getProxyEditor(theState) {
|
|
||||||
|
|
||||||
const scopedCss = css`
|
|
||||||
|
|
||||||
table.editor {
|
|
||||||
border-collapse: collapse;
|
|
||||||
/*border-style: hidden;*/
|
|
||||||
width: 100%;
|
|
||||||
margin: 0.5em 0;
|
|
||||||
background-color: #f3f5f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabledEditor, .exportsEditor {
|
|
||||||
/* Empty, but not undefined. */
|
|
||||||
}
|
|
||||||
|
|
||||||
table.editor ::-webkit-input-placeholder {
|
|
||||||
color: #c9c9c9;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.editor th.shrink,
|
|
||||||
table.editor td.shrink {
|
|
||||||
width: 1%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.editor td, table.editor th {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
text-align: left;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.editor input,
|
|
||||||
table.editor button,
|
|
||||||
table.editor select
|
|
||||||
{
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ADD PANEL */
|
|
||||||
table.editor tr.addPanel td,
|
|
||||||
table.editor tr.addPanel td input
|
|
||||||
{
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
table.editor tr.addPanel td > select[name="newProxyType"],
|
|
||||||
table.editor tr.addPanel td:nth-last-child(2) input /* PORT */
|
|
||||||
{
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
table.editor tr.addPanel td:nth-last-child(2) /* PORT */
|
|
||||||
{
|
|
||||||
min-width: 4em;
|
|
||||||
}
|
|
||||||
/* PROXY ROW */
|
|
||||||
table.editor tr.proxyRow td:nth-child(2), /* type */
|
|
||||||
table.editor tr.proxyRow td:nth-child(4) /* port */
|
|
||||||
{
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
table.editor tr.proxyRow input[name="crededHostname"] {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.editor th:not(:last-child) {
|
|
||||||
padding: 0 0.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.editor input:not([type="submit"]),
|
|
||||||
table.editor select,
|
|
||||||
table.editor select:hover {
|
|
||||||
border: none;
|
|
||||||
background: inherit !important;
|
|
||||||
}
|
|
||||||
table.editor select,
|
|
||||||
table.editor select:hover {
|
|
||||||
-webkit-appearance: menulist !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
table.editor input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* BUTTONS */
|
|
||||||
table.editor input[type="submit"],
|
|
||||||
table.editor button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.only {
|
|
||||||
/*height: 100%;*/
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
table.editor .add {
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
table.editor .export {
|
|
||||||
/*padding-right: 2px;*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LAST COLUMN: BUTTONS */
|
|
||||||
table.editor tr > *:nth-last-child(1), /* Buttons */
|
|
||||||
table.tabledEditor tr > *:nth-last-child(2), /* Port */
|
|
||||||
table.tabledEditor tr.proxyRow > td:first-child /* Type */
|
|
||||||
{
|
|
||||||
text-align: center;
|
|
||||||
padding: 0;
|
|
||||||
position: relative;
|
|
||||||
min-width: 1em;
|
|
||||||
}
|
|
||||||
/* LAST-2 COLUMN: HOSTNAME
|
|
||||||
table.editor td:nth-last-child(3) {
|
|
||||||
padding-left: 2px;
|
|
||||||
}*/
|
|
||||||
.noPad {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.padLeft {
|
|
||||||
padding-left: 2px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea.textarea {
|
|
||||||
width: 100% !important;
|
|
||||||
min-height: 100%;
|
|
||||||
height: 6em;
|
|
||||||
border-width: 1px 0 0 0;
|
|
||||||
/*border: none;*/
|
|
||||||
}
|
|
||||||
|
|
||||||
table.editor input:invalid {
|
|
||||||
color: red !important;
|
|
||||||
border-radius: 0;
|
|
||||||
border-bottom: 1px dotted red !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const UI_RAW = 'ui-proxy-string-raw';
|
|
||||||
const MAX_PORT = 65535;
|
|
||||||
const onlyPort = function onlyPort(event) {
|
|
||||||
|
|
||||||
if (!event.ctrlKey && (/^\D$/.test(event.key) || /^\d$/.test(event.key) && parseInt(`${this.value}${event.key}`) > MAX_PORT)) {
|
|
||||||
event.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Digits, Alt, Tab, Enter, etc.
|
|
||||||
return true;
|
|
||||||
|
|
||||||
};
|
|
||||||
const splitBySemi = (proxyString) => proxyString
|
|
||||||
.replace(/#.*$/mg, '')
|
|
||||||
.trim()
|
|
||||||
.split(/\s*;\r?\n\s*/g)
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter((s) => s);
|
|
||||||
|
|
||||||
const joinBySemi = (strs) => strs.join(';\n');
|
|
||||||
const normalizeProxyString = (str) => joinBySemi(splitBySemi(str));
|
|
||||||
|
|
||||||
const PROXY_TYPE_LABEL_PAIRS = [['PROXY', 'PROXY/HTTP'],['HTTPS'],['SOCKS4'],['SOCKS5'],['SOCKS']];
|
|
||||||
|
|
||||||
|
|
||||||
const SwitchButton = (props) =>
|
|
||||||
(
|
|
||||||
<button
|
|
||||||
type="button" disabled={props.ifInputsDisabled}
|
|
||||||
class={'emoji' + ' ' + scopedCss.export + ' ' + scopedCss.only}
|
|
||||||
title={props.title}
|
|
||||||
onClick={props.onClick}
|
|
||||||
>⇄</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
class TabledEditor extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
selectedNewType: 'HTTPS',
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTypeSelect(that, event) {
|
|
||||||
|
|
||||||
that.setState({
|
|
||||||
selectedNewType: event.target.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
showInvalidMessage(that, event) {
|
|
||||||
|
|
||||||
that.props.funs.showErrors({message: event.target.validationMessage});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModeSwitch(that) {
|
|
||||||
|
|
||||||
that.props.onSwitch();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAdd(that, event) {
|
|
||||||
|
|
||||||
const form = event.target;
|
|
||||||
const elements = Array.from(form.elements).reduce((acc, el, index) => {
|
|
||||||
|
|
||||||
acc[el.name || index] = el.value;
|
|
||||||
el.value = '';
|
|
||||||
return acc;
|
|
||||||
|
|
||||||
}, {});
|
|
||||||
const type = that.state.selectedNewType;
|
|
||||||
const crededHostname = elements.newHostname;
|
|
||||||
const port = elements.newPort;
|
|
||||||
|
|
||||||
const newValue = `${that.props.proxyStringRaw};\n${type} ${crededHostname}:${port}`
|
|
||||||
.trim().replace(/(\s*;\n\s*)+/, ';\n');
|
|
||||||
that.props.setProxyStringRaw(true, newValue);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDelete(that, {proxyAsString, index}) {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
const proxyStrings = splitBySemi(that.props.proxyStringRaw);
|
|
||||||
proxyStrings.splice(index, 1);
|
|
||||||
|
|
||||||
that.props.setProxyStringRaw(true, joinBySemi(proxyStrings) );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
raisePriority(that, {proxyAsString, index}) {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
if (index < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const proxyStrings = splitBySemi(that.props.proxyStringRaw);
|
|
||||||
proxyStrings.splice(index - 1, 2, proxyStrings[index], proxyStrings[index-1]);
|
|
||||||
|
|
||||||
that.props.setProxyStringRaw(true, joinBySemi(proxyStrings) );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit(that, event) {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
that.handleAdd(that, event);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render(props) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={linkEvent(this, this.handleSubmit)}>
|
|
||||||
<table class={scopedCss.editor + ' ' + scopedCss.tabledEditor}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2" class={scopedCss.shrink}>протокол</th>
|
|
||||||
<th>домен / IP</th>
|
|
||||||
<th class={scopedCss.shrink}>порт</th>
|
|
||||||
<th>
|
|
||||||
<SwitchButton title="импорт/экспорт" onClick={linkEvent(this, this.handleModeSwitch)}/>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{/* ADD NEW PROXY STARTS. */}
|
|
||||||
<tr class={scopedCss.addPanel}>
|
|
||||||
<td colspan="2">
|
|
||||||
<select reqiured
|
|
||||||
class={scopedCss.noPad}
|
|
||||||
name="newProxyType"
|
|
||||||
onChange={linkEvent(this, this.handleTypeSelect)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
PROXY_TYPE_LABEL_PAIRS.map(
|
|
||||||
([type, label]) =>
|
|
||||||
(<option value={type} selected={type === this.state.selectedNewType}>
|
|
||||||
{label || type}
|
|
||||||
</option>)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{/* LAST-2: HOSTNAME */}
|
|
||||||
<input required disabled={props.ifInputsDisabled}
|
|
||||||
class={scopedCss.noPad}
|
|
||||||
placeholder="89.140.125.17"
|
|
||||||
name="newHostname"
|
|
||||||
onInvalid={linkEvent(this, this.showInvalidMessage)}
|
|
||||||
tabindex="1"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{/* LAST-1: PORT */}
|
|
||||||
<input required type="number" disabled={props.ifInputsDisabled}
|
|
||||||
class={scopedCss.noPad + ' ' + scopedCss.padLeft + ' ' + scopedCss.only}
|
|
||||||
placeholder="9150"
|
|
||||||
min="0" step="1" max={MAX_PORT} pattern="[0-9]{1,5}"
|
|
||||||
name="newPort"
|
|
||||||
onInvalid={linkEvent(this, this.showInvalidMessage)}
|
|
||||||
onkeydown={onlyPort}
|
|
||||||
tabindex="2"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{/* LAST: ADD BUTTON */}
|
|
||||||
<input type="submit" disabled={props.ifInputsDisabled}
|
|
||||||
class={scopedCss.add + ' ' + scopedCss.only}
|
|
||||||
title="Добавить прокси" value="+"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/* ADD NEW PROXY ENDS. */}
|
|
||||||
{
|
|
||||||
splitBySemi(this.props.proxyStringRaw).map((proxyAsStringRaw, index) => {
|
|
||||||
|
|
||||||
const proxyAsString = proxyAsStringRaw.trim();
|
|
||||||
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
creds,
|
|
||||||
hostname,
|
|
||||||
port,
|
|
||||||
} = theState.utils.parseProxyScheme(proxyAsString);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr class={scopedCss.proxyRow}>
|
|
||||||
<td>
|
|
||||||
<button type="button" disabled={props.ifInputsDisabled}
|
|
||||||
class={scopedCss.only} title="Удалить"
|
|
||||||
onClick={() => this.handleDelete(this, {proxyAsString, index})}
|
|
||||||
>X</button>
|
|
||||||
</td>
|
|
||||||
<td>{type}</td>
|
|
||||||
<td><input value={`${creds && `${creds}@`}${hostname}`} name="crededHostname" readonly/></td>
|
|
||||||
<td>{port}</td>
|
|
||||||
<td>
|
|
||||||
<button type="button" disabled={props.ifInputsDisabled}
|
|
||||||
class={scopedCss.only} title="Повысить приоритет"
|
|
||||||
onClick={() => this.raisePriority(this, {proxyAsString, index})}
|
|
||||||
>▲</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<a href="https://github.com/anticensority/runet-censorship-bypass/wiki/Прокси-и-пароль">Запароленные прокси?</a>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getInitState = () => ({
|
|
||||||
ifHasErrors: false,
|
|
||||||
stashedExports: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
class ExportsEditor extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
|
|
||||||
super(props);
|
|
||||||
this.state = getInitState();
|
|
||||||
this.resetState = linkEvent(this, this.resetState);
|
|
||||||
this.showApply = linkEvent(undefined, props.setProxyStringRaw);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
resetState(that, event) {
|
|
||||||
|
|
||||||
that.props.setProxyStringRaw(true, that.props.proxyStringRaw);
|
|
||||||
if (that.state.ifHasErrors) {
|
|
||||||
that.props.funs.setStatusTo(''); // Clear errors
|
|
||||||
}
|
|
||||||
that.setState(getInitState());
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getErrorsInStashedExports() {
|
|
||||||
|
|
||||||
if(this.state.stashedExports === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const errors = splitBySemi(this.state.stashedExports)
|
|
||||||
.map((proxyAsString) => {
|
|
||||||
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
creds,
|
|
||||||
hostname,
|
|
||||||
port,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
} = theState.utils.parseProxyScheme(proxyAsString);
|
|
||||||
const crededAddr = `${creds ? `${creds}@` : ''}${hostname}:${port}`;
|
|
||||||
|
|
||||||
const knownTypes = PROXY_TYPE_LABEL_PAIRS.map(([type, label]) => type);
|
|
||||||
if( !knownTypes.includes(type.toUpperCase()) ) {
|
|
||||||
return new Error(
|
|
||||||
`Неверный тип ${type}. Известные типы: ${knownTypes.join(', ')}.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!(crededAddr && /^(?:.+@)?[^:]+:\d+$/.test(crededAddr))) {
|
|
||||||
return new Error(
|
|
||||||
`Адрес прокси "${crededAddr || ''}" не соответствует формату "<опц_логин>:<опц_пароль>@<домен_или_IP>:<порт_из_цифр>".`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (password && !username) {
|
|
||||||
return new Error('Вашему пользователю не хватает имени?');
|
|
||||||
}
|
|
||||||
const portInt = parseInt(port);
|
|
||||||
if (portInt < 0 || portInt > 65535) {
|
|
||||||
return new Error(
|
|
||||||
`Порт "${port}" должен быть целым числом от 0 до 65535.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}).filter((e) => e);
|
|
||||||
return errors && errors.length && errors;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModeSwitch(that, event) {
|
|
||||||
|
|
||||||
if (that.state.ifHasErrors) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
that.props.onSwitch();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTextareaChange(that, event) {
|
|
||||||
|
|
||||||
that.setState({
|
|
||||||
stashedExports: normalizeProxyString(event.target.value),
|
|
||||||
});
|
|
||||||
const errors = that.getErrorsInStashedExports();
|
|
||||||
if (errors) {
|
|
||||||
that.props.setProxyStringRaw(false);
|
|
||||||
that.setState({ifHasErrors: true});
|
|
||||||
that.props.funs.showErrors(...errors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// No errors.
|
|
||||||
that.props.setProxyStringRaw(true, that.state.stashedExports);
|
|
||||||
that.setState({
|
|
||||||
stashedExports: false,
|
|
||||||
ifHasErrors: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit(that, event) {
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
this.handleModeSwitch(this, event);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render(props) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={linkEvent(this, this.handleSubmit)}>
|
|
||||||
<table class={scopedCss.editor + ' ' + scopedCss.exportsEditor}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width: 100%">
|
|
||||||
{
|
|
||||||
this.state.stashedExports === false
|
|
||||||
? 'Комментарии вырезаются!'
|
|
||||||
: (this.state.ifHasErrors
|
|
||||||
? (<span><a href="" onClick={this.resetState} style="color: red">Сбросьте изменения</a> или поправьте</span>)
|
|
||||||
: (<a href="" onClick={this.resetState}>Сбросить изменения</a>)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</th>
|
|
||||||
<th style="width: 1%">
|
|
||||||
<SwitchButton title="Переключиться в табличный режим" onClick={linkEvent(this, this.handleModeSwitch)}/>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><textarea
|
|
||||||
class={scopedCss.textarea}
|
|
||||||
spellcheck={false}
|
|
||||||
placeholder={`
|
|
||||||
SOCKS5 localhost:9050; # Tor Expert
|
|
||||||
SOCKS5 localhost:9150; # Tor Browser
|
|
||||||
HTTPS 11.22.33.44:3143;
|
|
||||||
PROXY foobar.com:8080; # Not HTTP!`.trim()}
|
|
||||||
onChange={linkEvent(this, this.handleTextareaChange)}
|
|
||||||
onFocus={this.showApply}
|
|
||||||
value={
|
|
||||||
this.state.stashedExports !== false
|
|
||||||
? this.state.stashedExports
|
|
||||||
: (this.props.proxyStringRaw || '').replace(/\s*;\n\s*/g, ';\n')
|
|
||||||
}
|
|
||||||
/></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const migrate = (proxyStringRaw) => {
|
|
||||||
/* In the old format \n\r? could be used as a separator. */
|
|
||||||
|
|
||||||
return proxyStringRaw
|
|
||||||
.replace(/#.*$/mg, '') // Strip comments.
|
|
||||||
.replace(/[^\S\r\n]*DIRECT[^\S\r\n]*/g, '') // Remove DIRECT from old versions.
|
|
||||||
/*
|
|
||||||
.split( /(?:[^\S\r\n]*(?:;|\r?\n)+[^\S\r\n]*)+/g )
|
|
||||||
.map( (p) => p.trim() )
|
|
||||||
.filter((p) => p)
|
|
||||||
.join(';\n');
|
|
||||||
*/
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
let waitingTillMount = [];
|
|
||||||
|
|
||||||
return class ProxyEditor extends Component {
|
|
||||||
|
|
||||||
constructor(props/*{ conf, onNewValue, ifInputsDisabled }*/) {
|
|
||||||
|
|
||||||
super(props);
|
|
||||||
const oldValue = typeof props.conf.value === 'string' && props.conf.value;
|
|
||||||
const newValue = migrate(oldValue || localStorage.getItem(UI_RAW) || '');
|
|
||||||
this.state = {
|
|
||||||
proxyStringRaw: newValue,
|
|
||||||
ifExportsMode: false,
|
|
||||||
ifValid: true,
|
|
||||||
};
|
|
||||||
this.handleSwitch = () => this.setState({ifExportsMode: !this.state.ifExportsMode});
|
|
||||||
waitingTillMount.push(newValue); // Wait till mount or eat bugs.
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
|
|
||||||
if (waitingTillMount.length) {
|
|
||||||
this.mayEmitNewValue(this.props.value, waitingTillMount.pop());
|
|
||||||
waitingTillMount = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUnmount() {
|
|
||||||
|
|
||||||
waitingTillMount = [];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
mayEmitNewValue(oldValue, newValue, ifValidityChanged) {
|
|
||||||
|
|
||||||
if ( // Reject: 1) both `false` OR 2) both `===`.
|
|
||||||
ifValidityChanged || ( Boolean(oldValue) || Boolean(newValue) ) && oldValue !== newValue
|
|
||||||
) {
|
|
||||||
this.props.onNewValue(this.state.ifValid, newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render(originalProps) {
|
|
||||||
|
|
||||||
const props = Object.assign({
|
|
||||||
proxyStringRaw: this.state.proxyStringRaw,
|
|
||||||
onSwitch: this.handleSwitch,
|
|
||||||
setProxyStringRaw: (ifValid, newValue) => {
|
|
||||||
|
|
||||||
const ifValidityChanged = this.state.ifValid !== ifValid;
|
|
||||||
if (!ifValid) {
|
|
||||||
if (ifValidityChanged || ifValid === undefined) {
|
|
||||||
this.props.onNewValue(ifValid);
|
|
||||||
this.setState({ ifValid });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldValue = this.state.proxyStringRaw;
|
|
||||||
localStorage.setItem(UI_RAW, newValue);
|
|
||||||
this.setState({proxyStringRaw: newValue, ifValid});
|
|
||||||
this.mayEmitNewValue(oldValue, newValue, ifValidityChanged);
|
|
||||||
|
|
||||||
},
|
|
||||||
}, originalProps);
|
|
||||||
|
|
||||||
return this.state.ifExportsMode
|
|
||||||
? createElement(ExportsEditor, props)
|
|
||||||
: createElement(TabledEditor, props);
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,176 +0,0 @@
|
||||||
import Inferno, { linkEvent } from 'inferno';
|
|
||||||
import Component from 'inferno-component';
|
|
||||||
import css from 'csjs-inject';
|
|
||||||
|
|
||||||
export default function getTabPannel({ flags, baseCss }) {
|
|
||||||
|
|
||||||
const scopedCss = css`
|
|
||||||
|
|
||||||
/*.tabContainer {
|
|
||||||
padding: 0;
|
|
||||||
}*/
|
|
||||||
.tabContainer li label {
|
|
||||||
display: inline-block; /* Needed for ::first-letter below. */
|
|
||||||
}
|
|
||||||
.tabContainer li label::first-letter {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
:root.ifInsideOptionsPage .tabContainer {
|
|
||||||
padding: 0.3em 0 0.4em 0;
|
|
||||||
}
|
|
||||||
:root.ifInsideOptionsPage nav.mainNav > section:not(:last-child):not([data-key=ownProxies]):not([data-key=mods]) {
|
|
||||||
border-bottom: 1px solid var(--cr-options-headline);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.ifInsideOptionsPage .navLabels {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* HIDE starts. */
|
|
||||||
|
|
||||||
:root:not(.ifInsideOptionsPage) .mainNav input:not(:checked) + section
|
|
||||||
{
|
|
||||||
/* Hide, but preclude width resizes. */
|
|
||||||
height: 0px !important;
|
|
||||||
line-height: 0px !important;
|
|
||||||
padding-top: 0 !important;
|
|
||||||
padding-bottom: 0 !important;
|
|
||||||
margin-top: 0 !important;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
display: block;
|
|
||||||
visibility: hidden;
|
|
||||||
transform: scaleY(0) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root:not(.ifInsideOptionsPage) .mainNav input:not(:checked) + section *
|
|
||||||
{
|
|
||||||
margin-top: 0 !important;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* HIDE ends. */
|
|
||||||
|
|
||||||
.navLabels {
|
|
||||||
background-color: var(--cr-grey-panel);
|
|
||||||
text-align: center;
|
|
||||||
min-width: 25em;
|
|
||||||
}
|
|
||||||
.navLabels li label {
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
border: 1px solid var(--ribbon-color);
|
|
||||||
border-radius: 0.2em;
|
|
||||||
|
|
||||||
background-color: white;
|
|
||||||
color: var(--ribbon-color);
|
|
||||||
|
|
||||||
padding: 0.2em 0.3em 0.3em 0.2em;
|
|
||||||
line-height: 0.8em;
|
|
||||||
margin: 0.1em 0;
|
|
||||||
}
|
|
||||||
.navLabels li label:hover {
|
|
||||||
background-color: var(--blue-bg);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LABELS starts. */
|
|
||||||
|
|
||||||
input[name="selectedTabLabel"]:checked + label:not(:hover)
|
|
||||||
{
|
|
||||||
background-color: var(--blue-bg);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
/* ★★★★★ */
|
|
||||||
.navLabels label:before {
|
|
||||||
content: '★';
|
|
||||||
padding-right: 0.1em;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
.navLabels li label:hover:before,
|
|
||||||
input[name="selectedTabLabel"]:checked + label:before
|
|
||||||
{
|
|
||||||
visibility: initial;
|
|
||||||
}
|
|
||||||
.navLabels li {
|
|
||||||
margin: 0 0.125em; /* 1.5px */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LABELS ends. */
|
|
||||||
|
|
||||||
.mainNav {
|
|
||||||
padding-top: 0.6em;
|
|
||||||
padding-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (flags.ifInsideOptionsPage) {
|
|
||||||
document.documentElement.classList.add(scopedCss.ifInsideOptionsPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return class TabPannel extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
|
|
||||||
super(props);
|
|
||||||
const fromHash = props.hashParams.get('tab');
|
|
||||||
this.state = {
|
|
||||||
chosenTabKeyRaw: fromHash,
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
render(props) {
|
|
||||||
|
|
||||||
const indexedTabs = props.tabs.filter((tab) => tab.label);
|
|
||||||
|
|
||||||
let [chosenTabIndex] = indexedTabs
|
|
||||||
.map((tab, index) => tab.key === this.state.chosenTabKeyRaw ? index : false)
|
|
||||||
.filter((index) => index !== false);
|
|
||||||
if (!(chosenTabIndex >= 0)) {
|
|
||||||
chosenTabIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chosenTabKey = indexedTabs[chosenTabIndex].key;
|
|
||||||
if (chosenTabKey !== props.hashParams.get('tab')) {
|
|
||||||
props.hashParams.set('tab', chosenTabKey);
|
|
||||||
window.location.hash = props.hashParams.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<nav class={scopedCss.navLabels}>
|
|
||||||
<ul class='horizontalList'>
|
|
||||||
{
|
|
||||||
indexedTabs.map((tab, index) =>
|
|
||||||
(<li>
|
|
||||||
<input type="radio" name="selectedTabLabel" id={'radioLabel' + index} checked={chosenTabIndex === index} class="off"/>
|
|
||||||
<label onClick={() => this.setState({chosenTabKeyRaw: tab.key})} for={'radioLabel' + index} class={scopedCss.navLabel}>{tab.label}</label>
|
|
||||||
</li>)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
<hr/>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<nav class={'horPadded ' + scopedCss.mainNav}>
|
|
||||||
{
|
|
||||||
[].concat(...props.tabs.map((tab, index) => [
|
|
||||||
(<input type="checkbox" name="selectedTab" id={'radioTab' + index} checked={
|
|
||||||
|
|
||||||
chosenTabKey === tab.key || (props.alwaysShownWith[tab.key] || []).includes(chosenTabKey)
|
|
||||||
|
|
||||||
} class="off"/>),
|
|
||||||
(<section id={'tab' + index} class={scopedCss.tabContainer} data-key={tab.key}>{tab.content}</section>),
|
|
||||||
]))
|
|
||||||
}
|
|
||||||
</nav>
|
|
||||||
<hr/>
|
|
||||||
</div>);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,154 +0,0 @@
|
||||||
export default function append(document, { flags }) {
|
|
||||||
|
|
||||||
// innerText converts \n to <br>, so:
|
|
||||||
document.querySelector('style').innerHTML = `
|
|
||||||
/* GLOBAL VARIABLES */
|
|
||||||
|
|
||||||
body {
|
|
||||||
--ribbon-color: #4169e1;
|
|
||||||
--blue-bg: dodgerblue;
|
|
||||||
--default-grey: #bfbfbf;
|
|
||||||
--cr-options-headline: #d3d3d3;
|
|
||||||
--cr-icon-selected: #d7d7d7;
|
|
||||||
--cr-popup-border: #bababa;
|
|
||||||
--cr-grey-panel: #f2f2f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* BASE ELEMENTS */
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
max-width: 25em;
|
|
||||||
}
|
|
||||||
a, a:visited {
|
|
||||||
color: var(--ribbon-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
label {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
div, section, header, ul, ol {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
header {
|
|
||||||
margin-bottom: 0.3em
|
|
||||||
}
|
|
||||||
ul, ol {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
.nowrap {
|
|
||||||
white-space: nowrap;
|
|
||||||
word-break: keep-all;
|
|
||||||
}
|
|
||||||
.nowrap {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.middledChildren > li,
|
|
||||||
.middledChildren > li > * {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
input[type="radio"], input[type="checkbox"] {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
input[type="radio"], label {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid var(--cr-popup-border);
|
|
||||||
margin: 0 0 0.6em 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
em {
|
|
||||||
font-style: normal;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* IF INSIDE OPTIONS */
|
|
||||||
${
|
|
||||||
flags.ifInsideOptionsPage
|
|
||||||
? `
|
|
||||||
|
|
||||||
ul, ol {
|
|
||||||
margin-left: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
` : `
|
|
||||||
|
|
||||||
ul, ol {
|
|
||||||
/*Here is a flex bug:
|
|
||||||
() antizapret [update] (i)
|
|
||||||
() anticensority very_long_foobar [update] (i) <- Sic!
|
|
||||||
Also: options page is wider, check it too.
|
|
||||||
But: fixed 100% width conflicts with margins/paddings.
|
|
||||||
So: use only when needed and avoid margins.
|
|
||||||
FYI: setting left-margin fixes problem too, but margins are not wanted.
|
|
||||||
Fix this problem below:
|
|
||||||
*/
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* COMMON CLASSES */
|
|
||||||
|
|
||||||
.off {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.horPadded {
|
|
||||||
padding-left: 1.4em;
|
|
||||||
padding-right: 1.4em;
|
|
||||||
}
|
|
||||||
.horizontalList,
|
|
||||||
.horizontalList li {
|
|
||||||
line-height: 100%;
|
|
||||||
}
|
|
||||||
.horizontalList li {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Flexes */
|
|
||||||
|
|
||||||
.horFlex {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.horFlex > input:not([type="button"]) {
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fonts/Icons */
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "emoji";
|
|
||||||
src:url("../lib/fonts/emoji.woff") format("woff");
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
.emoji {
|
|
||||||
font-family: "emoji";
|
|
||||||
}
|
|
||||||
|
|
||||||
svg.icon {
|
|
||||||
display: inline-block;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
stroke-width: 0;
|
|
||||||
stroke: currentColor;
|
|
||||||
fill: currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,80 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import Inferno from 'inferno';
|
|
||||||
import createElement from 'inferno-create-element';
|
|
||||||
import appendGlobalCss from './globalCss';
|
|
||||||
import css from 'csjs-inject';
|
|
||||||
import getApp from './components/App';
|
|
||||||
|
|
||||||
chrome.runtime.getBackgroundPage( (bgWindow) =>
|
|
||||||
bgWindow.apis.errorHandlers.installListenersOn(
|
|
||||||
window, 'PUP', async() => {
|
|
||||||
/*
|
|
||||||
`Extension context invalidated` error is thrown if `window.closed` is true and call to
|
|
||||||
`window.chrome.i18n` or other `window.chrome` api happens. Use bgWindow.chrome instead.
|
|
||||||
Use winChrome for tab-related calls like winChrome.tabs.getCurrent.
|
|
||||||
*/
|
|
||||||
window.winChrome = window.chrome;
|
|
||||||
window.chrome = bgWindow.chrome;
|
|
||||||
let theState;
|
|
||||||
{
|
|
||||||
const apis = bgWindow.apis;
|
|
||||||
|
|
||||||
theState = {
|
|
||||||
utils: bgWindow.utils,
|
|
||||||
apis: apis,
|
|
||||||
flags: {
|
|
||||||
/* Shortcuts to boolean values. */
|
|
||||||
ifNotControlled: !apis.errorHandlers.ifControllable,
|
|
||||||
ifMini: apis.version.ifMini,
|
|
||||||
},
|
|
||||||
bgWindow,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// IF INSIDE OPTIONS TAB
|
|
||||||
|
|
||||||
const currentTab = await new Promise(
|
|
||||||
(resolve) => winChrome.tabs.query(
|
|
||||||
{active: true, currentWindow: true},
|
|
||||||
([tab]) => resolve(tab),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
// winChrome.runtime.sendMessage({ currentTab, eventName: 'POPUP_OPENED' });
|
|
||||||
|
|
||||||
theState.flags.ifInsideOptionsPage = !(currentTab && currentTab.url) || /.*:\/\/extensions\/\?options=/g.test(currentTab.url) || currentTab.url.startsWith('about:addons');
|
|
||||||
theState.flags.ifInsideEdgeOptionsPage = theState.flags.ifInsideOptionsPage && currentTab && currentTab.url && currentTab.url.startsWith('edge://');
|
|
||||||
|
|
||||||
theState.currentTab = currentTab;
|
|
||||||
|
|
||||||
// If opened not via popup and not via options modal.
|
|
||||||
// E.g., if opened via copy-pasting an URL into the address bar from somewhere.
|
|
||||||
// If browser is not Chrome (Opera, e.g.) then options page may be opened in a separate tab
|
|
||||||
// and then you will get a false positive.
|
|
||||||
theState.flags.ifOpenedUnsafely = Boolean(await new Promise(
|
|
||||||
(resolve) => winChrome.tabs.getCurrent(resolve),
|
|
||||||
));
|
|
||||||
|
|
||||||
// STATE DEFINED, COMPOSE.
|
|
||||||
|
|
||||||
appendGlobalCss(document, theState);
|
|
||||||
// Extendable css classes.
|
|
||||||
|
|
||||||
Inferno.render(
|
|
||||||
createElement(getApp(theState), theState),
|
|
||||||
document.getElementById('app-root'),
|
|
||||||
);
|
|
||||||
// READY TO RENDER
|
|
||||||
|
|
||||||
const show = () => { document.documentElement.style.visibility = 'initial'; };
|
|
||||||
|
|
||||||
if (theState.flags.ifInsideOptionsPage) {
|
|
||||||
show();
|
|
||||||
} else {
|
|
||||||
setTimeout(show, 200); // Mac bug: https://bugs.chromium.org/p/chromium/issues/detail?id=428044
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
//const BabiliPlugin = require('babili-webpack-plugin');
|
|
||||||
|
|
||||||
module.exports = (env, ...flags) => ({
|
|
||||||
entry: './src/index.js',
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, 'dist'),
|
|
||||||
filename: `bundle.min.js`,
|
|
||||||
publicPath: './dist/',
|
|
||||||
},
|
|
||||||
devtool: 'source-map',
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.jsx?$/,
|
|
||||||
include: path.join(__dirname, 'src'),
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
presets: ['flow'],
|
|
||||||
plugins: [
|
|
||||||
'dynamic-import-webpack',
|
|
||||||
'inferno',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/*{
|
|
||||||
loader: './lib/transform-loader?csjs-injectify',
|
|
||||||
},*/
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
resolve: {
|
|
||||||
aliasFields: [],
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: env === 'prod' ?
|
|
||||||
[
|
|
||||||
/* Production */
|
|
||||||
//new BabiliPlugin(),
|
|
||||||
] : [
|
|
||||||
/* Development */
|
|
||||||
],
|
|
||||||
});
|
|
|
@ -1,37 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Устранение проблем</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>Устранение проблем</h1>
|
|
||||||
<ol>
|
|
||||||
<li><a data-in-bg="false" href class="view-errors">Детали последних ошибок</a></li>
|
|
||||||
<li><a data-in-bg="false" href class="reset-settings">Сбросить настройки</a></li>
|
|
||||||
<li><a data-in-bg="false" href="https://github.com/anticensority/runet-censorship-bypass/wiki/Если-расширение-не-работает" target="_blank">Файл самопомощи</a></li>
|
|
||||||
<li><a data-in-bg="false" href="https://groups.google.com/g/anticensority">Напишите нам!</a></li>
|
|
||||||
</ol>
|
|
||||||
<h2>Для продвинутых</h2>
|
|
||||||
<ol>
|
|
||||||
<li><a data-in-bg="false" href="https://github.com/anticensority/runet-censorship-bypass/wiki/Как-прочитать-логи%3F-%28для-продвинутых%29">Как прочитать логи?</a></li>
|
|
||||||
<li><a data-in-bg="false" href="../debug/index.html">Отладка PAC-скрипта</a></li>
|
|
||||||
</ol>
|
|
||||||
<hr/>
|
|
||||||
<h1>Troubleshooting</h1>
|
|
||||||
<ol>
|
|
||||||
<li><a data-in-bg="false" href class="view-errors">Details of last errors</a></li>
|
|
||||||
<li><a data-in-bg="false" href class="reset-settings">Reset settings</a></li>
|
|
||||||
<li><a data-in-bg="false" href="https://github.com/anticensority/runet-censorship-bypass/wiki/Если-расширение-не-работает" target="_blank">Troubleshoot guide (ru)</a></li>
|
|
||||||
<li><a data-in-bg="false" href="https://groups.google.com/g/anticensority">Write to us!</a></li>
|
|
||||||
</ol>
|
|
||||||
<h2>Advanced</h2>
|
|
||||||
<ol>
|
|
||||||
<li><a data-in-bg="false" href="https://github.com/anticensority/runet-censorship-bypass/wiki/Как-прочитать-логи%3F-%28для-продвинутых%29">How to read logs?</a></li>
|
|
||||||
<li><a data-in-bg="false" href="../debug/index.html">PAC-script debugging</a></li>
|
|
||||||
</ol>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
<script src="../lib/keep-links-clickable.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,25 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
chrome.runtime.getBackgroundPage( (backgroundPage) =>
|
|
||||||
backgroundPage.apis.errorHandlers.installListenersOn(
|
|
||||||
window, 'TRBL', () => {
|
|
||||||
|
|
||||||
document.querySelectorAll('.reset-settings').forEach((el) => {
|
|
||||||
|
|
||||||
el.onclick = () => {
|
|
||||||
|
|
||||||
backgroundPage.localStorage.clear();
|
|
||||||
chrome.storage.local.clear( () => chrome.runtime.reload() );
|
|
||||||
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.view-errors').forEach((el) => {
|
|
||||||
|
|
||||||
el.onclick = () =>
|
|
||||||
backgroundPage.apis.errorHandlers.viewError('all');
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
|
@ -1,85 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const Storage = require('symlink-to/project-root/tools/sinon-storage');
|
|
||||||
const Chai = require('chai');
|
|
||||||
const Mocha = require('mocha');
|
|
||||||
|
|
||||||
const CachelessRequire = require('symlink-to/project-root/tools/cacheless-require')(module);
|
|
||||||
|
|
||||||
Mocha.describe('window.apis.pacKitchen', function () {
|
|
||||||
|
|
||||||
Mocha.beforeEach(function() {
|
|
||||||
|
|
||||||
global.chrome = CachelessRequire('sinon-chrome/extensions');
|
|
||||||
global.window = {
|
|
||||||
chrome: global.chrome,
|
|
||||||
localStorage: new Storage(),
|
|
||||||
};
|
|
||||||
CachelessRequire('../00-init-apis.js');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
Mocha.it('is exported with correct default values', function () {
|
|
||||||
|
|
||||||
CachelessRequire('../35-pac-kitchen-api.js');
|
|
||||||
Chai.expect(window.apis.pacKitchen, 'to be exported as global').to.exist;
|
|
||||||
|
|
||||||
const mods = window.apis.pacKitchen.getPacMods();
|
|
||||||
Chai.expect(
|
|
||||||
mods, 'to expose default configs on first run'
|
|
||||||
).to.exist;
|
|
||||||
Chai.expect(mods.ifNoMods, 'to impose modifications by default (prohibits DIRECT)').to.be.false;
|
|
||||||
|
|
||||||
const orderedMods = window.apis.pacKitchen.getOrderedConfigs();
|
|
||||||
Chai.expect(orderedMods, 'to have method for getting ordered configs').to.exist;
|
|
||||||
{
|
|
||||||
const several = 9;
|
|
||||||
Chai.expect(orderedMods.length, 'to have several ordered configs').to.be.above(several);
|
|
||||||
}
|
|
||||||
|
|
||||||
Chai.expect(
|
|
||||||
Object.keys(mods).length,
|
|
||||||
'pacModifiers to inherit default configs keys as props and add extra props'
|
|
||||||
).to.be.above(orderedMods.length);
|
|
||||||
|
|
||||||
Chai.expect(
|
|
||||||
orderedMods.every((mod) => mods[mod.key] === mod.dflt),
|
|
||||||
'all configs to be default on first run'
|
|
||||||
).to.be.ok;
|
|
||||||
|
|
||||||
const excMods = window.apis.pacKitchen.getOrderedConfigs('exceptions');
|
|
||||||
Chai.expect(excMods.length, 'to have several ordered mods under category "exceptions"').to.be.above(0);
|
|
||||||
|
|
||||||
const proxyMods = window.apis.pacKitchen.getOrderedConfigs('ownProxies');
|
|
||||||
Chai.expect(proxyMods.length, 'to have several ordered mods under category "ownProxies"').to.be.above(0);
|
|
||||||
|
|
||||||
const generalMods = window.apis.pacKitchen.getOrderedConfigs('general');
|
|
||||||
Chai.expect(generalMods.length, 'to have several ordered mods without category').to.be.above(0);
|
|
||||||
|
|
||||||
Chai.expect(
|
|
||||||
orderedMods.length, 'to be a sum of categorized (and ordered) mods'
|
|
||||||
).to.be.equal(
|
|
||||||
proxyMods.length + excMods.length + generalMods.length
|
|
||||||
);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
Mocha.it('is installed (by modifying `chrome.proxy.settings.set`)', function () {
|
|
||||||
|
|
||||||
const originalSet = chrome.proxy.settings.set;
|
|
||||||
CachelessRequire('../35-pac-kitchen-api.js');
|
|
||||||
Chai.expect(originalSet.notCalled, 'original set not to be called during install').to.be.true;
|
|
||||||
Chai.expect(originalSet, 'settings.set to be modified during install').not.to.be.equal(chrome.proxy.settings.set);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
Mocha.afterEach(function() {
|
|
||||||
|
|
||||||
delete global.window;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const Chai = require('chai');
|
|
||||||
const Mocha = require('mocha');
|
|
||||||
|
|
||||||
const CachelessRequire = require('symlink-to/project-root/tools/cacheless-require')(module);
|
|
||||||
|
|
||||||
Mocha.describe('window.utils', function () {
|
|
||||||
|
|
||||||
const initApis = '../00-init-apis.js';
|
|
||||||
|
|
||||||
Mocha.beforeEach(function() {
|
|
||||||
|
|
||||||
global.window = {};
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
Mocha.it('is exported as global', function () {
|
|
||||||
|
|
||||||
CachelessRequire(initApis);
|
|
||||||
Chai.expect(window.utils, 'to be exported as global').to.exist;
|
|
||||||
Chai.expect(window.apis.version.ifMini, 'to be marked as not MINI version by default').to.be.false;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
Mocha.afterEach(function() {
|
|
||||||
|
|
||||||
delete global.window;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,365 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
const mandatory = window.utils.mandatory;
|
|
||||||
const httpLib = window.apis.httpLib;
|
|
||||||
const clarify = window.apis.errorsLib.clarify;
|
|
||||||
|
|
||||||
// IP REGEX starts.
|
|
||||||
|
|
||||||
const portOpt = '(:\\d+)?'; // The only capturing group, sic!
|
|
||||||
const ipv4portOpt = '(?:[0-9]{1,3}\\.){3}[0-9]{1,3}' + portOpt;
|
|
||||||
const ipv6nake = '(?:[0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}';
|
|
||||||
const ipv6portOpt = '(?:' + ipv6nake + '|' + '\\[' + ipv6nake + '\\]' + portOpt + ')';
|
|
||||||
|
|
||||||
const ipv4Re = new RegExp('^' + ipv4portOpt + '$');
|
|
||||||
const ipv6Re = new RegExp('^' + ipv6portOpt + '$');
|
|
||||||
|
|
||||||
const _match = function _match(ipRe, str) {
|
|
||||||
|
|
||||||
const m = (str.match(ipRe) || []).filter( (c) => c );
|
|
||||||
const port = m.length > 1 ? m.pop() : false;
|
|
||||||
return {ifMatched: m.length, port: port};
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const _test = {
|
|
||||||
|
|
||||||
ipv4: _match.bind(null, ipv4Re),
|
|
||||||
ipv6: _match.bind(null, ipv6Re),
|
|
||||||
ipv4v6: function(str) {
|
|
||||||
|
|
||||||
let mr = this.ipv4(str);
|
|
||||||
if (mr.ifMatched) {
|
|
||||||
mr.ifv4 = true;
|
|
||||||
mr.canonical = str.replace(mr.port, '');
|
|
||||||
return mr;
|
|
||||||
}
|
|
||||||
mr = this.ipv6(str);
|
|
||||||
if (mr.ifMatched) {
|
|
||||||
mr.ifv6 = true;
|
|
||||||
mr.canonical = str.replace(mr.port, '').replace(/[\[\]]/g, '');
|
|
||||||
return mr;
|
|
||||||
}
|
|
||||||
return mr;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
// IP REGEX ends.
|
|
||||||
|
|
||||||
const _state = window.utils.createStorage('ip-to-host');
|
|
||||||
const ip2host = '';
|
|
||||||
|
|
||||||
const privates = {};
|
|
||||||
|
|
||||||
const _createHostObj = function _addHostObj(hostStr) {
|
|
||||||
|
|
||||||
return (privates._strToHostObj[hostStr] = {host: hostStr});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const _getHostObj = function _getHostObj(hostStr) {
|
|
||||||
|
|
||||||
return privates._strToHostObj[hostStr] || _createHostObj(hostStr);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const reinit = function reinit() {
|
|
||||||
|
|
||||||
privates._strToHostObj = [
|
|
||||||
/* Please, don't use proxies directly (without PAC-script). */
|
|
||||||
'n.thenewone.lol',
|
|
||||||
's.thenewone.lol',
|
|
||||||
// antizapret.prostovpn.org:
|
|
||||||
'proxy.antizapret.prostovpn.org',
|
|
||||||
'proxy-ssl.antizapret.prostovpn.org',
|
|
||||||
'proxy-nossl.antizapret.prostovpn.org',
|
|
||||||
'proxy-fbtw-ssl.antizapret.prostovpn.org',
|
|
||||||
].reduce((acc, hostname) => Object.assign(acc, { [hostname]: { host: hostname }}), {
|
|
||||||
// Defaults:
|
|
||||||
localhost: { host: 'localhost' },
|
|
||||||
});
|
|
||||||
|
|
||||||
privates._ipToHostObj = {
|
|
||||||
'127.0.0.1': { host: 'localhost' },
|
|
||||||
'0.0.0.0': { host: 'localhost' },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Persisted.
|
|
||||||
const ipToHost = _state(ip2host);
|
|
||||||
if (ipToHost) {
|
|
||||||
for( const ip of Object.keys(ipToHost) ) {
|
|
||||||
const host = ipToHost[ip];
|
|
||||||
privates._ipToHostObj[ip] = _getHostObj(host);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
reinit();
|
|
||||||
|
|
||||||
const generateRandomHexString = function generateRandomHexString(minLen, maxLen) {
|
|
||||||
|
|
||||||
return Array.from(window.crypto.getRandomValues(new Uint8Array(maxLen)))
|
|
||||||
.slice(minLen + Math.floor(Math.random()*(maxLen - minLen)))
|
|
||||||
.map((i) => i.toString(16)).join('');
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const getIpsFor = function getIpsFor(host, cb = mandatory()) {
|
|
||||||
|
|
||||||
if (host.trim() === 'localhost') {
|
|
||||||
return cb(null, ['127.0.0.1', '0.0.0.0', '::1']);
|
|
||||||
}
|
|
||||||
const types = [1, 28];
|
|
||||||
const promises = types.map(
|
|
||||||
(type) => new Promise((resolve) =>
|
|
||||||
httpLib.get(
|
|
||||||
`https://dns.google.com/resolve?type=${type}&name=${host}&random_padding=${generateRandomHexString(30,500)}`,
|
|
||||||
(err, res) => {
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
try {
|
|
||||||
res = JSON.parse(res);
|
|
||||||
console.log('JSON parsed.');
|
|
||||||
if (err || res.Status) {
|
|
||||||
const msg = ['Answer', 'Comment', 'Status']
|
|
||||||
.filter( (prop) => res[prop] )
|
|
||||||
.map( (prop) => prop + ': ' + JSON.stringify( res[prop] ) )
|
|
||||||
.join(', \n');
|
|
||||||
err = clarify(err || {}, 'Сервер (json): ' + msg, {data: res});
|
|
||||||
} else {
|
|
||||||
res = res.Answer || [];
|
|
||||||
res = res.filter(
|
|
||||||
(record) => types.includes(record.type)
|
|
||||||
).map( (ans) => ans.data );
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
err = clarify(
|
|
||||||
e,
|
|
||||||
'Сервер (текст): ' + res, err ? {data: err} : undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve([err, res]);
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
Promise.all(promises).then(
|
|
||||||
([[v4err, v4res], [v6err, v6res]]) => {
|
|
||||||
|
|
||||||
if(v4err) {
|
|
||||||
return cb(v4err, v4res);
|
|
||||||
}
|
|
||||||
const ips = v4res;
|
|
||||||
let warns = [];
|
|
||||||
if (!v6err) {
|
|
||||||
ips.push(...v6res);
|
|
||||||
} else {
|
|
||||||
warns = [v6err];
|
|
||||||
}
|
|
||||||
cb(null, ips, ...warns);
|
|
||||||
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const _canonize = function canonize(addrArr) {
|
|
||||||
|
|
||||||
const ipSet = new Set();
|
|
||||||
const hostSet = new Set();
|
|
||||||
|
|
||||||
for( const addr of addrArr ) {
|
|
||||||
|
|
||||||
const ipm = _test.ipv4v6(addr);
|
|
||||||
if (ipm.ifMatched) {
|
|
||||||
ipSet.add( ipm.canonical );
|
|
||||||
} else {
|
|
||||||
hostSet.add( addr.replace(/:\d+$/, '') );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Canonized hosts/ips:', hostSet.size + '/' + ipSet.size);
|
|
||||||
return [ipSet, hostSet];
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const self = window.apis.ipToHost = {
|
|
||||||
|
|
||||||
persistData() {
|
|
||||||
|
|
||||||
console.log('Persisting ipToHost...', privates);
|
|
||||||
const ipToHost = {};
|
|
||||||
for( const ip of Object.keys(privates._ipToHostObj) ) {
|
|
||||||
ipToHost[ip] = privates._ipToHostObj[ip].host;
|
|
||||||
}
|
|
||||||
_state(ip2host, ipToHost);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
resetToDefaults() {
|
|
||||||
|
|
||||||
_state(ip2host, null);
|
|
||||||
reinit();
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
_purgeOldIpsForSync(hostStr) {
|
|
||||||
|
|
||||||
console.log('Purging old IPs for', hostStr);
|
|
||||||
for(const ip of Object.keys(privates._ipToHostObj)) {
|
|
||||||
if (hostStr === privates._ipToHostObj[ip].host) {
|
|
||||||
delete privates._ipToHostObj[ip];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
_addAsync(hostStr, cb = mandatory()) {
|
|
||||||
|
|
||||||
new Promise((resolveIps, reject) => {
|
|
||||||
|
|
||||||
const ipm = _test.ipv4v6(hostStr);
|
|
||||||
if (ipm.ifMatched) {
|
|
||||||
return resolveIps([hostStr]);
|
|
||||||
}
|
|
||||||
|
|
||||||
getIpsFor(hostStr, (err, ips, ...warns) => {
|
|
||||||
|
|
||||||
console.log('Got IPs + err?:', ips, err);
|
|
||||||
if (err) {
|
|
||||||
reject([err, null, ...warns]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolveIps(ips);
|
|
||||||
});
|
|
||||||
|
|
||||||
}).then(
|
|
||||||
(ips) => {
|
|
||||||
|
|
||||||
this._purgeOldIpsForSync(hostStr);
|
|
||||||
// Object may be shared, string can't.
|
|
||||||
const hostObj = _getHostObj(hostStr);
|
|
||||||
for(const ip of ips) {
|
|
||||||
privates._ipToHostObj[ip] = hostObj;
|
|
||||||
}
|
|
||||||
cb();
|
|
||||||
|
|
||||||
},
|
|
||||||
(args) => cb(...args)
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
_updateAllAsync(cb = mandatory()) {
|
|
||||||
|
|
||||||
const hostArr = Object.keys(privates._strToHostObj)
|
|
||||||
.filter((hostStr) => hostStr !== 'localhost');
|
|
||||||
|
|
||||||
console.log('Update all:', hostArr);
|
|
||||||
|
|
||||||
const promises = hostArr.map(
|
|
||||||
(hostStr) => new Promise(
|
|
||||||
(resolve) => this._addAsync(
|
|
||||||
hostStr,
|
|
||||||
(...args) => resolve(args)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
Promise.all( promises ).then( (cbsRes) => {
|
|
||||||
|
|
||||||
let ipErrors = cbsRes.map( ([err]) => err ).filter( (err) => err );
|
|
||||||
let warns = [];
|
|
||||||
if (ipErrors.length) {
|
|
||||||
warns = [clarify(
|
|
||||||
ipErrors,
|
|
||||||
'Не удалось получить один или несколько IP адресов для' +
|
|
||||||
' прокси-серверов. Иконка для уведомления об обходе' +
|
|
||||||
' блокировок может не отображаться.'
|
|
||||||
)];
|
|
||||||
} else {}
|
|
||||||
cb(null, null, ...warns);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_replaceAllAsync(hostArr = mandatory(), cb) {
|
|
||||||
|
|
||||||
if (typeof(hostArr) === 'function') {
|
|
||||||
cb = hostArr;
|
|
||||||
hostArr = Object.keys(privates._strToHostObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resetToDefaults();
|
|
||||||
for(const hostStr of hostArr) {
|
|
||||||
_createHostObj(hostStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updateAllAsync(cb);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
// PUBLIC
|
|
||||||
|
|
||||||
updateAllAsync(cb = mandatory()) {
|
|
||||||
|
|
||||||
this._updateAllAsync((err, ...args) => {
|
|
||||||
|
|
||||||
if (!err) {
|
|
||||||
this.persistData();
|
|
||||||
}
|
|
||||||
cb(err, ...args);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
replaceAllAsync(addrArr, cb = mandatory()) {
|
|
||||||
|
|
||||||
console.log('Replacing...', addrArr);
|
|
||||||
const [ipSet, hostSet] = _canonize(addrArr);
|
|
||||||
for( const ip of ipSet ) {
|
|
||||||
const host = _getHostObj(ip);
|
|
||||||
privates._ipToHostObj[ip] = host;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._replaceAllAsync([...ipSet, ...hostSet], (allErr, ...args) => {
|
|
||||||
|
|
||||||
if (!allErr) {
|
|
||||||
this.persistData();
|
|
||||||
}
|
|
||||||
cb(allErr, ...args);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
get(ip) {
|
|
||||||
|
|
||||||
const tmp = privates._ipToHostObj[ip];
|
|
||||||
return tmp && tmp.host;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
window.utils.addRequestResponder(
|
|
||||||
'ip-to-host-update-all', (...args) => self.updateAllAsync(...args)
|
|
||||||
);
|
|
||||||
window.utils.addRequestResponder(
|
|
||||||
'ip-to-host-replace-all', (...args) => self.replaceAllAsync(...args)
|
|
||||||
);
|
|
||||||
window.utils.addRequestResponder(
|
|
||||||
'ip-to-host-reset-to-defaults', (cb) => {
|
|
||||||
self.resetToDefaults();
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,169 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
const timeouted = window.utils.timeouted;
|
|
||||||
|
|
||||||
const isProxied = (requestDetails) => false;
|
|
||||||
const isProxySideError = (details) =>
|
|
||||||
/* About !main_frame: Main frame websocket errors are followed by webnavigation errors
|
|
||||||
which chrome-internals code resets the state of the popup.
|
|
||||||
*/
|
|
||||||
details.error === 'net::ERR_TUNNEL_CONNECTION_FAILED' && details.type !== 'main_frame' && isProxied(details) ||
|
|
||||||
details.error === 'NS_ERROR_CONNECTION_REFUSED' && Boolean(details.proxyInfo);
|
|
||||||
|
|
||||||
const urlToA = (url) => new URL(url).host.link(
|
|
||||||
encodeURIComponent(url),
|
|
||||||
);
|
|
||||||
|
|
||||||
const isProxyErrorHandledAsync = async (details) => {
|
|
||||||
|
|
||||||
if (!isProxySideError(details)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let fromPageHref = '';
|
|
||||||
let toUrlHref = '';
|
|
||||||
let fromPageHtml = '';
|
|
||||||
let youMayReportHtml = '';
|
|
||||||
const initiator = details.initiator !== 'null' && details.initiator;
|
|
||||||
try {
|
|
||||||
if (initiator) {
|
|
||||||
fromPageHref = new URL(initiator).href; // Sanitize: only urls, not other stuff.
|
|
||||||
fromPageHtml = ` со страницы ${urlToA(fromPageHref)}`;
|
|
||||||
}
|
|
||||||
toUrlHref = new URL(details.url).href;
|
|
||||||
youMayReportHtml = ` Вы можете <b>${'сообщить об ошибке'.link(
|
|
||||||
encodeURIComponent(
|
|
||||||
'/pages/report-proxy-error/index.html?' +
|
|
||||||
new URLSearchParams({
|
|
||||||
fromPageHref,
|
|
||||||
requestFailedTo: toUrlHref,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)}</b> администратору прокси.`;
|
|
||||||
} catch(e) {
|
|
||||||
/* For malformed urls. */
|
|
||||||
console.log('Error handling malformed URLs:', details);
|
|
||||||
const msg = `Error handling malformed URLs: ${JSON.stringify(details, null, 2)}`;
|
|
||||||
throw new TypeError(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service workers have tabId = -1, get active tubId for them.
|
|
||||||
const tabId = details.tabId < 0
|
|
||||||
? await new Promise((resolve) => chrome.tabs.query(
|
|
||||||
{ active: true },
|
|
||||||
([tab]) => resolve(tab.id)),
|
|
||||||
)
|
|
||||||
: details.tabId;
|
|
||||||
|
|
||||||
const [oldPopup, oldText, oldColor] = await new Promise((resolve) =>
|
|
||||||
chrome.browserAction.getPopup({ tabId }, (oldPopup) =>
|
|
||||||
chrome.browserAction.getBadgeText({ tabId }, (oldText) =>
|
|
||||||
chrome.browserAction.getBadgeBackgroundColor({ tabId }, (oldColor) => resolve([
|
|
||||||
oldPopup,
|
|
||||||
oldText,
|
|
||||||
oldColor,
|
|
||||||
])),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const popupPrefix = chrome.runtime.getURL(`/pages/options/index.html?status=<span style="color: red">🔥 Прокси-сервер отказался обслуживать запрос к%20`);
|
|
||||||
if (decodeURIComponent(oldPopup).startsWith(popupPrefix)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const popup = `${popupPrefix}${urlToA(details.url)}${fromPageHtml}</span>. Это могло быть намеренно или по ошибке.${youMayReportHtml}#tab=exceptions`;
|
|
||||||
|
|
||||||
chrome.browserAction.setPopup({
|
|
||||||
tabId,
|
|
||||||
popup,
|
|
||||||
});
|
|
||||||
|
|
||||||
chrome.browserAction.setBadgeBackgroundColor({
|
|
||||||
tabId,
|
|
||||||
color: 'red',
|
|
||||||
});
|
|
||||||
chrome.browserAction.setBadgeText({
|
|
||||||
tabId,
|
|
||||||
text: '❗',
|
|
||||||
});
|
|
||||||
|
|
||||||
let limit = 5;
|
|
||||||
let ifOnTurn = true;
|
|
||||||
let ifError = false;
|
|
||||||
const flip = () => {
|
|
||||||
|
|
||||||
if (!ifOnTurn && !--limit || ifError) {
|
|
||||||
clearInterval(timer);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chrome.browserAction.setBadgeText({
|
|
||||||
tabId,
|
|
||||||
text: ifOnTurn ? '❗' : '',
|
|
||||||
}, () => {
|
|
||||||
ifError = chrome.runtime.lastError;
|
|
||||||
});
|
|
||||||
ifOnTurn = !ifOnTurn;
|
|
||||||
};
|
|
||||||
flip();
|
|
||||||
const timer = setInterval(flip, 500);
|
|
||||||
|
|
||||||
const restoringHandler = timeouted((eventDetails) => {
|
|
||||||
|
|
||||||
if(eventDetails && tabId !== ((eventDetails.currentTab || eventDetails).id || eventDetails.tabId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearInterval(timer);
|
|
||||||
|
|
||||||
chrome.browserAction.setPopup({ tabId, popup: oldPopup});
|
|
||||||
chrome.browserAction.setBadgeBackgroundColor({ tabId, color: oldColor});
|
|
||||||
chrome.browserAction.setBadgeText({ tabId, text: oldText});
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.removeListener(restoringHandler);
|
|
||||||
chrome.tabs.onRemoved.removeListener(restoringHandler);
|
|
||||||
chrome.tabs.onReplaced.removeListener(restoringHandler);
|
|
||||||
chrome.webNavigation.onBeforeNavigate.removeListener(restoringHandler);
|
|
||||||
});
|
|
||||||
chrome.runtime.onMessage.addListener(restoringHandler);
|
|
||||||
chrome.tabs.onRemoved.addListener(restoringHandler);
|
|
||||||
chrome.tabs.onReplaced.addListener(restoringHandler); // When does it happen?
|
|
||||||
chrome.webNavigation.onBeforeNavigate.addListener(restoringHandler);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
chrome.webNavigation.onErrorOccurred.addListener(timeouted(async (details) => {
|
|
||||||
|
|
||||||
const tabId = details.tabId;
|
|
||||||
if ( !(details.frameId === 0 && tabId >= 0) ||
|
|
||||||
[
|
|
||||||
'net::ERR_BLOCKED_BY_CLIENT',
|
|
||||||
'net::ERR_ABORTED',
|
|
||||||
].includes(details.error) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (await isProxyErrorHandledAsync(details)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.browserAction.setPopup({
|
|
||||||
tabId,
|
|
||||||
popup: './pages/options/index.html?status=Правый клик по иконке — меню инструментов!#tab=exceptions',
|
|
||||||
});
|
|
||||||
|
|
||||||
chrome.browserAction.setBadgeBackgroundColor({
|
|
||||||
tabId,
|
|
||||||
color: '#4285f4',
|
|
||||||
});
|
|
||||||
chrome.browserAction.setBadgeText({
|
|
||||||
tabId,
|
|
||||||
text: '●●●',
|
|
||||||
});
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
chrome.webRequest.onErrorOccurred.addListener(
|
|
||||||
timeouted(isProxyErrorHandledAsync),
|
|
||||||
{urls: ['<all_urls>']},
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
{
|
|
||||||
const chromified = window.utils.chromified;
|
|
||||||
|
|
||||||
const lastErrors = [];
|
|
||||||
const lastErrorsLength = 20;
|
|
||||||
|
|
||||||
const IF_COLL_KEY = 'err-to-exc-if-coll';
|
|
||||||
|
|
||||||
const privates = {
|
|
||||||
ifCollecting: window.localStorage[IF_COLL_KEY] || false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const that = window.apis.lastNetErrors = {
|
|
||||||
get ifCollecting() {
|
|
||||||
|
|
||||||
return privates.ifCollecting;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
set ifCollecting(newValue) {
|
|
||||||
|
|
||||||
privates.ifCollecting = window.localStorage[IF_COLL_KEY] = newValue;
|
|
||||||
|
|
||||||
},
|
|
||||||
get: () => lastErrors,
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.webRequest.onErrorOccurred.addListener(chromified((err/*Ignored*/, details) => {
|
|
||||||
|
|
||||||
if (!that.ifCollecting || [
|
|
||||||
'net::ERR_BLOCKED_BY_CLIENT',
|
|
||||||
'net::ERR_ABORTED',
|
|
||||||
'net::ERR_CACHE_MISS',
|
|
||||||
'net::ERR_INSUFFICIENT_RESOURCES',
|
|
||||||
].includes(details.error) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const last = lastErrors[0];
|
|
||||||
if (last && details.error === last.error && details.url === last.url) {
|
|
||||||
// Dup.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErrors.unshift(details);
|
|
||||||
if (lastErrors.length > lastErrorsLength) {
|
|
||||||
lastErrors.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
}),
|
|
||||||
{urls: ['<all_urls>']}
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,191 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// Shows user browserAction icon if any part of the current site is being
|
|
||||||
// blocked and proxied.
|
|
||||||
|
|
||||||
/*
|
|
||||||
In what moment the title of the previous icon is cleared?
|
|
||||||
By my observations it usually takes place near tabs.onUpdate of tab status
|
|
||||||
to "loading".
|
|
||||||
So if you set a title earlier it may be cleared by browser.
|
|
||||||
It pertains not only to page refesh but to newly opened pages too.
|
|
||||||
Crazy parallel Chrome.
|
|
||||||
**/
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
const chromified = window.utils.chromified;
|
|
||||||
|
|
||||||
const _tabCallbacks = {};
|
|
||||||
|
|
||||||
const afterTabUpdated = function afterTabUpdated(tabId, cb) {
|
|
||||||
|
|
||||||
if (_tabCallbacks[tabId]) {
|
|
||||||
_tabCallbacks[tabId].push(cb);
|
|
||||||
} else {
|
|
||||||
_tabCallbacks[tabId] = [cb];
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTabUpdate = function onTabUpdate(tabId) {
|
|
||||||
|
|
||||||
if (_tabCallbacks[tabId]) {
|
|
||||||
_tabCallbacks[tabId].map( (f) => f() );
|
|
||||||
delete _tabCallbacks[tabId];
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
chrome.tabs.onUpdated.addListener( onTabUpdate );
|
|
||||||
|
|
||||||
const setRedBadge = (opts) => {
|
|
||||||
|
|
||||||
chrome.browserAction.setBadgeBackgroundColor({
|
|
||||||
color: '#db4b2f',
|
|
||||||
});
|
|
||||||
chrome.browserAction.setBadgeText(opts);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTitle = function updateTitle(requestDetails, proxyHost, cb) {
|
|
||||||
|
|
||||||
chrome.browserAction.getTitle(
|
|
||||||
{tabId: requestDetails.tabId},
|
|
||||||
chromified((err, title) => {
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
// E.g., no tab with such id happens.
|
|
||||||
// Because requestDetails may be stale.
|
|
||||||
console.log('Notifier error ignored (this is normal, it happens):', err);
|
|
||||||
return cb();
|
|
||||||
}
|
|
||||||
const ifTitleSetAlready = /\n/.test(title);
|
|
||||||
|
|
||||||
const hostname = new URL( requestDetails.url ).hostname;
|
|
||||||
|
|
||||||
let ifShouldUpdateTitle = false;
|
|
||||||
const indent = ' ';
|
|
||||||
const proxyTitle = 'Прокси:';
|
|
||||||
|
|
||||||
if (!ifTitleSetAlready) {
|
|
||||||
|
|
||||||
title = 'Разблокированы:\n' + indent + hostname + '\n'
|
|
||||||
+ proxyTitle + '\n' + indent + proxyHost;
|
|
||||||
ifShouldUpdateTitle = true;
|
|
||||||
|
|
||||||
setRedBadge({
|
|
||||||
tabId: requestDetails.tabId,
|
|
||||||
text: requestDetails.type === 'main_frame' ? '1' : '%1',
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
const hostsProxiesPair = title.split(proxyTitle);
|
|
||||||
|
|
||||||
if (hostsProxiesPair[1].indexOf(proxyHost) === -1) {
|
|
||||||
title = title.replace(
|
|
||||||
hostsProxiesPair[1],
|
|
||||||
hostsProxiesPair[1] + '\n' + indent + proxyHost
|
|
||||||
);
|
|
||||||
ifShouldUpdateTitle = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hostsProxiesPair[0].indexOf(hostname) === -1) {
|
|
||||||
|
|
||||||
title = title.replace(
|
|
||||||
proxyTitle,
|
|
||||||
indent + hostname + '\n' + proxyTitle
|
|
||||||
);
|
|
||||||
ifShouldUpdateTitle = true;
|
|
||||||
|
|
||||||
const _cb = cb;
|
|
||||||
cb = () => chrome.browserAction.getBadgeText(
|
|
||||||
{tabId: requestDetails.tabId},
|
|
||||||
(result) => {
|
|
||||||
|
|
||||||
setRedBadge({
|
|
||||||
tabId: requestDetails.tabId,
|
|
||||||
text: (isNaN( result.charAt(0)) && result.charAt(0) || '')
|
|
||||||
+ (hostsProxiesPair[0].split('\n').length - 1),
|
|
||||||
});
|
|
||||||
return _cb();
|
|
||||||
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ifShouldUpdateTitle) {
|
|
||||||
chrome.browserAction.setTitle({
|
|
||||||
title: title,
|
|
||||||
tabId: requestDetails.tabId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return cb();
|
|
||||||
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
let previousUpdateTitleFinished = Promise.resolve();
|
|
||||||
|
|
||||||
const tryProxyAndInform = function tryProxyAndInform(requestDetails) {
|
|
||||||
|
|
||||||
const host = window.apis.ipToHost.get( requestDetails.ip );
|
|
||||||
if (!host) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
If we fetch a resource from a proxy address it is almost never proxied and
|
|
||||||
shouldn't be shown.
|
|
||||||
Think about localhost as a proxy and a user working with a web site on localhost.
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
Host is constructed from hostname and port. Hostname never contains port,
|
|
||||||
it is an ip or a domain name. See hostname and host
|
|
||||||
in `new URL('https://localhost:8080')`.
|
|
||||||
*/
|
|
||||||
const hostnameFromUrl = new URL(requestDetails.url).hostname;
|
|
||||||
const hostnameFromProxy = new URL(`https://${host}`).hostname;
|
|
||||||
if (hostnameFromUrl === requestDetails.ip || hostnameFromUrl === hostnameFromProxy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ifMainFrame = requestDetails.type === 'main_frame';
|
|
||||||
|
|
||||||
previousUpdateTitleFinished = previousUpdateTitleFinished.then(
|
|
||||||
() => new Promise(
|
|
||||||
(resolve) => {
|
|
||||||
const cb = () => updateTitle( requestDetails, host, resolve );
|
|
||||||
return ifMainFrame
|
|
||||||
? afterTabUpdated(requestDetails.tabId, cb) : cb();
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRequest = function onRequest(requestDetails) {
|
|
||||||
|
|
||||||
const ifInsideTabWithIp = requestDetails.tabId !== -1 && requestDetails.ip;
|
|
||||||
if (ifInsideTabWithIp) {
|
|
||||||
tryProxyAndInform(requestDetails);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
for(const eventName of ['onResponseStarted', 'onErrorOccurred']) {
|
|
||||||
chrome.webRequest[eventName].addListener(
|
|
||||||
onRequest,
|
|
||||||
{urls: ['<all_urls>']}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html style="display: none; will-change: contents, display">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Последние ошибки</title>
|
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
background-color: #f3f5f6;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
td, th {
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
a:not(:hover) {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
tr > td:first-child {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
main {
|
|
||||||
display: block-inline;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
h1,h2,h3,h4 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#addBtn {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h3>Список последних ошибок</h3>
|
|
||||||
Новые сверху, количество ограничено 20тью.
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Номер</th>
|
|
||||||
<th>Хост</th>
|
|
||||||
<th>Ошибка</th>
|
|
||||||
<th><a href id="allBtn">Все</a></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="errorsTable"></tbody>
|
|
||||||
</table>
|
|
||||||
<button id="addBtn">Добавить в исключения</button>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,79 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
chrome.runtime.getBackgroundPage( (bgWindow) =>
|
|
||||||
bgWindow.apis.errorHandlers.installListenersOn(
|
|
||||||
window, 'LERR', () => {
|
|
||||||
|
|
||||||
const tbody = document.getElementById('errorsTable');
|
|
||||||
const errors = bgWindow.apis.lastNetErrors.get().map(
|
|
||||||
({url, error}, index) => ({ message: error, hostname: new URL(url).hostname, ifChecked: false })
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderTbody = () => {
|
|
||||||
|
|
||||||
const exc = bgWindow.apis.pacKitchen.getPacMods().exceptions || {};
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
if (!errors.length) {
|
|
||||||
tbody.innerHTML = '<tr><td colspan="4">Ошибок пока не было.</td></tr>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
errors.forEach((err, index) => {
|
|
||||||
|
|
||||||
const ifProxy = exc[err.hostname];
|
|
||||||
let style = '';
|
|
||||||
if (ifProxy !== undefined) {
|
|
||||||
style = `style="color: ${ifProxy ? 'green' : 'red' }"`;
|
|
||||||
}
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.innerHTML = `
|
|
||||||
<td>${index}</td>
|
|
||||||
<td ${style}>${err.hostname}</td>
|
|
||||||
<td>${err.message}</td>
|
|
||||||
<td><input type="checkbox" ${ err.ifChecked ? 'checked' : '' }></td>
|
|
||||||
`;
|
|
||||||
tr.querySelector('input').onchange = function() {
|
|
||||||
|
|
||||||
errors[index].ifChecked = this.checked;
|
|
||||||
return false;
|
|
||||||
|
|
||||||
};
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('allBtn').onclick = () => {
|
|
||||||
|
|
||||||
const ifAllChecked = errors.every((err) => err.ifChecked);
|
|
||||||
if (ifAllChecked) {
|
|
||||||
errors.forEach((err) => { err.ifChecked = false; })
|
|
||||||
} else {
|
|
||||||
errors.forEach((err) => { err.ifChecked = true; })
|
|
||||||
}
|
|
||||||
renderTbody();
|
|
||||||
return false;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('addBtn').onclick = () => {
|
|
||||||
|
|
||||||
const mutatedMods = bgWindow.apis.pacKitchen.getPacMods();
|
|
||||||
const exc = mutatedMods.exceptions || {};
|
|
||||||
mutatedMods.exceptions = errors.reduce((acc, err) => {
|
|
||||||
|
|
||||||
if (err.ifChecked) {
|
|
||||||
acc[err.hostname] = true;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
|
|
||||||
}, exc);
|
|
||||||
bgWindow.apis.pacKitchen.keepCookedNowAsync(mutatedMods, (err) => alert(err || 'Сделано!'));
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
renderTbody();
|
|
||||||
document.documentElement.style.display = '';
|
|
||||||
|
|
||||||
})
|
|
||||||
);
|
|
|
@ -1,32 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Сообщить об ошибке прокси-сервера</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
font-size: 1.3em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>Сообщить об ошибке прокси-сервера</h1>
|
|
||||||
Перешлите администратору вашего прокси следующее:
|
|
||||||
<fieldset>
|
|
||||||
<pre id="errorInfo"></pre>
|
|
||||||
</fieldset>
|
|
||||||
Вот известные нам электронные адреса для популярных прокси-серверов (кликните по email для открытия шаблона письма):
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
Только если вы используете <a
|
|
||||||
href="https://antizapret.prostovpn.org">Антизапрет</a>:
|
|
||||||
<a
|
|
||||||
href="mailto:antizapret@prostovpn.org">antizapret@prostovpn.org</a>.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
<script src="../lib/keep-links-clickable.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,39 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
chrome.runtime.getBackgroundPage( (bgWindow) =>
|
|
||||||
bgWindow.apis.errorHandlers.installListenersOn(
|
|
||||||
window, 'PRERR', () => {
|
|
||||||
|
|
||||||
const params = new URLSearchParams(location.search.substr(1));
|
|
||||||
const requestFailedTo = params.get('requestFailedTo');
|
|
||||||
const fromPageHref = params.get('fromPageHref') || requestFailedTo;
|
|
||||||
|
|
||||||
const acr = bgWindow.apis.antiCensorRu;
|
|
||||||
const pacKey = acr.getCurrentPacProviderKey();
|
|
||||||
const pacModTime = acr.getLastModifiedForKey(pacKey);
|
|
||||||
|
|
||||||
const errorReport = `
|
|
||||||
Your proxy blocked the following request:
|
|
||||||
* Request was from page: ${fromPageHref}
|
|
||||||
* To address: ${requestFailedTo}
|
|
||||||
* Used PAC-script: ${pacKey}
|
|
||||||
* Its Last-Modified HTTP-header: ${pacModTime}
|
|
||||||
I think it's a mistake! Could you, please, take action to fix it.
|
|
||||||
Thank you!
|
|
||||||
|
|
||||||
Ваш прокси-сервер заблокировал следующий запрос:
|
|
||||||
* Запрос был со страницы: ${fromPageHref}
|
|
||||||
* Адрес запроса: ${requestFailedTo}
|
|
||||||
* Мой PAC-скрипт: ${pacKey}
|
|
||||||
* Его HTTP-заголовок Last-Modified: ${pacModTime}
|
|
||||||
Я думаю, это произошло по ошибке! Пожалуйста, примите действия для её исправления.
|
|
||||||
Спасибо!
|
|
||||||
`.trim();
|
|
||||||
errorInfo.innerText = errorReport;
|
|
||||||
document.querySelectorAll('a[href^="mailto:"]').forEach((a) => {
|
|
||||||
|
|
||||||
a.href = `${a.href}?subject=${encodeURIComponent(new URL(requestFailedTo).hostname)} TUNNEL_CONNECTION_FAILED&body=${encodeURIComponent(errorReport)}`;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
|
@ -1,8 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
window.apis.version.ifMini = true;
|
|
||||||
chrome.browserAction.setBadgeText({text: 'M'});
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const pacUrls = [
|
|
||||||
// GitHub.io (anticensority), cached:
|
|
||||||
'https://anticensority.github.io/generated-pac-scripts/anticensority.pac',
|
|
||||||
// GitHub repo (anticensority), cached:
|
|
||||||
'https://raw.githubusercontent.com/anticensority/generated-pac-scripts/master/anticensority.pac',
|
|
||||||
];
|
|
||||||
|
|
||||||
const commonContext = {
|
|
||||||
version: '1.66',
|
|
||||||
anticensorityPacUrls: [
|
|
||||||
...pacUrls,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.contexts = {};
|
|
||||||
|
|
||||||
const extra_permissions = ', "webRequest", "webRequestBlocking", "webNavigation"';
|
|
||||||
|
|
||||||
exports.contexts.full = Object.assign({}, commonContext, {
|
|
||||||
versionSuffix: '',
|
|
||||||
nameSuffixEn: '',
|
|
||||||
nameSuffixRu: '',
|
|
||||||
extra_permissions,
|
|
||||||
persistent: '',
|
|
||||||
scripts_0x: '',
|
|
||||||
scripts_2x: ', "20-ip-to-host-api.js"',
|
|
||||||
scripts_8x: ', "80-error-menu.js", "83-last-errors.js", "85-block-informer.js"',
|
|
||||||
});
|
|
||||||
|
|
||||||
exports.contexts.mini = Object.assign({}, commonContext, {
|
|
||||||
versionSuffix: '-mini',
|
|
||||||
nameSuffixEn: ' MINI',
|
|
||||||
nameSuffixRu: ' МИНИ',
|
|
||||||
extra_permissions: '',
|
|
||||||
persistent: '"persistent": false,',
|
|
||||||
scripts_0x: '',
|
|
||||||
scripts_2x: ', "20-for-mini-only.js"',
|
|
||||||
scripts_8x: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
exports.contexts.firefox = Object.assign({}, commonContext, {
|
|
||||||
versionSuffix: '',
|
|
||||||
nameSuffixEn: '',
|
|
||||||
nameSuffixRu: '',
|
|
||||||
extra_permissions,
|
|
||||||
persistent: '',
|
|
||||||
scripts_0x: ', "01-chrome-proxy-settings.js"',
|
|
||||||
scripts_2x: ', "20-ip-to-host-api.js"',
|
|
||||||
scripts_8x: ', "80-error-menu.js", "83-last-errors.js", "85-block-informer.js"',
|
|
||||||
});
|
|
||||||
|
|
||||||
exports.contexts.beta = Object.assign({}, commonContext, {
|
|
||||||
anticensorityPacUrls: [
|
|
||||||
'https://raw.githubusercontent.com/anticensority/for-testing/master/anticensority.pac',
|
|
||||||
'https://anticensority.github.io/for-testing/anticensority.pac',
|
|
||||||
],
|
|
||||||
version: '1.14',
|
|
||||||
versionSuffix: '',
|
|
||||||
nameSuffixEn: ' FOR TESTING',
|
|
||||||
nameSuffixRu: ' ДЛЯ ТЕСТОВ',
|
|
||||||
extra_permissions,
|
|
||||||
persistent: '',
|
|
||||||
scripts_0x: '',
|
|
||||||
scripts_2x: ', "20-ip-to-host-api.js"',
|
|
||||||
scripts_8x: ', "80-error-menu.js", "83-last-errors.js", "85-block-informer.js"',
|
|
||||||
});
|
|
|
@ -1,10 +0,0 @@
|
||||||
'ust strict';
|
|
||||||
|
|
||||||
module.exports = (parentModule) => function cachelessRequire(path) {
|
|
||||||
|
|
||||||
for(let key of Object.keys(require.cache)) {
|
|
||||||
delete require.cache[key];
|
|
||||||
}
|
|
||||||
return parentModule.require(path);
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,48 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const Sinon = require('sinon');
|
|
||||||
|
|
||||||
module.exports = function storageMock() {
|
|
||||||
|
|
||||||
let storage = {};
|
|
||||||
|
|
||||||
return new Proxy({
|
|
||||||
setItem: Sinon.spy(function(key, value) {
|
|
||||||
storage[key] = value || '';
|
|
||||||
}),
|
|
||||||
getItem: Sinon.spy(function(key) {
|
|
||||||
return key in storage ? storage[key] : null;
|
|
||||||
}),
|
|
||||||
removeItem: Sinon.spy(function(key) {
|
|
||||||
delete storage[key];
|
|
||||||
}),
|
|
||||||
get length() {
|
|
||||||
return Object.keys(storage).length;
|
|
||||||
},
|
|
||||||
key: Sinon.spy(function(i) {
|
|
||||||
throw new Error('Not implemented!');
|
|
||||||
}),
|
|
||||||
clear: Sinon.spy(function() {
|
|
||||||
storage = {};
|
|
||||||
}),
|
|
||||||
}, {
|
|
||||||
get: function(target, name) {
|
|
||||||
|
|
||||||
if (name in target) {
|
|
||||||
return target[name];
|
|
||||||
}
|
|
||||||
return target.getItem(name);
|
|
||||||
|
|
||||||
},
|
|
||||||
set: function(target, prop, value) {
|
|
||||||
|
|
||||||
if (prop in target) {
|
|
||||||
target[prop] = value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return target.setItem(prop, value);
|
|
||||||
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
48
manifests/base.json.mjs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
export const render = ({ version, edition }) => {
|
||||||
|
let localizedSuffix;
|
||||||
|
switch (edition) {
|
||||||
|
case 'main':
|
||||||
|
localizedSuffix = '';
|
||||||
|
break;
|
||||||
|
case 'test':
|
||||||
|
localizedSuffix = ` __MSG_FOR_TESTING__`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new TypeError(`Edition type ${edition} is unknown.`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"manifest_version": 3,
|
||||||
|
|
||||||
|
"default_locale": "en",
|
||||||
|
"name": `__MSG_ExtensionName__${localizedSuffix}`,
|
||||||
|
"version": `${version}`,
|
||||||
|
"description": "__MSG_ExtensionDescription__",
|
||||||
|
"homepage_url": "https://github.com/anticensority/runet-censorship-bypass",
|
||||||
|
"icons": {
|
||||||
|
"128": "/icons/default-128.png"
|
||||||
|
},
|
||||||
|
"author": "anticensority+owners@googlegroups.com",
|
||||||
|
"permissions": [
|
||||||
|
"activeTab"
|
||||||
|
, "contextMenus"
|
||||||
|
, "storage"
|
||||||
|
, "clipboardWrite"
|
||||||
|
],
|
||||||
|
|
||||||
|
"action": {
|
||||||
|
"default_title":
|
||||||
|
`__MSG___ | __MSG_Version__: ${version + localizedSuffix}`,
|
||||||
|
"default_popup": "/src/pages/options/index.html"
|
||||||
|
},
|
||||||
|
"options_ui": {
|
||||||
|
"page": "/src/pages/options/index.html"
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"_execute_action": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+P"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
16
manifests/chromium.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"permissions": [
|
||||||
|
"offscreen"
|
||||||
|
, "activeTab"
|
||||||
|
, "contextMenus"
|
||||||
|
, "storage"
|
||||||
|
, "clipboardWrite"
|
||||||
|
],
|
||||||
|
|
||||||
|
"minimum_chrome_version": "88.0.0.0",
|
||||||
|
|
||||||
|
"background": {
|
||||||
|
"service_worker": "/src/bg/index.mjs",
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
|
}
|
13
manifests/firefox.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"strict_min_version": "91.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"background": {
|
||||||
|
"scripts": ["/src/bg/index.mjs"],
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
|
}
|