From 2fdc13bfe6e5081fb46dd5cfea26ab20dfed9b36 Mon Sep 17 00:00:00 2001 From: Renan Bernordi <altendorfme@gmail.com> Date: Fri, 22 Nov 2024 17:41:07 -0300 Subject: [PATCH] commit inicial --- .github/workflows/release.yml | 87 +++++ .gitignore | 93 +++++ Dockerfile | 28 ++ LICENSE.md | 674 +++++++++++++++++++++++++++++++++ README.md | 115 ++++++ TEST_URLS.md | 49 +++ app/.env.sample | 16 + app/api.php | 109 ++++++ app/assets/js/scripts.js | 67 ++++ app/assets/opengraph.png | Bin 0 -> 7367 bytes app/assets/svg/archive.svg | 1 + app/assets/svg/bookmark.svg | 1 + app/assets/svg/bypass.svg | 1 + app/assets/svg/code.svg | 1 + app/assets/svg/error.svg | 1 + app/assets/svg/link.svg | 1 + app/assets/svg/marreta.svg | 1 + app/assets/svg/search.svg | 1 + app/assets/svg/warning.svg | 1 + app/composer.json | 5 + app/config.php | 196 ++++++++++ app/inc/Cache.php | 78 ++++ app/inc/Rules.php | 352 +++++++++++++++++ app/inc/URLAnalyzer.php | 686 ++++++++++++++++++++++++++++++++++ app/index.php | 205 ++++++++++ app/p.php | 65 ++++ default.conf | 43 +++ docker-compose.yml | 19 + env.sh | 19 + start.sh | 55 +++ 30 files changed, 2970 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 TEST_URLS.md create mode 100644 app/.env.sample create mode 100644 app/api.php create mode 100644 app/assets/js/scripts.js create mode 100644 app/assets/opengraph.png create mode 100644 app/assets/svg/archive.svg create mode 100644 app/assets/svg/bookmark.svg create mode 100644 app/assets/svg/bypass.svg create mode 100644 app/assets/svg/code.svg create mode 100644 app/assets/svg/error.svg create mode 100644 app/assets/svg/link.svg create mode 100644 app/assets/svg/marreta.svg create mode 100644 app/assets/svg/search.svg create mode 100644 app/assets/svg/warning.svg create mode 100644 app/composer.json create mode 100644 app/config.php create mode 100644 app/inc/Cache.php create mode 100644 app/inc/Rules.php create mode 100644 app/inc/URLAnalyzer.php create mode 100644 app/index.php create mode 100644 app/p.php create mode 100644 default.conf create mode 100644 docker-compose.yml create mode 100644 env.sh create mode 100644 start.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..74cb9a9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,87 @@ +name: 🛠️ Main +run-name: 🚀 Deploy de versão + +on: + push: + tags: + - '*.*.*' + +env: + DOCKER_REGISTRY: ghcr.io + DOCKER_IMAGE_NAME: ${{ github.repository }} + +jobs: + docker-build: + name: 🐳 Build e Push + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: 📥 Checkout código + uses: actions/checkout@v4 + + - name: 🏷️ Extrair versão da tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: 🔧 Configurar QEMU + uses: docker/setup-qemu-action@v3 + + - name: 🛠️ Configurar Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64,linux/arm64,linux/arm/v7 + + - name: 📋 Extrair metadata Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: 🔐 Login no Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: 🏗️ Build e Push + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + publish-release: + name: 📦 Publicar Release + runs-on: ubuntu-latest + needs: docker-build + permissions: + contents: write + + steps: + - name: 📥 Checkout código + uses: actions/checkout@v4 + + - name: 🏷️ Extrair versão da tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: 📝 Criar Release + uses: softprops/action-gh-release@v1 + with: + name: "🎉 Release v${{ steps.get_version.outputs.VERSION }}" + tag_name: ${{ steps.get_version.outputs.VERSION }} + generate_release_notes: true + draft: false + prerelease: false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13b3549 --- /dev/null +++ b/.gitignore @@ -0,0 +1,93 @@ +vendor/ +composer.lock +.env +app/logs/*.log +app/cache/*.html +TODO.md + +# Created by https://www.toptal.com/developers/gitignore/api/composer,windows,macos,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=composer,windows,macos,linux + +### Composer ### +composer.phar +/vendor/ + +# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control +# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file +# composer.lock + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/composer,windows,macos,linux \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..50f3342 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM php:8.0-fpm + +RUN apt-get update && apt-get install -y nginx nano procps unzip git htop + +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + +COPY default.conf /etc/nginx/sites-available/default + +RUN mkdir -p /app +COPY app/ /app/ + +WORKDIR /app +RUN composer install --no-interaction --optimize-autoloader + +COPY env.sh /usr/local/bin/env.sh +RUN chmod +x /usr/local/bin/env.sh + +COPY start.sh /usr/local/bin/start.sh +RUN chmod +x /usr/local/bin/start.sh + +RUN mkdir -p /app/cache /app/logs +RUN chown -R www-data:www-data /app && chmod -R 755 /app + +VOLUME ["/app/cache", "/app/logs"] + +EXPOSE 80 + +CMD ["/bin/bash", "-c", "/usr/local/bin/env.sh && /usr/local/bin/start.sh"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e72bfdd --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +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 +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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 <https://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: + + <program> Copyright (C) <year> <name of author> + 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 +<https://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 +<https://www.gnu.org/licenses/why-not-lgpl.html>. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..041b0ca --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# 🛠️ Marreta + +[](https://github.com/manualdousuario/marreta/blob/master/README.md) + +Marreta é uma ferramenta para analisar URLs e acessar conteúdo na web sem dor de cabeça. + +## ✨ O que tem de legal? + +- Limpa e arruma URLs automaticamente +- Remove parâmetros chatos de rastreamento +- Força HTTPS pra manter tudo seguro +- Troca de user agent pra evitar bloqueios +- DNS esperto +- Deixa o HTML limpinho e otimizado +- Conserta URLs relativas sozinho +- Permite colocar seus próprios estilos +- Remove elementos indesejados +- Cache, cache! +- Bloqueia domínios que você não quer +- Permite configurar headers e cookies do seu jeito +- Tudo com SSL/TLS +- PHP-FPM +- OPcache ligado + +## 🐳 Docker + +### Antes de começar + +Só precisa ter instalado: +- Docker e docker compose + +### Produção + +`curl -o ./docker-compose.yml https://raw.githubusercontent.com/manualdousuario/marreta/main/docker-compose.yml` + +Se necessario + +`nano docker-compose.yml` + +``` +services: + marreta: + container_name: marreta + image: ghcr.io/manualdousuario/marreta/marreta:latest + ports: + - "80:80" + environment: + - SITE_NAME= + - SITE_DESCRIPTION= + - SITE_URL= + - DNS_SERVERS= +``` + +- `SITE_NAME`: Nome do seu Marreta +- `SITE_DESCRIPTION`: Conta pra que serve +- `SITE_URL`: Onde vai rodar, endereço completo com `https://` +- `DNS_SERVERS`: Quais servidores DNS usar `94.140.14.14, 94.140.15.15` + +Agora pode rodar `docker compose up -d` + +#### Desenvolvimento + +1. Primeiro, clona o projeto: +```bash +git clone https://github.com/manualdousuario/marreta/ +cd marreta +``` + +2. Cria o arquivo de configuração: +```bash +cp app/.env.sample app/.env +``` + +3. Configura do seu jeito no `app/.env`: +```env +SITE_NAME="Marreta" +SITE_DESCRIPTION="Sua arma secreta contra sites sovinas!" +SITE_URL=http://localhost +DNS_SERVERS=94.140.14.14, 94.140.15.15 +``` + +4. Roda tudo: +```bash +docker-compose up -d +``` + +Pronto! Vai estar rodando em `http://localhost` 🎉 + +## ⚙️ Personalizando + +No `Rules.php` você pode configurar regras diferentes pra cada site e regras globais + +Em `config.php` você tem a lista os sites que não quer permitir ou não permitem extrair dados e configurações de User Agents + +## 🛠️ Manutenção + +### Logs + +Ver o que tá acontecendo: +```bash +docker-compose logs app +``` + +### Limpando o cache + +Quando precisar limpar: +```bash +docker-compose exec app rm -rf /app/cache/* +``` + +--- + +Feito com ❤️! Se tiver dúvidas ou sugestões, abre uma issue que a gente ajuda! 😉 + +Instancia publica em [marreta.pcdomanual.com](https://marreta.pcdomanual.com)! diff --git a/TEST_URLS.md b/TEST_URLS.md new file mode 100644 index 0000000..0ed8e92 --- /dev/null +++ b/TEST_URLS.md @@ -0,0 +1,49 @@ +# Bloqueados +wsj.com +bloomberg.com +piaui.folha.uol.com.br +jota.info +haaretz.com +haaretz.co.il +washingtonpost.com +gauchazh.clicrbs.com.br +economist.com + +# Testados: +## Brasil +https://www1.folha.uol.com.br/poder/2024/11/justica-argentina-emite-mandados-de-prisao-contra-61-foragidos-do-81.shtml +https://g1.globo.com/politica/noticia/2024/11/20/pf-devera-concluir-inquerito-contra-atos-do-8-de-janeiro-nesta-semana.ghtml +https://www.estadao.com.br/politica/planalto-entregou-para-pf-acesso-a-computadores-de-grupo-que-tentou-matar-lula-alckmin-e-moraes/ +https://www.correio24horas.com.br/minha-bahia/bahia-registra-primeira-morte-por-coqueluche-em-cinco-anos-1124 +https://correio.rac.com.br/campinasermc/iphan-analisa-pedido-de-tombamento-emergencial-da-fazenda-santa-elisa-1.1591950 +https://veja.abril.com.br/politica/pt-pede-arquivamento-de-projeto-de-anistia-apos-pf-revelar-plano-para-matar-lula/ +https://super.abril.com.br/cultura/consciencia-negra-conheca-os-adinkras-simbolos-africanos-que-estao-no-seu-cotidiano/ +https://quatrorodas.abril.com.br/noticias/latinncap-c3aircross-corolla-teste-seguranca/#google_vignette +https://www.abcmais.com/policia/casa-e-destruida-por-incendio-em-novo-hamburgo-2/ +https://www.agazeta.com.br/es/cotidiano/negros-sao-a-maioria-entre-pobres-e-analfabetos-no-es-aponta-estudo-1124 +https://www.gazetadopovo.com.br/republica/moraes-nao-tem-condicoes-de-julgar-suposto-plano-golpista-diz-rogerio-marinho/ +https://revistagalileu.globo.com/colunistas/the-conversation/noticia/2024/11/por-que-nos-sentimos-melhor-quando-acordamos-sem-despertador-neurologista-explica.ghtml +https://oglobo.globo.com/epoca/noticia/2024/09/07/moeda-rara-do-seculo-xviii-encontrada-dentro-de-lata-de-caramelo-sera-leiloada-na-inglaterra-e-pode-valer-ate-r-220-mil.ghtml +https://valor.globo.com/financas/esg/noticia/2024/11/18/anbima-propoe-novas-regras-para-ofertas-de-titulos-de-renda-fixa-sustentaveis.ghtml +https://www.seudinheiro.com/2024/patrocinado/conteudo-eqi/fii-arrecada-r-137-milhoes-com-venda-de-galpoes-e-e-recomendado-por-analistas-veja/ +https://www.nsctotal.com.br/noticias/veja-os-bairros-mais-populosos-de-florianopolis-segundo-o-ibge +https://exame.com/invest/mercados/nvidia-bate-consenso-mais-uma-vez-e-reporta-lucro-de-us-193-bilhoes-no-terceiro-trimestre/ +https://www.dgabc.com.br/Noticia/4177263/casa-de-acolhimento-a-mulheres-vitimas-de-violencia-e-invadida-em-sto-andre +https://diarinho.net/materia/657650/Gasolina-subiu-mais-de-R--0-50-desde-janeiro--veja-os-precos-da-pesquisa- +https://www.em.com.br/gerais/2024/11/6993175-incendio-fecha-shopping-na-regiao-centro-sul-de-bh.html +https://mais.opovo.com.br/colunistas/ariadne-araujo/2024/11/20/quem-quer-casar-com-um-heroi-de-guerra-ucraniano.html +https://www.folhadelondrina.com.br/folha-2/cena-musical-em-londrina-idosos-no-palco-e-na-plateia-3267044e.html?d=1 +https://www.uol.com.br/esporte/futebol/ultimas-noticias/2024/11/20/bahia-x-palmeiras-campeonato-brasileiro-2024-rodada-34.htm +https://www.opovo.com.br/noticias/politica/2024/11/21/atuacao-de-faccoes-nas-eleicoes-teve-compra-de-votos-e-ate-enviado-do-rj-para-intimidacoes-no-ceara.html + +## Validar + +https://www1-folha-uol-com-br.cdn.ampproject.org/v/s/www1.folha.uol.com.br/amp/colunas/tatibernardi/2024/10/e-se-amo-tanto-que-amo-errado.shtml + +## Internacional +https://www.nytimes.com/2024/11/20/us/politics/matt-gaetz-venmo-payments-sex.html +https://www.ft.com/content/cab2e834-e5fa-4dd0-b757-76c243ac2eeb +https://foreignpolicy.com/2024/11/20/trump-energy-policy-electric-vehicles-power-grid-infrastructure-oil-gas-climate/ +https://www.wired.com/story/waymo-robotaxi-driverless-future/ +https://www.forbes.com/sites/cyrusfarivar/2024/11/20/under-trump-tariffs-made-in-vietnam-will-be-the-new-made-in-china/ +https://observador.pt/especiais/mais-formacao-investimento-na-frota-reforco-dos-tecnicos-de-emergencia-e-autonomia-o-que-precisa-o-inem-segundo-varios-especialistas/ \ No newline at end of file diff --git a/app/.env.sample b/app/.env.sample new file mode 100644 index 0000000..3069463 --- /dev/null +++ b/app/.env.sample @@ -0,0 +1,16 @@ +# Arquivo de exemplo para configuração de variáveis de ambiente +# Copie este arquivo para .env e ajuste os valores conforme necessário + +# Nome do site exibido no cabeçalho e meta tags +SITE_NAME=Marreta + +# Descrição do site usada em meta tags e SEO +SITE_DESCRIPTION="Sua arma secreta contra sites sovinas!" + +# URL base do site (sem barra no final) +# Use https://localhost para desenvolvimento local +SITE_URL=https://localhost + +# Lista de servidores DNS para resolução de domínios +# Recomendado: AdGuard DNS (94.140.14.14, 94.140.15.15) +DNS_SERVERS='94.140.14.14,94.140.15.15' diff --git a/app/api.php b/app/api.php new file mode 100644 index 0000000..509ad6d --- /dev/null +++ b/app/api.php @@ -0,0 +1,109 @@ +<?php +/** + * API para análise de URLs + * + * Este arquivo implementa um endpoint REST que recebe URLs via GET + * e retorna resultados processados em formato JSON. + * + * Funcionalidades: + * - Validação de URLs + * - Análise de conteúdo + * - Tratamento de erros + * - Suporte a CORS + */ + +require_once 'config.php'; +require_once 'inc/URLAnalyzer.php'; + +// Define o tipo de conteúdo como JSON +header('Content-Type: application/json'); + +// Habilita CORS (Cross-Origin Resource Sharing) +header('Access-Control-Allow-Origin: *'); +header('Access-Control-Allow-Methods: GET'); + +// Obtém a URL da requisição a partir do path +$path = $_SERVER['REQUEST_URI']; +$prefix = '/api/'; + +if (strpos($path, $prefix) === 0) { + $url = urldecode(substr($path, strlen($prefix))); + + /** + * Função para enviar resposta JSON padronizada + * + * @param array $data Dados a serem enviados na resposta + * @param int $statusCode Código de status HTTP + */ + function sendResponse($data, $statusCode = 200) { + http_response_code($statusCode); + $response = [ + 'status' => $statusCode + ]; + + if (isset($data['error'])) { + $response['error'] = $data['error']; + } else if (isset($data['url'])) { + $response['url'] = $data['url']; + } + + echo json_encode($response); + exit; + } + + // Validação básica da URL + if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) { + sendResponse([ + 'error' => [ + 'code' => 'INVALID_URL', + 'message' => MESSAGES['INVALID_URL']['message'] + ] + ], 400); + } + + try { + // Instancia o analisador de URLs + $analyzer = new URLAnalyzer(); + + // Tenta analisar a URL fornecida + $analyzer->analyze($url); + + // Se a análise for bem-sucedida, retorna a URL processada + sendResponse([ + 'url' => SITE_URL . '/p/' . $url + ], 200); + + } catch (Exception $e) { + // Tratamento de erros com mapeamento para códigos HTTP apropriados + $message = $e->getMessage(); + $statusCode = 400; + $errorCode = 'GENERIC_ERROR'; + + // Mapeia a mensagem de erro para o código e status apropriados + foreach (MESSAGES as $key => $value) { + if (strpos($message, $value['message']) !== false) { + $statusCode = ($value['type'] === 'error') ? 400 : 503; + $errorCode = $key; + break; + } + } + + // Adiciona header de erro para melhor tratamento no cliente + header('X-Error-Message: ' . $message); + + sendResponse([ + 'error' => [ + 'code' => $errorCode, + 'message' => $message + ] + ], $statusCode); + } +} else { + // Retorna erro 404 para endpoints não encontrados + sendResponse([ + 'error' => [ + 'code' => 'NOT_FOUND', + 'message' => MESSAGES['NOT_FOUND']['message'] + ] + ], 404); +} diff --git a/app/assets/js/scripts.js b/app/assets/js/scripts.js new file mode 100644 index 0000000..a4b49ea --- /dev/null +++ b/app/assets/js/scripts.js @@ -0,0 +1,67 @@ +/** + * Funções JavaScript para validação de formulário e manipulação de erros + */ + +/** + * Valida o formulário antes do envio + * + * Verifica: + * - Se a URL não está vazia + * - Se a URL começa com http:// ou https:// + * - Se a URL tem um formato válido + * + * @returns {boolean} True se a URL for válida, False caso contrário + */ +function validateForm() { + const urlInput = document.getElementById('url'); + const url = urlInput.value.trim(); + + // Verifica se a URL não está vazia + if (!url) { + showError('Por favor, insira uma URL'); + return false; + } + + // Verifica se a URL começa com http:// ou https:// + if (!/^https?:\/\//i.test(url)) { + showError('A URL deve começar com http:// ou https://'); + return false; + } + + // Tenta criar um objeto URL para validar o formato + try { + new URL(url); + } catch (e) { + showError('Formato de URL inválido'); + return false; + } + + return true; +} + +/** + * Exibe uma mensagem de erro abaixo do formulário + * + * Remove qualquer mensagem de erro existente antes de exibir a nova. + * A mensagem é exibida com um ícone de erro e formatação em vermelho. + * + * @param {string} message Mensagem de erro a ser exibida + */ +function showError(message) { + const form = document.getElementById('urlForm'); + const existingError = form.querySelector('.error-message'); + + // Remove mensagem de erro anterior se existir + if (existingError) { + existingError.remove(); + } + + // Cria e adiciona nova mensagem de erro + const errorDiv = document.createElement('div'); + errorDiv.className = 'error-message mt-4 text-base text-red-600'; + errorDiv.innerHTML = ` + <img src="assets/svgs/error.svg" class="inline-block w-5 h-5 mr-2" alt="Error icon"> + ${message}`; + + form.appendChild(errorDiv); +} diff --git a/app/assets/opengraph.png b/app/assets/opengraph.png new file mode 100644 index 0000000000000000000000000000000000000000..3374a71460352140886db03ea69938ca5966532b GIT binary patch literal 7367 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 O7OmiRW)Dq}xc>ojyi8>P literal 0 HcmV?d00001 diff --git a/app/assets/svg/archive.svg b/app/assets/svg/archive.svg new file mode 100644 index 0000000..bf61a2a --- /dev/null +++ b/app/assets/svg/archive.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#3B82F6" d="M75 75L41 41C25.9 25.9 0 36.6 0 57.9V168c0 13.3 10.7 24 24 24H134.1c21.4 0 32.1-25.9 17-41l-34-34c41.4-41.4 98.6-67 161.9-67c95.2 0 174.2 69.3 189.4 160.3c2.2 13 14.3 21.7 27.4 19.5s21.7-14.3 19.5-27.4C497.3 102.7 395.2 16 279 16C196.5 16 121.7 47.4 75 75zm367 367l34 34c15.1 15.1 41 4.4 41-16.9V349c0-13.3-10.7-24-24-24H377.9c-21.4 0-32.1 25.9-17 41l34 34c-41.4 41.4-98.6 67-161.9 67c-95.2 0-174.2-69.3-189.4-160.3c-2.2-13-14.3-21.7-27.4-19.5s-21.7 14.3-19.5 27.4C14.7 409.3 116.8 496 233 496c82.5 0 157.3-31.4 204-59z"/></svg> diff --git a/app/assets/svg/bookmark.svg b/app/assets/svg/bookmark.svg new file mode 100644 index 0000000..ae5aa25 --- /dev/null +++ b/app/assets/svg/bookmark.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="#FBBF24" d="M0 48C0 21.5 21.5 0 48 0l0 48V441.4l130.1-92.9c8.3-6 19.6-6 27.9 0L336 441.4V48H48V0H336c26.5 0 48 21.5 48 48V488c0 9-5 17.2-13 21.3s-17.6 3.4-24.9-1.8L192 397.5 37.9 507.5c-7.3 5.2-16.9 5.9-24.9 1.8S0 497 0 488V48z"/></svg> diff --git a/app/assets/svg/bypass.svg b/app/assets/svg/bypass.svg new file mode 100644 index 0000000..278657f --- /dev/null +++ b/app/assets/svg/bypass.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="#10B981" d="M128 32H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h64v64c0 17.7 14.3 32 32 32s32-14.3 32-32V96c0-35.3-28.7-64-64-64zM448 32h96c17.7 0 32 14.3 32 32s-14.3 32-32 32H480v64c0 17.7-14.3 32-32 32s-32-14.3-32-32V96c0-35.3 28.7-64 64-64zM448 480h96c17.7 0 32 14.3 32 32s-14.3 32-32 32H448c-35.3 0-64-28.7-64-64V416c0-17.7 14.3-32 32-32s32 14.3 32 32v64zM128 480H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c35.3 0 64-28.7 64-64V416c0-17.7-14.3-32-32-32s-32 14.3-32 32v64zM320 256a64 64 0 1 0 -128 0 64 64 0 1 0 128 0zM209.1 359.2L187.7 376h200.5l-21.4-16.8c-1.4-1.1-2.7-2.3-3.9-3.6l-48-48c-7.9-7.9-20.7-7.9-28.6 0l-48 48c-1.2 1.2-2.5 2.4-3.9 3.6zM576 208c0-79.5-64.5-144-144-144s-144 64.5-144 144c0 27.7 7.8 53.5 21.3 75.5c-1.6-1.3-3.3-2.5-4.9-3.8L99.5 80.4c-10.1-8.1-24.9-6.4-32.9 3.7s-6.4 24.9 3.7 32.9L459.2 416.7c.2 .2 .4 .3 .6 .5c9.6 7.8 23.8 6.4 31.6-3.2c40.2-49.6 40.2-122.5 0-172.1c9.7-18.8 15.2-40.1 15.2-62.7L608.8 288.8c10.1 8.1 24.9 6.4 32.9-3.7s6.4-24.9-3.7-32.9L576 208z"/></svg> diff --git a/app/assets/svg/code.svg b/app/assets/svg/code.svg new file mode 100644 index 0000000..4e37657 --- /dev/null +++ b/app/assets/svg/code.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="#6B7280" d="M392.8 1.2c-17-4.9-34.7 5-39.6 22l-128 448c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l128-448c4.9-17-5-34.7-22-39.6zm80.6 120.1c-12.5 12.5-12.5 32.8 0 45.3L562.7 256l-89.4 89.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l112-112c12.5-12.5 12.5-32.8 0-45.3l-112-112c-12.5-12.5-32.8-12.5-45.3 0zm-306.7 0c-12.5-12.5-32.8-12.5-45.3 0l-112 112c-12.5 12.5-12.5 32.8 0 45.3l112 112c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256l89.4-89.4c12.5-12.5 12.5-32.8 0-45.3z"/></svg> diff --git a/app/assets/svg/error.svg b/app/assets/svg/error.svg new file mode 100644 index 0000000..3be2e0b --- /dev/null +++ b/app/assets/svg/error.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#EF4444" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h80c13.3 0 24-10.7 24-24s-10.7-24-24-24h-80c-13.3 0-24 10.7-24 24s10.7 24 24 24zm-24-72h128c13.3 0 24-10.7 24-24s-10.7-24-24-24H192c-13.3 0-24 10.7-24 24s10.7 24 24 24z"/></svg> diff --git a/app/assets/svg/link.svg b/app/assets/svg/link.svg new file mode 100644 index 0000000..a3fc594 --- /dev/null +++ b/app/assets/svg/link.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="#3B82F6" d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/></svg> diff --git a/app/assets/svg/marreta.svg b/app/assets/svg/marreta.svg new file mode 100644 index 0000000..867e0ea --- /dev/null +++ b/app/assets/svg/marreta.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="#3B82F6" d="M283.9 378.6l18.3-60.1c18-4.1 34.2-16 43.1-33.8l64-128c10.5-21.1 8.4-45.2-3.7-63.6l52.7-76.6c3.7-5.4 10.4-8 16.7-6.5s11.2 6.7 12.2 13.1l16.2 104.1 105.1-7.4c6.5-.5 12.7 3.1 15.5 9s1.8 12.9-2.6 17.8L550.1 224l71.3 77.5c4.4 4.8 5.5 11.9 2.6 17.8s-9 9.5-15.5 9l-105.1-7.4L487.3 425c-1 6.5-5.9 11.7-12.2 13.1s-13-1.1-16.7-6.5l-59.7-86.7-91.4 52.2c-5.7 3.3-12.8 2.7-17.9-1.4s-7.2-10.9-5.3-17.2zm28.3-101.7c-9.3 10.9-25.2 14.4-38.6 7.7l-65.9-32.9s0 0 0 0L122 208.8s0 0 0 0L17.7 156.6C1.9 148.7-4.5 129.5 3.4 113.7l40-80C48.8 22.8 59.9 16 72 16l120 0c5 0 9.9 1.2 14.3 3.4l78.2 39.1 81.8 40.9c15.8 7.9 22.2 27.1 14.3 42.9l-64 128c-1.2 2.4-2.7 4.6-4.4 6.6zM107.6 237.4l85.9 42.9L90.9 485.5c-11.9 23.7-40.7 33.3-64.4 21.5S-6.8 466.2 5.1 442.5L107.6 237.4z"/></svg> \ No newline at end of file diff --git a/app/assets/svg/search.svg b/app/assets/svg/search.svg new file mode 100644 index 0000000..e296446 --- /dev/null +++ b/app/assets/svg/search.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#FFFFFF" d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg> diff --git a/app/assets/svg/warning.svg b/app/assets/svg/warning.svg new file mode 100644 index 0000000..18801c7 --- /dev/null +++ b/app/assets/svg/warning.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#FBBF24" d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg> diff --git a/app/composer.json b/app/composer.json new file mode 100644 index 0000000..c5b7e79 --- /dev/null +++ b/app/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "vlucas/phpdotenv": "^5.6.1" + } +} diff --git a/app/config.php b/app/config.php new file mode 100644 index 0000000..381ac93 --- /dev/null +++ b/app/config.php @@ -0,0 +1,196 @@ +<?php +/** + * Arquivo de configuração principal + * + * Este arquivo contém todas as configurações globais do sistema, incluindo: + * - Carregamento de variáveis de ambiente + * - Definições de constantes do sistema + * - Configurações de segurança + * - Mensagens do sistema + * - Configurações de bots e user agents + * - Lista de domínios bloqueados + */ + +require_once __DIR__ . '/vendor/autoload.php'; + +// Carrega as variáveis de ambiente do arquivo .env +$dotenv = Dotenv\Dotenv::createImmutable(__DIR__); +$dotenv->load(); + +/** + * Configurações básicas do sistema + */ +define('SITE_NAME', isset($_ENV['SITE_NAME']) ? $_ENV['SITE_NAME'] : 'Marreta'); +define('SITE_DESCRIPTION', isset($_ENV['SITE_DESCRIPTION']) ? $_ENV['SITE_DESCRIPTION'] : 'Sua arma secreta contra sites sovinas!'); +define('SITE_URL', isset($_ENV['SITE_URL']) ? $_ENV['SITE_URL'] : 'https://' . $_SERVER['HTTP_HOST']); +define('MAX_ATTEMPTS', 3); // Número máximo de tentativas para acessar uma URL +define('DNS_SERVERS', isset($_ENV['DNS_SERVERS']) ? $_ENV['DNS_SERVERS'] : '94.140.14.14, 94.140.15.15'); +define('CACHE_DIR', __DIR__ . '/cache'); + +/** + * Mensagens do sistema + * + * Array associativo contendo todas as mensagens de erro e avisos + * que podem ser exibidas ao usuário durante a execução do sistema + */ +define('MESSAGES', [ + 'BLOCKED_DOMAIN' => [ + 'message' => 'Este domínio está bloqueado para extração.', + 'type' => 'error' + ], + 'DNS_FAILURE' => [ + 'message' => 'Falha ao resolver DNS para o domínio. Verifique se a URL está correta.', + 'type' => 'warning' + ], + 'HTTP_ERROR' => [ + 'message' => 'O servidor retornou um erro ao tentar acessar a página. Tente novamente mais tarde.', + 'type' => 'warning' + ], + 'CONNECTION_ERROR' => [ + 'message' => 'Erro ao conectar com o servidor. Verifique sua conexão e tente novamente.', + 'type' => 'warning' + ], + 'CONTENT_ERROR' => [ + 'message' => 'Não foi possível obter o conteúdo. Tente usar os serviços de arquivo.', + 'type' => 'warning' + ], + 'INVALID_URL' => [ + 'message' => 'Formato de URL inválido', + 'type' => 'error' + ], + 'NOT_FOUND' => [ + 'message' => 'Página não encontrada', + 'type' => 'error' + ], + 'GENERIC_ERROR' => [ + 'message' => 'Ocorreu um erro ao processar sua solicitação.', + 'type' => 'warning' + ] +]); + +/** + * Configurações dos bots + * + * Define os user agents e headers específicos para diferentes bots + * que podem ser utilizados para fazer requisições + */ +define('BOT_CONFIGS', [ + 'Googlebot' => [ + 'user_agent' => 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', + 'headers' => [ + 'From' => 'googlebot(at)googlebot.com', + 'X-Robots-Tag' => 'noindex' + ] + ], + 'Bingbot' => [ + 'user_agent' => 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)', + 'headers' => [ + 'From' => 'bingbot(at)microsoft.com', + 'X-Robots-Tag' => 'noindex', + 'X-MSEdge-Bot' => 'true' + ] + ], + 'GPTBot' => [ + 'user_agent' => 'Mozilla/5.0 (compatible; GPTBot/1.0; +https://openai.com/gptbot)', + 'headers' => [ + 'From' => 'gptbot(at)openai.com', + 'X-Robots-Tag' => 'noindex', + 'X-OpenAI-Bot' => 'true' + ] + ] +]); + +/** + * Lista de User Agents + * + * Extrai os user agents da configuração dos bots + * Mantido para compatibilidade com código legado + */ +define('USER_AGENTS', array_column(BOT_CONFIGS, 'user_agent')); + +/** + * Lista de domínios bloqueados + * + * Define os domínios que não podem ser acessados pelo sistema + * por questões de política de uso ou restrições técnicas + */ +define('BLOCKED_DOMAINS', [ + // Sites de notícias + 'wsj.com', + 'bloomberg.com', + 'piaui.folha.uol.com.br', + 'jota.info', + 'haaretz.com', + 'haaretz.co.il', + 'washingtonpost.com', + 'gauchazh.clicrbs.com.br', + 'economist.com', + // Tracking + 'metaffiliation.com', + 'google-analytics.com', + 'googletagmanager.com', + 'doubleclick.net', + 'analytics.google.com', + 'mixpanel.com', + 'segment.com', + 'amplitude.com', + 'hotjar.com', + 'kissmetrics.com', + 'crazyegg.com', + 'optimizely.com', + 'newrelic.com', + 'pingdom.com', + 'statcounter.com', + 'chartbeat.com', + 'mouseflow.com', + 'fullstory.com', + 'heap.io', + 'clearbrain.com', + // Redes sociais + 'facebook.com', + 'instagram.com', + 'twitter.com', + 'x.com', + 'linkedin.com', + 'tiktok.com', + 'pinterest.com', + 'snapchat.com', + 'reddit.com', + 'bsky.app', + 'threads.net', + // Streaming + 'netflix.com', + 'hulu.com', + 'disneyplus.com', + 'primevideo.com', + 'spotify.com', + 'youtube.com', + 'twitch.tv', + // E-commerce + 'amazon.com', + 'ebay.com', + 'aliexpress.com', + 'mercadolivre.com.br', + 'shopify.com', + // Compartilhamento de arquivos + 'mega.nz', + 'mediafire.com', + 'wetransfer.com', + 'dropbox.com', + 'torrent9.pe', + 'thepiratebay.org', + // Sites adultos + 'pornhub.com', + 'xvideos.com', + 'xnxx.com', + 'onlyfans.com', + // Apostas e jogos + 'bet365.com', + 'betfair.com', + 'pokerstars.com', + 'casino.com', + // Outros sites populares + 'github.com', + 'stackoverflow.com', + 'wikipedia.org' +]); diff --git a/app/inc/Cache.php b/app/inc/Cache.php new file mode 100644 index 0000000..db4fb0d --- /dev/null +++ b/app/inc/Cache.php @@ -0,0 +1,78 @@ +<?php +/** + * Classe responsável pelo gerenciamento de cache do sistema + * + * Esta classe implementa funcionalidades para armazenar e recuperar + * conteúdo em cache, utilizando o sistema de arquivos como storage. + * O cache é organizado por URLs convertidas em IDs únicos usando SHA-256. + */ +class Cache { + /** + * @var string Diretório onde os arquivos de cache serão armazenados + */ + private $cacheDir; + + /** + * Construtor da classe + * + * Inicializa o diretório de cache e cria-o se não existir + */ + public function __construct() { + $this->cacheDir = CACHE_DIR; + if (!file_exists($this->cacheDir)) { + mkdir($this->cacheDir, 0777, true); + } + } + + /** + * Gera um ID único para uma URL + * + * @param string $url URL para qual será gerado o ID + * @return string Hash SHA-256 da URL normalizada + */ + public function generateId($url) { + // Remove protocolo e www + $url = preg_replace('#^https?://(www\.)?#', '', $url); + // Gera ID único usando SHA-256 + return hash('sha256', $url); + } + + /** + * Verifica se existe cache para uma determinada URL + * + * @param string $url URL a ser verificada + * @return bool True se existir cache, False caso contrário + */ + public function exists($url) { + $id = $this->generateId($url); + $cachePath = $this->cacheDir . '/' . $id . '.html'; + return file_exists($cachePath); + } + + /** + * Recupera o conteúdo em cache de uma URL + * + * @param string $url URL do conteúdo a ser recuperado + * @return string|null Conteúdo em cache ou null se não existir + */ + public function get($url) { + if (!$this->exists($url)) { + return null; + } + $id = $this->generateId($url); + $cachePath = $this->cacheDir . '/' . $id . '.html'; + return file_get_contents($cachePath); + } + + /** + * Armazena conteúdo em cache para uma URL + * + * @param string $url URL associada ao conteúdo + * @param string $content Conteúdo a ser armazenado em cache + */ + public function set($url, $content) { + $id = $this->generateId($url); + $cachePath = $this->cacheDir . '/' . $id . '.html'; + file_put_contents($cachePath, $content); + } +} diff --git a/app/inc/Rules.php b/app/inc/Rules.php new file mode 100644 index 0000000..75a2785 --- /dev/null +++ b/app/inc/Rules.php @@ -0,0 +1,352 @@ +<?php +/** + * Classe responsável pelo gerenciamento de regras de manipulação de conteúdo + * + * Esta classe implementa um sistema de regras para diferentes domínios web, + * permitindo a personalização do comportamento do sistema para cada site. + * Inclui funcionalidades para remoção de paywalls, elementos específicos, + * manipulação de cookies e execução de códigos customizados. + */ +class Rules { + /** + * Array associativo contendo regras específicas para cada domínio + * + * Configurações possíveis para cada domínio: + * @var array + * + * - idElementRemove: IDs de elementos HTML que devem ser removidos + * - classElementRemove: Classes de elementos HTML que devem ser removidos + * - scriptTagRemove: Scripts que devem ser removidos + * - cookies: Cookies que devem ser definidos ou removidos + * - classAttrRemove: Classes que devem ser removidas de elementos + * - clearStorage: Se deve limpar o storage do navegador + * - customCode: Código JavaScript personalizado para execução + * - excludeGlobalRules: Array de regras globais a serem excluídas + * - userAgent: User Agent personalizado + * - headers: Headers HTTP personalizados + * - fixRelativeUrls: Habilita correção de URLs relativas + */ + private $domainRules = [ + 'nsctotal.com.br' => [ + 'userAgent' => '', + 'headers' => '' + ], + 'globo.com' => [ + 'idElementRemove' => ['cookie-banner-lgpd', 'paywall-cpt', 'mc-read-more-wrapper', 'paywall-cookie-content', 'paywall-cpt'], + 'classElementRemove' => ['banner-lgpd', 'article-related-link__title', 'article-related-link__picture', 'paywall-denied', 'banner-subscription'], + 'scriptTagRemove' => ['tiny.js', 'signup.js', 'paywall.js'], + 'cookies' => [ + 'piano_d' => null, + 'piano_if' => null, + 'piano_user_id' => null + ], + 'classAttrRemove' => ['wall', 'protected-content', 'cropped-block'], + 'clearStorage' => true, + ], + 'folha.uol.com.br' => [ + 'idElementRemove' => ['paywall-flutuante', 'paywall', 'paywall-signup'], + 'classElementRemove' => ['banner-assinatura', 'paywall-container'], + 'scriptTagRemove' => ['paywall.js', 'content-gate.js'], + 'cookies' => [ + 'paywall_visit' => null, + 'folha_id' => null, + 'paywall_access' => 'true' + ], + 'clearStorage' => true + ], + 'estadao.com.br' => [ + 'idElementRemove' => ['paywall', 'paywall-container'], + 'classElementRemove' => ['paywall-content', 'signin-wall', 'pay-wall'], + 'scriptTagRemove' => ['paywall.js', 'pywll.js'], + 'cookies' => [ + 'estadao_paywall' => null + ], + 'clearStorage' => true + ], + 'exame.com' => [ + 'fixRelativeUrls' => true, + ], + 'diarinho.net' => [ + 'fixRelativeUrls' => true, + ], + 'em.com.br' => [ + 'fixRelativeUrls' => true, + ], + 'opovo.com.br' => [ + 'fixRelativeUrls' => true, + 'classElementRemove' => ['screen-loading', 'overlay-advise'], + ], + 'folhadelondrina.com.br' => [ + 'fixRelativeUrls' => true, + ], + 'crusoe.com.br' => [ + 'cookies' => [ + 'crs_subscriber' => '1' + ] + ], + 'economist.com' => [ + 'cookies' => [ + 'ec_limit' => 'allow' + ], + 'scriptTagRemove' => ['wrapperMessagingWithoutDetection.js'], + 'customCode' => ' + var artBodyContainer = document.querySelector("article.article"); + var artBody = artBodyContainer.innerHTML; + checkPaywall(); + function checkPaywall() { + let paywallBox = document.querySelector(".layout-article-regwall"); + if (paywallBox) { + artBodyContainer.innerHTML = artBody; + } + } + ' + ], + 'ft.com' => [ + 'cookies' => [ + 'next-flags' => null, + 'next:ads' => null + ], + 'clearStorage' => true, + 'customHeaders' => [ + 'Referer' => 'https://www.google.com.br/' + ] + ], + 'nytimes.com' => [ + 'cookies' => [ + 'nyt-gdpr' => '1', + 'nyt-purr' => 'cfh' + ], + 'clearStorage' => true + ], + 'correio24horas.com.br' => [ + 'idElementRemove' => ['paywall'], + 'classElementRemove' => ['paywall'], + 'classAttrRemove' => ['hide', 'is-active'], + 'cookies' => [ + 'premium_access' => '1' + ] + ], + 'abril.com.br' => [ + 'cookies' => [ + 'paywall_access' => 'true' + ], + 'classElementRemove' => ['piano-offer-overlay'], + 'classAttrRemove' => ['disabledByPaywall'], + 'idElementRemove' => ['piano_offer'] + ], + 'foreignpolicy.com' => [ + 'idElementRemove' => ['paywall_bg'], + 'classAttrRemove' => ['overlay-no-scroll', 'overlay-no-scroll'], + ], + 'wired.com' => [ + 'clearStorage' => true + ], + 'dgabc.com.br' => [ + 'customCode' => ' + var email = "colaborador@dgabc.com.br"; + $(".NoticiaExclusivaNaoLogado").hide(); + $(".NoticiaExclusivaLogadoSemPermissao").hide(); + $(".linhaSuperBanner").show(); + $(".footer").show(); + $(".NoticiaExclusivaLogado").show(); + ', + 'fixRelativeUrls' => true, + ], + 'forbes.com' => [ + 'classElementRemove' => ['zephr-backdrop', 'zephr-generic-modal'], + 'excludeGlobalRules' => [ + 'classElementRemove' => [ + 'paywall' => [ + 'premium-article', + ], + ], + ], + ], + 'seudinheiro.com' => [ + 'idElementRemove' => ['premium-paywall'], + ], + 'technologyreview.com' => [ + 'cookies' => [ + 'xbc' => null, + '_pcid' => null, + '_pcus' => null, + '__tbc' => null, + '__pvi' => null, + '_pctx' => null + ], + 'clearStorage' => true + ] + ]; + + // Regras globais expandidas + private $globalRules = [ + 'classElementRemove' => [ + 'paywall' => [ + 'subscription', + 'subscriber-content', + 'premium-content', + 'signin-wall', + 'register-wall', + 'paid-content', + 'premium-article', + 'subscription-box', + 'piano-offer', + 'piano-inline', + 'piano-modal', + 'paywall-container', + 'paywall-overlay', + 'paywall-wrapper', + 'paywall-notification' + ], + 'social' => [ + 'social-share', + 'social-buttons', + 'share-container' + ], + 'newsletter' => [ + 'newsletter-popup', + 'subscribe-form', + 'signup-overlay' + ] + ], + 'scriptTagRemove' => [ + 'tracking' => [ + 'ga.js', + 'fbevents.js', + 'pixel.js', + 'chartbeat', + 'analytics.js', + ], + 'paywall' => [ + 'wall.js', + 'paywall.js', + 'subscriber.js', + 'piano.js', + 'tiny.js', + 'pywll.js', + 'content-gate.js', + 'signwall.js', + 'pw.js', + 'pw-', + 'piano-', + 'tinypass.js', + 'tinypass.min.js', + 'tp.min.js', + 'premium.js' + ], + 'cookies' => [ + 'cookie', + 'gdpr', + 'lgpd' + ], + 'misc' => [ + 'push', + 'sw.js', + 'stats.js' + ] + ] + ]; + + /** + * Obtém o domínio base removendo o prefixo www + * + * @param string $domain Domínio completo + * @return string Domínio base sem www + */ + private function getBaseDomain($domain) { + return preg_replace('/^www\./', '', $domain); + } + + /** + * Divide um domínio em suas partes constituintes + * + * @param string $domain Domínio a ser dividido + * @return array Array com todas as combinações possíveis do domínio + */ + private function getDomainParts($domain) { + $domain = $this->getBaseDomain($domain); + $parts = explode('.', $domain); + + $combinations = []; + for ($i = 0; $i < count($parts) - 1; $i++) { + $combinations[] = implode('.', array_slice($parts, $i)); + } + + usort($combinations, function($a, $b) { + return strlen($b) - strlen($a); + }); + + return $combinations; + } + + /** + * Obtém as regras específicas para um domínio + * + * @param string $domain Domínio para buscar regras + * @return array|null Array com regras mescladas ou null se não encontrar + */ + public function getDomainRules($domain) { + $domainParts = $this->getDomainParts($domain); + + foreach ($this->domainRules as $pattern => $rules) { + if ($this->getBaseDomain($domain) === $this->getBaseDomain($pattern)) { + return $this->mergeWithGlobalRules($rules); + } + } + + foreach ($domainParts as $part) { + foreach ($this->domainRules as $pattern => $rules) { + if ($part === $this->getBaseDomain($pattern)) { + return $this->mergeWithGlobalRules($rules); + } + } + } + + return null; + } + + /** + * Mescla regras específicas do domínio com regras globais + * + * @param array $rules Regras específicas do domínio + * @return array Regras mescladas + */ + private function mergeWithGlobalRules($rules) { + $globalRules = $this->getGlobalRules(); + + if (isset($rules['excludeGlobalRules']) && is_array($rules['excludeGlobalRules'])) { + foreach ($rules['excludeGlobalRules'] as $ruleType => $categories) { + if (isset($globalRules[$ruleType])) { + foreach ($categories as $category => $itemsToExclude) { + if (isset($globalRules[$ruleType][$category])) { + $globalRules[$ruleType][$category] = array_diff( + $globalRules[$ruleType][$category], + $itemsToExclude + ); + } + } + } + } + } + + foreach ($globalRules as $ruleType => $categories) { + if (!isset($rules[$ruleType])) { + $rules[$ruleType] = []; + } + foreach ($categories as $category => $items) { + $rules[$ruleType] = array_merge($rules[$ruleType], $items); + } + } + + return $rules; + } + + /** + * Retorna todas as regras globais + * + * @return array Array com todas as regras globais + */ + public function getGlobalRules() { + return $this->globalRules; + } +} diff --git a/app/inc/URLAnalyzer.php b/app/inc/URLAnalyzer.php new file mode 100644 index 0000000..309ac3e --- /dev/null +++ b/app/inc/URLAnalyzer.php @@ -0,0 +1,686 @@ +<?php +/** + * Classe responsável pela análise e processamento de URLs + * + * Esta classe implementa funcionalidades para: + * - Análise e limpeza de URLs + * - Cache de conteúdo + * - Resolução DNS + * - Requisições HTTP com múltiplas tentativas + * - Processamento de conteúdo baseado em regras específicas por domínio + */ + +require_once 'Rules.php'; +require_once 'Cache.php'; + +class URLAnalyzer { + /** + * @var array Lista de User Agents disponíveis para requisições + */ + private $userAgents; + + /** + * @var int Número máximo de tentativas para obter conteúdo + */ + private $maxAttempts; + + /** + * @var array Lista de servidores DNS para resolução + */ + private $dnsServers; + + /** + * @var Rules Instância da classe de regras + */ + private $rules; + + /** + * @var Cache Instância da classe de cache + */ + private $cache; + + /** + * Construtor da classe + * Inicializa as dependências necessárias + */ + public function __construct() { + $this->userAgents = USER_AGENTS; + $this->maxAttempts = MAX_ATTEMPTS; + $this->dnsServers = explode(',', DNS_SERVERS); + $this->rules = new Rules(); + $this->cache = new Cache(); + } + + /** + * Registra erros no arquivo de log + * + * @param string $url URL que gerou o erro + * @param string $error Mensagem de erro + */ + private function logError($url, $error) { + $timestamp = date('Y-m-d H:i:s'); + $logEntry = "[{$timestamp}] URL: {$url} - Error: {$error}" . PHP_EOL; + file_put_contents(__DIR__ . '/../logs/error.log', $logEntry, FILE_APPEND); + } + + /** + * Método principal para análise de URLs + * + * @param string $url URL a ser analisada + * @return string Conteúdo processado da URL + * @throws Exception Em caso de erros durante o processamento + */ + public function analyze($url) { + try { + $cleanUrl = $this->cleanUrl($url); + + if ($this->cache->exists($cleanUrl)) { + return $this->cache->get($cleanUrl); + } + + $parsedUrl = parse_url($cleanUrl); + $domain = $parsedUrl['host']; + + // Verificação de domínios bloqueados + foreach (BLOCKED_DOMAINS as $blockedDomain) { + if (strpos($domain, $blockedDomain) !== false) { + $error = 'Este domínio está bloqueado para extração.'; + $this->logError($cleanUrl, $error); + throw new Exception($error); + } + } + + $resolvedIp = $this->resolveDns($cleanUrl); + if (!$resolvedIp) { + $error = 'Falha ao resolver DNS para o domínio'; + $this->logError($cleanUrl, $error); + throw new Exception($error); + } + + $content = $this->fetchWithMultipleAttempts($cleanUrl, $resolvedIp); + + if (empty($content)) { + $error = 'Não foi possível obter o conteúdo. Tente usar serviços de arquivo.'; + $this->logError($cleanUrl, $error); + throw new Exception($error); + } + + $content = $this->processContent($content, $domain, $cleanUrl); + + $this->cache->set($cleanUrl, $content); + + return $content; + } catch (Exception $e) { + $this->logError($url, $e->getMessage()); + throw $e; + } + } + + /** + * Tenta obter o conteúdo da URL com múltiplas tentativas + * + * @param string $url URL para buscar conteúdo + * @param string $resolvedIp IP resolvido do domínio + * @return string Conteúdo obtido + * @throws Exception Se todas as tentativas falharem + */ + private function fetchWithMultipleAttempts($url, $resolvedIp) { + $attempts = 0; + $errors = []; + + while ($attempts < $this->maxAttempts) { + try { + $content = $this->fetchWithCurl($url, $resolvedIp, $attempts); + if (!empty($content)) { + return $content; + } + } catch (Exception $e) { + $errors[] = $e->getMessage(); + } + + $attempts++; + usleep(500000); // 0.5 segundo de espera entre tentativas + } + + throw new Exception("Falha ao obter conteúdo após {$this->maxAttempts} tentativas. Erros: " . implode(', ', $errors)); + } + + /** + * Realiza requisição HTTP usando cURL + * + * @param string $url URL para requisição + * @param string $resolvedIp IP resolvido do domínio + * @param int $attempts Número da tentativa atual + * @return string Conteúdo obtido + * @throws Exception Em caso de erro na requisição + */ + private function fetchWithCurl($url, $resolvedIp, $attempts) { + $parsedUrl = parse_url($url); + $host = $parsedUrl['host']; + + $domainRules = $this->getDomainRules(parse_url($url, PHP_URL_HOST)); + + $userAgent = $this->userAgents[$attempts % count($this->userAgents)]; + if (isset($domainRules['userAgent'])) { + $userAgent = $domainRules['userAgent']; + } + + $curlOptions = [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_TIMEOUT => 30, + CURLOPT_ENCODING => '', + CURLOPT_USERAGENT => $userAgent, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_RESOLVE => ["{$host}:80:{$resolvedIp}", "{$host}:443:{$resolvedIp}"], + CURLOPT_DNS_SERVERS => implode(',', $this->dnsServers) + ]; + + if (!isset($domainRules['headers'])) { + $headers = [ + 'Host: ' . $host, + 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language: pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7', + 'Cache-Control: no-cache', + 'Pragma: no-cache' + ]; + + if ($domainRules !== null && isset($domainRules['customHeaders'])) { + foreach ($domainRules['customHeaders'] as $headerName => $headerValue) { + $headers[] = $headerName . ': ' . $headerValue; + } + } + + $curlOptions[CURLOPT_HTTPHEADER] = $headers; + $curlOptions[CURLOPT_COOKIESESSION] = true; + $curlOptions[CURLOPT_FRESH_CONNECT] = true; + } + + if ($domainRules !== null && isset($domainRules['cookies'])) { + $cookies = []; + foreach ($domainRules['cookies'] as $name => $value) { + if ($value !== null) { + $cookies[] = $name . '=' . $value; + } + } + if (!empty($cookies)) { + $curlOptions[CURLOPT_COOKIE] = implode('; ', $cookies); + } + } + + $ch = curl_init(); + curl_setopt_array($ch, $curlOptions); + + $content = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + + curl_close($ch); + + if ($error) { + throw new Exception("Erro CURL: " . $error); + } + + if ($httpCode >= 400) { + throw new Exception("Erro HTTP: " . $httpCode); + } + + return $content; + } + + /** + * Limpa e normaliza uma URL + * + * @param string $url URL para limpar + * @return string URL limpa e normalizada + */ + private function cleanUrl($url) { + $url = strtolower($url); + $url = trim($url); + + $parsedUrl = parse_url($url); + + if (!isset($parsedUrl['scheme'])) { + $url = 'https://' . $url; + $parsedUrl = parse_url($url); + } + + $cleanUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host']; + + if (isset($parsedUrl['path'])) { + $path = preg_replace('#/+#', '/', $parsedUrl['path']); + $cleanUrl .= $path; + } + + if (isset($parsedUrl['query'])) { + parse_str($parsedUrl['query'], $params); + $params = $this->filterUrlParams($params); + + if (!empty($params)) { + ksort($params); + $cleanUrl .= '?' . http_build_query($params); + } + } + + return rtrim($cleanUrl, '/'); + } + + /** + * Filtra parâmetros da URL removendo tracking e sessão + * + * @param array $params Parâmetros da URL + * @return array Parâmetros filtrados + */ + private function filterUrlParams($params) { + $filteredParams = []; + + foreach ($params as $key => $value) { + if (empty($value) && $value !== '0') { + continue; + } + + if ($this->isTrackingParam($key)) { + continue; + } + + if ($this->isSessionParam($key)) { + continue; + } + + if ($this->isCacheParam($key)) { + continue; + } + + if ($this->isContentParam($key)) { + $filteredParams[$key] = $value; + } + } + + return $filteredParams; + } + + /** + * Verifica se um parâmetro é de tracking + * + * @param string $param Nome do parâmetro + * @return bool True se for parâmetro de tracking + */ + private function isTrackingParam($param) { + $trackingPatterns = [ + '/^utm_/', // Google Analytics + '/^fbclid$/', // Facebook + '/^gclid$/', // Google Ads + '/^msclkid$/', // Microsoft + '/^mc_/', // Mailchimp + '/^pk_/', // Piwik/Matomo + '/^n_/', // Navegg + '/^dclid$/', // DoubleClick + '/^_hs/', // HubSpot + '/^_ga/', // Google Analytics + '/^_gl/', // Google Analytics linker + '/^ref$/', // Referrer + '/^source$/', // Source tracking + '/^medium$/', // Medium tracking + '/^campaign$/', // Campaign tracking + '/^affiliate$/', // Affiliate tracking + '/^partner$/', // Partner tracking + '/^_openstat$/', // OpenStat + '/^yclid$/', // Yandex + '/^_hsenc$/', // HubSpot + '/^_hsmi$/', // HubSpot + '/^mkt_tok$/', // Marketo + '/^igshid$/', // Instagram + ]; + + foreach ($trackingPatterns as $pattern) { + if (preg_match($pattern, $param)) { + return true; + } + } + + return false; + } + + /** + * Verifica se um parâmetro é de sessão + * + * @param string $param Nome do parâmetro + * @return bool True se for parâmetro de sessão + */ + private function isSessionParam($param) { + $sessionPatterns = [ + '/sess(ion)?[_-]?id/i', + '/^sid$/', + '/^s$/', + '/_?sess$/', + '/^PHPSESSID$/', + '/^JSESSIONID$/', + '/^ASP\.NET_SessionId$/', + '/^CFID$/', + '/^CFTOKEN$/', + '/^skey$/', + '/^token$/', + '/^auth[_-]?token$/', + '/^access[_-]?token$/', + ]; + + foreach ($sessionPatterns as $pattern) { + if (preg_match($pattern, $param)) { + return true; + } + } + + return false; + } + + /** + * Verifica se um parâmetro é de cache + * + * @param string $param Nome do parâmetro + * @return bool True se for parâmetro de cache + */ + private function isCacheParam($param) { + $cachePatterns = [ + '/^v$/', + '/^ver$/', + '/^version$/', + '/^rev$/', + '/^revision$/', + '/^cache$/', + '/^nocache$/', + '/^_t$/', + '/^timestamp$/', + '/^time$/', + '/^[0-9]+$/', + '/^_=[0-9]+$/', + ]; + + foreach ($cachePatterns as $pattern) { + if (preg_match($pattern, $param)) { + return true; + } + } + + return false; + } + + /** + * Verifica se um parâmetro é de conteúdo + * + * @param string $param Nome do parâmetro + * @return bool True se for parâmetro de conteúdo + */ + private function isContentParam($param) { + $contentPatterns = [ + '/^id$/', + '/^page$/', + '/^category$/', + '/^cat$/', + '/^tag$/', + '/^type$/', + '/^format$/', + '/^view$/', + '/^layout$/', + '/^style$/', + '/^lang$/', + '/^locale$/', + '/^currency$/', + '/^filter$/', + '/^sort$/', + '/^order$/', + '/^q$/', + '/^search$/', + '/^query$/', + '/^year$/', + '/^month$/', + '/^day$/', + '/^date$/', + '/^author$/', + '/^topic$/', + '/^section$/', + ]; + + foreach ($contentPatterns as $pattern) { + if (preg_match($pattern, $param)) { + return true; + } + } + + if (preg_match('/^[a-z0-9]{4,}$/i', $param)) { + return true; + } + + return false; + } + + /** + * Resolve DNS para um domínio + * + * @param string $url URL para resolver DNS + * @return string|false IP resolvido ou false em caso de falha + */ + private function resolveDns($url) { + $parsedUrl = parse_url($url); + $domain = $parsedUrl['host']; + + foreach ($this->dnsServers as $dnsServer) { + $dnsQuery = [ + 'name' => $domain, + 'type' => 'A', + 'do' => true, + 'cd' => false + ]; + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $dnsServer . '?' . http_build_query($dnsQuery), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + 'Accept: application/dns-json', + ], + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2 + ]); + + $response = curl_exec($ch); + $error = curl_error($ch); + curl_close($ch); + + if (!$error && $response) { + $dnsData = json_decode($response, true); + if (isset($dnsData['Answer'])) { + foreach ($dnsData['Answer'] as $record) { + if ($record['type'] === 1) { + return $record['data']; + } + } + } + } + } + + $ip = gethostbyname($domain); + return ($ip !== $domain) ? $ip : false; + } + + /** + * Obtém regras específicas para um domínio + * + * @param string $domain Domínio para buscar regras + * @return array|null Regras do domínio ou null se não encontrar + */ + private function getDomainRules($domain) { + return $this->rules->getDomainRules($domain); + } + + /** + * Remove classes específicas de um elemento + * + * @param DOMElement $element Elemento DOM + * @param array $classesToRemove Classes a serem removidas + */ + private function removeClassNames($element, $classesToRemove) { + if (!$element->hasAttribute('class')) { + return; + } + + $classes = explode(' ', $element->getAttribute('class')); + $newClasses = array_filter($classes, function($class) use ($classesToRemove) { + return !in_array(trim($class), $classesToRemove); + }); + + if (empty($newClasses)) { + $element->removeAttribute('class'); + } else { + $element->setAttribute('class', implode(' ', $newClasses)); + } + } + + /** + * Corrige URLs relativas em um documento DOM + * + * @param DOMDocument $dom Documento DOM + * @param DOMXPath $xpath Objeto XPath + * @param string $baseUrl URL base para correção + */ + private function fixRelativeUrls($dom, $xpath, $baseUrl) { + $parsedBase = parse_url($baseUrl); + $baseHost = $parsedBase['scheme'] . '://' . $parsedBase['host']; + + $elements = $xpath->query("//*[@src]"); + if ($elements !== false) { + foreach ($elements as $element) { + if ($element instanceof DOMElement) { + $src = $element->getAttribute('src'); + if (strpos($src, 'http') !== 0 && strpos($src, '//') !== 0) { + $src = ltrim($src, '/'); + $element->setAttribute('src', $baseHost . '/' . $src); + } + } + } + } + + $elements = $xpath->query("//*[@href]"); + if ($elements !== false) { + foreach ($elements as $element) { + if ($element instanceof DOMElement) { + $href = $element->getAttribute('href'); + if (strpos($href, 'http') !== 0 && strpos($href, '//') !== 0 && strpos($href, '#') !== 0 && strpos($href, 'mailto:') !== 0) { + $href = ltrim($href, '/'); + $element->setAttribute('href', $baseHost . '/' . $href); + } + } + } + } + } + + /** + * Processa o conteúdo HTML aplicando regras do domínio + * + * @param string $content Conteúdo HTML + * @param string $domain Domínio do conteúdo + * @param string $url URL completa + * @return string Conteúdo processado + */ + private function processContent($content, $domain, $url) { + $dom = new DOMDocument(); + $dom->preserveWhiteSpace = true; + libxml_use_internal_errors(true); + @$dom->loadHTML(mb_convert_encoding($content, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + libxml_clear_errors(); + + $xpath = new DOMXPath($dom); + + $domainRules = $this->getDomainRules($domain); + if ($domainRules !== null) { + if (isset($domainRules['fixRelativeUrls']) && $domainRules['fixRelativeUrls'] === true) { + $this->fixRelativeUrls($dom, $xpath, $url); + } + + if (isset($domainRules['customStyle'])) { + $styleElement = $dom->createElement('style'); + $styleContent = ''; + foreach ($domainRules['customStyle'] as $selector => $rules) { + if (is_array($rules)) { + $styleContent .= $selector . ' { ' . implode('; ', $rules) . ' } '; + } else { + $styleContent .= $selector . ' { ' . $rules . ' } '; + } + } + $styleElement->appendChild($dom->createTextNode($styleContent)); + $dom->getElementsByTagName('head')[0]->appendChild($styleElement); + } + + if (isset($domainRules['customCode'])) { + $scriptElement = $dom->createElement('script'); + $scriptElement->setAttribute('type', 'text/javascript'); + $scriptElement->appendChild($dom->createTextNode($domainRules['customCode'])); + $dom->getElementsByTagName('body')[0]->appendChild($scriptElement); + } + + if (isset($domainRules['classAttrRemove'])) { + foreach ($domainRules['classAttrRemove'] as $class) { + $elements = $xpath->query("//*[contains(@class, '$class')]"); + if ($elements !== false) { + foreach ($elements as $element) { + $this->removeClassNames($element, [$class]); + } + } + } + } + + if (isset($domainRules['idElementRemove'])) { + foreach ($domainRules['idElementRemove'] as $id) { + $elements = $xpath->query("//*[@id='$id']"); + if ($elements !== false) { + foreach ($elements as $element) { + if ($element->parentNode) { + $element->parentNode->removeChild($element); + } + } + } + } + } + + if (isset($domainRules['classElementRemove'])) { + foreach ($domainRules['classElementRemove'] as $class) { + $elements = $xpath->query("//*[contains(@class, '$class')]"); + if ($elements !== false) { + foreach ($elements as $element) { + if ($element->parentNode) { + $element->parentNode->removeChild($element); + } + } + } + } + } + + if (isset($domainRules['scriptTagRemove'])) { + foreach ($domainRules['scriptTagRemove'] as $script) { + $elements = $xpath->query("//script[contains(@src, '$script')] | //script[contains(text(), '$script')]"); + if ($elements !== false) { + foreach ($elements as $element) { + if ($element->parentNode) { + $element->parentNode->removeChild($element); + } + } + } + } + } + } + + $elements = $xpath->query("//*[@style]"); + if ($elements !== false) { + foreach ($elements as $element) { + if ($element instanceof DOMElement) { + $style = $element->getAttribute('style'); + $style = preg_replace('/(max-height|height|overflow|position|display)\s*:\s*[^;]+;?/', '', $style); + $element->setAttribute('style', $style); + } + } + } + + return $dom->saveHTML(); + } +} diff --git a/app/index.php b/app/index.php new file mode 100644 index 0000000..ff9d8ca --- /dev/null +++ b/app/index.php @@ -0,0 +1,205 @@ +<?php +/** + * Página principal do sistema + * + * Este arquivo implementa a interface web principal, incluindo: + * - Formulário para análise de URLs + * - Tratamento de mensagens de erro/sucesso + * - Documentação da API + * - Interface para bookmarklet + * - Links para serviços alternativos + */ + +require_once 'config.php'; + +// Inicialização de variáveis +$message = ''; +$message_type = ''; +$url = ''; + +// Processa mensagens de erro/alerta da query string +if (isset($_GET['message']) && isset(MESSAGES[$_GET['message']])) { + $message_key = $_GET['message']; + $message = MESSAGES[$message_key]['message']; + $message_type = MESSAGES[$message_key]['type']; +} + +// Processa submissão do formulário +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['url'])) { + $url = filter_var($_POST['url'], FILTER_SANITIZE_URL); + if (filter_var($url, FILTER_VALIDATE_URL)) { + header('Location: ' . SITE_URL . '/p/' . urlencode($url)); + exit; + } else { + $message = MESSAGES['INVALID_URL']['message']; + $message_type = MESSAGES['INVALID_URL']['type']; + } +} +?> +<!DOCTYPE html> +<html lang="pt-BR"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title><?php echo SITE_NAME; ?></title> + <link rel="icon" href="assets/svg/marreta.svg" type="image/svg+xml"> + <meta property="og:type" content="website" /> + <meta property="og:url" content="<?php echo SITE_URL; ?>" /> + <meta property="og:title" content="<?php echo SITE_NAME; ?>" /> + <meta property="og:description" content="<?php echo htmlspecialchars(SITE_DESCRIPTION); ?>" /> + <meta property="og:image" content="/assets/opengraph.png" /> + <script src="https://cdn.tailwindcss.com"></script> +</head> +<body class="bg-gray-50 min-h-screen"> + <div class="container mx-auto px-4 py-8 max-w-4xl"> + <!-- Cabeçalho da página --> + <div class="text-center mb-8"> + <h1 class="text-4xl font-bold text-gray-800 mb-4"> + <img src="assets/svg/marreta.svg" class="inline-block w-12 h-12 mb-2" alt="Marreta icon"> + <?php echo SITE_NAME; ?> + </h1> + <p class="text-gray-600 text-lg"><?php echo SITE_DESCRIPTION; ?></p> + </div> + + <!-- Formulário principal de análise de URLs --> + <div class="bg-white rounded-xl shadow-lg p-8 mb-8"> + <form id="urlForm" method="POST" onsubmit="return validateForm()" class="space-y-6"> + <div class="relative"> + <div class="flex items-stretch"> + <span class="inline-flex items-center px-5 rounded-l-lg border border-r-0 border-gray-300 bg-gray-50 text-gray-500"> + <img src="assets/svg/link.svg" class="w-6 h-6" alt="Link icon"> + </span> + <input type="url" + name="url" + id="url" + class="flex-1 block w-full rounded-none rounded-r-lg text-lg py-4 border border-l-0 border-gray-300 bg-gray-50 focus:border-blue-500 focus:ring-blue-500 shadow-sm bg-gray-50" + placeholder="Digite a URL (ex: https://exemplo.com)" + value="<?php echo htmlspecialchars($url); ?>" + required + pattern="https?://.+" + title="Por favor, insira uma URL válida começando com http:// ou https://"> + </div> + <button type="submit" + class="mt-4 w-full inline-flex justify-center items-center px-6 py-4 border border-transparent text-lg font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"> + <img src="assets/svg/search.svg" class="w-6 h-6 mr-3" alt="Search icon"> + Analisar + </button> + </div> + </form> + + <!-- Área de mensagens de erro/alerta --> + <?php if ($message): ?> + <div class="mt-6 <?php echo $message_type === 'error' ? 'bg-red-50 border-red-400' : 'bg-yellow-50 border-yellow-400'; ?> border-l-4 p-4 rounded-r"> + <div class="flex"> + <div class="flex-shrink-0"> + <?php if ($message_type === 'error'): ?> + <img src="assets/svg/error.svg" class="w-6 h-6" alt="Error icon"> + <?php else: ?> + <img src="assets/svg/warning.svg" class="w-6 h-6" alt="Warning icon"> + <?php endif; ?> + </div> + <div class="ml-3"> + <p class="text-base <?php echo $message_type === 'error' ? 'text-red-700' : 'text-yellow-700'; ?>"> + <?php echo htmlspecialchars($message); ?> + </p> + </div> + </div> + </div> + <?php endif; ?> + </div> + + <!-- Exemplo de uso direto --> + <div class="mt-8 text-center text-base text-gray-500"> + <p> + <img src="assets/svg/code.svg" class="inline-block w-5 h-5 mr-2" alt="Code icon"> + Acesso direto: + <pre class="bg-gray-100 p-3 rounded-lg text-sm overflow-x-auto"><?php echo SITE_URL; ?>/p/https://exemplo.com</pre> + </p> + </div> + + <!-- Documentação da API --> + <div class="bg-white rounded-xl shadow-lg p-8 mt-8"> + <h2 class="text-xl font-semibold text-gray-800 mb-6 flex items-center"> + <img src="assets/svg/code.svg" class="w-6 h-6 mr-3" alt="Code icon"> + API REST + </h2> + <div class="space-y-4"> + <p class="text-gray-600"> + O <?php echo SITE_NAME; ?> disponibiliza uma API REST para integração com outros sistemas: + </p> + <div class="bg-gray-50 rounded-lg p-4"> + <h3 class="font-medium text-gray-800 mb-2">Endpoint:</h3> + <pre class="bg-gray-100 p-3 rounded-lg text-sm overflow-x-auto">GET <?php echo SITE_URL; ?>/api/https://exemplo.com</pre> + </div> + <div class="bg-gray-50 rounded-lg p-4"> + <h3 class="font-medium text-gray-800 mb-2">Resposta de sucesso:</h3> + <pre class="bg-gray-100 p-3 rounded-lg text-sm overflow-x-auto"> +{ + "status": 200, + "url": "<?php echo SITE_URL; ?>/p/https://exemplo.com" +}</pre> + </div> + <div class="bg-gray-50 rounded-lg p-4"> + <h3 class="font-medium text-gray-800 mb-2">Resposta de erro:</h3> + <pre class="bg-gray-100 p-3 rounded-lg text-sm overflow-x-auto"> +{ + "status": 400, + "error": { + "code": "INVALID_URL", + "message": "URL inválida" + } +}</pre> + </div> + </div> + </div> + + <!-- Seção de Bookmarklet --> + <div class="bg-white rounded-xl shadow-lg p-8 mt-8 mb-8"> + <h2 class="text-xl font-semibold text-gray-800 mb-6 flex items-center"> + <img src="assets/svg/bookmark.svg" class="w-6 h-6 mr-3" alt="Bookmark icon"> + Adicione aos Favoritos + </h2> + <div class="space-y-4"> + <p class="text-gray-600"> + Arraste o botão abaixo para sua barra de favoritos para acessar o <?php echo SITE_NAME; ?> rapidamente em qualquer página: + </p> + <div class="flex justify-center"> + <a href="javascript:(function(){let currentUrl=window.location.href;window.location.href='<?php echo SITE_URL; ?>/p/'+encodeURIComponent(currentUrl);})()" + class="inline-flex items-center px-6 py-3 border-2 border-blue-500 font-medium rounded-lg text-blue-600 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200 cursor-move" + onclick="return false;"> + <img src="assets/svg/marreta.svg" class="w-5 h-5 mr-2" alt="Marreta icon"> + Abrir no <?php echo SITE_NAME; ?> + </a> + </div> + </div> + </div> + + <!-- Serviços alternativos --> + <div class="bg-white rounded-xl shadow-lg p-8 mt-8"> + <h2 class="text-xl font-semibold text-gray-800 mb-6"> + <img src="assets/svg/archive.svg" class="inline-block w-6 h-6 mr-3" alt="Archive icon"> + Serviços alternativos + </h2> + <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> + <a href="https://archive.today" target="_blank" + class="flex items-center p-4 rounded-lg border-2 border-gray-200 hover:border-blue-500 hover:bg-blue-50 transition-all duration-200"> + <img src="assets/svg/archive.svg" class="w-8 h-8 mr-4" alt="Archive.today icon"> + <div> + <div class="font-medium text-lg text-gray-800">archive.today</div> + </div> + </a> + <a href="https://12ft.io" target="_blank" + class="flex items-center p-4 rounded-lg border-2 border-gray-200 hover:border-green-500 hover:bg-green-50 transition-all duration-200"> + <img src="assets/svg/bypass.svg" class="w-8 h-8 mr-4" alt="12ft.io icon"> + <div> + <div class="font-medium text-lg text-gray-800">12ft.io</div> + </div> + </a> + </div> + </div> + </div> + + <!-- Scripts JavaScript --> + <script src="assets/js/scripts.js"></script> +</body> +</html> diff --git a/app/p.php b/app/p.php new file mode 100644 index 0000000..f7e4a5e --- /dev/null +++ b/app/p.php @@ -0,0 +1,65 @@ +<?php +/** + * Processador de URLs + * + * Este arquivo é responsável por: + * - Receber URLs através do path /p/ + * - Validar o formato da URL + * - Processar o conteúdo usando o URLAnalyzer + * - Exibir o conteúdo processado ou redirecionar em caso de erro + * + * Exemplo de uso: + * /p/https://exemplo.com + */ + +require_once 'config.php'; +require_once 'inc/URLAnalyzer.php'; + +// Extrai a URL do path da requisição +$path = $_SERVER['REQUEST_URI']; +$prefix = '/p/'; + +if (strpos($path, $prefix) === 0) { + // Remove o prefixo e decodifica a URL + $url = urldecode(substr($path, strlen($prefix))); + + // Valida o formato da URL + if (filter_var($url, FILTER_VALIDATE_URL)) { + $analyzer = new URLAnalyzer(); + try { + // Tenta analisar e processar a URL + $content = $analyzer->analyze($url); + // Exibe o conteúdo processado + echo $content; + exit; + } catch (Exception $e) { + $errorMessage = $e->getMessage(); + $errorType = 'GENERIC_ERROR'; // Tipo padrão de erro + + // Mapeia a mensagem de erro para um tipo específico + if (strpos($errorMessage, 'bloqueado') !== false) { + $errorType = 'BLOCKED_DOMAIN'; + } elseif (strpos($errorMessage, 'DNS') !== false) { + $errorType = 'DNS_FAILURE'; + } elseif (strpos($errorMessage, 'HTTP: 4') !== false || strpos($errorMessage, 'HTTP: 5') !== false) { + $errorType = 'HTTP_ERROR'; + } elseif (strpos($errorMessage, 'CURL') !== false) { + $errorType = 'CONNECTION_ERROR'; + } elseif (strpos($errorMessage, 'obter conteúdo') !== false) { + $errorType = 'CONTENT_ERROR'; + } + + // Redireciona para a página inicial com mensagem de erro + header('Location: /?message=' . $errorType); + exit; + } + } else { + // URL inválida + header('Location: /?message=INVALID_URL'); + exit; + } +} else { + // Path inválido + header('Location: /?message=NOT_FOUND'); + exit; +} diff --git a/default.conf b/default.conf new file mode 100644 index 0000000..01d9930 --- /dev/null +++ b/default.conf @@ -0,0 +1,43 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + + root /app; + index index.php index.html index.htm; + + server_name _; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + + location ^~ /logs/ { + deny all; + return 403; + } + + location ^~ /cache/ { + deny all; + return 403; + } + + location / { + try_files $uri $uri/ $uri/index.php?$args; + } + + location ~ ^/(api|p)/ { + try_files $uri $uri/ /$1.php; + } + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass 127.0.0.1:9000; + } + + location ~ /\.ht { + deny all; + } + + access_log /dev/null; +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0179e77 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +services: + marreta: + container_name: marreta + image: ghcr.io/manualdousuario/marreta:latest + ports: + - "80:80" + volumes: + - marreta_cache:/app/cache + - marreta_logs:/app/logs + environment: + - SITE_NAME=${SITE_NAME:-} + - SITE_DESCRIPTION=${SITE_DESCRIPTION:-} + - SITE_URL=${SITE_URL:-} + - DNS_SERVERS=${DNS_SERVERS:-} + restart: unless-stopped + +volumes: + marreta_cache: + marreta_logs: diff --git a/env.sh b/env.sh new file mode 100644 index 0000000..61f85a3 --- /dev/null +++ b/env.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +if [ -n "${SITE_NAME}" ]; then + echo "SITE_NAME=${SITE_NAME}" >> /app/.env +fi + +if [ -n "${SITE_DESCRIPTION}" ]; then + echo "SITE_DESCRIPTION=${SITE_DESCRIPTION}" >> /app/.env +fi + +if [ -n "${SITE_URL}" ]; then + echo "SITE_URL=${SITE_URL}" >> /app/.env +fi + +if [ -n "${DNS_SERVERS}" ]; then + echo "DNS_SERVERS=${DNS_SERVERS}" >> /app/.env +fi + +echo "Variáveis de ambiente salvas com sucesso." diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..ee7fd2e --- /dev/null +++ b/start.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +echo "Iniciando rotinas." + +check_nginx() { + if ! pgrep nginx > /dev/null; then + echo "Falha ao iniciar webservice." + exit 1 + else + echo "Webservice iniciou." + fi +} + +check_php_fpm() { + if ! pgrep php-fpm > /dev/null; then + echo "Falha ao iniciar o PHP." + exit 1 + else + echo "PHP iniciou." + fi +} + +if [ ! -d /var/run/php ]; then + mkdir -p /var/run/php + chown -R www-data:www-data /var/run/php +fi + +echo "Iniciando PHP..." +php-fpm & + +sleep 3 + +check_php_fpm + +sleep 3 + +echo "Testando configuração do webservice..." +nginx -t +if [ $? -ne 0 ]; then + echo "Configuração do webservice invalida." + exit 1 +else + echo "Configuração valida do webservice." +fi + +echo "Iniciando webservice..." +nginx -g "daemon off;" & + +sleep 3 + +check_nginx + +wait -n + +exit $?