diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/.gitignore b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/.gitignore new file mode 100644 index 0000000..6061cfd --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/.gitignore @@ -0,0 +1,15 @@ +config/config.local.ini +config/local/config.local.ini +ui/node_modules +node_modules/* +public/* +vendor/* +.DS_Store +composer.phar +/.php-cs-fixer.cache +logs/* +!/logs/index.php +assets/logs/* +!/assets/logs/index.php +tmp/* +!/tmp/index.php diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/.htaccess b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/.htaccess new file mode 100644 index 0000000..2fcaf3f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/.htaccess @@ -0,0 +1,195 @@ +# Do not show directory indexes +Options -Indexes + +# Do not use multiviews +Options -MultiViews + +# Do follow symbolic links +Options +FollowSymLinks +# Options +SymLinksifOwnerMatch + +# Character encoding: serve text/html or text/plain as UTF-8 +AddDefaultCharset UTF-8 + +# ----------------------------------------------------------------------------------------------- +# Protect from XSS with Apache headers +# ----------------------------------------------------------------------------------------------- + + + # prevent site from being loaded in an iframe on another site + Header always append X-Frame-Options SAMEORIGIN + + # To prevent cross site scripting (IE8+ proprietary) + Header set X-XSS-Protection "1; mode=block" + + + +# ----------------------------------------------------------------------------------------------- +# Prevent access to various types of files +# +# Note that some of these rules are duplicated by RewriteRules or other .htaccess files. +# ----------------------------------------------------------------------------------------------- + +# Block access to inc, info, info.json/php, module/php, sh, sql and composer files +# ----------------------------------------------------------------------------------------------- + + + + Require all denied + + + Order allow,deny + + + + +# Block bak, conf, dist, ini, log, orig, sh, sql, swo, swp, ~, ... +# ----------------------------------------------------------------------------------------------- + + + + Require all denied + + + Order allow,deny + + + +DirectoryIndex index.php index.html index.htm + + +# ----------------------------------------------------------------------------------------------- +# Enable Apache mod_rewrite +# ----------------------------------------------------------------------------------------------- + + + + RewriteEngine On + + # Optionally set a rewrite base if rewrites are not working properly on your server. + # ----------------------------------------------------------------------------------------------- + # In addition, if your site directory starts with a "~" you will most likely have to use this. + # https://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritebase + + # For ex.: + # RewriteBase / + # RewriteBase /p/ + # RewriteBase /~user/ + + # To redirect HTTP requests to HTTPS, uncomment these lines: + # ----------------------------------------------------------------------------------------------- + # RewriteCond %{HTTPS} !=on + # RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + + # If using load balancer/AWS or behind proxy, use lines below rather lines before: + # ----------------------------------------------------------------------------------------------- + # RewriteCond %{HTTP:X-Forwarded-Proto} =https + # RewriteRule ^ - [env=proto:https] + # RewriteCond %{HTTP:X-Forwarded-Proto} =http + # RewriteRule ^ - [env=proto:http] + + + # Uncomment one of lines below and adjust to enable Strict-Transport-Security (HSTS): + # Header always set Strict-Transport-Security "max-age=31536000;" + # Header always set Strict-Transport-Security "max-age=31536000; includeSubdomains" + # Header always set Strict-Transport-Security "max-age=31536000; preload" + # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + + + # ----------------------------------------------------------------------------------------------- + # Access Restrictions + # ----------------------------------------------------------------------------------------------- + + # Keep http requests out of specific files and directories + # ----------------------------------------------------------------------------------------------- + + # Prevent all the following rules from blocking images in ui directories + RewriteRule (^|/)ui/images/.+\.(jpg|jpeg|png|gif|webp|svg)$ - [L] + + # Prevent all the following rules from blocking css directory + RewriteRule (^|/)ui/css/[^/]+\.css$ - [L] + + # Prevent all the following rules from blocking js files + RewriteRule (^|/)ui/js/.+\.(map|css|js)$ - [L,QSA] + + # Prevent all the following rules from blocking sensor + RewriteRule (^|/)sensor/(|index\.php)$ - [L,QSA] + + # Prevent all the following rules from blocking installer + RewriteRule (^|/)install/(|index\.php)$ - [L,QSA] + + RewriteRule (^|/)(|index\.php)$ index.php [L,QSA] + + + # Block access to any htaccess files + RewriteCond %{REQUEST_URI} (^|/)(\.htaccess|htaccess\..*)$ [NC,OR] + + # Block access to assets directory + RewriteCond %{REQUEST_URI} (^|/)assets($|/.*$) [NC,OR] + + # Block access to the /app directories + RewriteCond %{REQUEST_URI} (^|/)app($|/.*$) [NC,OR] + + # Block access to the /sensor directories + RewriteCond %{REQUEST_URI} (^|/)sensor($|/.*$) [NC,OR] + + # Block access to the /config directories + RewriteCond %{REQUEST_URI} (^|/)config($|/.*$) [NC,OR] + + # Block access to the /tmp directories + RewriteCond %{REQUEST_URI} (^|/)tmp($|/.*$) [NC,OR] + + # Block access to the /libs directories + RewriteCond %{REQUEST_URI} (^|/)libs($|/.*$) [NC,OR] + + # Block access to the /install directories + RewriteCond %{REQUEST_URI} (^|/)install($|/.*$) [NC,OR] + + # Block access to the /vendor directories + RewriteCond %{REQUEST_URI} (^|/)vendor($|/.*$) [NC,OR] + + # Block access to the /db directories + RewriteCond %{REQUEST_URI} (^|/)db($|/.*$) [NC] + + RewriteRule ^.*$ - [F,L] + + + # Block access throughout to temporary files ending with tilde created by certain editors + RewriteCond %{REQUEST_URI} \.(html?|inc|json|lock|module|php|py|rb|sh|sql|tpl|tmpl|twig)~$ [NC,OR] + + # Block access to markdown and other software documents + RewriteCond %{REQUEST_URI} (^|/)(COPYRIGHT|INSTALL|README|CHANGELOG|LICENSE|AUTHORS|CODE_OF_CONDUCT|LEGALNOTICE|RELEASE_NOTES|SECURITY|FILE_ID|htaccess)(|\.txt|\.md|\.textile|\.diz|\.DIZ)$ [NC,OR] + + # Block access to source code files + RewriteCond %{REQUEST_URI} (^|/).+\.(sh|php|txt|yml|conf|ini|log|sql|json|js|md|sh|xml|lock)$ [NC,OR] + + # Block access to hidden files + RewriteCond %{REQUEST_URI} (^|/)\..+$ [NC,OR] + + # Block access to names of potential backup file extensions within wire or site directories + RewriteCond %{REQUEST_URI} (^|/).+\.(bak|old|sql|sw[op]|(bak|php|sql)[./]+.*)[\d.]*$ [NC,OR] + + # Block access to docker files if any left + RewriteCond %{REQUEST_URI} (^|/)(docker|Docker).*[\d.]*$ [NC,OR] + + # Block archive file types commonly used for backups + # ----------------------------------------------------------------------------------------------- + # This blocks requests for zip, rar, tar, gz, and tgz files that are sometimes left on servers + + RewriteCond %{REQUEST_URI} \.(zip|rar|tar|gz|tgz)$ [NC] + + RewriteRule ^.*$ - [F,L] + + #RewriteCond %{REQUEST_FILENAME} !-f + #RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !(favicon\.ico|robots\.txt) + + # ----------------------------------------------------------------------------------------------- + # Pass control to tirreno if all the above directives allow us to this point. + # For regular VirtualHosts (most installs) + # ----------------------------------------------------------------------------------------------- + + RewriteRule .* index.php [L,QSA] + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] + + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/.profile b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/.profile new file mode 100644 index 0000000..60cba85 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/.profile @@ -0,0 +1,8 @@ +chmod 740 composer.json +chmod 740 composer.lock +chmod -R 755 tmp +chmod -R 740 config +chmod -R 755 assets +chmod -R 755 assets/rules +chmod -R 755 assets/logs +chmod -R 755 assets/lists diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/AUTHORS.md b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/AUTHORS.md new file mode 100644 index 0000000..ecee465 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/AUTHORS.md @@ -0,0 +1,3 @@ +# Authors + +- Tirreno Technologies Sàrl diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/CHANGELOG.md b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/CHANGELOG.md new file mode 100644 index 0000000..1bb50d8 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/CHANGELOG.md @@ -0,0 +1,100 @@ +# tirreno changelog + +## tirreno v0.9.9 + +* user activity sparklines charts +* sessions stat collection +* settings for `LOGBOOK_LIMIT` +* settings for `ALLOW_FORGOT_PASSWORD` +* unique UA on language change +* suspicious url substrings update +* textarea url/query rendering fixes +* default current time on invalid eventTime in sensor and 0 on invalid httpCode +* default N/A on empty userName +* jquery-autocomplete update +* minor bug fixes + +## tirreno v0.9.8 + +* field audit trail via new event type `field_edit` +* optional payload for event types `page_search` and `account_email_change` +* sequential load of inner pages elements +* inactive rules visualisation +* extended list for user-agent vulnerability check +* several rules tuning +* Fat-Free Framework update +* minor bug fixes + +## tirreno v0.9.7 + +* DataTables 2.3.2 +* different type for event bars charts +* live clock +* new `page_error` event type +* chart for logbook page +* save server time zone timestamp for logbook instead of UTC +* /assets dir for logs, custom rules and suspicious words lists +* prevent parallel requests on daterange switch and rule play +* use only DOM API instead of innerHTML +* improved counties grid and map queries +* fix timestamps localization in panels +* enhanced .htaccess + +## tirreno v0.9.6 + +* flexible rule system +* heroku automated deployment +* improved main page load +* sorting columns whitelists for prevention of ajax blind sql injections +* blacklist chart improvements + chart class reorganization +* add `B23` and `B24` rules +* device detector updates +* /config/local dir for custom configs and extensions + +## tirreno v0.9.5 + +* new search filters for blacklist and users +* review queue and automated blacklisting thresholds +* device detector update +* database updates module +* improved notifications +* default 1D range for grids +* blacklist API + +## tirreno v0.9.4 + +* new search filters for types of events and IPs +* failed login attempt widget for the dashboard screen +* inactive session limit +* new chart for events +* clickable rules +* highlighting reviewed users +* js improvements +* minor bug fixes +* lint configurations +* [wip] review queue threshold settings + +## tirreno v0.9.3 + +* user graph on /event plot +* clickable countries on svg map +* improve /blacklist management +* index.php in all subdirs to prevent directory listing +* force utf-8 sensor input + +## tirreno v0.9.2 + +* XSS vulnerability patch +* optimize getLastEvent() + +## tirreno v0.9.1 + +* data plotting with hour and minute resolution +* minor bug fixes +* dependencies update +* js linting improvements +* logbook search fix + +## tirreno v0.9.0 + +* initial release diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/CODE_OF_CONDUCT.md b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8e01f7e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/CODE_OF_CONDUCT.md @@ -0,0 +1,67 @@ +# CONTRIBUTOR COVENANT CODE OF CONDUCT + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +Demonstrating empathy and kindness toward other people +Being respectful of differing opinions, viewpoints, and experiences +Giving and gracefully accepting constructive feedback +Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +Focusing on what is best not just for us as individuals, but for the overall community +Examples of unacceptable behavior include: + +The use of sexualized language or imagery, and sexual attention or advances of any kind +Trolling, insulting or derogatory comments, and personal or political attacks +Public or private harassment +Publishing others’ private information, such as a physical or email address, without their explicit permission +Other conduct which could reasonably be considered inappropriate in a professional setting +Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at team@tirreno.com. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +1. Correction +Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +2. Warning +Community Impact: A violation through a single incident or series of actions. + +Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +3. Temporary Ban +Community Impact: A serious violation of community standards, including sustained inappropriate behavior. + +Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +4. Permanent Ban +Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +Consequence: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html]. \ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/Dockerfile b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/Dockerfile new file mode 100644 index 0000000..26842ea --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/Dockerfile @@ -0,0 +1,19 @@ +FROM php:8.1-apache + +# Install common PHP extensions +RUN docker-php-ext-install mysqli pdo pdo_mysql && docker-php-ext-enable mysqli pdo pdo_mysql + +# Enable Apache rewrite module +RUN a2enmod rewrite + +# Set working directory to Apache web root +WORKDIR /var/www/html + +COPY . . + +# Make sure permissions are set correctly +RUN chown -R www-data:www-data /var/www/html + +EXPOSE 80 + +CMD ["apache2-foreground"] diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/FILE_ID.DIZ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/FILE_ID.DIZ new file mode 100644 index 0000000..4ff223b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/FILE_ID.DIZ @@ -0,0 +1,60 @@ + + + 888 + 888 888 + 888 + 88888888 888 888.d88 888.d88 + 888 888 888P" 888P" + 888 888 888 888 + 888 888 888 888 + 888 888 888 888 + + .d88b. 88.d8b. .d88b. + d8P Y8b 888 "88b d88""88b + 88888888 888 888 888 888 + Y8b .qq 888 888 888 888 + d8bddr 888 888 R88 88P + + + tirreno - open-source security + platform + + + 888 his is the + 8 beginning + 8 of a new day. + + God has given + me this day to + use as I will. + + I can waste it + or use it for good + + What I do today + is very important + because I am + exchanging a day + of my life for it. + + When tomorrow + comes, this day + will be gone + forever, leaving + something in its + place I have + traded for it + + I want it to be + gain, not loss... + Good, not evil... + Success, not failure... + In order that I shall + not forget the + price I paid for it. + + + * + * * + + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/LEGALNOTICE.md b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/LEGALNOTICE.md new file mode 100644 index 0000000..1854016 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/LEGALNOTICE.md @@ -0,0 +1,64 @@ +JAVASCRIPT AND ADDITIONAL COMPONENTS + + accept-language-parser + License: MIT + URL: https://github.com/opentable/accept-language-parser + + DataTables + License: MIT + URL: https://datatables.net/ + + devbridge-jQuery-Autocomplete + License: https://github.com/devbridge/jQuery-Autocomplete/blob/master/license.txt + URL: https://github.com/devbridge/jQuery-Autocomplete + + jQuery + License: MIT + URL: https://github.com/jquery/jquery + + Tooltipster (tooltipster-master) + License: MIT + URL: https://github.com/calebjacob/tooltipster + + uPlot + License: MIT + URL: https://github.com/leeoniya/uPlot + + Choices + License: MIT + URL: https://github.com/Choices-js/Choices + + jVectorMap + License: AGPL-3.0 + URL: https://jvectormap.com/ + + TinyCode's "Cross My Heart" game + URL: https://github.com/KilledByAPixel/TinyCode/blob/master/games/CrossMyHeart/index256.html + + Flag-icons by Lipis + License: MIT + URL: https://github.com/lipis/flag-icons + + Icons: Icons used in this project are provided by Streamline (https://streamlinehq.com) + +PHP COMPONENTS AND LIBRARIES + + Fat-Free Framework + License: GPL v3.0 + URL: https://github.com/bcosca/fatfree-core + + PHPMailer + License: LGPL v2.1 + URL: https://github.com/PHPMailer/PHPMailer + + Ruler + License: MIT + URL: https://github.com/bobthecow/Ruler + + php-saml by OneLogin + License: MIT + URL: https://github.com/onelogin/php-saml + + xmlseclibs by Rob Richards + License: BSD-3-Clause + URL: https://github.com/robrichards/xmlseclibs diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/LICENSE b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/README.md b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/README.md new file mode 100644 index 0000000..b2669e2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/README.md @@ -0,0 +1,126 @@ +# tirreno + +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/ec30c28f67de476f8b98d2798079bdf0)](https://app.codacy.com/gh/TirrenoTechnologies/tirreno/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) +[![Docker Pulls](https://img.shields.io/docker/pulls/tirreno/tirreno?style=flat)](https://hub.docker.com/r/tirreno/tirreno/) + +

+ + tirreno screenshot + +

+ +The open-source security analytics. + +tirreno helps organizations to understand, monitor, and protect their digital platforms from cyber threats, account threats, and abuse. Proactively defend against internal and external threats, ensure sovereignty through on-premises deployment, and secure your digital platforms. + +* **Granular visibility**: Understand your application with detailed event tracking and smart monitoring. Capture all events: logins/logouts, data modification, page visits, API calls, along with user context and application errors to get complete operational awareness. +* **Continuous monitoring**: Monitor user behaviour in near real time by choosing from a list of relevant event types. Proactively find cyber threats, account threats, and abuse in your digital platform, and respond to threats quickly based on in-app intelligence. +* **Single user view**: Get comprehensive visibility into individual user activity. Analyze behaviour patterns, risk scores, connected identities, and activity timelines for specific users. Investigate suspicious activity and review detailed session data to make informed security decisions. +* **User risk assessment**: Set up security rules to automatically identify malicious activity, and assess risk tailored to your specific requirements. +* **Case management and auto-decision**: Accounts with risky events are sent to manual review or automatic suspension. Security team can investigate flagged cases and make informed decisions based on comprehensive security analytics. +* **Field audit trail**: Automatically track modifications to important fields, including what changed and when. Centralized field audit trail lets you easily review data changes, streamlining audit and compliance. +* **Enhanced threat intelligence**: tirreno can be enhanced with first-party threat intelligence via API enrichment. Eliminate blind spots and get the threat intelligence you need to build a multi-layered defense for your application. (Optional) + +tirreno is a "low-tech" PHP and PostgreSQL software application that can be downloaded and installed on your own web server. After a straightforward five-minute installation process, you can immediately access real-time threat analytics. + +## Online demo + +Check out the online demo at [play.tirreno.com](https://play.tirreno.com) (admin/tirreno). + +## Requirements + +* **PHP**: Version 8.0 to 8.3 +* **PostgreSQL**: Version 12 or greater +* **PHP extensions**: `PDO_PGSQL`, `cURL` +* **HTTP web server**: `Apache` with `mod_rewrite` and `mod_headers` enabled +* **Operating system**: A Unix-like system is recommended +* **Minimum hardware requirements**: + * **PostgreSQL**: 512 MB RAM (4 GB recommended) + * **Application**: 128 MB RAM (1 GB recommended) + * **Storage**: Approximately 3 GB PostgreSQL storage per 1 million events + +## Quickstart install + +1. [Download](https://www.tirreno.com/download.php) the latest version of tirreno (ZIP file). +2. Extract the tirreno-master.zip file to the location where you want it installed on your web server. +3. Navigate to `http://your-domain.example/install/index.php` in a browser to launch the installation process. +4. After the successful installation, delete the `install/` directory and its contents. +5. Navigate to `http://your-domain.example/signup/` in a browser to create administrator account. +6. For cron jobs setup insert the following schedule (every 10 minutes) expression with `crontab -e` command or by editing `/var/spool/cron/your-web-server` file: + +``` +*/10 * * * * cd /path/to/tirreno && /usr/bin/php /path/to/tirreno/index.php /cron >> /path/to/tirreno/assets/logs/error.log 2>&1 +``` + +## Using a docker-based installation (optional) + +To run tirreno within docker container you may use image published on [dockerhub](https://hub.docker.com/r/tirreno/tirreno). + +```bash +docker pull tirreno/tirreno:latest +``` + +## Using Heroku (optional) + +Click [here](https://heroku.com/deploy?template=https://github.com/tirrenotechnologies/tirreno) to launch heroku deployment. + +## Documentation + +See the [User Guide](https://docs.tirreno.com/) for details on how to use tirreno. + +## Optional non-free capabilities + +The open-sourced tirreno code is intended to be used for free as a standalone application. It provides general statistics, rule engine and risk-based alerting to a monitored system. As is, this tooling may be sufficient for bringing insights about user activity and behavioral patterns in a wide range of use cases, especially as a solution for small and medium-sized organizations. + +However, if you are looking to cover more advanced usage scenarios, the additional tirreno API enrichment capabilities can be enabled via [monthly-paid subscription](https://www.tirreno.com/pricing/). + +## About + +The tirreno project started as a proprietary system in 2021 and was open-sourced (AGPL) in December 2024. + +Behind tirreno is a blend of extraordinary engineers and professionals, with over a decade of experience in online business operations. We solve real people's challenges through love in ascétique code and sovereign technologies. + +tirreno is not VC-motivated. Our inspiration comes from the daily threats posed by organized cybercriminals, driving us to reimagine protection that has never existed before. + +The tirreno wordmark stands beyond the horizon line, as a metaphor for the evolutionary cycle of the threat landscape, and our commitment to stay ahead of it. + +## Why the name tirreno? + +History suggests that the Tyrrhenian people may have lived in Tuscany and eastern Switzerland as far back as 800 BC. The term "Tyrrhenian” became more commonly associated with the Etruscans, and it is from them that the Tyrrhenian Sea derives its name — a name still in use today. This name is believed to be an exonym, possibly meaning “tower”. + +While working on the logo, we conducted our own historical study and traced mentions of 'tirreno' back to the 15th-century printed edition of the Vulgate (the Latin Bible). We kept it lowercase to stay true to the original — quite literally, by the book. + +The tirreno wordmark, positioned beyond a horizon line, as a metaphor for the constant evolution of the fraud landscape and our commitment to staying ahead of change. + +## Links + +* [Website](https://www.tirreno.com) +* [Live demo](https://play.tirreno.com) +* [Documentation](https://docs.tirreno.com) +* [Mattermost community](https://chat.tirreno.com) + +## Reporting a security issue + +If you've found a security-related issue with tirreno, please email security@tirreno.com. Submitting the issue on GitHub exposes the vulnerability to the public, making it easy to exploit. We will publicly disclose the security issue after it has been resolved. + +After receiving a report, tirreno will take the following steps: + +* Confirm that the report has been received and is being addressed. +* Attempt to reproduce the problem and confirm the vulnerability. +* Release new versions of all the affected packages. +* Announce the problem prominently in the release notes. +* If requested, give credit to the reporter. + +## License + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License (AGPL) as published by the Free Software Foundation version 3. + +The name "tirreno" is a registered trademark of tirreno technologies sàrl, and tirreno technologies sàrl hereby declines to grant a trademark license to "tirreno" pursuant to the GNU Affero General Public License version 3 Section 7(e), without a separate agreement with tirreno technologies sàrl. + +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 GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see [GNU Affero General Public License v3](https://www.gnu.org/licenses/agpl-3.0.txt). + +## Authors + +tirreno Copyright (C) 2025 tirreno technologies sàrl, Vaud, Switzerland. (License AGPLv3) diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/RELEASE_NOTES.md b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/RELEASE_NOTES.md new file mode 100644 index 0000000..277c3c4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/RELEASE_NOTES.md @@ -0,0 +1,7 @@ +Release song: https://youtu.be/nVfTGzlD4_Y + +tirreno is announcing version v0.9.9. + +This release enhances user visiual analytics, improves user-agent treatment +and introduces several manual app settings, including `.php` formatted lists +of suspicious words. diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/SECURITY.md b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/SECURITY.md new file mode 100644 index 0000000..58fa69c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Reporting a Vulnerability + +If you've found a security-related issue with Tirreno, please email security@tirreno.com. Submitting to GitHub makes the vulnerability public, making it easy to exploit. We'll do a public disclosure of the security issue once it's been fixed. + +After receiving a report, Tirreno will take the following steps: + +- Confirmation that the issue has been received and that it's in the process of being addressed. +- Attempt to reproduce the problem and confirm the vulnerability. +- Create a patch or implement the necessary changes to address the vulnerability. +- Release a new version of all the affected versions. +- Prominently announce the problem in the release notes. +- If requested, give credit to the reporter. diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app.json b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app.json new file mode 100644 index 0000000..48aab78 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app.json @@ -0,0 +1,47 @@ +{ + "name": "tirreno", + "description": "Open-source user intelligence platform. Monitor, analyze, and protect your app from cyber and account threats. ", + "website": "https://www.tirreno.com", + "repository": "https://github.com/tirrenotechnologies/tirreno", + "logo": "https://www.tirreno.com/tirreno-logo-public.png", + "keywords": [ + "tirreno", + "fraud-detection", + "audit-trail", + "analytics", + "monitoring", + "bot-detection", + "antispam", + "application-monitoring" + ], + "success_url": "/install/index.php?mode=schema", + "addons": [ + { + "plan": "heroku-postgresql", + "options": { + "version": "14" + } + }, + { + "plan": "scheduler" + } + ], + "formation": { + "web": { + "quantity": 1, + "size": "basic" + } + }, + "buildpacks": [ + { + "url": "heroku/php" + } + ], + "env": { + "FORCE_HTTPS": { + "description": "Secure HTTP for SITE variable", + "required": true, + "value": "True" + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app.manifest b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app.manifest new file mode 100644 index 0000000..9e61f80 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app.manifest @@ -0,0 +1,14 @@ +{ + "id": "com.tirrenotechnologies.tirreno.cloudron", + "title": "tirreno", + "version": "1.0.0", + "build": "1", + "description": "Cloudron package for tirreno", + "author": "Auto-generated", + "website": "https://github.com/tirrenotechnologies/tirreno", + "admin": false, + "tags": ["php", "auto-generated"], + "logo": "https://github.com/fluidicon.png", + "documentation": "https://github.com/tirrenotechnologies/tirreno", + "changelog": "Initial packaging" +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Assets/Rule.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Assets/Rule.php new file mode 100644 index 0000000..4f6d02b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Assets/Rule.php @@ -0,0 +1,19 @@ +ENRICHED_ATTRIBUTES = array_keys(\Utils\Constants::get('ENRICHING_ATTRIBUTES')); + } + + public function proceedPostRequest(array $params): array { + $cmd = $params['cmd'] ?? ''; + + return match ($cmd) { + 'resetKey' => $this->resetApiKey($params), + 'updateApiUsage' => $this->updateApiUsage($params), + 'enrichAll' => $this->enrichAll($params), + default => [] + }; + } + + public function getUsageStats(int $operatorId): array { + $model = new \Models\ApiKeys(); + $apiKeys = $model->getKeys($operatorId); + + $isOwner = true; + if (!$apiKeys) { + $coOwnerModel = new \Models\ApiKeyCoOwner(); + $coOwnerModel->getCoOwnership($operatorId); + + if ($coOwnerModel->loaded()) { + $isOwner = false; + $apiKeys[] = $model->getKeyById($coOwnerModel->api); + } + } + + if (!$isOwner) { + return ['data' => []]; + } + + $resultKeys = []; + + foreach ($apiKeys as $key) { + $subscriptionStats = []; + if ($key->token !== null) { + [$code, $response, $error] = $this->getSubscriptionStats($key->token); + $subscriptionStats = strlen($error) > 0 || $code > 201 ? [] : $response; + } + + $remaining = $subscriptionStats['remaining'] ?? null; + $total = $subscriptionStats['total'] ?? null; + $used = $remaining !== null && $total !== null ? $total - $remaining : null; + + $resultKeys[] = [ + 'id' => $key->id, + 'key' => $key->key, + 'apiToken' => $key->token ?? null, + 'sub_status' => $subscriptionStats['status'] ?? null, + 'sub_calls_left' => $remaining, + 'sub_calls_used' => $used, + 'sub_calls_limit' => $total, + 'sub_next_billed' => $subscriptionStats['next_billed_at'] ?? null, + 'sub_update_url' => $subscriptionStats['update_url'] ?? null, + 'sub_plan_id' => $subscriptionStats['current_subscription_plan']['sub_id'] ?? null, + 'sub_plan_api_calls' => $subscriptionStats['current_subscription_plan']['api_calls'] ?? null, + //'all_subscription_plans' => $subscriptionStats['all_subscription_plans'] ?? null, + ]; + } + + return ['data' => $resultKeys]; + } + + public function getOperatorApiKeysDetails(int $operatorId): array { + [$isOwner, $apiKeys] = $this->getOperatorApiKeys($operatorId); + + $resultKeys = []; + + foreach ($apiKeys as $key) { + $resultKeys[] = [ + 'id' => $key->id, + 'key' => $key->key, + 'created_at' => $key->created_at, + 'skip_enriching_attributes' => $key->skip_enriching_attributes, + 'enrichedAttributes' => $this->getEnrichedAttributes($key), + 'retention_policy' => $key->retention_policy, + 'skip_blacklist_sync' => $key->skip_blacklist_sync, + 'apiToken' => $key->token ?? null, + ]; + } + + return [$isOwner, $resultKeys]; + } + + private function getSubscriptionStats(string $token): array { + $api = \Utils\Variables::getEnrichtmentApi(); + + $options = [ + 'method' => 'GET', + 'header' => [ + 'Authorization: Bearer ' . $token, + 'User-Agent: ' . $this->f3->get('USER_AGENT'), + ], + ]; + + /** @var array{request: array, body: string, headers: array, engine: string, cached: bool, error: string} $result */ + $result = \Web::instance()->request( + url: sprintf('%s/usage-stats', $api), + options: $options, + ); + + $matches = []; + preg_match('/^HTTP\/(\d+)(?:\.\d)? (\d{3})/', $result['headers'][0], $matches); + $statusCode = (int) ($matches[2] ?? 0); + + $errorMessage = $result['error']; + $jsonResponse = json_decode($result['body'], true); + + return [$statusCode, $jsonResponse, $errorMessage]; + } + + public function resetApiKey(array $params): array { + $pageParams = []; + $errorCode = $this->validateResetApiKey($params); + + if ($errorCode) { + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $keyId = isset($params['keyId']) ? (int) $params['keyId'] : null; + + $model = new \Models\ApiKeys(); + $model->getKeyById($keyId); + $model->resetKey($keyId, $model->creator); + + $pageParams['SUCCESS_MESSAGE'] = $this->f3->get('AdminApi_reset_success_message'); + } + + return $pageParams; + } + + public function enrichAll(array $data): array { + $pageParams = []; + $errorCode = $this->validateEnrichAll($data); + + if ($errorCode) { + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + $model = new \Models\Users(); + $accountsForEnrichment = $model->notCheckedUsers($apiKey); + + $actionType = new \Type\QueueAccountOperationActionType(\Type\QueueAccountOperationActionType::ENRICHMENT); + $accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + + $accountOpQueueModel->addBatchIds($accountsForEnrichment, $apiKey); + + $pageParams['SUCCESS_MESSAGE'] = $this->f3->get('AdminApi_manual_enrichment_success_message'); + } + + return $pageParams; + } + + public function validateEnrichAll(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + return false; + } + + public function validateResetApiKey(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $keyId = isset($params['keyId']) ? (int) $params['keyId'] : null; + if (!$keyId) { + return \Utils\ErrorCodes::API_KEY_ID_DOESNT_EXIST; + } + + if ($keyId !== $this->getCurrentOperatorApiKeyId()) { + return \Utils\ErrorCodes::API_KEY_WAS_CREATED_FOR_ANOTHER_USER; + } + + return false; + } + + private function validateApiKeyAccess(int $keyId, int $operatorId): bool { + $model = new \Models\ApiKeys(); + $model->getByKeyAndOperatorId($keyId, $operatorId); + + if (!$model->loaded()) { + $coOwnerModel = new \Models\ApiKeyCoOwner(); + $coOwnerModel->getCoOwnership($operatorId); + + if (!$coOwnerModel->loaded()) { + return false; + } + } + + return true; + } + + public function getEnrichedAttributes(\Models\ApiKeys $key): array { + $enrichedAttributes = []; + $skipAttributes = \json_decode($key->skip_enriching_attributes); + foreach ($this->ENRICHED_ATTRIBUTES as $attribute) { + $enrichedAttributes[$attribute] = !\in_array($attribute, $skipAttributes); + } + + return $enrichedAttributes; + } + + public function updateApiUsage(array $params): array { + $errorCode = $this->validateUpdateApiUsage($params); + $pageParams = []; + + if ($errorCode) { + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $keyId = isset($params['keyId']) ? (int) $params['keyId'] : null; + $model = new \Models\ApiKeys(); + $model->getKeyById($keyId); + + if ($params['apiToken'] !== null) { + $apiToken = trim($params['apiToken']); + [$code, , $error] = $this->getSubscriptionStats($apiToken); + if (strlen($error) > 0 || $code > 201) { + $pageParams['ERROR_CODE'] = \Utils\ErrorCodes::SUBSCRIPTION_KEY_INVALID_UPDATE; + return $pageParams; + } + $model->updateInternalToken($apiToken); + } + + $enrichedAttributes = $params['enrichedAttributes'] ?? []; + $skipEnrichingAttributes = \array_diff($this->ENRICHED_ATTRIBUTES, \array_keys($enrichedAttributes)); + $model->updateSkipEnrichingAttributes($skipEnrichingAttributes); + + $skipBlacklistSync = !isset($params['exchangeBlacklist']); + $model->updateSkipBlacklistSynchronisation($skipBlacklistSync); + + $pageParams['SUCCESS_MESSAGE'] = $this->f3->get('AdminApi_data_enrichment_success_message'); + } + + return $pageParams; + } + + public function validateUpdateApiUsage(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $keyId = isset($params['keyId']) ? (int) $params['keyId'] : null; + if (!$keyId) { + return \Utils\ErrorCodes::API_KEY_ID_DOESNT_EXIST; + } + + $currentOperator = $this->f3->get('CURRENT_USER'); + $operatorId = $currentOperator->id; + if (!$this->validateApiKeyAccess($keyId, $operatorId)) { + return \Utils\ErrorCodes::API_KEY_WAS_CREATED_FOR_ANOTHER_USER; + } + + $enrichedAttributes = $params['enrichedAttributes'] ?? []; + $unknownAttributes = \array_diff(\array_keys($enrichedAttributes), $this->ENRICHED_ATTRIBUTES); + if ($unknownAttributes) { + return \Utils\ErrorCodes::UNKNOWN_ENRICHMENT_ATTRIBUTES; + } + + return false; + } + + public function getNotCheckedEntitiesForLoggedUser(): bool { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $controller = new \Controllers\Admin\Enrichment\Data(); + + return $controller->getNotCheckedExists($apiKey); + } + + public function getScheduledForEnrichment(): bool { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $actionType = new \Type\QueueAccountOperationActionType(\Type\QueueAccountOperationActionType::ENRICHMENT); + $accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + + // do not use isInQueue() to prevent true on failed state + return $accountOpQueueModel->actionIsInQueueProcessing($apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Api/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Api/Navigation.php new file mode 100644 index 0000000..ff01308 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Api/Navigation.php @@ -0,0 +1,36 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getUsageStats(): array { + $currentOperator = $this->f3->get('CURRENT_USER'); + $operatorId = $currentOperator->id; + + return $operatorId ? (new Data())->getUsageStats($operatorId) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Api/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Api/Page.php new file mode 100644 index 0000000..c63ae7c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Api/Page.php @@ -0,0 +1,59 @@ +f3->get('CURRENT_USER'); + $operatorId = $currentOperator->id; + + $scheduledForEnrichment = $dataController->getScheduledForEnrichment(); + + $pageParams = [ + 'LOAD_AUTOCOMPLETE' => true, + 'LOAD_DATATABLE' => true, + 'HTML_FILE' => 'admin/api.html', + 'JS' => 'admin_api.js', + 'API_URL' => \Utils\Variables::getSiteWithProtocol() . '/sensor/', + 'SCHEDULED_FOR_ENRICHMENT' => $scheduledForEnrichment, + ]; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $params['id'] = $operatorId; + + $operationResponse = $dataController->proceedPostRequest($params); + + $pageParams = array_merge($pageParams, $operationResponse); + $pageParams['CMD'] = $params['cmd'] ?? null; + } + + // set these params after proccessing POST request + [$isOwner, $apiKeys] = $dataController->getOperatorApiKeysDetails($operatorId); + $pageParams['IS_OWNER'] = $isOwner; + $pageParams['API_KEYS'] = $apiKeys; + + $pageParams['NOT_CHECKED'] = $dataController->getNotCheckedEntitiesForLoggedUser(); + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Api/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Api/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Api/index.php @@ -0,0 +1,3 @@ +getAllBlacklistedItems(); + } + + public function removeItemFromBlacklist(int $itemId, string $type, int $apiKey): void { + $model = null; + + if ($type === 'ip') { + $model = new \Models\Ip(); + } + if ($type === 'email') { + $model = new \Models\Email(); + } + if ($type === 'phone') { + $model = new \Models\Phone(); + } + + $model->updateFraudFlag([$itemId], false, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Blacklist/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Blacklist/Navigation.php new file mode 100644 index 0000000..62c47dd --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Blacklist/Navigation.php @@ -0,0 +1,51 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getList(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } + + public function removeItemFromList(): array { + $dataController = new Data(); + + $apiKey = $this->getCurrentOperatorApiKeyId(); + $id = $this->f3->get('REQUEST.id'); + $type = $this->f3->get('REQUEST.type'); + + $dataController->removeItemFromBlacklist($id, $type, $apiKey); + $successCode = \Utils\ErrorCodes::ITEM_HAS_BEEN_SUCCESSFULLY_REMOVED_FROM_BLACK_LIST; + + return [ + 'success' => $successCode, + 'id' => $id, + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Blacklist/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Blacklist/Page.php new file mode 100644 index 0000000..1bb64c4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Blacklist/Page.php @@ -0,0 +1,37 @@ +f3->get('AdminBlacklist_search_placeholder'); + + $pageParams = [ + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + 'LOAD_UPLOT' => true, + 'LOAD_DATATABLE' => true, + 'LOAD_CHOICES' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/blacklist.html', + 'JS' => 'admin_blacklist.js', + 'ENTITY_TYPES' => \Utils\Constants::get('ENTITY_TYPES'), + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Blacklist/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Blacklist/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Blacklist/index.php @@ -0,0 +1,3 @@ + $this->enrichEntity($params), + default => [] + }; + } + + public function enrichEntity(array $params): array { + $dataController = new \Controllers\Admin\Enrichment\Data(); + $apiKey = $this->getCurrentOperatorApiKeyId(); + $enrichmentKey = $this->getCurrentOperatorEnrichmentKeyString(); + $type = $params['type']; + $search = $params['search'] ?? null; + $entityId = isset($params['entityId']) ? (int) $params['entityId'] : null; + + return $dataController->enrichEntity($type, $search, $entityId, $apiKey, $enrichmentKey); + } + + public function checkIfOperatorHasAccess(int $botId): bool { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $model = new \Models\Bot(); + + return $model->checkAccess($botId, $apiKey); + } + + public function getBotDetails(int $botId): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $model = new \Models\Bot(); + + return $model->getFullBotInfoById($botId, $apiKey); + } + + public function isEnrichable(): bool { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $model = new \Models\ApiKeys(); + + return $model->attributeIsEnrichable('ua', $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bot/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bot/Navigation.php new file mode 100644 index 0000000..aa3d93f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bot/Navigation.php @@ -0,0 +1,40 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getBotDetails(): array { + $dataController = new Data(); + $botId = $this->f3->get('REQUEST.botId'); + $hasAccess = $dataController->checkIfOperatorHasAccess($botId); + if (!$hasAccess) { + $this->f3->error(404); + } + + return $dataController->getBotDetails($botId); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bot/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bot/Page.php new file mode 100644 index 0000000..e934171 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bot/Page.php @@ -0,0 +1,59 @@ +integerParam($this->f3->get('PARAMS.botId')); + $hasAccess = $dataController->checkIfOperatorHasAccess($botId); + + if (!$hasAccess) { + $this->f3->error(404); + } + + $bot = $dataController->getBotDetails($botId); + $pageTitle = $this->getInternalPageTitleWithPostfix($bot['id']); + $isEnrichable = $dataController->isEnrichable(); + + $pageParams = [ + 'LOAD_DATATABLE' => true, + 'LOAD_JVECTORMAP' => true, + 'LOAD_ACCEPT_LANGUAGE_PARSER' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/bot.html', + 'BOT' => $bot, + 'PAGE_TITLE' => $pageTitle, + 'LOAD_UPLOT' => true, + 'JS' => 'admin_bot.js', + 'IS_ENRICHABLE' => $isEnrichable, + ]; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $operationResponse = $dataController->proceedPostRequest($params); + + $pageParams = array_merge($pageParams, $operationResponse); + $pageParams['CMD'] = $params['cmd']; + // recall bot data + $pageParams['BOT'] = $dataController->getBotDetails($botId); + } + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bot/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bot/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bot/index.php @@ -0,0 +1,3 @@ +getAllBots(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bots/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bots/Navigation.php new file mode 100644 index 0000000..aeccd8d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bots/Navigation.php @@ -0,0 +1,35 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getList(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bots/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bots/Page.php new file mode 100644 index 0000000..8b61c9d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bots/Page.php @@ -0,0 +1,36 @@ +f3->get('AdminBots_search_placeholder'); + + $pageParams = [ + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + 'LOAD_UPLOT' => true, + 'LOAD_DATATABLE' => true, + 'LOAD_ACCEPT_LANGUAGE_PARSER' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/bots.html', + 'JS' => 'admin_bots.js', + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bots/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bots/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Bots/index.php @@ -0,0 +1,3 @@ +model = new \Models\Context\Data(); + } + + public function getContextByAccountIds(array $accountIds, int $apiKey): array { + return $this->getContext($accountIds, $apiKey); + } + + public function getContextByAccountId(int $accountId, int $apiKey): array { + $accountIds = [$accountId]; + $context = $this->getContext($accountIds, $apiKey); + + return $context[$accountId] ?? []; + } + + private function getContext(array $accountIds, int $apiKey): array { + return $this->model->getContext($accountIds, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Context/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Context/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Context/index.php @@ -0,0 +1,3 @@ +getAllCountries(); + + $ids = array_column($result['data'], 'id'); + if ($ids) { + $model = new \Models\Country(); + $model->updateTotalsByEntityIds($ids, $apiKey); + $result['data'] = $model->refreshTotals($result['data'], $apiKey); + } + + return $result; + } + + public function getMap(int $apiKey): array { + $result = []; + + $model = new \Models\Map(); + + $ispId = $this->f3->get('REQUEST.ispId'); + $userId = $this->f3->get('REQUEST.userId'); + $botId = $this->f3->get('REQUEST.botId'); + $domainId = $this->f3->get('REQUEST.domainId'); + $resourceId = $this->f3->get('REQUEST.resourceId'); + + if (isset($userId) && is_numeric($userId)) { + $result = $model->getCountriesByUserId($userId, $apiKey); + } + + if (isset($ispId) && is_numeric($ispId)) { + $result = $model->getCountriesByIspId($ispId, $apiKey); + } + + if (isset($domainId) && is_numeric($domainId)) { + $result = $model->getCountriesByDomainId($domainId, $apiKey); + } + + if (isset($botId) && is_numeric($botId)) { + $result = $model->getCountriesByBotId($botId, $apiKey); + } + + if (isset($resourceId) && is_numeric($resourceId)) { + $result = $model->getCountriesByResourceId($resourceId, $apiKey); + } + + if (!$result) { + $dateFrom = $this->f3->get('REQUEST.dateFrom'); + $dateTo = $this->f3->get('REQUEST.dateTo'); + + $dateFrom = ($dateFrom) ? date('Y-m-d H:i:s', strtotime($dateFrom)) : null; + $dateTo = ($dateTo) ? date('Y-m-d H:i:s', strtotime($dateTo)) : null; + + $result = $model->getAllCountries($dateFrom, $dateTo, $apiKey); + } + + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Countries/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Countries/Navigation.php new file mode 100644 index 0000000..e8aeea6 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Countries/Navigation.php @@ -0,0 +1,41 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getList(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } + + public function getMap(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getMap($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Countries/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Countries/Page.php new file mode 100644 index 0000000..166a1c1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Countries/Page.php @@ -0,0 +1,35 @@ +f3->get('AdminCountries_search_placeholder'); + + $pageParams = [ + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + 'LOAD_JVECTORMAP' => true, + 'LOAD_DATATABLE' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/countries.html', + 'JS' => 'admin_countries.js', + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Countries/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Countries/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Countries/index.php @@ -0,0 +1,3 @@ +getCurrentOperatorApiKeyId(); + $model = new \Models\Country(); + + return $model->checkAccess($countryId, $apiKey); + } + + public function getCountryById(int $countryId): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $model = new \Models\Country(); + + return $model->getCountryById($countryId, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Country/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Country/Navigation.php new file mode 100644 index 0000000..2d2ed36 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Country/Navigation.php @@ -0,0 +1,29 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Country/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Country/Page.php new file mode 100644 index 0000000..e5790e9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Country/Page.php @@ -0,0 +1,49 @@ +integerParam($this->f3->get('PARAMS.countryId')); + + $hasAccess = $dataController->checkIfOperatorHasAccess($countryId); + + if (!$hasAccess) { + $this->f3->error(404); + } + + $country = $dataController->getCountryById($countryId); + $pageTitle = $this->getInternalPageTitleWithPostfix($country['value']); + + $pageParams = [ + 'LOAD_DATATABLE' => true, + 'LOAD_UPLOT' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'LOAD_ACCEPT_LANGUAGE_PARSER' => true, + 'HTML_FILE' => 'admin/country.html', + 'COUNTRY' => $country, + 'PAGE_TITLE' => $pageTitle, + 'JS' => 'admin_country.js', + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Country/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Country/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Country/index.php @@ -0,0 +1,3 @@ +enrichEntity(); + } + + public function saveRule(): array { + $controller = new \Controllers\Admin\Rules\Navigation(); + + return $controller->saveRule(); + } + + public function removeFromBlacklist(): array { + $controller = new \Controllers\Admin\Blacklist\Navigation(); + + return $controller->removeItemFromList(); + } + + public function removeFromWatchlist(): array { + $controller = new \Controllers\Admin\Watchlist\Navigation(); + + return $controller->removeUserFromList(); + } + + public function manageUser(): array { + $controller = new \Controllers\Admin\User\Navigation(); + + return $controller->manageUser(); + } + + // GET requests + public function checkRule(): array { + $controller = new \Controllers\Admin\Rules\Navigation(); + + return $controller->checkRule(); + } + + public function getTimeFrameTotal(): array { + $controller = new \Controllers\Admin\Totals\Navigation(); + + return $controller->getTimeFrameTotal(); + } + + public function getCountries(): array { + $controller = new \Controllers\Admin\Countries\Navigation(); + + return $controller->getList(); + } + + public function getMap(): array { + $controller = new \Controllers\Admin\Countries\Navigation(); + + return $controller->getMap(); + } + + public function getIps(): array { + $controller = new \Controllers\Admin\IPs\Navigation(); + + return $controller->getList(); + } + + public function getEvents(): array { + $controller = new \Controllers\Admin\Events\Navigation(); + + return $controller->getList(); + } + + public function getLogbook(): array { + $controller = new \Controllers\Admin\Logbook\Navigation(); + + return $controller->getList(); + } + + public function getUsers(): array { + $controller = new \Controllers\Admin\Users\Navigation(); + + return $controller->getList(); + } + + public function getBots(): array { + $controller = new \Controllers\Admin\Bots\Navigation(); + + return $controller->getList(); + } + + public function getDevices(): array { + $controller = new \Controllers\Admin\Devices\Navigation(); + + return $controller->getList(); + } + + public function getResources(): array { + $controller = new \Controllers\Admin\Resources\Navigation(); + + return $controller->getList(); + } + + public function getDashboardStat(): array { + $controller = new \Controllers\Admin\Home\Navigation(); + + return $controller->getDashboardStat(); + } + + public function getTopTen(): array { + $controller = new \Controllers\Admin\Home\Navigation(); + + return $controller->getTopTen(); + } + + public function getChart(): array { + $controller = new \Controllers\Admin\Home\Navigation(); + + return $controller->getChart(); + } + + public function getEventDetails(): array { + $controller = new \Controllers\Admin\Events\Navigation(); + + return $controller->getEventDetails(); + } + + public function getLogbookDetails(): array { + $controller = new \Controllers\Admin\Logbook\Navigation(); + + return $controller->getLogbookDetails(); + } + + public function getEmailDetails(): array { + $controller = new \Controllers\Admin\Emails\Navigation(); + + return $controller->getEmailDetails(); + } + + public function getPhoneDetails(): array { + $controller = new \Controllers\Admin\Phones\Navigation(); + + return $controller->getPhoneDetails(); + } + + public function getUserDetails(): array { + $controller = new \Controllers\Admin\UserDetails\Navigation(); + + return $controller->getUserDetails(); + } + + public function getUserEnrichmentDetails(): array { + $controller = new \Controllers\Admin\UserDetails\Navigation(); + + return $controller->getUserEnrichmentDetails(); + } + + public function getNotCheckedEntitiesCount(): array { + $controller = new \Controllers\Admin\Enrichment\Navigation(); + + return $controller->getNotCheckedEntitiesCount(); + } + + public function getEmails(): array { + $controller = new \Controllers\Admin\Emails\Navigation(); + + return $controller->getList(); + } + + public function getPhones(): array { + $controller = new \Controllers\Admin\Phones\Navigation(); + + return $controller->getList(); + } + + public function getFieldAuditTrail(): array { + $controller = new \Controllers\Admin\Payloads\FieldAuditTrail\Navigation(); + + return $controller->getList(); + } + + public function getUserScoreDetails(): array { + $controller = new \Controllers\Admin\User\Navigation(); + + return $controller->getUserScoreDetails(); + } + + public function getIsps(): array { + $controller = new \Controllers\Admin\ISPs\Navigation(); + + return $controller->getList(); + } + + public function getDomains(): array { + $controller = new \Controllers\Admin\Domains\Navigation(); + + return $controller->getList(); + } + + public function getReviewUsersQueue(): array { + $controller = new \Controllers\Admin\ReviewQueue\Navigation(); + + return $controller->getList(); + } + + public function getReviewUsersQueueCount(): array { + $controller = new \Controllers\Admin\ReviewQueue\Navigation(); + + return $controller->getNumberOfNotReviewedUsers(false, true); // do not use cache, overall count + } + + public function getIspDetails(): array { + $controller = new \Controllers\Admin\ISP\Navigation(); + + return $controller->getIspDetails(); + } + + public function getIpDetails(): array { + $controller = new \Controllers\Admin\IP\Navigation(); + + return $controller->getIpDetails(); + } + + public function getDeviceDetails(): array { + $controller = new \Controllers\Admin\Devices\Navigation(); + + return $controller->getDeviceDetails(); + } + + public function getBotDetails(): array { + $controller = new \Controllers\Admin\Bot\Navigation(); + + return $controller->getBotDetails(); + } + + public function getDomainDetails(): array { + $controller = new \Controllers\Admin\Domain\Navigation(); + + return $controller->getDomainDetails(); + } + + public function getSearchResults(): array { + $controller = new \Controllers\Admin\Search\Navigation(); + + return $controller->getSearchResults(); + } + + public function getBlacklist(): array { + $controller = new \Controllers\Admin\Blacklist\Navigation(); + + return $controller->getList(); + } + + public function getUsageStats(): array { + $controller = new \Controllers\Admin\Api\Navigation(); + + return $controller->getUsageStats(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Data/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Data/Navigation.php new file mode 100644 index 0000000..d17fab0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Data/Navigation.php @@ -0,0 +1,204 @@ +validateCsrfToken(); + if ($errorCode) { + $this->f3->error(403); + } + + $this->redirectIfUnlogged(); + + $this->dataController = new Data(); + $this->response = new \Views\Json(); + } + + // POST requests + public function enrichEntity(): void { + $this->response->data = $this->dataController->enrichEntity(); + } + + public function manageUser(): void { + $this->response->data = $this->dataController->manageUser(); + } + + public function removeFromWatchlist(): void { + $this->response->data = $this->dataController->removeFromWatchlist(); + } + + public function removeFromBlacklist(): void { + $this->response->data = $this->dataController->removeFromBlacklist(); + } + + public function saveRule(): void { + $this->response->data = $this->dataController->saveRule(); + } + + // GET requests + public function checkRule(): void { + $this->response->data = $this->dataController->checkRule(); + } + + public function getTimeFrameTotal(): void { + $this->response->data = $this->dataController->getTimeFrameTotal(); + } + + public function getCountries(): void { + $this->response->data = $this->dataController->getCountries(); + } + + public function getMap(): void { + $this->response->data = $this->dataController->getMap(); + } + + public function getIps(): void { + $this->response->data = $this->dataController->getIps(); + } + + public function getEvents(): void { + $this->response->data = $this->dataController->getEvents(); + } + + public function getLogbook(): void { + $this->response->data = $this->dataController->getLogbook(); + } + + public function getUsers(): void { + $this->response->data = $this->dataController->getUsers(); + } + + public function getBots(): void { + $this->response->data = $this->dataController->getBots(); + } + + public function getDevices(): void { + $this->response->data = $this->dataController->getDevices(); + } + + public function getResources(): void { + $this->response->data = $this->dataController->getResources(); + } + + public function getTopTen(): void { + $this->response->data = $this->dataController->getTopTen(); + } + + public function getDashboardStat(): void { + $this->response->data = $this->dataController->getDashboardStat(); + } + + public function getChart(): void { + $this->response->data = $this->dataController->getChart(); + } + + public function getEventDetails(): void { + $this->response->data = $this->dataController->getEventDetails(); + } + + public function getLogbookDetails(): void { + $this->response->data = $this->dataController->getLogbookDetails(); + } + + public function getEmailDetails(): void { + $this->response->data = $this->dataController->getEmailDetails(); + } + + public function getPhoneDetails(): void { + $this->response->data = $this->dataController->getPhoneDetails(); + } + + public function getUserDetails(): void { + $this->response->data = $this->dataController->getUserDetails(); + } + + public function getUserEnrichmentDetails(): void { + $this->response->data = $this->dataController->getUserEnrichmentDetails(); + } + + public function getNotCheckedEntitiesCount(): void { + $this->response->data = $this->dataController->getNotCheckedEntitiesCount(); + } + + public function getEmails(): void { + $this->response->data = $this->dataController->getEmails(); + } + + public function getPhones(): void { + $this->response->data = $this->dataController->getPhones(); + } + + public function getFieldAuditTrail(): void { + $this->response->data = $this->dataController->getFieldAuditTrail(); + } + + public function getUserScoreDetails(): void { + $this->response->data = $this->dataController->getUserScoreDetails(); + } + + public function getIsps(): void { + $this->response->data = $this->dataController->getIsps(); + } + + public function getDomains(): void { + $this->response->data = $this->dataController->getDomains(); + } + + public function getIspDetails(): void { + $this->response->data = $this->dataController->getIspDetails(); + } + + public function getIpDetails(): void { + $this->response->data = $this->dataController->getIpDetails(); + } + + public function getDeviceDetails(): void { + $this->response->data = $this->dataController->getDeviceDetails(); + } + + public function getBotDetails(): void { + $this->response->data = $this->dataController->getBotDetails(); + } + + public function getDomainDetails(): void { + $this->response->data = $this->dataController->getDomainDetails(); + } + + public function getReviewUsersQueue(): void { + $this->response->data = $this->dataController->getReviewUsersQueue(); + } + + public function getReviewUsersQueueCount(): void { + $this->response->data = $this->dataController->getReviewUsersQueueCount(); + } + + public function getSearchResults(): void { + $this->response->data = $this->dataController->getSearchResults(); + } + + public function getBlacklist(): void { + $this->response->data = $this->dataController->getBlacklist(); + } + + public function getUsageStats(): void { + $this->response->data = $this->dataController->getUsageStats(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Data/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Data/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Data/index.php @@ -0,0 +1,3 @@ +f3->get('REQUEST.ipId'); + $userId = $this->f3->get('REQUEST.userId'); + $resourceId = $this->f3->get('REQUEST.resourceId'); + + if (isset($ipId) && is_numeric($ipId)) { + $result = $model->getDevicesByIpId($ipId); + } + + if (isset($userId) && is_numeric($userId)) { + $result = $model->getDevicesByUserId($userId); + } + + if (isset($resourceId) && is_numeric($resourceId)) { + $result = $model->getDevicesByResourceId($resourceId); + } + + if (!$result) { + $result = $model->getAllDevices(); + } + + return $result; + } + + public function getDeviceDetails(int $apiKey): array { + $params = $this->f3->get('GET'); + $id = $params['id']; + $model = new \Models\Device(); + + $details = $model->getFullDeviceInfoById($id, $apiKey); + $details['enrichable'] = $this->isEnrichable($apiKey); + + $tsColumns = ['created']; + \Utils\TimeZones::localizeTimestampsForActiveOperator($tsColumns, $details); + + return $details; + } + + private function isEnrichable(int $apiKey): bool { + $model = new \Models\ApiKeys(); + + return $model->attributeIsEnrichable('ua', $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Devices/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Devices/Navigation.php new file mode 100644 index 0000000..15729b5 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Devices/Navigation.php @@ -0,0 +1,33 @@ +getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } + + public function getDeviceDetails(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getDeviceDetails($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Devices/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Devices/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Devices/index.php @@ -0,0 +1,3 @@ + $this->enrichEntity($params), + default => [] + }; + } + + public function enrichEntity(array $params): array { + $dataController = new \Controllers\Admin\Enrichment\Data(); + $apiKey = $this->getCurrentOperatorApiKeyId(); + $enrichmentKey = $this->getCurrentOperatorEnrichmentKeyString(); + $type = $params['type']; + $search = $params['search'] ?? null; + $entityId = isset($params['entityId']) ? (int) $params['entityId'] : null; + + return $dataController->enrichEntity($type, $search, $entityId, $apiKey, $enrichmentKey); + } + + public function checkIfOperatorHasAccess(int $domainId): bool { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $model = new \Models\Domain(); + + return $model->checkAccess($domainId, $apiKey); + } + + public function getDomainDetails(int $domainId): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + $model = new \Models\Domain(); + $result = $model->getFullDomainInfoById($domainId, $apiKey); + + $tsColumns = ['lastseen']; + \Utils\TimeZones::localizeTimestampsForActiveOperator($tsColumns, $result); + + return $result; + } + + public function isEnrichable(): bool { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $model = new \Models\ApiKeys(); + + return $model->attributeIsEnrichable('domain', $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domain/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domain/Navigation.php new file mode 100644 index 0000000..8efe39b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domain/Navigation.php @@ -0,0 +1,41 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getDomainDetails(): array { + $dataController = new Data(); + $domainId = $this->f3->get('REQUEST.domainId'); + $hasAccess = $dataController->checkIfOperatorHasAccess($domainId); + + if (!$hasAccess) { + $this->f3->error(404); + } + + return $dataController->getDomainDetails($domainId); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domain/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domain/Page.php new file mode 100644 index 0000000..fcb5001 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domain/Page.php @@ -0,0 +1,60 @@ +integerParam($this->f3->get('PARAMS.domainId')); + $hasAccess = $dataController->checkIfOperatorHasAccess($domainId); + + if (!$hasAccess) { + $this->f3->error(404); + } + + $domain = $dataController->getDomainDetails($domainId); + $pageTitle = $this->getInternalPageTitleWithPostfix($domain['domain']); + $isEnrichable = $dataController->isEnrichable(); + + $pageParams = [ + 'LOAD_DATATABLE' => true, + 'LOAD_JVECTORMAP' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/domain.html', + 'DOMAIN' => $domain, + 'PAGE_TITLE' => $pageTitle, + 'LOAD_UPLOT' => true, + 'LOAD_ACCEPT_LANGUAGE_PARSER' => true, + 'JS' => 'admin_domain.js', + 'IS_ENRICHABLE' => $isEnrichable, + ]; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $operationResponse = $dataController->proceedPostRequest($params); + + $pageParams = array_merge($pageParams, $operationResponse); + $pageParams['CMD'] = $params['cmd']; + // recall domain data + $pageParams['DOMAIN'] = $dataController->getDomainDetails($domainId); + } + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domain/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domain/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domain/index.php @@ -0,0 +1,3 @@ +f3->get('REQUEST.domainId'); + + if (isset($domainId)) { + $result = $model->getDomainsBySameIpDomainId($domainId); + } + + if (!$result) { + $result = $model->getAllDomains(); + } + + $ids = array_column($result['data'], 'id'); + if ($ids) { + $model = new \Models\Domain(); + $model->updateTotalsByEntityIds($ids, $apiKey); + $result['data'] = $model->refreshTotals($result['data'], $apiKey); + } + + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domains/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domains/Navigation.php new file mode 100644 index 0000000..abbb054 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domains/Navigation.php @@ -0,0 +1,35 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getList(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domains/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domains/Page.php new file mode 100644 index 0000000..5bdb2e3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domains/Page.php @@ -0,0 +1,35 @@ +f3->get('AdminDomains_search_placeholder'); + + $pageParams = [ + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + 'LOAD_UPLOT' => true, + 'LOAD_DATATABLE' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/domains.html', + 'JS' => 'admin_domains.js', + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domains/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domains/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Domains/index.php @@ -0,0 +1,3 @@ +f3->get('REQUEST.userId'); + + if (isset($userId) && is_numeric($userId)) { + $result = $model->getEmailsByUserId($userId); + } + + return $result; + } + + public function getEmailDetails(int $apiKey): array { + $params = $this->f3->get('GET'); + $id = $params['id']; + $model = new \Models\Email(); + + $details = $model->getEmailDetails($id, $apiKey); + $details['enrichable'] = $this->isEnrichable($apiKey); + + $tsColumns = ['email_created', 'email_lastseen', 'domain_lastseen', 'domain_created']; + \Utils\TimeZones::localizeTimestampsForActiveOperator($tsColumns, $details); + + return $details; + } + + private function isEnrichable(int $apiKey): bool { + $model = new \Models\ApiKeys(); + + return $model->attributeIsEnrichable('email', $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Emails/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Emails/Navigation.php new file mode 100644 index 0000000..f24d246 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Emails/Navigation.php @@ -0,0 +1,33 @@ +getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } + + public function getEmailDetails(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getEmailDetails($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Emails/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Emails/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Emails/index.php @@ -0,0 +1,3 @@ + \Utils\ErrorCodes::ENRICHMENT_API_KEY_DOES_NOT_EXIST]; + } + set_error_handler([\Utils\ErrorHandler::class, 'exceptionErrorHandler']); + $search = $search !== null ? ['value' => $search] : null; + $result = $this->enrichEntityProcess($type, $search, $entityId, $apiKey, $enrichmentKey); + restore_error_handler(); + + return $result; + } + + private function enrichEntityProcess(string $type, ?array $search, ?int $entityId, int $apiKey, ?string $enrichmentKey): array { + $processErrorMessage = ['ERROR_CODE' => \Utils\ErrorCodes::ENRICHMENT_API_UNKNOWN_ERROR]; + + if ($type === 'device') { + if ($entityId !== null) { + $model = new \Models\Device(); + $device = $model->getFullDeviceInfoById($entityId, $apiKey); + if ($device !== []) { + $entityId = $device['ua_id']; + $type = 'ua'; + } else { + return $processErrorMessage; + } + } else { + return $processErrorMessage; + } + } + + $model = new \Models\ApiKeys(); + $attributes = $model->enrichableAttributes($apiKey); + + if (!array_key_exists($type, $attributes)) { + return ['ERROR_CODE' => \Utils\ErrorCodes::ENRICHMENT_API_ATTRIBUTE_IS_UNAVAILABLE]; + } + + $modelDb = null; + $modelResult = null; + $extraModel = null; + switch ($type) { + case 'ip': + $modelDb = new \Models\Ip(); + $modelResult = new \Models\Enrichment\Ip(); + $extraModel = new \Models\Enrichment\LocalhostIp(); + break; + case 'email': + $modelDb = new \Models\Email(); + $modelResult = new \Models\Enrichment\Email(); + break; + case 'domain': + $modelDb = new \Models\Domain(); + $modelResult = new \Models\Enrichment\DomainFound(); + $extraModel = new \Models\Enrichment\DomainNotFound(); + break; + case 'phone': + $modelDb = new \Models\Phone(); + $modelResult = new \Models\Enrichment\PhoneValid(); + $extraModel = new \Models\Enrichment\PhoneInvalid(); + break; + case 'ua': + $modelDb = new \Models\Device(); + $modelResult = new \Models\Enrichment\Device(); + break; + } + + if ($modelDb === null) { + return $processErrorMessage; + } + + $value = $entityId !== null ? $modelDb->extractById($entityId, $apiKey) : $search; + + if ($value === null || $value === []) { + return $processErrorMessage; + } + + $apiError = null; + + try { + [$statusCode, $response,] = $this->enrichEntityByValue($type, $value, $enrichmentKey); + $error = \Utils\ApiResponseFormats::getErrorResponseFormat(); + $apiError = \Utils\ApiResponseFormats::matchResponse($response[$type] ?? [], $error) ? $response[$type]['error'] : null; + } catch (\ErrorException $e) { + return $processErrorMessage; + } + + if ($statusCode === 403) { + return ['ERROR_CODE' => \Utils\ErrorCodes::ENRICHMENT_API_KEY_OVERUSE]; + } + + if ($type === 'ip') { + // do not raise on bogon ip + if ($apiError === \Utils\Constants::get('ENRICHMENT_IP_IS_NOT_FOUND')) { + return ['ERROR_CODE' => \Utils\ErrorCodes::ENRICHMENT_API_IP_NOT_FOUND]; + } elseif ($apiError !== null && $apiError !== \Utils\Constants::get('ENRICHMENT_IP_IS_BOGON') || $statusCode !== 200 || $response[$type] === null) { + return $processErrorMessage; + } + } elseif ($apiError !== null || $statusCode !== 200 || $response[$type] === null) { + return $processErrorMessage; + } + + try { + $modelResult->init($response[$type]); + } catch (\ErrorException $e) { + if ($extraModel === null) { + return $processErrorMessage; + } + try { + $extraModel->init($response[$type]); + $modelResult = $extraModel; + } catch (\ErrorException $e) { + return $processErrorMessage; + } + } + + // change value in db only if $entityId was passed + if ($entityId !== null) { + try { + $modelResult->updateEntityInDb($entityId, $apiKey); + } catch (\ErrorException $e) { + return $processErrorMessage; + } + } + + return [ + $type => $response[$type], + 'SUCCESS_MESSAGE' => $this->f3->get('AdminEnrichment_success_message'), + ]; + } + + private function validateResponse(string $requestType, int $statusCode, ?array $result, string $errorMessage): bool|string|int { + if (!is_array($result)) { + return \Utils\ErrorCodes::ENRICHMENT_API_UNKNOWN_ERROR; + } + + if ($statusCode === 200 && is_array($result) && is_array($result[$requestType])) { + return false; + } + + $details = $result['detail'] ?? null; + if ($details) { + if (is_array($details)) { + $messages = array_map(function ($detail) { + if (isset($detail['msg']) && $detail['msg'] !== null && $detail['msg'] !== '') { + return $detail['msg']; + } + }, $details); + $messages = implode('; ', $messages); + } else { + $messages = $details; + } + } + + if (strlen($errorMessage) > 0) { + \Utils\Logger::log('Enrichment API web error', $errorMessage); + } + + if (!isset($messages) || strlen($messages) < 1) { + return \Utils\ErrorCodes::ENRICHMENT_API_UNKNOWN_ERROR; + } + + return $messages; + } + + private function enrichEntityByValue(string $type, array $value, string $enrichmentKey): array { + $apiUrl = \Utils\Variables::getEnrichtmentApi(); + $postFields = [ + $type => $value['value'], + ]; + + $options = [ + 'method' => 'POST', + 'header' => [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $enrichmentKey, + 'User-Agent: ' . $this->f3->get('USER_AGENT'), + ], + 'content' => \json_encode($postFields), + ]; + + /** @var array{request: array, body: string, headers: array, engine: string, cached: bool, error: string} $result */ + $result = \Web::instance()->request( + url: sprintf('%s/query', $apiUrl), + options: $options, + ); + + $matches = []; + preg_match('/^HTTP\/(\d+)(?:\.\d)? (\d{3})/', $result['headers'][0], $matches); + + $jsonResponse = json_decode($result['body'], true); + $statusCode = (int) ($matches[2] ?? 0); + + $errorMessages = $this->validateResponse($type, $statusCode, $jsonResponse, $result['error']); + + return [$statusCode, $jsonResponse, $errorMessages]; + } + + public function getNotCheckedEntitiesCount(int $apiKey): array { + $model = new \Models\ApiKeys(); + $models = $model->enrichableAttributes($apiKey); + $result = []; + + foreach ($models as $type => $model) { + $result[$type] = (new $model())->countNotChecked($apiKey); + } + + return $result; + } + + public function getNotCheckedExists(int $apiKey): bool { + $model = new \Models\ApiKeys(); + $models = $model->enrichableAttributes($apiKey); + + foreach ($models as $model) { + if ((new $model())->notCheckedExists($apiKey)) { + return true; + } + } + + return false; + } + + public function getNotCheckedEntitiesByUserId(int $userId, int $apiKey): array { + $model = new \Models\ApiKeys(); + $models = $model->enrichableAttributes($apiKey); + $result = []; + + foreach ($models as $type => $model) { + $result[$type] = (new $model())->notCheckedForUserId($userId, $apiKey); + } + + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Enrichment/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Enrichment/Navigation.php new file mode 100644 index 0000000..6f6101e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Enrichment/Navigation.php @@ -0,0 +1,39 @@ +getCurrentOperatorApiKeyId(); + $enrichmentKey = $this->getCurrentOperatorEnrichmentKeyString(); + $params = $this->f3->get('POST'); + $type = $params['type']; + $search = $params['search'] ?? null; + $entityId = $params['entityId'] ?? null; + + return $dataController->enrichEntity($type, $search, $entityId, $apiKey, $enrichmentKey); + } + + public function getNotCheckedEntitiesCount(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getNotCheckedEntitiesCount($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Enrichment/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Enrichment/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Enrichment/index.php @@ -0,0 +1,3 @@ +f3->get('REQUEST.ipId'); + $ispId = $this->f3->get('REQUEST.ispId'); + $userId = $this->f3->get('REQUEST.userId'); + $botId = $this->f3->get('REQUEST.botId'); + $domainId = $this->f3->get('REQUEST.domainId'); + $countryId = $this->f3->get('REQUEST.countryId'); + $resourceId = $this->f3->get('REQUEST.resourceId'); + + if (isset($ipId) && is_numeric($ipId)) { + $result = $model->getEventsByIpId($ipId); + } + + if (isset($ispId)) { + $result = $model->getEventsByIspId($ispId); + } + + if (isset($domainId) && is_numeric($domainId)) { + $result = $model->getEventsByDomainId($domainId); + } + + if (isset($userId) && is_numeric($userId)) { + $result = $model->getEventsByUserId($userId); + } + + if (isset($countryId) && is_numeric($countryId)) { + $result = $model->getEventsByCountryId($countryId); + } + + if (isset($resourceId) && is_numeric($resourceId)) { + $result = $model->getEventsByResourceId($resourceId); + } + + if (isset($botId) && is_numeric($botId)) { + $result = $model->getEventsByDeviceId($botId); + } + + if (!$result) { + $result = $model->getAllEvents(); + } + + return $result; + } + + public function getEventDetails(int $apiKey): array { + $params = $this->f3->get('GET'); + $id = $params['id']; + $model = new \Models\Event(); + + $result = $model->getEventDetails($id, $apiKey); + + $tsColumns = ['device_created', 'latest_decision', 'added_to_review', 'score_updated_at', 'event_time']; + \Utils\TimeZones::localizeTimestampsForActiveOperator($tsColumns, $result); + + if (isset($result['event_type_id']) && $result['event_type_id'] === \Utils\Constants::get('FIELD_EDIT_EVENT_TYPE_ID')) { + $model = new \Models\FieldAuditTrail(); + + $result['event_payload'] = json_encode($model->getByEventId($id, $apiKey)); + } + + return $result; + } + + public function getAllEventTypes(): array { + $model = new \Models\EventType(); + + return $model->getAll(); + } + + public function getAllDeviceTypes(): array { + return \Utils\Constants::get('DEVICE_TYPES'); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Events/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Events/Navigation.php new file mode 100644 index 0000000..a7b0394 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Events/Navigation.php @@ -0,0 +1,41 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getList(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } + + public function getEventDetails(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getEventDetails($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Events/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Events/Page.php new file mode 100644 index 0000000..51ba1b4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Events/Page.php @@ -0,0 +1,43 @@ +f3->get('AdminEvents_search_placeholder'); + $controller = new Data(); + $apiKey = $this->getCurrentOperatorApiKeyId(); + $rulesController = new \Controllers\Admin\Rules\Data(); + + $pageParams = [ + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + 'LOAD_ACCEPT_LANGUAGE_PARSER' => true, + 'LOAD_UPLOT' => true, + 'LOAD_DATATABLE' => true, + 'LOAD_CHOICES' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/events.html', + 'JS' => 'admin_events.js', + 'EVENT_TYPES' => $controller->getAllEventTypes(), + 'DEVICE_TYPES' => $controller->getAllDeviceTypes(), + 'RULES' => $rulesController->getAllRulesByApiKey($apiKey), + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Events/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Events/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Events/index.php @@ -0,0 +1,3 @@ +f3->get('REQUEST'); + $mode = $request['mode']; + $modelMap = \Utils\Constants::get('CHART_MODEL_MAP'); + + $model = array_key_exists($mode, $modelMap) ? new $modelMap[$mode]() : null; + + return $model ? $model->getData($apiKey) : [[], []]; + } + + public function getStat(int $apiKey): array { + $request = $this->f3->get('REQUEST'); + $dateRange = $this->getDatesRange($request); + $mode = $request['mode']; + + $model = new \Models\Dashboard(); + + $result = [ + 'total' => null, + 'allTimeTotal' => null, + ]; + + switch ($mode) { + case 'totalEvents': + $result['total'] = $model->getTotalEvents($dateRange, $apiKey); + $result['allTimeTotal'] = $model->getTotalEvents(null, $apiKey); + break; + case 'totalUsers': + $result['total'] = $model->getTotalUsers($dateRange, $apiKey); + $result['allTimeTotal'] = $model->getTotalUsers(null, $apiKey); + break; + case 'totalIps': + $result['total'] = $model->getTotalIps($dateRange, $apiKey); + $result['allTimeTotal'] = $model->getTotalIps(null, $apiKey); + break; + case 'totalCountries': + $result['total'] = $model->getTotalCountries($dateRange, $apiKey); + $result['allTimeTotal'] = $model->getTotalCountries(null, $apiKey); + break; + case 'totalUrls': + $result['total'] = $model->getTotalResources($dateRange, $apiKey); + $result['allTimeTotal'] = $model->getTotalResources(null, $apiKey); + break; + case 'totalUsersForReview': + $result['total'] = $model->getTotalUsersForReview($dateRange, $apiKey); + $result['allTimeTotal'] = $model->getTotalUsersForReview(null, $apiKey); + break; + case 'totalBlockedUsers': + $result['total'] = $model->getTotalBlockedUsers($dateRange, $apiKey); + $result['allTimeTotal'] = $model->getTotalBlockedUsers(null, $apiKey); + break; + } + + return $result; + } + + public function getTopTen(int $apiKey): array { + $params = $this->f3->get('GET'); + $dateRange = $this->getDatesRange($params); + $mode = $params['mode']; + $modelMap = \Utils\Constants::get('TOP_TEN_MODELS_MAP'); + + $model = array_key_exists($mode, $modelMap) ? new $modelMap[$mode]() : null; + $data = $model ? $model->getList($apiKey, $dateRange) : []; + $total = count($data); + + return [ + 'draw' => $params['draw'] ?? 1, + 'recordsTotal' => $total, + 'recordsFiltered' => $total, + 'data' => $data, + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Home/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Home/Navigation.php new file mode 100644 index 0000000..8d43055 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Home/Navigation.php @@ -0,0 +1,47 @@ +redirectIfUnlogged('/login'); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getDashboardStat(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getStat($apiKey) : []; + } + + public function getTopTen(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getTopTen($apiKey) : []; + } + + public function getChart(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getChart($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Home/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Home/Page.php new file mode 100644 index 0000000..03207e7 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Home/Page.php @@ -0,0 +1,31 @@ + true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/home.html', + 'JS' => 'admin_dashboard.js', + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Home/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Home/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Home/index.php @@ -0,0 +1,3 @@ + $this->enrichEntity($params), + default => [] + }; + } + + public function enrichEntity(array $params): array { + $dataController = new \Controllers\Admin\Enrichment\Data(); + $apiKey = $this->getCurrentOperatorApiKeyId(); + $enrichmentKey = $this->getCurrentOperatorEnrichmentKeyString(); + $type = $params['type']; + $search = $params['search'] ?? null; + $entityId = isset($params['entityId']) ? (int) $params['entityId'] : null; + + return $dataController->enrichEntity($type, $search, $entityId, $apiKey, $enrichmentKey); + } + + public function checkIfOperatorHasAccess(int $ipId): bool { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $model = new \Models\Ip(); + + return $model->checkAccess($ipId, $apiKey); + } + + public function getIpDetails(int $ipId): array { + $result = $this->getFullIpInfoById($ipId); + + return [ + 'full_country' => $result['full_country'], + 'country_id' => $result['country_id'], + 'country_iso' => $result['country_iso'], + 'asn' => $result['asn'], + 'blocklist' => $result['blocklist'], + 'fraud_detected' => $result['fraud_detected'], + 'data_center' => $result['data_center'], + 'vpn' => $result['vpn'], + 'tor' => $result['tor'], + 'relay' => $result['relay'], + 'starlink' => $result['starlink'], + 'ispid' => $result['ispid'], + ]; + } + + public function getFullIpInfoById(int $ipId): array { + $model = new \Models\Ip(); + $result = $model->getFullIpInfoById($ipId); + $result['lastseen'] = \Utils\ElapsedDate::short($result['lastseen']); + + return $result; + } + + public function isEnrichable(): bool { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $model = new \Models\ApiKeys(); + + return $model->attributeIsEnrichable('ip', $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IP/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IP/Navigation.php new file mode 100644 index 0000000..e1e6445 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IP/Navigation.php @@ -0,0 +1,41 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getIpDetails(): array { + $dataController = new Data(); + $ipId = $this->f3->get('REQUEST.ipId'); + $hasAccess = $dataController->checkIfOperatorHasAccess($ipId); + + if (!$hasAccess) { + $this->f3->error(404); + } + + return $dataController->getIpDetails($ipId); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IP/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IP/Page.php new file mode 100644 index 0000000..6f5931b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IP/Page.php @@ -0,0 +1,58 @@ +integerParam($this->f3->get('PARAMS.ipId')); + $hasAccess = $dataController->checkIfOperatorHasAccess($ipId); + + if (!$hasAccess) { + $this->f3->error(404); + } + + $ip = $dataController->getFullIpInfoById($ipId); + $pageTitle = $this->getInternalPageTitleWithPostfix($ip['ip']); + $isEnrichable = $dataController->isEnrichable(); + + $pageParams = [ + 'LOAD_DATATABLE' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/ip.html', + 'PAGE_TITLE' => $pageTitle, + 'IP' => $ip, + 'LOAD_UPLOT' => true, + 'LOAD_ACCEPT_LANGUAGE_PARSER' => true, + 'JS' => 'admin_ip.js', + 'IS_ENRICHABLE' => $isEnrichable, + ]; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $operationResponse = $dataController->proceedPostRequest($params); + + $pageParams = array_merge($pageParams, $operationResponse); + $pageParams['CMD'] = $params['cmd']; + // recall ip data + $pageParams['IP'] = $dataController->getFullIpInfoById($ipId); + } + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IP/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IP/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IP/index.php @@ -0,0 +1,3 @@ +f3->get('REQUEST.ispId'); + $userId = $this->f3->get('REQUEST.userId'); + $botId = $this->f3->get('REQUEST.botId'); + $domainId = $this->f3->get('REQUEST.domainId'); + $countryId = $this->f3->get('REQUEST.countryId'); + $resourceId = $this->f3->get('REQUEST.resourceId'); + + if (isset($userId) && is_numeric($userId)) { + $result = $model->getIpsByUserId($userId); + } + + if (isset($ispId)) { + $result = $model->getIpsByIspId($ispId); + } + + if (isset($domainId) && is_numeric($domainId)) { + $result = $model->getIpsByDomainId($domainId); + } + + if (isset($countryId) && is_numeric($countryId)) { + $result = $model->getIpsByCountryId($countryId); + } + + if (isset($botId) && is_numeric($botId)) { + $result = $model->getIpsByDeviceId($botId); + } + + if (isset($resourceId) && is_numeric($resourceId)) { + $result = $model->getIpsByResourceId($resourceId); + } + + if (!$result) { + $result = $model->getAllIps(); + } + + $ids = array_column($result['data'], 'id'); + if ($ids) { + $model = new \Models\Ip(); + $model->updateTotalsByEntityIds($ids, $apiKey); + $result['data'] = $model->refreshTotals($result['data'], $apiKey); + } + + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IPs/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IPs/Navigation.php new file mode 100644 index 0000000..edba63f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IPs/Navigation.php @@ -0,0 +1,35 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getList(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IPs/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IPs/Page.php new file mode 100644 index 0000000..e109e40 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IPs/Page.php @@ -0,0 +1,37 @@ +f3->get('AdminIps_search_placeholder'); + + $pageParams = [ + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + 'LOAD_UPLOT' => true, + 'LOAD_DATATABLE' => true, + 'LOAD_CHOICES' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/ips.html', + 'JS' => 'admin_ips.js', + 'IP_TYPES' => \Utils\Constants::get('IP_TYPES'), + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IPs/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IPs/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/IPs/index.php @@ -0,0 +1,3 @@ +getCurrentOperatorApiKeyId(); + $model = new \Models\Isp(); + + return $model->checkAccess($ispId, $apiKey); + } + + public function getFullIspInfoById(int $ispId): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $model = new \Models\Isp(); + $result = $model->getFullIspInfoById($ispId, $apiKey); + $result['lastseen'] = \Utils\ElapsedDate::short($result['lastseen']); + + return $result; + } + + private function getNumberOfIpsByIspId(int $ispId): int { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $model = new \Models\Isp(); + + return $model->getIpCountById($ispId, $apiKey); + } + + public function getIspDetails(int $ispId): array { + $result = []; + $data = $this->getFullIspInfoById($ispId); + + if (array_key_exists('asn', $data)) { + $result = [ + 'asn' => $data['asn'], + 'total_fraud' => $data['total_fraud'], + 'total_visit' => $data['total_visit'], + 'total_account' => $data['total_account'], + 'total_ip' => $this->getNumberOfIpsByIspId($ispId), + ]; + } + + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISP/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISP/Navigation.php new file mode 100644 index 0000000..05141f0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISP/Navigation.php @@ -0,0 +1,41 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getIspDetails(): array { + $dataController = new Data(); + $ispId = $this->f3->get('REQUEST.ispId'); + $hasAccess = $dataController->checkIfOperatorHasAccess($ispId); + + if (!$hasAccess) { + $this->f3->error(404); + } + + return $dataController->getIspDetails($ispId); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISP/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISP/Page.php new file mode 100644 index 0000000..1bf5657 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISP/Page.php @@ -0,0 +1,47 @@ +integerParam($this->f3->get('PARAMS.ispId')); + $hasAccess = $dataController->checkIfOperatorHasAccess($ispId); + + if (!$hasAccess) { + $this->f3->error(404); + } + + $isp = $dataController->getFullIspInfoById($ispId); + $pageTitle = $this->getInternalPageTitleWithPostfix(strval($isp['asn'])); + + $pageParams = [ + 'LOAD_DATATABLE' => true, + 'LOAD_JVECTORMAP' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/isp.html', + 'ISP' => $isp, + 'PAGE_TITLE' => $pageTitle, + 'LOAD_UPLOT' => true, + 'LOAD_ACCEPT_LANGUAGE_PARSER' => true, + 'JS' => 'admin_isp.js', + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISP/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISP/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISP/index.php @@ -0,0 +1,3 @@ +f3->get('REQUEST.userId'); + $domainId = $this->f3->get('REQUEST.domainId'); + $countryId = $this->f3->get('REQUEST.countryId'); + $resourceId = $this->f3->get('REQUEST.resourceId'); + + if (isset($userId) && is_numeric($userId)) { + $result = $model->getIspsByUserId($userId); + } + + if (isset($domainId) && is_numeric($domainId)) { + $result = $model->getIspsByDomainId($domainId); + } + + if (isset($countryId) && is_numeric($countryId)) { + $result = $model->getIspsByCountryId($countryId); + } + + if (isset($resourceId) && is_numeric($resourceId)) { + $result = $model->getIspsByResourceId($resourceId); + } + + if (!$result) { + $result = $model->getAllIsps(); + } + + $ids = array_column($result['data'], 'id'); + if ($ids) { + $model = new \Models\Isp(); + $model->updateTotalsByEntityIds($ids, $apiKey); + $result['data'] = $model->refreshTotals($result['data'], $apiKey); + } + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISPs/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISPs/Navigation.php new file mode 100644 index 0000000..0813564 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISPs/Navigation.php @@ -0,0 +1,35 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getList(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISPs/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISPs/Page.php new file mode 100644 index 0000000..b115636 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISPs/Page.php @@ -0,0 +1,35 @@ +f3->get('AdminIsps_search_placeholder'); + + $pageParams = [ + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + 'LOAD_UPLOT' => true, + 'LOAD_DATATABLE' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/isps.html', + 'JS' => 'admin_isps.js', + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISPs/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISPs/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ISPs/index.php @@ -0,0 +1,3 @@ +getAllLogbookEvents(); + } + + public function getLogbookDetails(int $apiKey): array { + $params = $this->f3->get('GET'); + $id = $params['id']; + $model = new \Models\Logbook(); + + return $model->getLogbookDetails($id, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Logbook/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Logbook/Navigation.php new file mode 100644 index 0000000..c350549 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Logbook/Navigation.php @@ -0,0 +1,42 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + // TODO: daterange ~= event_logbook.started? + public function getList(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } + + public function getLogbookDetails(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getLogbookDetails($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Logbook/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Logbook/Page.php new file mode 100644 index 0000000..6ae9564 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Logbook/Page.php @@ -0,0 +1,35 @@ +f3->get('AdminLogbook_search_placeholder'); + + $pageParams = [ + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + 'LOAD_UPLOT' => true, + 'LOAD_DATATABLE' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/logbook.html', + 'JS' => 'admin_logbook.js', + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Logbook/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Logbook/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Logbook/index.php @@ -0,0 +1,3 @@ +performSearch($params); + } + + public function performSearch(array $params): array { + $pageParams = [ + 'SEARCH_VALUES' => $params, + ]; + + $apiKey = $this->getCurrentOperatorApiKeyId(); + $enrichmentKey = $this->getCurrentOperatorEnrichmentKeyString(); + $errorCode = $this->validateSearch($params, $enrichmentKey); + + if ($errorCode) { + $pageParams['ERROR_CODE'] = $errorCode; + + return $pageParams; + } + + $type = $params['type']; + + $controller = new \Controllers\Admin\Enrichment\Data(); + $result = $controller->enrichEntity($type, $params['search'], null, $apiKey, $enrichmentKey); + + if (isset($result['ERROR_CODE'])) { + $pageParams['ERROR_CODE'] = $result['ERROR_CODE']; + + return $pageParams; + } + + $this->saveSearch($params); + + // TODO: return alert_list back in next release + if (array_key_exists('alert_list', $result[$type])) { + unset($result[$type]['alert_list']); + } + + if ($type === 'phone') { + unset($result[$type]['valid']); + unset($result[$type]['validation_error']); + } + + if ($type === 'email') { + unset($result[$type]['data_breaches']); + } + + $pageParams['RESULT'] = [$type => $result[$type]]; + + return $pageParams; + } + + private function validateSearch(array $params, string $enrichmentKey): bool|int { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $api = \Utils\Variables::getEnrichtmentApi(); + + if (!$enrichmentKey || !$api) { + return \Utils\ErrorCodes::ENRICHMENT_API_KEY_DOES_NOT_EXIST; + } + + $type = $params['type'] ?? null; + $types = $this->f3->get('AdminManualCheck_form_types'); + if (!$type || !array_key_exists($type, $types)) { + return \Utils\ErrorCodes::TYPE_DOES_NOT_EXIST; + } + + $search = $params['search'] ?? null; + if (!$search || strlen($search) < 1) { + return \Utils\ErrorCodes::SEARCH_QUERY_DOES_NOT_EXIST; + } + + return false; + } + + private function saveSearch(array $params): void { + $history = new \Models\ManualCheckHistoryQuery(); + $history->add($params); + } + + public function getSearchHistory(int $operatorId): ?array { + $model = new \Models\ManualCheckHistory(); + + return $model->getRecentByOperator($operatorId); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ManualCheck/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ManualCheck/Navigation.php new file mode 100644 index 0000000..6e18e96 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ManualCheck/Navigation.php @@ -0,0 +1,29 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ManualCheck/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ManualCheck/Page.php new file mode 100644 index 0000000..4133034 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ManualCheck/Page.php @@ -0,0 +1,60 @@ + true, + 'LOAD_DATATABLE' => true, + 'HTML_FILE' => 'admin/manualCheck.html', + 'JS' => 'admin_manual_check.js', + ]; + + $currentOperator = $this->f3->get('CURRENT_USER'); + $operatorId = $currentOperator->id; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $params['operator'] = $operatorId; + $operationResponse = $dataController->proceedPostRequest($params); + $pageParams = array_merge($pageParams, $operationResponse); + } + + $pageParams['HISTORY'] = $dataController->getSearchHistory($operatorId); + + return parent::applyPageParams($pageParams); + } + + public static function stylizeKey(string $key): string { + $f3 = \Base::instance(); + $overwrites = $f3->get('AdminManualCheck_key_overwrites'); + + if (array_key_exists($key, $overwrites)) { + return $overwrites[$key]; + } + + if ($key === 'profiles' || $key === 'data_breach') { + $key = sprintf('no_%s', $key); + } + + return ucfirst(str_replace('_', ' ', $key)); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ManualCheck/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ManualCheck/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ManualCheck/index.php @@ -0,0 +1,3 @@ +f3->get('REQUEST.userId'); + + if (isset($userId) && is_numeric($userId)) { + $result = $model->getDataByUserId($userId); + } + + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Payloads/FieldAuditTrail/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Payloads/FieldAuditTrail/Navigation.php new file mode 100644 index 0000000..cf84cda --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Payloads/FieldAuditTrail/Navigation.php @@ -0,0 +1,27 @@ +getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Payloads/FieldAuditTrail/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Payloads/FieldAuditTrail/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Payloads/FieldAuditTrail/index.php @@ -0,0 +1,3 @@ +f3->get('REQUEST.userId'); + + if (isset($userId) && is_numeric($userId)) { + $result = $model->getPhonesByUserId($userId); + } + + $ids = array_column($result['data'], 'id'); + if ($ids) { + $model = new \Models\Phone(); + $model->updateTotalsByEntityIds($ids, $apiKey); + $result['data'] = $model->refreshTotals($result['data'], $apiKey); + } + + return $result; + } + + public function getPhoneDetails(int $apiKey): array { + $params = $this->f3->get('GET'); + $id = $params['id']; + $model = new \Models\Phone(); + + $details = $model->getPhoneDetails($id, $apiKey); + $details['enrichable'] = $this->isEnrichable($apiKey); + + $tsColumns = ['created', 'lastseen']; + \Utils\TimeZones::localizeTimestampsForActiveOperator($tsColumns, $details); + + return $details; + } + + private function isEnrichable(int $apiKey): bool { + $model = new \Models\ApiKeys(); + + return $model->attributeIsEnrichable('phone', $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Phones/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Phones/Navigation.php new file mode 100644 index 0000000..67636c3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Phones/Navigation.php @@ -0,0 +1,33 @@ +getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } + + public function getPhoneDetails(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getPhoneDetails($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Phones/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Phones/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Phones/index.php @@ -0,0 +1,3 @@ +getCurrentOperatorApiKeyId(); + $model = new \Models\Resource(); + + return $model->checkAccess($resourceId, $apiKey); + } + + public function getResourceById(int $resourceId): array { + $model = new \Models\Resource(); + $result = $model->getResourceById($resourceId); + $result['lastseen'] = \Utils\ElapsedDate::short($result['lastseen']); + + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resource/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resource/Navigation.php new file mode 100644 index 0000000..51ba559 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resource/Navigation.php @@ -0,0 +1,29 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resource/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resource/Page.php new file mode 100644 index 0000000..70530c5 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resource/Page.php @@ -0,0 +1,53 @@ +integerParam($this->f3->get('PARAMS.resourceId')); + $hasAccess = $dataController->checkIfOperatorHasAccess($resourceId); + + if (!$hasAccess) { + $this->f3->error(404); + } + + $resource = $dataController->getResourceById($resourceId); + + $pageTitle = $resource['url']; + if ($resource['title']) { + $pageTitle .= sprintf(' (%s)', $resource['title']); + } + + $pageTitle = $this->getInternalPageTitleWithPostfix($pageTitle); + + $pageParams = [ + 'LOAD_DATATABLE' => true, + 'LOAD_JVECTORMAP' => true, + 'LOAD_UPLOT' => true, + 'LOAD_ACCEPT_LANGUAGE_PARSER' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/resource.html', + 'RESOURCE' => $resource, + 'PAGE_TITLE' => $pageTitle, + 'JS' => 'admin_resource.js', + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resource/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resource/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resource/index.php @@ -0,0 +1,3 @@ +getAllResources(); + + $ids = array_column($result['data'], 'id'); + if ($ids) { + $model = new \Models\Resource(); + $model->updateTotalsByEntityIds($ids, $apiKey); + $result['data'] = $model->refreshTotals($result['data'], $apiKey); + } + + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resources/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resources/Navigation.php new file mode 100644 index 0000000..98f67cf --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resources/Navigation.php @@ -0,0 +1,35 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getList(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resources/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resources/Page.php new file mode 100644 index 0000000..5b2b7b4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resources/Page.php @@ -0,0 +1,35 @@ +f3->get('AdminResources_search_placeholder'); + + $pageParams = [ + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + 'LOAD_UPLOT' => true, + 'LOAD_DATATABLE' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/resources.html', + 'JS' => 'admin_resources.js', + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resources/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resources/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Resources/index.php @@ -0,0 +1,3 @@ +getAllUnderReviewUsers() ?? []; + } + + public function getNumberOfNotReviewedUsers(int $apiKey, bool $useCache = true, bool $overall = false): array { + $currentOperator = $this->f3->get('CURRENT_USER'); + $takeFromCache = $this->canTakeNumberOfNotReviewedUsersFromCache($currentOperator); + + $total = $currentOperator->review_queue_cnt; + if (!$useCache || !$takeFromCache) { + $model = new \Models\Grid\ReviewQueue\Grid($apiKey); + $total = !$overall ? $model->getTotalUnderReviewUsers() : $model->getTotalUnderReviewUsersOverall(); + + if ($total > 999) { + $total = 999; + } + + $data = [ + 'id' => $currentOperator->id, + 'review_queue_cnt' => $total, + ]; + + $model = new \Models\Operator(); + $model->updateReviewedQueueCnt($data); + } + + return ['total' => $total]; + } + + private function canTakeNumberOfNotReviewedUsersFromCache(\Models\Operator $currentOperator): bool { + $diff = PHP_INT_MAX; + $currentTime = gmdate('Y-m-d H:i:s'); + $updatedAt = $currentOperator->review_queue_updated_at; + + if ($updatedAt) { + $dt1 = new \DateTime($currentTime); + $dt2 = new \DateTime($updatedAt); + + $diff = $dt1->getTimestamp() - $dt2->getTimestamp(); + } + + $cacheTime = $this->f3->get('REVIEWED_QUEUE_CNT_CACHE_TIME'); + + return $cacheTime > $diff; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ReviewQueue/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ReviewQueue/Navigation.php new file mode 100644 index 0000000..f9c9373 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ReviewQueue/Navigation.php @@ -0,0 +1,41 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getList(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } + + public function getNumberOfNotReviewedUsers(bool $useCache = true, bool $overall = false): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getNumberOfNotReviewedUsers($apiKey, $useCache, $overall) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ReviewQueue/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ReviewQueue/Page.php new file mode 100644 index 0000000..4d6f376 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ReviewQueue/Page.php @@ -0,0 +1,39 @@ +f3->get('AdminReviewQueue_search_placeholder'); + $apiKey = $this->getCurrentOperatorApiKeyId(); + $rulesController = new \Controllers\Admin\Rules\Data(); + + $pageParams = [ + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + 'LOAD_UPLOT' => true, + 'LOAD_DATATABLE' => true, + 'LOAD_CHOICES' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/reviewQueue.html', + 'JS' => 'admin_review_queue.js', + 'RULES' => $rulesController->getAllRulesByApiKey($apiKey), + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ReviewQueue/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ReviewQueue/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/ReviewQueue/index.php @@ -0,0 +1,3 @@ + $this->changeThresholdValues($params), + 'refreshRules' => $this->refreshRules($params), + default => [] + }; + } + + private function refreshRules(array $params): array { + $pageParams = []; + $errorCode = $this->validateRefreshRules($params); + + if ($errorCode) { + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $model = new \Models\Rules(); + + // get all rules from db by uid; will not return classes with filename mismatch or invalid classname + $currentRules = $model->getAll(); + + $sortedRules = []; + foreach ($currentRules as $rule) { + $sortedRules[$rule['uid']] = $rule; + } + + $localClasses = \Utils\RulesClasses::getRulesClasses(false); + $mainClasses = \Utils\RulesClasses::getRulesClasses(true); + + $iterates = [[], [], [], [], [], []]; + $metUids = []; + + $parentClass = \Controllers\Admin\Rules\Set\BaseRule::class; + $mtd = 'defineCondition'; + + // local classes first to keep ability to override default classes + $allClassesFromFiles = $localClasses['imported'] + $mainClasses['imported']; + + foreach ($allClassesFromFiles as $uid => $cls) { + $valid = true; + + $name = constant("$cls::NAME") ?? ''; + $descr = constant("$cls::DESCRIPTION") ?? ''; + $attr = constant("$cls::ATTRIBUTES") ?? []; + + $obj = [ + 'uid' => $uid, + 'name' => $name, + 'descr' => $descr, + 'attributes' => $attr, + ]; + + // check constants + if (!is_string($name) || !is_string($descr) || !is_array($attr)) { + $valid = false; + $obj['name'] = ''; + $obj['descr'] = ''; + $obj['attributes'] = []; + // check if rule is child class of BaseRule and defineCondition() was implemented + } elseif (!is_subclass_of($cls, $parentClass) || (new \ReflectionMethod($cls, $mtd))->isAbstract()) { + $valid = false; + } + + $status = $this->addRule($sortedRules, $obj, $valid, $model); + $iterates[($status === null ? 0 : 1 + intval($status)) * 2 + intval($valid)][] = $uid; + $metUids[] = $uid; + } + + $flipMetUids = array_flip($metUids); + $newMissingRules = []; + $oldMissingCnt = 0; + foreach ($sortedRules as $uid => $rule) { + if (!array_key_exists($uid, $flipMetUids)) { + if (!$rule['missing']) { + $newMissingRules[$uid] = $rule; + $model->setMissingByUid($uid); + } else { + $oldMissingCnt += 1; + } + } + } + + //$successCnt = count($iterates[5]) + count($iterates[3]); + //$warningCnt = count($iterates[4]) + count($iterates[2]); + + $newValidCnt = count($iterates[5]); + $newInvalidCnt = count($iterates[4]); + $updValidCnt = count($iterates[3]); + $updInvalidCnt = count($iterates[2]); + $missingCnt = count($newMissingRules); + + $messages = []; + + $messages[] = $this->getStatusNotification($newValidCnt, 'Added %s rule%s: %s', $iterates[5]); + $messages[] = $this->getStatusNotification($updValidCnt, 'Updated %s rule%s: %s', $iterates[3]); + + $msg = join(";\n", array_filter($messages)); + + if ($msg) { + $pageParams['SUCCESS_MESSAGE'] = $msg; + } + + $messages = []; + + $messages[] = $this->getStatusNotification($newInvalidCnt, 'Added %s invalid rule%s: %s', $iterates[4]); + $messages[] = $this->getStatusNotification($updInvalidCnt, 'Updated %s invalid rule%s: %s', $iterates[2]); + $messages[] = $this->getStatusNotification($missingCnt, 'Missing %s rule%s: %s', array_column($newMissingRules, 'uid')); + + $msg = join(";\n", array_filter($messages)); + + if ($msg) { + $pageParams['ERROR_MESSAGE'] = $msg; + } + + if (!array_key_exists('ERROR_MESSAGE', $pageParams) && !array_key_exists('SUCCESS_MESSAGE', $pageParams)) { + $activeCnt = count($iterates[1]); + $invalidCnt = count($iterates[0]); + + $msg = sprintf('Rules refreshed (%s rule%s active', $activeCnt, ($activeCnt > 1 ? 's' : '')); + if ($invalidCnt) { + $msg .= sprintf(', %s invalid', $invalidCnt); + } + if ($oldMissingCnt) { + $msg .= sprintf(', %s missing', $oldMissingCnt); + } + + $msg .= ')'; + $pageParams['SUCCESS_MESSAGE'] = $msg; + } + } + + return $pageParams; + } + + private function getStatusNotification(int $cnt, string $template, array $data): ?string { + if (!$cnt) { + return null; + } + + $s = join(', ', array_slice($data, 0, 10, true)) . ($cnt > 10 ? '…' : '.'); + + return sprintf($template, strval($cnt), ($cnt > 1 ? 's' : ''), $s); + } + + private function addRule(array $existingArray, array $obj, bool $valid, \Models\Rules $model): ?bool { + $data = $existingArray[$obj['uid']] ?? null; + $r = null; + + sort($obj['attributes']); + + if ($data === null) { + $r = true; + } else { + $data['attributes'] = json_decode($data['attributes']); + sort($data['attributes']); + + foreach ($obj as $key => $value) { + if ($value !== $data[$key]) { + $r = false; + break; + } + } + + if ($r !== false && $data['validated'] !== $valid) { + $r = false; + } + } + + if ($r !== null || $data['missing']) { + $model->addRule($obj['uid'], $obj['name'], $obj['descr'], $obj['attributes'], $valid); + } + + return ($data !== null && $data['missing']) ? true : $r; + } + + private function validateRefreshRules(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + return false; + } + + public function changeThresholdValues(array $params): array { + $pageParams = []; + $errorCode = $this->validateThresholdValues($params); + + if ($errorCode) { + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $keyId = isset($params['keyId']) ? (int) $params['keyId'] : null; + + $model = new \Models\ApiKeys(); + $model->getKeyById($keyId); + + $blacklistThreshold = (int) ($params['blacklist-threshold'] ?? -1); + $reviewQueueThreshold = (int) ($params['review-queue-threshold'] ?? 0); + + $recalculateReviewQueueCnt = $model->review_queue_threshold !== $reviewQueueThreshold; + + $model->updateBlacklistThreshold($blacklistThreshold); + $model->updateReviewQueueThreshold($reviewQueueThreshold); + + if ($recalculateReviewQueueCnt) { + $controller = new \Controllers\Admin\ReviewQueue\Data(); + $controller->getNumberOfNotReviewedUsers($keyId, false, true); + } + + $pageParams['SUCCESS_MESSAGE'] = $this->f3->get('AdminThresholdValues_update_success_message'); + } + + return $pageParams; + } + + private function validateThresholdValues(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $keyId = isset($params['keyId']) ? (int) $params['keyId'] : null; + if (!$keyId) { + return \Utils\ErrorCodes::API_KEY_ID_DOESNT_EXIST; + } + + if ($keyId !== $this->getCurrentOperatorApiKeyId()) { + return \Utils\ErrorCodes::API_KEY_WAS_CREATED_FOR_ANOTHER_USER; + } + + $blacklistThreshold = (int) ($params['blacklist-threshold'] ?? -1); + if ($blacklistThreshold < -1 || $blacklistThreshold > 18) { + return \Utils\ErrorCodes::BLACKLIST_THRESHOLD_DOES_NOT_EXIST; + } + + $reviewQueueThreshold = (int) ($params['review-queue-threshold'] ?? 0); + if ($reviewQueueThreshold < 0 || $reviewQueueThreshold > 100) { + return \Utils\ErrorCodes::REVIEW_QUEUE_THRESHOLD_DOES_NOT_EXIST; + } + + if ($reviewQueueThreshold <= $blacklistThreshold) { + return \Utils\ErrorCodes::BLACKLIST_THRESHOLD_EXCEEDS_REVIEW_QUEUE_THRESHOLD; + } + + return false; + } + + public function getRulesForLoggedUser(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $this->getAllAttrFilteredRulesByApiKey($apiKey); + } + + public function saveUserRule(string $ruleUid, int $score): void { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $model = new \Models\OperatorsRules(); + $model->updateRule($ruleUid, $score, $apiKey); + } + + public function saveRuleProportion(string $ruleUid, float $proportion): void { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $model = new \Models\OperatorsRules(); + $model->updateRuleProportion($ruleUid, $proportion, $apiKey); + } + + public function getRuleProportion(int $totalUsers, int $ruleUsers): float { + if ($ruleUsers === 0 || $totalUsers === 0) { + return 0.0; + } + + $proportion = (float) (100 * $ruleUsers) / (float) $totalUsers; + + // if number is too small make it a bit greater so it will be written in db as 0 < n < 1 + return abs($proportion) < 0.001 ? 0.001 : $proportion; + } + + // return array of uids on each account of triggered rules + private function evaluateRules(array $accountIds, array $rules, int $apiKey): array { + $result = array_fill_keys($accountIds, []); + + $context = []; + $record = []; + + foreach (array_chunk($accountIds, \Utils\Variables::getRuleUsersBatchSize()) as $batch) { + $context = $this->contextController->getContextByAccountIds($batch, $apiKey); + foreach ($batch as $user) { + $record = $context[$user] ?? null; + if ($record) { + foreach ($rules as $rule) { + if ($this->executeRule($rule, $record)) { + $result[$user][] = $rule->uid; + } + } + } + } + } + + return $result; + } + + private function executeRule(Set\BaseRule $rule, array $params): bool { + $executed = false; + + try { + $rule->updateParams($params); + $executed = $rule->execute(); + } catch (\Throwable $e) { + if (defined($rule->uid)) { + $model = new \Models\Rules(); + $model->setInvalidByUid($rule->uid); + } + + error_log('Failed to execute rule class ' . $rule->uid . ': ' . $e->getMessage()); + } + + return $executed; + } + + public function checkRule(string $ruleUid): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + $model = new \Models\Users(); + $users = $model->getAllUsersIdsOrdered($apiKey); + $accounts = []; + foreach ($users as $user) { + $accounts[$user['accountid']] = $user; + } + $accountIds = array_keys($accounts); + + $this->buildEvaluationModels($ruleUid); + + $targetRule = $this->rulesModel->getRuleWithOperatorValue($ruleUid, $apiKey); + + if ($targetRule === [] || !array_key_exists($ruleUid, $this->rulesMap)) { + return [0, []]; + } + + $results = $this->evaluateRules($accountIds, [$this->rulesMap[$ruleUid]], $apiKey); + $matchingAccountIds = array_keys(array_filter($results, static function ($value): bool { + return $value !== []; + })); + + $result = []; + foreach ($matchingAccountIds as $id) { + if (array_key_exists($id, $accounts)) { + $result[$id] = $accounts[$id]; + } + } + + return [count($accountIds), $result]; + } + + public function evaluateUser(int $accountId, int $apiKey, bool $preparedModels = false): void { + if (!$preparedModels || !$this->rulesModel) { + $this->buildEvaluationModels(); + } + + foreach ($this->totalModels as $model) { + $model->updateTotalsByAccountIds([$accountId], $apiKey); + } + + $operatorRules = $this->getAllRulesWithOperatorValues($this->rulesModel, $apiKey); + $rules = array_intersect_key($this->rulesMap, $operatorRules); + + $result = $this->evaluateRules([$accountId], $rules, $apiKey); + $uids = $result[$accountId]; + $details = []; + + foreach ($uids as $uid) { + $details[] = ['uid' => $uid, 'score' => $operatorRules[$uid]['value']]; + } + + $data = [ + 'score' => $this->normalizeScore($details), + 'details' => json_encode($details), + ]; + + $this->userController->updateUserStatus($accountId, $data, $apiKey); + } + + public function buildEvaluationModels(?string $uid = null): void { + $this->totalModels = []; + foreach (\Utils\Constants::get('RULES_TOTALS_MODELS') as $className) { + $this->totalModels[] = new $className(); + } + + $this->contextController = new \Controllers\Admin\Context\Data(); + $this->userController = new \Controllers\Admin\User\Data(); + $this->rulesModel = new \Models\OperatorsRules(); + + $rb = new \Ruler\RuleBuilder(); + + if ($uid) { + $ruleObj = \Utils\RulesClasses::getSingleRuleObject($uid, $rb); + $this->rulesMap = $ruleObj ? [$uid => $ruleObj] : []; + } else { + $this->rulesMap = \Utils\RulesClasses::getAllRulesObjects($rb); + } + } + + private function normalizeScore(array $data): int { + $scores = array_column($data, 'score'); + $totalScore = max(array_sum($scores), 0); + + $filterScores = array_filter($scores, function ($value) { + return $value > 0; + }); + + $matches = count($filterScores); + + return max((int) (99 - ($totalScore * (pow($matches, 1.1) - $matches + 1))), 0); + } + + // only valid, not missing, with fitting attributes, returning associative array + private function getAllRulesWithOperatorValues(\Models\OperatorsRules $rulesModel, int $apiKey): array { + $model = new \Models\ApiKeys(); + $skipAttributes = $model->getSkipEnrichingAttributes($apiKey); + + $rules = $rulesModel->getAllValidRulesByOperator($apiKey); + + $results = $this->filterRulesByAttributesAddTypes($rules, $skipAttributes); + + return $results; + } + + // with fitting attributes and sorted, returning as regular array + private function getAllAttrFilteredRulesByApiKey(int $apiKey): array { + $model = new \Models\ApiKeys(); + $skipAttributes = $model->getSkipEnrichingAttributes($apiKey); + + $model = new \Models\OperatorsRules(); + $rules = $model->getAllRulesByOperator($apiKey); + + $results = $this->filterRulesByAttributesAddTypes($rules, $skipAttributes); + + usort($results, static function ($a, $b): int { + if ($a['validated'] !== $b['validated']) { + return ($b['validated'] <=> $a['validated']); + } + + if ((int) ($a['missing'] === true) !== (int) ($b['missing'] === true)) { + return ((int) $a['missing'] <=> (int) $b['missing']); + } + + return $a['uid'] <=> $b['uid']; + }); + + return $results; + } + + // do not filter by attributes if data is needed only for rendering info + public function getAllRulesByApiKey(int $apiKey): array { + $model = new \Models\OperatorsRules(); + $rules = $model->getAllRulesByOperator($apiKey); + + $results = []; + foreach ($rules as $rule) { + $rule['type'] = \Utils\RulesClasses::getRuleTypeByUid($rule['uid']); + $results[] = $rule; + } + + usort($results, static function ($a, $b): int { + if ($a['validated'] !== $b['validated']) { + return ($b['validated'] <=> $a['validated']); + } + + if ((int) ($a['missing'] === true) !== (int) ($b['missing'] === true)) { + return ((int) $a['missing'] <=> (int) $b['missing']); + } + + return $a['uid'] <=> $b['uid']; + }); + + return $results; + } + + private function filterRulesByAttributesAddTypes(array $rules, array $skipAttributes): array { + $results = []; + + foreach ($rules as $id => $row) { + if (!count(array_intersect(json_decode($row['attributes']), $skipAttributes))) { + $row['type'] = \Utils\RulesClasses::getRuleTypeByUid($row['uid']); + $results[$id] = $row; + } + } + + return $results; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Navigation.php new file mode 100644 index 0000000..3492843 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Navigation.php @@ -0,0 +1,61 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function saveRule(): array { + $params = $this->f3->get('POST'); + $key = explode('_', $params['rule']); + $ruleUid = end($key); + $score = $params['value']; + + $dataController = new Data(); + $dataController->saveUserRule($ruleUid, $score); + + return ['success' => true]; + } + + public function checkRule(): array { + set_time_limit(0); + ini_set('max_execution_time', 0); + + $params = $this->f3->get('GET'); + $ruleUid = $params['ruleUid']; + + $dataController = new Data(); + [$allUsersCnt, $users] = $dataController->checkRule($ruleUid); + $proportion = $dataController->getRuleProportion($allUsersCnt, count($users)); + $dataController->saveRuleProportion($ruleUid, $proportion); + + return [ + 'users' => array_slice($users, 0, \Utils\Constants::get('RULE_CHECK_USERS_PASSED_TO_CLIENT')), + 'count' => count($users), + 'proportion' => $proportion, + 'proportion_updated_at' => date('Y-m-d H:i:s'), + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Page.php new file mode 100644 index 0000000..17a8b06 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Page.php @@ -0,0 +1,67 @@ +getRulesForLoggedUser(); + $searchPlacholder = $this->f3->get('AdminRules_search_placeholder'); + + $currentOperator = $this->f3->get('CURRENT_USER'); + $operatorId = $currentOperator->id; + + $ruleValues = [ + ['value' => -20, 'text' => $this->f3->get('AdminRules_weight_minus20')], + ['value' => 0, 'text' => $this->f3->get('AdminRules_weight_0')], + ['value' => 10, 'text' => $this->f3->get('AdminRules_weight_10')], + ['value' => 20, 'text' => $this->f3->get('AdminRules_weight_20')], + ['value' => 70, 'text' => $this->f3->get('AdminRules_weight_70')], + ]; + + $pageParams = [ + 'LOAD_DATATABLE' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/rules.html', + 'JS' => 'admin_rules.js', + 'RULE_VALUES' => $ruleValues, + 'RULES' => $rules, + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + ]; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $params['id'] = $operatorId; + $operationResponse = $dataController->proceedPostRequest($params); + + $pageParams = array_merge($pageParams, $operationResponse); + $pageParams['CMD'] = $params['cmd']; + $pageParams['RULES'] = $dataController->getRulesForLoggedUser(); + } + + // set api_keys param after proccessing POST request + [$isOwner, $apiKeys] = $dataController->getOperatorApiKeys($operatorId); + + $pageParams['IS_OWNER'] = $isOwner; + $pageParams['API_KEYS'] = $apiKeys; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A01.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A01.php new file mode 100644 index 0000000..83b5f72 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A01.php @@ -0,0 +1,45 @@ += $windowSize - 1) { + if ($cnt > $maximumAttempts) { + $tooManyLoginAttempts = true; + break; + } + if ($params['event_type'][$start] === $loginFail) { + --$cnt; + } + ++$start; + } + } + + $params['event_many_failed_login_attempts'] = $tooManyLoginAttempts; + + return $params; + } + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['event_many_failed_login_attempts']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A02.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A02.php new file mode 100644 index 0000000..c553512 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A02.php @@ -0,0 +1,31 @@ + $event) { + if ($event === $loginFail && \Utils\Rules::eventDeviceIsNew($params, $idx)) { + $suspiciousLoginFailed = true; + break; + } + } + + $params['event_failed_login_on_new_device'] = $suspiciousLoginFailed; + + return $params; + } + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['event_failed_login_on_new_device']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A03.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A03.php new file mode 100644 index 0000000..b09e91f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A03.php @@ -0,0 +1,31 @@ + 1 && count(array_unique($params['eip_country_id'])) > 1) { + foreach (array_keys($params['event_device']) as $idx) { + if (\Utils\Rules::eventDeviceIsNew($params, $idx) && \Utils\Rules::countryIsNewByIpId($params, $params['event_ip'][$idx])) { + $eventNewDeviceNewCountry = true; + break; + } + } + } + + $params['event_new_device_and_new_country'] = $eventNewDeviceNewCountry; + + return $params; + } + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['event_new_device_and_new_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A04.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A04.php new file mode 100644 index 0000000..4bc4632 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A04.php @@ -0,0 +1,31 @@ + 1 && $params['eip_unique_cidrs'] > 1) { + foreach (array_keys($params['event_device']) as $idx) { + if (\Utils\Rules::eventDeviceIsNew($params, $idx) && \Utils\Rules::cidrIsNewByIpId($params, $params['event_ip'][$idx])) { + $eventNewDeviceNewCidr = true; + break; + } + } + } + + $params['event_new_device_and_new_cidr'] = $eventNewDeviceNewCidr; + + return $params; + } + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['event_new_device_and_new_cidr']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A05.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A05.php new file mode 100644 index 0000000..9d228af --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A05.php @@ -0,0 +1,33 @@ + 1) { + foreach (array_keys($params['event_device']) as $idx) { + if ($params['event_type'][$idx] === $passwordChange && \Utils\Rules::eventDeviceIsNew($params, $idx)) { + $passwordChangeOnNewDevice = true; + break; + } + } + } + + $params['event_password_change_on_new_device'] = $passwordChangeOnNewDevice; + + return $params; + } + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['event_password_change_on_new_device']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A06.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A06.php new file mode 100644 index 0000000..b186677 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A06.php @@ -0,0 +1,35 @@ + 1) { + foreach ($params['event_type'] as $idx => $event) { + if ($event === $pwdChange) { + if (\Utils\Rules::countryIsNewByIpId($params, $params['event_ip'][$idx])) { + $pwdChangeInNewCountry = true; + break; + } + } + } + } + + $params['event_password_change_in_new_country'] = $pwdChangeInNewCountry; + + return $params; + } + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['event_password_change_in_new_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A07.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A07.php new file mode 100644 index 0000000..b36e444 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A07.php @@ -0,0 +1,33 @@ + 1) { + foreach ($params['event_type'] as $idx => $event) { + if ($event === $passwordChange && \Utils\Rules::cidrIsNewByIpId($params, $params['event_ip'][$idx])) { + $passwordChangeInNewCidr = true; + break; + } + } + } + + $params['event_password_change_in_new_cidr'] = $passwordChangeInNewCidr; + + return $params; + } + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['event_password_change_in_new_cidr']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A08.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A08.php new file mode 100644 index 0000000..3db399b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/A08.php @@ -0,0 +1,42 @@ + 1 && count($langCount) > 1) { + foreach ($params['event_device'] as $idx => $deviceId) { + if (\Utils\Rules::eventDeviceIsNew($params, $idx)) { + $innerId = array_search($deviceId, $params['eup_device_id']); + $lang = strtoupper(explode('-', preg_replace('/;.*$/', '', trim(explode(',', $params['eup_lang'][$innerId] ?? '')[0])))[0]); + if ($langCount[$lang] === 1) { + $newBrowserLanguage = true; + break; + } + } + } + } + + $params['event_new_browser_language'] = $newBrowserLanguage; + + return $params; + } + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['event_new_browser_language']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B01.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B01.php new file mode 100644 index 0000000..bd8bde4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B01.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ea_total_country']->greaterThan(3), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B02.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B02.php new file mode 100644 index 0000000..82bd318 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B02.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['event_password_changed']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B03.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B03.php new file mode 100644 index 0000000..e98fa47 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B03.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['event_email_changed']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B04.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B04.php new file mode 100644 index 0000000..77d9002 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B04.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['event_multiple_5xx_http']->greaterThan(\Utils\Constants::get('RULE_MAXIMUM_NUMBER_OF_500_CODES')), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B05.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B05.php new file mode 100644 index 0000000..5f0118d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B05.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['event_multiple_4xx_http']->greaterThanOrEqualTo(\Utils\Constants::get('RULE_MAXIMUM_NUMBER_OF_404_CODES')), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B06.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B06.php new file mode 100644 index 0000000..811390f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B06.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['event_vulnerable_url']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B07.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B07.php new file mode 100644 index 0000000..8078b3c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B07.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ea_fullname_has_numbers']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B08.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B08.php new file mode 100644 index 0000000..bbcd331 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B08.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ea_days_since_last_visit']->greaterThan(30), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B09.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B09.php new file mode 100644 index 0000000..2349bd2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B09.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ea_days_since_last_visit']->greaterThan(90), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B10.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B10.php new file mode 100644 index 0000000..1388e24 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B10.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ea_days_since_last_visit']->greaterThan(365), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B11.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B11.php new file mode 100644 index 0000000..3c8a4f3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B11.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['ea_days_since_account_creation']->notEqualTo(-1), + $this->rb['ea_days_since_account_creation']->lessThan(1), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B12.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B12.php new file mode 100644 index 0000000..6aaf586 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B12.php @@ -0,0 +1,17 @@ +rb->logicalAnd( + $this->rb['ea_days_since_account_creation']->notEqualTo(-1), + $this->rb['ea_days_since_account_creation']->lessThan(7), + $this->rb['ea_days_since_account_creation']->greaterThanOrEqualTo(1), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B13.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B13.php new file mode 100644 index 0000000..f2a1bf0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B13.php @@ -0,0 +1,17 @@ +rb->logicalAnd( + $this->rb['ea_days_since_account_creation']->notEqualTo(-1), + $this->rb['ea_days_since_account_creation']->lessThan(30), + $this->rb['ea_days_since_account_creation']->greaterThanOrEqualTo(7), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B14.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B14.php new file mode 100644 index 0000000..0428c10 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B14.php @@ -0,0 +1,17 @@ +30 days)'; + public const DESCRIPTION = 'The account has been created over 30 days ago.'; + public const ATTRIBUTES = []; + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['ea_days_since_account_creation']->notEqualTo(-1), + $this->rb['ea_days_since_account_creation']->lessThan(90), + $this->rb['ea_days_since_account_creation']->greaterThanOrEqualTo(30), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B15.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B15.php new file mode 100644 index 0000000..12a7875 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B15.php @@ -0,0 +1,17 @@ +90 days)'; + public const DESCRIPTION = 'The account has been created over 90 days ago.'; + public const ATTRIBUTES = []; + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['ea_days_since_account_creation']->notEqualTo(-1), + $this->rb['ea_days_since_account_creation']->lessThan(180), + $this->rb['ea_days_since_account_creation']->greaterThanOrEqualTo(90), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B16.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B16.php new file mode 100644 index 0000000..be147ba --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B16.php @@ -0,0 +1,16 @@ +180 days)'; + public const DESCRIPTION = 'The account has been created over 180 days ago.'; + public const ATTRIBUTES = []; + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['ea_days_since_account_creation']->notEqualTo(-1), + $this->rb['ea_days_since_account_creation']->greaterThanOrEqualTo(180), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B17.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B17.php new file mode 100644 index 0000000..8c4e08a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B17.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ea_total_country']->equalTo(1), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B18.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B18.php new file mode 100644 index 0000000..28e5e46 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B18.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['event_http_method_head']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B19.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B19.php new file mode 100644 index 0000000..f3eede5 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B19.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['event_session_night_time']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B20.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B20.php new file mode 100644 index 0000000..4ffeb3e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B20.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['event_session_multiple_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B21.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B21.php new file mode 100644 index 0000000..06c6705 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B21.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['event_session_multiple_device']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B22.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B22.php new file mode 100644 index 0000000..b2672e2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B22.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['event_session_multiple_ip']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B23.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B23.php new file mode 100644 index 0000000..43f3e4a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B23.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ea_fullname_has_spaces_hyphens']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B24.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B24.php new file mode 100644 index 0000000..5204976 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B24.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['event_empty_referer']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B25.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B25.php new file mode 100644 index 0000000..6ccae9a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/B25.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['ea_userid']->equalTo(\Utils\Constants::get('UNAUTHORIZED_USERID')), + $this->rb['event_2xx_http']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/BaseRule.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/BaseRule.php new file mode 100644 index 0000000..188688d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/BaseRule.php @@ -0,0 +1,51 @@ +uid = end(explode('\\', get_class($this))); + $this->rb = $rb ? $rb : (new \Ruler\RuleBuilder()); + $this->params = $params; + } + + abstract protected function defineCondition(); + + protected function prepareParams(array $params): array { + return $params; + } + + public function execute(): bool { + $this->context = $this->buildContext(); + $this->condition = $this->defineCondition(); + return $this->rb->create($this->condition)->evaluate($this->context); + } + + private function buildContext(): \Ruler\Context { + return new \Ruler\Context($this->prepareParams($this->params)); + } + + public function updateParams(array $params): void { + $this->params = $params; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C01.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C01.php new file mode 100644 index 0000000..9f6489d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C01.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C02.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C02.php new file mode 100644 index 0000000..083bd06 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C02.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C03.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C03.php new file mode 100644 index 0000000..3bd653d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C03.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C04.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C04.php new file mode 100644 index 0000000..19ddaaa --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C04.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C05.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C05.php new file mode 100644 index 0000000..13277c9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C05.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C06.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C06.php new file mode 100644 index 0000000..5b83dc9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C06.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C07.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C07.php new file mode 100644 index 0000000..8d2f20f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C07.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C08.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C08.php new file mode 100644 index 0000000..5ddcab0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C08.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C09.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C09.php new file mode 100644 index 0000000..f580e7d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C09.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C10.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C10.php new file mode 100644 index 0000000..27e93d6 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C10.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C11.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C11.php new file mode 100644 index 0000000..93ecdd0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C11.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C12.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C12.php new file mode 100644 index 0000000..8b9d55b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C12.php @@ -0,0 +1,22 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C13.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C13.php new file mode 100644 index 0000000..ad66309 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C13.php @@ -0,0 +1,22 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C14.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C14.php new file mode 100644 index 0000000..4b0ac33 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C14.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C15.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C15.php new file mode 100644 index 0000000..9def1b3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C15.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C16.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C16.php new file mode 100644 index 0000000..ffcf033 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/C16.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_has_specific_country']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D01.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D01.php new file mode 100644 index 0000000..9fac7bf --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D01.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eup_has_unknown_devices']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D02.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D02.php new file mode 100644 index 0000000..e5ce7bd --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D02.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eup_has_linux_system']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D03.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D03.php new file mode 100644 index 0000000..f6ded26 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D03.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eup_has_bot_devices']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D04.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D04.php new file mode 100644 index 0000000..b0eb227 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D04.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['eup_has_rare_browser']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D05.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D05.php new file mode 100644 index 0000000..7519daa --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D05.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['eup_has_rare_os']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D06.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D06.php new file mode 100644 index 0000000..0bbd928 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D06.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ea_total_device']->greaterThan(4), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D07.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D07.php new file mode 100644 index 0000000..27840b2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D07.php @@ -0,0 +1,34 @@ +rb->logicalAnd( + $this->rb['eup_desktop_devices_with_different_os']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D08.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D08.php new file mode 100644 index 0000000..bd2d539 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D08.php @@ -0,0 +1,28 @@ +rb->logicalAnd( + $this->rb['eup_phone_devices_count']->greaterThan(1), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D09.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D09.php new file mode 100644 index 0000000..1fae207 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D09.php @@ -0,0 +1,37 @@ +rb->logicalAnd( + $this->rb['eup_old_browser']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D10.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D10.php new file mode 100644 index 0000000..74ab72a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/D10.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['eup_vulnerable_ua']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E01.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E01.php new file mode 100644 index 0000000..218e2a7 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E01.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['le_is_invalid']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E02.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E02.php new file mode 100644 index 0000000..82c60f2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E02.php @@ -0,0 +1,19 @@ +rb->logicalAnd( + $this->rb['ld_days_since_domain_creation']->notEqualTo(-1), + $this->rb['ld_days_since_domain_creation']->lessThan(30), + //$this->rb['le_has_no_profiles']->equalTo(true), + $this->rb['le_has_no_data_breaches']->equalTo(true), + $this->rb['le_local_part_len']->greaterThan(0), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E03.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E03.php new file mode 100644 index 0000000..f718873 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E03.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['le_has_suspicious_str']->equalTo(true), + $this->rb['le_local_part_len']->greaterThan(0), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E04.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E04.php new file mode 100644 index 0000000..fd3f981 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E04.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['le_has_numeric_only_local_part']->equalTo(true), + $this->rb['le_local_part_len']->greaterThan(0), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E05.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E05.php new file mode 100644 index 0000000..aa00fb9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E05.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['le_email_has_consec_s_chars']->equalTo(true), + $this->rb['le_local_part_len']->greaterThan(0), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E06.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E06.php new file mode 100644 index 0000000..ed3ca33 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E06.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['le_email_has_consec_nums']->equalTo(true), + $this->rb['le_local_part_len']->greaterThan(0), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E07.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E07.php new file mode 100644 index 0000000..3519ed2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E07.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['le_with_long_local_part_length']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E08.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E08.php new file mode 100644 index 0000000..ceae4a0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E08.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['le_with_long_domain_length']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E09.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E09.php new file mode 100644 index 0000000..824df67 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E09.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ld_domain_free_email_provider']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E10.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E10.php new file mode 100644 index 0000000..4292acd --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E10.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['ld_website_is_disabled']->equalTo(true), + $this->rb['ld_domain_free_email_provider']->notEqualTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E11.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E11.php new file mode 100644 index 0000000..a565b96 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E11.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ld_is_disposable']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E12.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E12.php new file mode 100644 index 0000000..63908e3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E12.php @@ -0,0 +1,17 @@ +rb->logicalAnd( + $this->rb['ld_domain_free_email_provider']->equalTo(true), + //$this->rb['le_has_no_profiles']->equalTo(true), + $this->rb['le_has_no_data_breaches']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E13.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E13.php new file mode 100644 index 0000000..d4b1cd5 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E13.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['ld_days_since_domain_creation']->notEqualTo(-1), + $this->rb['ld_days_since_domain_creation']->lessThan(90), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E14.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E14.php new file mode 100644 index 0000000..0b9619b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E14.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ld_domain_without_mx_record']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E15.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E15.php new file mode 100644 index 0000000..96b85e4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E15.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['le_has_no_data_breaches']->equalTo(true), + $this->rb['le_local_part_len']->greaterThan(0), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E16.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E16.php new file mode 100644 index 0000000..9da1806 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E16.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ld_from_blockdomains']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E17.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E17.php new file mode 100644 index 0000000..a905799 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E17.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['ld_domain_free_email_provider']->equalTo(true), + $this->rb['le_email_in_blockemails']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E19.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E19.php new file mode 100644 index 0000000..8093a8a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E19.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['ee_email_count']->greaterThan(1), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E20.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E20.php new file mode 100644 index 0000000..ba3b343 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E20.php @@ -0,0 +1,18 @@ + 3 year old)'; + public const DESCRIPTION = 'Email belongs to long-established domain name registered at least 3 years ago.'; + public const ATTRIBUTES = ['domain']; + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['ld_days_since_domain_creation']->notEqualTo(-1), + $this->rb['ld_days_since_domain_creation']->greaterThan(365 * 3), + $this->rb['ld_disposable_domains']->notEqualTo(true), + $this->rb['ld_free_email_provider']->notEqualTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E21.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E21.php new file mode 100644 index 0000000..bd82ad3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E21.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['le_email_has_vowels']->equalTo(false), + $this->rb['le_local_part_len']->greaterThan(0), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E22.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E22.php new file mode 100644 index 0000000..26fbe30 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E22.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['le_email_has_consonants']->equalTo(false), + $this->rb['le_local_part_len']->greaterThan(0), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E23.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E23.php new file mode 100644 index 0000000..c113537 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E23.php @@ -0,0 +1,29 @@ +rb->logicalAnd( + $this->rb['ee_has_edu']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E24.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E24.php new file mode 100644 index 0000000..519761c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E24.php @@ -0,0 +1,29 @@ +rb->logicalAnd( + $this->rb['ee_has_gov']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E25.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E25.php new file mode 100644 index 0000000..7cf867b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E25.php @@ -0,0 +1,29 @@ +rb->logicalAnd( + $this->rb['ee_has_mil']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E26.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E26.php new file mode 100644 index 0000000..5e47280 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E26.php @@ -0,0 +1,29 @@ +rb->logicalAnd( + $this->rb['ee_has_apple']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E27.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E27.php new file mode 100644 index 0000000..892d572 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E27.php @@ -0,0 +1,17 @@ +rb->logicalAnd( + //$this->rb['le_has_no_profiles']->equalTo(false), + // do not trigger if le_data_breach is null, + $this->rb['le_data_breach']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E28.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E28.php new file mode 100644 index 0000000..a09282d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E28.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['le_email_has_no_digits']->equalTo(true), + $this->rb['le_local_part_len']->greaterThan(0), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E29.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E29.php new file mode 100644 index 0000000..7902594 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E29.php @@ -0,0 +1,15 @@ +3 years)'; + public const DESCRIPTION = 'The earliest data breach associated with the email appeared more than 3 years ago. Can be used as sign of aged email.'; + public const ATTRIBUTES = ['email']; + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['ee_days_since_first_breach']->greaterThan(365 * 3), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E30.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E30.php new file mode 100644 index 0000000..52c1d27 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/E30.php @@ -0,0 +1,17 @@ +rb->logicalAnd( + $this->rb['ld_tranco_rank']->greaterThan(100000), + $this->rb['ld_tranco_rank']->lessThan(4000000), + $this->rb['ld_domain_free_email_provider']->notEqualTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I01.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I01.php new file mode 100644 index 0000000..a83a00e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I01.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_tor']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I02.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I02.php new file mode 100644 index 0000000..f03002d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I02.php @@ -0,0 +1,25 @@ + 0; + }); + + $params['eip_domains_count_len'] = count($arrWithPositiveDomainsCounts) > 0; + + return $params; + } + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['eip_domains_count_len']->greaterThan(0), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I03.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I03.php new file mode 100644 index 0000000..1516a43 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I03.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_blocklist']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I04.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I04.php new file mode 100644 index 0000000..ce68fed --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I04.php @@ -0,0 +1,25 @@ + 1; + }); + + $params['eip_shared'] = count($arrWithPositiveSharedIps) > 0; + + return $params; + } + + protected function defineCondition() { + return $this->rb->logicalAnd( + $this->rb['eip_shared']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I05.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I05.php new file mode 100644 index 0000000..ee10353 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I05.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_vpn']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I06.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I06.php new file mode 100644 index 0000000..231d051 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I06.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_data_center']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I07.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I07.php new file mode 100644 index 0000000..eba2059 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I07.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_relay']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I08.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I08.php new file mode 100644 index 0000000..d4f13c3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I08.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['eip_starlink']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I09.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I09.php new file mode 100644 index 0000000..8b1fc3a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I09.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ea_total_ip']->greaterThan(9), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I10.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I10.php new file mode 100644 index 0000000..5ec7e43 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I10.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['eip_only_residential']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I11.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I11.php new file mode 100644 index 0000000..7569eae --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I11.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['eip_unique_cidrs']->equalTo(1), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I12.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I12.php new file mode 100644 index 0000000..1e5b9a4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/I12.php @@ -0,0 +1,32 @@ +rb->logicalAnd( + $this->rb['eip_lan']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/P01.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/P01.php new file mode 100644 index 0000000..43860ba --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/P01.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['lp_invalid_phone']->equalTo(true), + $this->rb['ep_phone_number']->notEqualTo([]), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/P02.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/P02.php new file mode 100644 index 0000000..8425e0b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/P02.php @@ -0,0 +1,21 @@ +rb->logicalAnd( + $this->rb['lp_country_code_in_eip_country_id']->equalTo(false), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/P03.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/P03.php new file mode 100644 index 0000000..729198a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/P03.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['ep_shared_phone']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/P04.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/P04.php new file mode 100644 index 0000000..f13ed6a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/P04.php @@ -0,0 +1,16 @@ +rb->logicalAnd( + $this->rb['lp_invalid_phone']->equalTo(false), + $this->rb['ep_phone_number']->notEqualTo([]), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/R01.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/R01.php new file mode 100644 index 0000000..286f0c7 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/R01.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['eip_has_fraud']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/R02.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/R02.php new file mode 100644 index 0000000..55a52b6 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/R02.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['le_fraud_detected']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/R03.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/R03.php new file mode 100644 index 0000000..69b7089 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/R03.php @@ -0,0 +1,15 @@ +rb->logicalAnd( + $this->rb['lp_fraud_detected']->equalTo(true), + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Rules/Set/index.php @@ -0,0 +1,3 @@ + $result]; + } + + $model = new \Models\Search\Domain(); + $result1 = $model->searchByDomain($query, $apiKey); + + $model = new \Models\Search\Ip(); + $result2 = $model->searchByIp($query, $apiKey); + + $model = new \Models\Search\Isp(); + $result3 = $model->searchByIsp($query, $apiKey); + + $model = new \Models\Search\User(); + $result4 = $model->searchByUserId($query, $apiKey); + $result5 = $model->searchByName($query, $apiKey); + + $model = new \Models\Search\Email(); + $result6 = $model->searchByEmail($query, $apiKey); + + $model = new \Models\Search\Phone(); + $result7 = $model->searchByPhone($query, $apiKey); + + $result = array_merge($result1, $result2, $result3, $result4, $result5, $result6, $result7); + $iters = count($result); + + for ($i = 0; $i < $iters; ++$i) { + $result[$i]['data'] = ['category' => $result[$i]['groupName']]; + } + + return [ + 'suggestions' => $result, + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Search/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Search/Navigation.php new file mode 100644 index 0000000..5742563 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Search/Navigation.php @@ -0,0 +1,29 @@ +getCurrentOperatorApiKeyId(); + $query = $this->f3->get('REQUEST.query'); + $dataController = new Data(); + + return $dataController->getSearchResults($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Search/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Search/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Search/index.php @@ -0,0 +1,3 @@ + $this->changeEmail($params), + 'changeTimeZone' => $this->changeTimeZone($params), + 'changePassword' => $this->changePassword($params), + 'closeAccount' => $this->closeAccount($params), + 'updateNotificationPreferences' => $this->updateNotificationPreferences($params), + 'changeRetentionPolicy' => $this->changeRetentionPolicy($params), + 'inviteCoOwner' => $this->inviteCoOwner($params), + 'removeCoOwner' => $this->removeCoOwner($params), + 'checkUpdates' => $this->checkUpdates($params), + default => [] + }; + } + + public function getSharedApiKeyOperators(int $operatorId): array { + $model = new \Models\ApiKeyCoOwner(); + + return $model->getSharedApiKeyOperators($operatorId); + } + + public function changePassword(array $params): array { + $pageParams = []; + $errorCode = $this->validateChangePassword($params); + + if ($errorCode) { + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $currentOperator = $this->f3->get('CURRENT_USER'); + $params['id'] = $currentOperator->id; + + $model = new \Models\Operator(); + $model->updatePassword($params); + + $pageParams['SUCCESS_MESSAGE'] = $this->f3->get('AdminSettings_changePassword_success_message'); + } + + return $pageParams; + } + + public function changeEmail(array $params): array { + $pageParams = []; + $errorCode = $this->validateChangeEmail($params); + + if ($errorCode) { + $pageParams['EMAIL_VALUES'] = $params; + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $currentOperator = $this->f3->get('CURRENT_USER'); + $operatorId = $currentOperator->id; + $email = $params['email']; + + // Create change email record + $changeEmailModel = new \Models\ChangeEmail(); + $changeEmailModel->add($operatorId, $email); + + // Send forgot password email + $this->sendChangeEmailEmail($currentOperator, $changeEmailModel); + + $pageParams['SUCCESS_MESSAGE'] = $this->f3->get('AdminSettings_changeEmail_success_message'); + } + + return $pageParams; + } + + public function changeTimeZone(array $params): array { + $pageParams = []; + $errorCode = $this->validateChangeTimeZone($params); + + if ($errorCode) { + $pageParams['TIME_ZONE_VALUES'] = $params; + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $currentOperator = $this->f3->get('CURRENT_USER'); + $params['id'] = $currentOperator->id; + + $model = new \Models\Operator(); + $model->updateTimeZone($params); + + // update operator in f3 hive for clock + $this->f3->set('CURRENT_USER', null); + $this->f3->set('CURRENT_USER', $this->getLoggedInOperator()); + + $pageParams['SUCCESS_MESSAGE'] = $this->f3->get('AdminTimeZone_changeTimeZone_success_message'); + } + + return $pageParams; + } + + public function closeAccount(array $data): array { + $pageParams = []; + $errorCode = $this->validateCloseAccount($data); + + if ($errorCode) { + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $currentOperator = $this->f3->get('CURRENT_USER'); + $currentOperator->closeAccount(); + $currentOperator->removeData(); + + $this->f3->clear('SESSION'); + session_commit(); + + $pageParams['SUCCESS_MESSAGE'] = $this->f3->get('AdminSettings_closeAccount_success_message'); + } + + return $pageParams; + } + + public function checkUpdates(array $data): array { + $pageParams = []; + $errorCode = $this->validateCheckUpdates($data); + + if ($errorCode) { + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $currentVersion = \Utils\VersionControl::versionString(); + + $result = \Web::instance()->request( + url: \Utils\Variables::getEnrichtmentApi() . '/version', + options: [ + 'method' => 'GET', + 'header' => [ + 'User-Agent: ' . $this->f3->get('USER_AGENT'), + ], + ], + ); + $matches = []; + preg_match('/^HTTP\/(\d+)(?:\.\d)? (\d{3})/', $result['headers'][0], $matches); + $jsonResponse = json_decode($result['body'], true); + $statusCode = (int) ($matches[2] ?? 0); + $errorMessage = $result['error']; + + if (strlen($errorMessage) > 0 || $statusCode !== 200 || !is_array($jsonResponse)) { + $pageParams['ERROR_CODE'] = \Utils\ErrorCodes::ENRICHMENT_API_IS_NOT_AVAILABLE; + } else { + if (version_compare($currentVersion, $jsonResponse['version'], '<')) { + $pageParams['SUCCESS_MESSAGE'] = sprintf('An update is available. Released date: %s.', $jsonResponse['release_date']); + } else { + $pageParams['SUCCESS_MESSAGE'] = 'Current version is up to date.'; + } + } + } + + return $pageParams; + } + + public function validateCheckUpdates(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + return false; + } + + public function updateNotificationPreferences(array $params): array { + $pageParams = []; + $errorCode = $this->validateUpdateNotificationPreferences($params); + + if ($errorCode) { + $pageParams['PROFILE_VALUES'] = $params; + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $unreviewedItemsReminderFrequency = $params['review-reminder-frequency']; + $currentOperator = $this->f3->get('CURRENT_USER'); + + $currentOperator->updateNotificationPreferences(\Type\UnreviewedItemsReminderFrequencyType::from($unreviewedItemsReminderFrequency)); + + $pageParams['SUCCESS_MESSAGE'] = $this->f3->get('AdminSettings_notificationPreferences_success_message'); + } + + return $pageParams; + } + + public function changeRetentionPolicy(array $params): array { + $pageParams = []; + $errorCode = $this->validateRetentionPolicy($params); + + if ($errorCode) { + $pageParams['RETENTION_POLICY_VALUES'] = $params; + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $keyId = isset($params['keyId']) ? (int) $params['keyId'] : null; + $model = new \Models\ApiKeys(); + $model->getKeyById($keyId); + $model->updateRetentionPolicy((int) ($params['retention-policy'] ?? 0)); + $pageParams['SUCCESS_MESSAGE'] = $this->f3->get('AdminRetentionPolicy_changeTimeZone_success_message'); + } + + return $pageParams; + } + + public function validateRetentionPolicy(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $keyId = isset($params['keyId']) ? (int) $params['keyId'] : null; + if (!$keyId) { + return \Utils\ErrorCodes::API_KEY_ID_DOESNT_EXIST; + } + + if ($keyId !== $this->getCurrentOperatorApiKeyId()) { + return \Utils\ErrorCodes::API_KEY_WAS_CREATED_FOR_ANOTHER_USER; + } + + $retentionPolicy = (int) ($params['retention-policy'] ?? 0); + if ($retentionPolicy < 0 || $retentionPolicy > 12) { + return \Utils\ErrorCodes::RETENTION_POLICY_DOES_NOT_EXIST; + } + + return false; + } + + public function inviteCoOwner(array $params): array { + $errorCode = $this->validateInvitingCoOwner($params); + $pageParams = []; + + if ($errorCode) { + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $currentOperator = $this->f3->get('CURRENT_USER'); + $operatorId = $currentOperator->id; + + $apiKeyModel = new \Models\ApiKeys(); + $key = $apiKeyModel->getKey($operatorId); + + $params['timezone'] = 'UTC'; + $operator = new \Models\Operator(); + $operator->add($params); + + $passwordReset = new \Models\ForgotPassword(); + $passwordReset->add($operator->id); + + $this->makeOperatorCoOwner($operator, $key); + $this->sendInvitationEmail($currentOperator, $operator, $passwordReset); + + $pageParams['SUCCESS_MESSAGE'] = $this->f3->get('AdminApi_add_co_owner_success_message'); + } + + return $pageParams; + } + + public function removeCoOwner(array $params): array { + $errorCode = $this->validateRemovingCoOwner($params); + $pageParams = []; + + if ($errorCode) { + $pageParams['ERROR_CODE'] = $errorCode; + } else { + $operatorId = isset($params['operatorId']) ? (int) $params['operatorId'] : null; + + $coOwnerModel = new \Models\ApiKeyCoOwner(); + $coOwnerModel->getCoOwnership($operatorId); + + $apiKeyObj = $this->getCurrentOperatorApiKeyObject(); + + if ($apiKeyObj->id === $coOwnerModel->api && $this->f3->get('CURRENT_USER')->id === $apiKeyObj->creator) { + $coOwnerModel->deleteCoOwnership(); + + $operatorModel = new \Models\Operator(); + $operatorModel->getOperatorById($operatorId); + $operatorModel->deleteAccount(); + + $pageParams['SUCCESS_MESSAGE'] = $this->f3->get('AdminApi_remove_co_owner_success_message'); + } else { + $pageParams['ERROR_MESSAGE'] = $this->f3->get('AdminApi_remove_co_owner_error_message'); + } + } + + return $pageParams; + } + + private function makeOperatorCoOwner(\Models\Operator $operator, \Models\ApiKeys $key): void { + $model = new \Models\ApiKeyCoOwner(); + $model->create($operator->id, $key->id); + } + + private function sendInvitationEmail(\Models\Operator $inviter, \Models\Operator $operator, \Models\ForgotPassword $forgotPassword): void { + $site = \Utils\Variables::getSite(); + + $inviterDisplayName = $inviter->email; + if ($inviter->firstname && $inviter->lastname) { + $inviterDisplayName = sprintf('%s %s (%s)', $inviter->firstname, $inviter->lastname, $inviterDisplayName); + } + + $toName = null; + $toAddress = $operator->email; + $renewKey = $forgotPassword->renew_key; + + $subject = $this->f3->get('AdminApi_invitation_email_subject'); + $message = $this->f3->get('AdminApi_invitation_email_body'); + + $renewUrl = sprintf('%s/password-recovering/%s', $site, $renewKey); + $message = sprintf($message, $inviterDisplayName, $renewUrl); + + \Utils\Mailer::send($toName, $toAddress, $subject, $message); + } + + private function validateInvitingCoOwner(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $email = $params['email'] ?? null; + if (!$email) { + return \Utils\ErrorCodes::EMAIL_DOES_NOT_EXIST; + } + + $audit = \Audit::instance(); + if (!$audit->email($email, true)) { + return \Utils\ErrorCodes::EMAIL_IS_NOT_CORRECT; + } + + $operatorsModel = new \Models\Operator(); + $operatorsModel->getByEmail($email); + if ($operatorsModel->loaded()) { + return \Utils\ErrorCodes::EMAIL_ALREADY_EXIST; + } + + return false; + } + + public function validateRemovingCoOwner(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $operatorId = isset($params['operatorId']) ? (int) $params['operatorId'] : null; + if (!$operatorId) { + return \Utils\ErrorCodes::OPERATOR_ID_DOES_NOT_EXIST; + } + + $apiKey = $this->getCurrentOperatorApiKeyId(); + $coOwnerModel = new \Models\ApiKeyCoOwner(); + $coOwnerModel->getCoOwnership($operatorId); + + if (!$coOwnerModel->loaded() || $coOwnerModel->api !== $apiKey) { + return \Utils\ErrorCodes::OPERATOR_IS_NOT_A_CO_OWNER; + } + + return false; + } + + public function validateChangePassword(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $currentPassword = $params['currentPassword']; + if (!$currentPassword) { + return \Utils\ErrorCodes::CURRENT_PASSWORD_DOES_NOT_EXIST; + } + + $currentOperator = $this->f3->get('CURRENT_USER'); + $operatorId = $currentOperator->id; + + $model = new \Models\Operator(); + $model->getOperatorById($operatorId); + if (!$model->verifyPassword($currentPassword)) { + return \Utils\ErrorCodes::CURRENT_PASSWORD_IS_NOT_CORRECT; + } + + $newPassword = $params['new-password']; + if (!$newPassword) { + return \Utils\ErrorCodes::NEW_PASSWORD_DOES_NOT_EXIST; + } + + $newPasswordLegth = strlen($newPassword); + $minPasswordLegth = $this->f3->get('MIN_PASSWORD_LENGTH'); + if ($newPasswordLegth < $minPasswordLegth) { + return \Utils\ErrorCodes::PASSWORD_IS_TO_SHORT; + } + + $passwordConfirmation = $params['password-confirmation']; + if (!$passwordConfirmation) { + return \Utils\ErrorCodes::PASSWORD_CONFIRMATION_DOES_NOT_EXIST; + } + + if ($newPassword !== $passwordConfirmation) { + return \Utils\ErrorCodes::PASSWORDS_ARE_NOT_EQUAL; + } + + return false; + } + + public function validateChangeEmail(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $email = $params['email'] ?? null; + if (!$email) { + return \Utils\ErrorCodes::EMAIL_DOES_NOT_EXIST; + } + + $audit = \Audit::instance(); + if (!$audit->email($email, true)) { + return \Utils\ErrorCodes::EMAIL_IS_NOT_CORRECT; + } + + $currentOperator = $this->f3->get('CURRENT_USER'); + if (\strtolower($currentOperator->email) === \strtolower($email)) { + return \Utils\ErrorCodes::EMAIL_IS_NOT_NEW; + } + + $operatorModel = new \Models\Operator(); + $operatorModel->getByEmail($email); + if ($operatorModel->loaded() && $currentOperator->id !== $operatorModel->id) { + return \Utils\ErrorCodes::EMAIL_ALREADY_EXIST; + } + + return false; + } + + public function validateChangeTimeZone(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $timezone = $params['timezone'] ?? null; + $timezones = $this->f3->get('timezones'); + if (!$timezone || !array_key_exists($timezone, $timezones)) { + return \Utils\ErrorCodes::TIME_ZONE_DOES_NOT_EXIST; + } + + return false; + } + + public function validateCloseAccount(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + return false; + } + + public function validateUpdateNotificationPreferences(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $unreviewedItemsReminderFrequency = $params['review-reminder-frequency'] ?? null; + if (!$unreviewedItemsReminderFrequency || !\Type\UnreviewedItemsReminderFrequencyType::tryFrom($unreviewedItemsReminderFrequency)) { + return \Utils\ErrorCodes::UNREVIEWED_ITEMS_REMINDER_FREQUENCY_DOES_NOT_EXIST; + } + + return false; + } + + private function sendChangeEmailEmail(\Models\Operator $currentOperator, \Models\ChangeEmail $changeEmailModel): void { + $url = \Utils\Variables::getSiteWithProtocol(); + + $toName = $currentOperator->firstname; + $toAddress = $changeEmailModel->email; + $renewKey = $changeEmailModel->renew_key; + + $subject = $this->f3->get('ChangeEmail_renew_email_subject'); + $message = $this->f3->get('ChangeEmail_renew_email_body'); + + $renewUrl = sprintf('%s/change-email/%s', $url, $renewKey); + $message = sprintf($message, $renewUrl); + + \Utils\Mailer::send($toName, $toAddress, $subject, $message); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Settings/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Settings/Navigation.php new file mode 100644 index 0000000..dc08104 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Settings/Navigation.php @@ -0,0 +1,29 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Settings/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Settings/Page.php new file mode 100644 index 0000000..25ee1f2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Settings/Page.php @@ -0,0 +1,69 @@ +f3->get('CURRENT_USER'); + $operatorId = $currentOperator->id; + + [$isOwner, $apiKeys] = $this->getOperatorApiKeys($operatorId); + + $pageParams = [ + 'LOAD_DATATABLE' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'HTML_FILE' => 'admin/settings.html', + 'JS' => 'admin_settings.js', + 'TIMEZONES' => \Utils\TimeZones::timeZonesList(), + 'CURRENT_VERSION' => \Utils\VersionControl::fullVersionString(), + ]; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $operationResponse = $dataController->proceedPostRequest($params); + + $pageParams = array_merge($pageParams, $operationResponse); + $pageParams['CMD'] = $params['cmd']; + + //$this->f3->reroute('/account'); + } + + // set shared_operatos and api_keys params after proccessing POST request + $coOwners = $dataController->getSharedApiKeyOperators($operatorId); + $pageParams['SHARED_OPERATORS'] = $coOwners; + + [$isOwner, $apiKeys] = $dataController->getOperatorApiKeys($operatorId); + + $pageParams['IS_OWNER'] = $isOwner; + $pageParams['API_KEYS'] = $apiKeys; + + $operatorModel = new \Models\Operator(); + $operatorModel->getOperatorById($operatorId); + $pageParams['PROFILE'] = $operatorModel->cast(); + + $changeEmailModel = new \Models\ChangeEmail(); + $changeEmailModel->getUnusedKeyByOperatorId($operatorId); + $pageParams['PENDING_CONFIRMATION_EMAIL'] = $changeEmailModel->email ?? null; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Settings/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Settings/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Settings/index.php @@ -0,0 +1,3 @@ + \Utils\ErrorCodes::TOTALS_INVALID_TYPE]; + + if (!in_array($type, ['ip', 'isp', 'domain', 'country', 'resource'])) { + return $processErrorMessage; + } + + $model = null; + + switch ($type) { + case 'ip': + $model = new \Models\Ip(); + break; + case 'isp': + $model = new \Models\Isp(); + break; + case 'domain': + $model = new \Models\Domain(); + break; + case 'country': + $model = new \Models\Country(); + break; + case 'resource': + $model = new \Models\Resource(); + break; + } + + $totals = $model->getTimeFrameTotal($ids, $startDate, $endDate, $apiKey); + + return [ + 'SUCCESS_MESSAGE' => $this->f3->get('AdminTotals_success_message'), + 'totals' => $totals, + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Totals/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Totals/Navigation.php new file mode 100644 index 0000000..092855c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Totals/Navigation.php @@ -0,0 +1,33 @@ +getCurrentOperatorApiKeyId(); + $params = $this->f3->get('GET'); + $ids = $params['ids']; + $type = $params['type']; + $startDate = $params['startDate']; + $endDate = $params['endDate']; + + return $dataController->getTimeFrameTotal($ids, $type, $startDate, $endDate, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Totals/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Totals/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Totals/index.php @@ -0,0 +1,3 @@ + $this->recalculateRiskScore($params), + 'reenrichment' => $this->enrichEntity($params), + 'delete' => $this->deleteUser($params), + default => [] + }; + } + + public function recalculateRiskScore(array $params): array { + $result = []; + set_error_handler([\Utils\ErrorHandler::class, 'exceptionErrorHandler']); + + try { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $userId = (int) $params['accountid']; + + [$score, $rules] = $this->getUserScore($userId, $apiKey); + $result = [ + 'SUCCESS_MESSAGE' => $this->f3->get('AdminUser_recalculate_risk_score_success_message'), + 'score' => $score, + 'rules' => $rules, + ]; + } catch (\ErrorException $e) { + $result = ['ERROR_CODE' => \Utils\ErrorCodes::RISK_SCORE_UPDATE_UNKNOWN_ERROR]; + } + + restore_error_handler(); + + return $result; + } + + public function enrichEntity(array $params): array { + $dataController = new \Controllers\Admin\Enrichment\Data(); + $apiKey = $this->getCurrentOperatorApiKeyId(); + $enrichmentKey = $this->getCurrentOperatorEnrichmentKeyString(); + $type = $params['type']; + $search = $params['search'] ?? null; + $entityId = isset($params['entityId']) ? (int) $params['entityId'] : null; + + return $dataController->enrichEntity($type, $search, $entityId, $apiKey, $enrichmentKey); + } + + public function deleteUser(array $params): void { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + if ($apiKey) { + $accountId = (int) $params['accountid']; + $actionType = new \Type\QueueAccountOperationActionType(\Type\QueueAccountOperationActionType::DELETE); + $accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + + $code = \Utils\ErrorCodes::REST_API_USER_ALREADY_SCHEDULED_FOR_DELETION; + if (!$accountOpQueueModel->isInQueue($accountId, $apiKey)) { + $code = \Utils\ErrorCodes::REST_API_USER_SUCCESSFULLY_ADDED_FOR_DELETION; + $accountOpQueueModel->add($accountId, $apiKey); + } + + $this->f3->set('SESSION.extra_message_code', $code); + $this->f3->reroute('/id'); + } + } + + public function getUserScoreDetails(int $userId, int $apiKey): array { + $model = new \Models\User(); + $user = $model->getUser($userId, $apiKey); + + return [ + 'score_details' => $model->getApplicableRulesByAccountId($userId, $apiKey, true), + 'score_calculated' => $user !== [] ? $user['score'] !== null : false, + ]; + } + + public function getUserById(int $accountId): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + $model = new \Models\User(); + $user = $model->getUser($accountId, $apiKey); + + $model = new \Models\Rules(); + $rules = $model->getAll(); + + $details = []; + if ($user['score_details']) { + $scoreDetails = json_decode($user['score_details'], true); + + foreach ($scoreDetails as $detail) { + $score = $detail['score'] ?? null; + $ruleUid = $detail['uid'] ?? null; + if ($score !== 0 && isset($rules[$ruleUid])) { + $item = $rules[$ruleUid]; + $item['score'] = $score; + $details[] = $item; + } + } + } + + usort($details, static function ($a, $b): int { + return $b['score'] <=> $a['score']; + }); + + $user['score_details'] = $details; + + $pageTitle = $user['userid']; + if ($user['firstname'] !== null && $user['firstname'] !== '') { + $pageTitle .= sprintf(' (%s)', $user['firstname']); + } + if ($user['lastname'] !== null && $user['lastname'] !== '') { + $pageTitle .= sprintf(' (%s)', $user['lastname']); + } + $user['page_title'] = $pageTitle; + + $tsColumns = ['created', 'lastseen', 'score_updated_at', 'latest_decision', 'updated', 'added_to_review']; + \Utils\TimeZones::localizeTimestampsForActiveOperator($tsColumns, $user); + + return $user; + } + + public function checkIfOperatorHasAccess(int $userId): bool { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $userModel = new \Models\User(); + + return $userModel->checkAccess($userId, $apiKey); + } + + public function checkEnrichmentAvailability(): bool { + return $this->getCurrentOperatorEnrichmentKeyString() !== null; + } + + public function addToWatchlist(int $accountId, int $apiKey): void { + $model = new \Models\Watchlist(); + $model->add($accountId, $apiKey); + } + + public function removeFromWatchlist(int $accountId, int $apiKey): void { + $model = new \Models\Watchlist(); + $model->remove($accountId, $apiKey); + } + + public function addToBlacklistQueue(int $accountId, bool $fraud, int $apiKey): void { + $actionType = new \Type\QueueAccountOperationActionType(\Type\QueueAccountOperationActionType::BLACKLIST); + $accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + $inQueue = $accountOpQueueModel->isInQueue($accountId, $apiKey); + + if (!$fraud) { + $this->setFraudFlag($accountId, false, $apiKey); // Directly remove blacklisted items + + if ($inQueue) { + $accountOpQueueModel->removeFromQueue(); // Cancel queued operation + } + } + + if (!$inQueue && $fraud) { + $accountOpQueueModel->add($accountId, $apiKey); + } + + $model = new \Models\User(); + $model->updateFraudFlag([$accountId], $apiKey, $fraud); + } + + public function addToCalulcateRiskScoreQueue(int $accountId): void { + $apiKey = $this->getCurrentOperatorApiKeyObject(); + $actionType = new \Type\QueueAccountOperationActionType(\Type\QueueAccountOperationActionType::CALCULATE_RISK_SCORE); + $accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + $inQueue = $accountOpQueueModel->isInQueue($accountId, $apiKey->id); + + if (!$inQueue) { + $accountOpQueueModel->add($accountId, $apiKey->id); + } + } + + /** + * @param array{accountId: int, key: int}[] $accounts + */ + public function addBatchToCalulcateRiskScoreQueue(array $accounts): void { + $actionType = new \Type\QueueAccountOperationActionType(\Type\QueueAccountOperationActionType::CALCULATE_RISK_SCORE); + $accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + + $accountOpQueueModel->addBatch($accounts); + } + + public function setReviewedFlag(int $accountId, bool $reviewed, int $apiKey): void { + $model = new \Models\User(); + $model->updateReviewedFlag($accountId, $apiKey, $reviewed); + } + + public function getUserScore(int $accountId, int $apiKey): array { + $total = 0; + $rules = []; + + $rulesController = new \Controllers\Admin\Rules\Data(); + $rulesController->evaluateUser($accountId, $apiKey); + + $model = new \Models\User(); + $rules = $model->getApplicableRulesByAccountId($accountId, $apiKey); + + $total = $rules[0]['total_score'] ?? 0; + array_walk($rules, function (&$rule): void { + unset($rule['total_score']); + }, $rules); + + return [$total, $rules]; + } + + public function validate(int $accountId, array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $userHasAccess = $this->checkIfOperatorHasAccess($accountId); + + return !$userHasAccess ? \Utils\ErrorCodes::OPERATOR_DOES_NOT_HAVE_ACCESS_TO_ACCOUNT : false; + } + + public function getScheduledForDeletion(int $userId): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $actionType = new \Type\QueueAccountOperationActionType(\Type\QueueAccountOperationActionType::DELETE); + $accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + + [$scheduled, $status] = $accountOpQueueModel->isInQueueStatus($userId, $apiKey); + + return [$scheduled, ($status === \Type\QueueAccountOperationStatusType::FAILED) ? \Utils\ErrorCodes::USER_DELETION_FAILED : null]; + } + + public function getScheduledForBlacklist(int $userId): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $actionType = new \Type\QueueAccountOperationActionType(\Type\QueueAccountOperationActionType::BLACKLIST); + $accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + + [$scheduled, $status] = $accountOpQueueModel->isInQueueStatus($userId, $apiKey); + + return [$scheduled, ($status === \Type\QueueAccountOperationStatusType::FAILED) ? \Utils\ErrorCodes::USER_BLACKLISTING_FAILED : null]; + } + + public function setFraudFlag(int $accountId, bool $fraud, int $apiKey): array { + $blacklistItemsModel = new \Models\BlacklistItems(); + + $ips = $blacklistItemsModel->getIpsRelatedToAccountWithinOperator($accountId, $apiKey); + $emails = $blacklistItemsModel->getEmailsRelatedToAccountWithinOperator($accountId, $apiKey); + $phones = $blacklistItemsModel->getPhonesRelatedToAccountWithinOperator($accountId, $apiKey); + + $relatedIpsIds = array_column($ips, 'id'); + $relatedEmailsIds = array_column($emails, 'id'); + $relatedPhonesIds = array_column($phones, 'id'); + + $ips = $blacklistItemsModel->getIpsRelatedToAccountWithinOperator($accountId, $apiKey); + $relatedIpsIds = array_column($ips, 'id'); + if (count($relatedIpsIds) !== 0) { + $model = new \Models\Ip(); + $model->updateFraudFlag($relatedIpsIds, $fraud, $apiKey); + } + + $emails = $blacklistItemsModel->getEmailsRelatedToAccountWithinOperator($accountId, $apiKey); + $relatedEmailsIds = array_column($emails, 'id'); + if (count($relatedEmailsIds) !== 0) { + $model = new \Models\Email(); + $model->updateFraudFlag($relatedEmailsIds, $fraud, $apiKey); + } + + $phones = $blacklistItemsModel->getPhonesRelatedToAccountWithinOperator($accountId, $apiKey); + $relatedPhonesIds = array_column($phones, 'id'); + if (count($relatedPhonesIds) !== 0) { + $model = new \Models\Phone(); + $model->updateFraudFlag($relatedPhonesIds, $fraud, $apiKey); + } + + return array_merge($ips, $emails, $phones); + } + + public function updateUserStatus(int $accountId, array $scoreData, int $apiKey): void { + $scoreData['addToReview'] = false; + + $keyModel = new \Models\ApiKeys(); + $keyModel->getKeyById($apiKey); + + $userModel = new \Models\User(); + + if ($scoreData['score'] <= $keyModel->blacklist_threshold) { + $this->addToBlacklistQueue($accountId, true, $apiKey); + } elseif ($scoreData['score'] <= $keyModel->review_queue_threshold) { + $data = $userModel->getUser($accountId, $apiKey); + $scoreData['addToReview'] = $data['added_to_review'] === null && $data['fraud'] === null; + } + + $userModel->updateUserStatus($accountId, $scoreData, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/User/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/User/Navigation.php new file mode 100644 index 0000000..089d00e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/User/Navigation.php @@ -0,0 +1,81 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function manageUser(): array { + $params = $this->f3->get('POST'); + $accountId = $params['userId'] ?? null; + + $dataController = new Data(); + $errorCode = $dataController->validate($accountId, $params); + $successCode = false; + + if (!$errorCode) { + $cmd = $params['type'] ?? null; + $apiKey = $this->getCurrentOperatorApiKeyId(); + switch ($cmd) { + case 'add': + $dataController->addToWatchlist($accountId, $apiKey); + $successCode = \Utils\ErrorCodes::USER_HAS_BEEN_SUCCESSFULLY_ADDED_TO_WATCH_LIST; + break; + + case 'remove': + $dataController->removeFromWatchlist($accountId, $apiKey); + $successCode = \Utils\ErrorCodes::USER_HAS_BEEN_SUCCESSFULLY_REMOVED_FROM_WATCH_LIST; + break; + + case 'fraud': + $dataController->addToBlacklistQueue($accountId, true, $apiKey); + $successCode = \Utils\ErrorCodes::USER_FRAUD_FLAG_HAS_BEEN_SET; + break; + + case 'legit': + $dataController->addToBlacklistQueue($accountId, false, $apiKey); + $successCode = \Utils\ErrorCodes::USER_FRAUD_FLAG_HAS_BEEN_UNSET; + break; + + case 'reviewed': + $dataController->setReviewedFlag($accountId, true, $apiKey); + $successCode = \Utils\ErrorCodes::USER_REVIEWED_FLAG_HAS_BEEN_SET; + break; + } + } + + return ['success' => $successCode]; + } + + public function getUserScoreDetails(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $params = $this->f3->get('GET'); + $userId = $params['userId']; + + $dataController = new Data(); + + return $dataController->getUserScoreDetails($userId, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/User/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/User/Page.php new file mode 100644 index 0000000..f97fb4e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/User/Page.php @@ -0,0 +1,70 @@ +integerParam($this->f3->get('PARAMS.userId')); + $hasAccess = $dataController->checkIfOperatorHasAccess($userId); + + if (!$hasAccess) { + $this->f3->error(404); + } + + [$scheduledForDeletion, $errorCode] = $dataController->getScheduledForDeletion($userId); + $user = $dataController->getUserById($userId); + + $pageTitle = $this->getInternalPageTitleWithPostfix($user['page_title']); + $enrichmentOn = $dataController->checkEnrichmentAvailability(); + + $pageParams = [ + 'LOAD_DATATABLE' => true, + 'LOAD_JVECTORMAP' => true, + 'LOAD_ACCEPT_LANGUAGE_PARSER' => true, + 'HTML_FILE' => 'admin/user.html', + 'LOAD_UPLOT' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'USER' => $user, + 'SCHEDULED_FOR_DELETION' => $scheduledForDeletion, + 'PAGE_TITLE' => $pageTitle, + 'ENRICHMENT' => $enrichmentOn, + 'JS' => 'admin_user.js', + 'ERROR_CODE' => $errorCode, + ]; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $operationResponse = $dataController->proceedPostRequest($params); + + $pageParams = array_merge($pageParams, $operationResponse); + $pageParams['CMD'] = $params['cmd']; + // recall user data + $pageParams['USER'] = $dataController->getUserById($userId); + } + + [$scheduledForBlacklist, $errorCode] = $dataController->getScheduledForBlacklist($userId); + if ($scheduledForBlacklist) { + $this->f3->set('SESSION.extra_message_code', $errorCode ?? \Utils\ErrorCodes::USER_BLACKLISTING_QUEUED); + } + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/User/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/User/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/User/index.php @@ -0,0 +1,3 @@ +getDetails($userId, $apiKey); + + $model = new \Models\UserDetails\Ip(); + $ipDetails = $model->getDetails($userId, $apiKey); + + $model = new \Models\UserDetails\Behaviour(); + $offset = \Utils\TimeZones::getCurrentOperatorOffset(); + + $dateRange = \Utils\TimeZones::getCurDayRange($offset); + $dayDetails = $model->getDayDetails($userId, $dateRange, $apiKey); + + $dateRange = \Utils\TimeZones::getLastNDaysRange(7, $offset); + $weekDetails = $model->getWeekDetails($userId, $dateRange, $apiKey); + + return [ + 'userDetails' => $userDetails, + 'ipDetails' => $ipDetails, + 'dayDetails' => $dayDetails, + 'weekDetails' => $weekDetails, + ]; + } + + public function checkIfOperatorHasAccess(int $userId, int $apiKey): bool { + $model = new \Models\UserDetails\Id(); + + return $model->checkAccess($userId, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/UserDetails/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/UserDetails/Navigation.php new file mode 100644 index 0000000..7d99453 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/UserDetails/Navigation.php @@ -0,0 +1,34 @@ +getCurrentOperatorApiKeyId(); + $userId = $this->f3->get('GET.userId'); + $hasAccess = $dataController->checkIfOperatorHasAccess($userId, $apiKey); + + if (!$hasAccess) { + $this->f3->error(404); + } + + return $dataController->getUserDetails($userId, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/UserDetails/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/UserDetails/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/UserDetails/index.php @@ -0,0 +1,3 @@ +f3->get('REQUEST.ipId'); + $ispId = $this->f3->get('REQUEST.ispId'); + $botId = $this->f3->get('REQUEST.botId'); + $domainId = $this->f3->get('REQUEST.domainId'); + $countryId = $this->f3->get('REQUEST.countryId'); + $resourceId = $this->f3->get('REQUEST.resourceId'); + + if (isset($ipId) && is_numeric($ipId)) { + $result = $model->getUsersByIpId($ipId, $apiKey); + } + + if (isset($ispId)) { + $result = $model->getUsersByIspId($ispId, $apiKey); + } + + if (isset($domainId) && is_numeric($domainId)) { + $result = $model->getUsersByDomainId($domainId, $apiKey); + } + + if (isset($countryId) && is_numeric($countryId)) { + $result = $model->getUsersByCountryId($countryId, $apiKey); + } + + if (isset($botId) && is_numeric($botId)) { + $result = $model->getUsersByDeviceId($botId, $apiKey); + } + + if (isset($resourceId) && is_numeric($resourceId)) { + $result = $model->getUsersByResourceId($resourceId, $apiKey); + } + + if (!$result) { + $result = $model->getAllUsers(); + } + + $ids = array_column($result['data'], 'id'); + if ($ids) { + $model = new \Models\User(); + $model->updateTotalsByAccountIds($ids, $apiKey); + $result['data'] = $model->refreshTotals($result['data'], $apiKey); + } + + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Users/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Users/Navigation.php new file mode 100644 index 0000000..fe991a9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Users/Navigation.php @@ -0,0 +1,35 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function getList(): array { + $apiKey = $this->getCurrentOperatorApiKeyId(); + + return $apiKey ? (new Data())->getList($apiKey) : []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Users/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Users/Page.php new file mode 100644 index 0000000..7a5ec52 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Users/Page.php @@ -0,0 +1,45 @@ +f3->get('AdminUsers_search_placeholder'); + $apiKey = $this->getCurrentOperatorApiKeyId(); + $rulesController = new \Controllers\Admin\Rules\Data(); + + $ruleUid = $this->f3->get('GET.ruleUid'); + $ruleUid = $ruleUid !== null ? strtoupper($ruleUid) : null; + + $pageParams = [ + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + 'LOAD_UPLOT' => true, + 'LOAD_DATATABLE' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'LOAD_CHOICES' => true, + 'HTML_FILE' => 'admin/users.html', + 'JS' => 'admin_users.js', + 'RULES' => $rulesController->getAllRulesByApiKey($apiKey), + 'DEFAULT_RULE' => $ruleUid, + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Users/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Users/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Users/index.php @@ -0,0 +1,3 @@ +getUsersByKey($apiKey); + } + + public function removeFromWatchlist(int $accountId, int $apiKey): void { + $watchListModel = new \Models\Watchlist(); + $watchListModel->remove($accountId, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Watchlist/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Watchlist/Navigation.php new file mode 100644 index 0000000..146f4b6 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Watchlist/Navigation.php @@ -0,0 +1,44 @@ +redirectIfUnlogged(); + + $pageController = new Page(); + $this->response = new \Views\Frontend(); + $this->response->data = $pageController->getPageParams(); + } + + public function removeUserFromList(): array { + $dataController = new Data(); + + $apiKey = $this->getCurrentOperatorApiKeyId(); + $userId = $this->f3->get('REQUEST.userId'); + + $dataController->removeFromWatchlist($userId, $apiKey); + $successCode = \Utils\ErrorCodes::USER_HAS_BEEN_SUCCESSFULLY_REMOVED_FROM_WATCH_LIST; + + return [ + 'success' => $successCode, + 'userId' => $userId, + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Watchlist/Page.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Watchlist/Page.php new file mode 100644 index 0000000..1dfee02 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Watchlist/Page.php @@ -0,0 +1,44 @@ +getCurrentOperatorApiKeyId(); + + $dataController = new Data(); + $users = $dataController->getImportantUsers($apiKey); + + $searchPlacholder = $this->f3->get('AdminUsers_search_placeholder'); + + $pageParams = [ + 'SEARCH_PLACEHOLDER' => $searchPlacholder, + 'IMPORTANT_USERS' => $users, + 'LOAD_DATATABLE' => true, + 'LOAD_UPLOT' => true, + 'LOAD_AUTOCOMPLETE' => true, + 'LOAD_ACCEPT_LANGUAGE_PARSER' => true, + 'HTML_FILE' => 'admin/watchlist.html', + 'JS' => 'admin_watchlist.js', + ]; + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Watchlist/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Watchlist/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Admin/Watchlist/index.php @@ -0,0 +1,3 @@ +getBodyProp('value', 'string'); + + $model = new \Models\BlacklistItems(); + $itemFound = $model->searchBlacklistedItem($value, $this->apiKey->id); + + $this->data = [ + 'value' => $value, + 'blacklisted' => $itemFound, + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Api/Endpoint.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Api/Endpoint.php new file mode 100644 index 0000000..4b974c1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Api/Endpoint.php @@ -0,0 +1,142 @@ +f3 = \Base::instance(); + $this->f3->set('ONERROR', function (): void { + $this->handleInternalServerError(); + }); + $this->connectToDb(false); + + $this->response = new \Views\Json(); + $this->responseType = new \Type\ResponseType(\Type\ResponseType::SINGLE); + } + + public function beforeRoute(): void { + $this->identify(); + $this->authenticate(); + $this->parseBody(); + } + + public function afterRoute(): void { + if (isset($this->error)) { + $errorI18nCode = sprintf('error_%s', $this->error); + $errorMessage = $this->f3->get($errorI18nCode); + $this->response->data = [ + 'code' => $this->error, + 'message' => $errorMessage, + ]; + $this->data = null; + } + + if (($this->data !== null)) { + $this->response->data = $this->data; + } + + echo $this->response->render(); + } + + protected function identify(): void { + $headers = $this->f3->get('HEADERS') ?? []; + + if (array_key_exists(self::API_KEY, $headers) && is_string($headers[self::API_KEY])) { + $this->apiKeyString = $headers[self::API_KEY]; + + return; + } + + $this->setError(400, \Utils\ErrorCodes::REST_API_KEY_DOES_NOT_EXIST); + } + + protected function authenticate(): void { + $model = new \Models\ApiKeys(); + $apiKey = $model->getKeyIdByHash($this->apiKeyString); + + if ($apiKey) { + $this->apiKey = $apiKey; + + return; + } + + $this->setError(401, \Utils\ErrorCodes::REST_API_KEY_IS_NOT_CORRECT); + } + + protected function authorize(string $subjectId): void { + if (!isset($this->apiKey) || isset($this->error)) { + exit; + } + + $apiKeyId = $this->apiKey->id; + $hasAccess = $this->authorizationModel->checkAccessByExternalId($subjectId, $apiKeyId); + + if ($hasAccess) { + return; + } + + $this->setError(403, \Utils\ErrorCodes::REST_API_NOT_AUTHORIZED); + } + + protected function getBodyProp(string $key, string $paramType = 'string'): string|int|array|null { + $value = $this->body[$key] ?? null; + + if (isset($value)) { + settype($value, $paramType); + } + + return $value; + } + + protected function setError(int $statusCode, int $errorCode): void { + $this->f3->status($statusCode); + $this->error = $errorCode; + $this->afterRoute(); + exit; + } + + private function parseBody(): void { + $body = $this->f3->get('BODY'); + $this->body = json_decode($body, true) ?? []; + } + + private function handleInternalServerError(): void { + $errorData = \Utils\ErrorHandler::getErrorDetails($this->f3); + \Utils\ErrorHandler::saveErrorInformation($this->f3, $errorData); + + $this->setError(500, 500); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Api/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Api/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Api/index.php @@ -0,0 +1,3 @@ +f3 = \Base::instance(); + + $keepSessionInDb = $this->f3->get('KEEP_SESSION_IN_DB') ?? null; + if (!$this->connectToDb($keepSessionInDb)) { + $this->f3->error(404); + } + + //Determine current user + $currentOperator = $this->getLoggedInOperator(); + + $this->f3->set('CURRENT_USER', $currentOperator); + + //Set CSRF token + //$rnd = mt_rand(); + //$this->f3->CSRF = sprintf('%s.%s', $this->f3->SEED, $this->f3->hash($rnd)); + } + + /** + * @todo This is only used at one place. We should remove or generalise it. + */ + public function validateCsrfToken(): int|bool { + $csrf = $this->f3->get('SESSION.csrf'); + $token = $this->f3->get('REQUEST.token'); + + if (!isset($token) || $token === '' || !isset($csrf) || $csrf === '' || $token !== $csrf) { + return \Utils\ErrorCodes::CSRF_ATTACK_DETECTED; + } + + return false; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Navigation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Navigation.php new file mode 100644 index 0000000..b2440d6 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Navigation.php @@ -0,0 +1,91 @@ +response = new \Views\Frontend(); + } + + /** + * set a new view. + */ + /* TODO: make sure that setView() is not needed + public function setView(BaseView $view) { + $this->response = $view; + }*/ + + /** + * kick start the View, which creates the response + * based on our previously set content data. + * finally echo the response or overwrite this method + * and do something else with it. + */ + public function afterroute(): void { + if (!$this->response) { + trigger_error('No View has been set.'); + } + + echo $this->response->render(); + } + + public function visitSignupPage(): void { + $this->redirectIfLogged(); + + $pageController = new \Controllers\Pages\Signup(); + $this->response->data = $pageController->getPageParams(); + } + + public function visitLoginPage(): void { + $this->redirectIfLogged(); + + $pageController = new \Controllers\Pages\Login(); + $this->response->data = $pageController->getPageParams(); + } + + public function visitForgotPasswordPage(): void { + $this->redirectIfLogged(); + + if (!\Utils\Variables::getForgotPasswordAllowed()) { + $this->f3->reroute('/'); + } + + $pageController = new \Controllers\Pages\ForgotPassword(); + $this->response->data = $pageController->getPageParams(); + } + + public function visitPasswordRecoveringPage(): void { + $this->redirectIfLogged(); + + $pageController = new \Controllers\Pages\PasswordRecovering(); + $this->response->data = $pageController->getPageParams(); + } + + public function visitLogoutPage(): void { + $this->redirectIfUnlogged(); + + $pageController = new \Controllers\Pages\Logout(); + $this->response->data = $pageController->getPageParams(); + } + + public function visitChangeEmailPage(): void { + $pageController = new \Controllers\Pages\ChangeEmail(); + $this->response->data = $pageController->getPageParams(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Base.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Base.php new file mode 100644 index 0000000..0caacc1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Base.php @@ -0,0 +1,150 @@ +f3 = \Base::instance(); + + if (!$this->f3->exists('SESSION.csrf')) { + // Set anti-CSRF token. + $this->f3->set('SESSION.csrf', bin2hex(\openssl_random_pseudo_bytes(16))); + } + + $this->f3->CSRF = $this->f3->get('SESSION.csrf'); + } + + public function isPostRequest(): bool { + return $this->f3->VERB === 'POST'; + } + + // TODO: reverse + public function getPageTitle(): string { + $title = $this->f3->get(sprintf('%s_page_title', $this->page)); + + return $this->getInternalPageTitleWithPostfix($title); + } + + public function getInternalPageTitleWithPostfix(string $title): string { + $title = $title ? $title : \Utils\Constants::get('UNAUTHORIZED_USERID'); + $safeTitle = htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + $title = sprintf('%s %s', $safeTitle, \Utils\Constants::get('PAGE_TITLE_POSTFIX')); + + return $title; + } + + public function getBreadcrumbTitle(): string { + $page = $this->page; + $i18nKey = sprintf('%s_breadcrumb_title', $page); + + return $this->f3->get($i18nKey) ?? ''; + } + + public function applyPageParams(array $params): array { + $errorCode = $params['ERROR_CODE'] ?? null; + $successCode = $params['SUCCESS_CODE'] ?? null; + + if (!isset($params['PAGE_TITLE'])) { + $pageTitle = $this->getPageTitle(); + $params['PAGE_TITLE'] = $pageTitle; + } + + if ($this->f3->get('EXTRA_CSS')) { + $params['EXTRA_CSS'] = $this->f3->get('EXTRA_CSS'); + } + + $breadCrumbTitle = $this->getBreadcrumbTitle(); + $params['BREADCRUMB_TITLE'] = $breadCrumbTitle; + $params['CURRENT_PATH'] = $this->f3->PATH; + + if ($errorCode) { + $errorI18nCode = sprintf('error_%s', $errorCode); + $errorMessage = $this->f3->get($errorI18nCode); + $params['ERROR_MESSAGE'] = $errorMessage; + } + + if ($successCode) { + $successI18nCode = sprintf('error_%s', $successCode); + $successMessage = $this->f3->get($successI18nCode); + $params['SUCCESS_MESSAGE'] = $successMessage; + } + + if (array_key_exists('ERROR_MESSAGE', $params)) { + $time = gmdate('Y-m-d H:i:s'); + \Utils\TimeZones::localizeForActiveOperator($time); + $params['ERROR_MESSAGE_TIMESTAMP'] = $time; + } + + if (array_key_exists('SUCCESS_MESSAGE', $params)) { + $time = gmdate('Y-m-d H:i:s'); + \Utils\TimeZones::localizeForActiveOperator($time); + $params['SUCCESS_MESSAGE_TIMESTAMP'] = $time; + } + + $currentOperator = $this->f3->get('CURRENT_USER'); + if ($currentOperator) { + $cnt = $currentOperator->review_queue_cnt > 999 ? 999 : ($currentOperator->review_queue_cnt ?? 0); + $params['NUMBER_OF_NOT_REVIEWED_USERS'] = $cnt; + + $offset = \Utils\TimeZones::getCurrentOperatorOffset(); + $now = time() + $offset; + $day = (int) ceil(($now - mktime(0, 0, 0, 1, 1, gmdate('Y'))) / (60 * 60 * 24)); + + $params['OFFSET'] = $offset; + $params['DAY'] = ($day < 10 ? '00' : ($day < 100 ? '0' : '')) . strval($day); + $params['TIME_HIS'] = date('H:i:s', $now); + $params['TIMEZONE'] = 'UTC' . (($offset < 0) ? '-' . date('H:i', -$offset) : '+' . date('H:i', $offset)); + } + + $params['ALLOW_EMAIL_PHONE'] = \Utils\Variables::getEmailPhoneAllowed(); + + $page = $this->page; + \Utils\DictManager::load($page); + + $code = $this->f3->get('SESSION.extra_message_code'); + if ($code !== null) { + $this->f3->clear('SESSION.extra_message_code'); + + if (!isset($params['SYSTEM_MESSAGES'])) { + $params['SYSTEM_MESSAGES'] = []; + } + + $params['SYSTEM_MESSAGES'][] = [ + 'text' => $this->f3->get('error_' . $code), + 'created_at' => date('Y-m-d H:i:s'), + ]; + } + + $extra = $this->f3->get('EXTRA_APPLY_PAGE_PARAMS'); + if ($extra && is_callable($extra)) { + $params = $extra($params, $page); + } + + return $params; + } + + public function integerParam($param): int { + $validated = filter_var($param, FILTER_VALIDATE_INT); + + return $validated !== false ? $validated : 0; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/ChangeEmail.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/ChangeEmail.php new file mode 100644 index 0000000..e109b9e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/ChangeEmail.php @@ -0,0 +1,79 @@ + 'changeEmail.html', + ]; + + $renewKey = $this->f3->get('PARAMS.renewKey'); + $errorCode = $this->validate($renewKey); + $pageParams['SUCCESS_CODE'] = $errorCode; + + if (!$errorCode) { + //logout + $this->f3->clear('SESSION'); + session_commit(); + + //change email + $changeEmailModel = new \Models\ChangeEmail(); + $changeEmailModel->getByRenewKey($renewKey); + + $newEmail = $changeEmailModel->email; + $operatorId = $changeEmailModel->operator_id; + + $changeEmailModel->deactivate(); + + $params = [ + 'id' => $operatorId, + 'email' => $newEmail, + ]; + $operatorModel = new \Models\Operator(); + $operatorModel->updateEmail($params); + + //update success message + $pageParams['SUCCESS_CODE'] = \Utils\ErrorCodes::EMAIL_CHANGED; + } + + return parent::applyPageParams($pageParams); + } + + private function validate($renewKey): int|false { + if (!$renewKey) { + return \Utils\ErrorCodes::CHANGE_EMAIL_KEY_DOES_NOT_EXIST; + } + + $changeEmailModel = new \Models\ChangeEmail(); + $changeEmailModel->getByRenewKey($renewKey); + if (!$changeEmailModel->loaded()) { + return \Utils\ErrorCodes::CHANGE_EMAIL_KEY_IS_NOT_CORRECT; + } + + $currentTime = time(); + $linkTime = strtotime($changeEmailModel->created_at); + $lifeTime = $this->f3->get('RENEW_PASSWORD_LINK_TIME'); + + if ($currentTime > $linkTime + $lifeTime) { + return \Utils\ErrorCodes::CHANGE_EMAIL_KEY_WAS_EXPIRED; + } + + return false; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Error.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Error.php new file mode 100644 index 0000000..473f1cf --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Error.php @@ -0,0 +1,28 @@ +getInternalPageTitleWithPostfix($errorData['code']); + + return [ + 'HTML_FILE' => 'error.html', + 'ERROR_DATA' => $errorData, + 'PAGE_TITLE' => $pageTitle, + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/ForgotPassword.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/ForgotPassword.php new file mode 100644 index 0000000..f5ab90e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/ForgotPassword.php @@ -0,0 +1,89 @@ + 'forgotPassword.html', + ]; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $errorCode = $this->validate($params); + + if (!$errorCode) { + $operatorModel = new \Models\Operator(); + $operatorModel->getActivatedByEmail($params['email']); + + if ($operatorModel->loaded()) { + // Create forgot password record. + $forgotPasswordModel = new \Models\ForgotPassword(); + $forgotPasswordModel->add($operatorModel->id); + + // Send forgot password email. + $this->sendPasswordRenewEmail($operatorModel, $forgotPasswordModel); + } + + // Random sleep between 0.5 and 1 second to prevent timing attacks. + usleep(rand(500000, 1000000)); + + // Always report back that the email was sent. + $pageParams['SUCCESS_CODE'] = \Utils\ErrorCodes::RENEW_KEY_CREATED; + } + + $pageParams['VALUES'] = $params; + $pageParams['ERROR_CODE'] = $errorCode; + } + + return parent::applyPageParams($pageParams); + } + + private function validate(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + if (!($params['email'] ?? null)) { + return \Utils\ErrorCodes::EMAIL_DOES_NOT_EXIST; + } + + return false; + } + + private function sendPasswordRenewEmail(\Models\Operator $operatorModel, \Models\ForgotPassword $forgotPasswordModel): void { + $url = \Utils\Variables::getSiteWithProtocol(); + + $toName = $operatorModel->firstname; + $toAddress = $operatorModel->email; + $renewKey = $forgotPasswordModel->renew_key; + + $subject = $this->f3->get('ForgotPassowrd_renew_password_subject'); + $message = $this->f3->get('ForgotPassowrd_renew_password_body'); + + $renewUrl = sprintf('%s/password-recovering/%s', $url, $renewKey); + $message = sprintf($message, $renewUrl); + + \Utils\Mailer::send($toName, $toAddress, $subject, $message); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Login.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Login.php new file mode 100644 index 0000000..88a484c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Login.php @@ -0,0 +1,78 @@ +f3->error(503); + } + + $pageParams = [ + 'HTML_FILE' => 'login.html', + 'JS' => 'user_main.js', + 'ALLOW_FORGOT_PASSWORD' => \Utils\Variables::getForgotPasswordAllowed(), + ]; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $errorCode = $this->validate($params); + + if (!$errorCode) { + $operatorsModel = new \Models\Operator(); + $operatorsModel->getActivatedByEmail($params['email']); + + if ($operatorsModel->loaded() && $operatorsModel->verifyPassword($params['password'])) { + $controller = new \Controllers\Admin\ReviewQueue\Navigation(); + $controller->getNumberOfNotReviewedUsers(true, true); // use cache, overall count + + $this->f3->set('SESSION.active_user_id', $operatorsModel->id); + $extra = $this->f3->get('EXTRA_LOGIN'); + if ($extra && is_callable($extra)) { + $params = $extra(); + } + $this->f3->reroute('/'); + } else { + $errorCode = \Utils\ErrorCodes::EMAIL_OR_PASSWORD_IS_NOT_CORRECT; + } + } + + $pageParams['VALUES'] = $params; + $pageParams['ERROR_CODE'] = $errorCode; + } + + return parent::applyPageParams($pageParams); + } + + private function validate(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + if (!$params['email']) { + return \Utils\ErrorCodes::EMAIL_DOES_NOT_EXIST; + } + + if (!$params['password']) { + return \Utils\ErrorCodes::PASSWORD_DOES_NOT_EXIST; + } + + return false; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Logout.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Logout.php new file mode 100644 index 0000000..90201dc --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Logout.php @@ -0,0 +1,43 @@ + 'logout.html', + 'JS' => 'user_main.js', + ]; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + + if (!$errorCode) { + $this->f3->clear('SESSION'); + session_commit(); + + $this->f3->reroute('/'); + } + + $pageParams['ERROR_CODE'] = $errorCode; + } + + return parent::applyPageParams($pageParams); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/PasswordRecovering.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/PasswordRecovering.php new file mode 100644 index 0000000..a2a379b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/PasswordRecovering.php @@ -0,0 +1,110 @@ + 'passwordRecovering.html', + ]; + + $renewKey = $this->f3->get('PARAMS.renewKey'); + $errorCode = $this->validate($renewKey); + $pageParams['SUCCESS_CODE'] = $errorCode; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $errorCode = $this->validatePost($params); + + $pageParams['SUCCESS_CODE'] = 0; + $pageParams['ERROR_CODE'] = $errorCode; + + if (!$errorCode) { + $forgotPasswordModel = new \Models\ForgotPassword(); + $forgotPasswordModel->getUnusedByRenewKey($renewKey); + $operatorId = $forgotPasswordModel->operator_id; + + $forgotPasswordModel->deactivate(); + + $params = [ + 'id' => $operatorId, + 'new-password' => $params['new-password'], + ]; + + $operatorModel = new \Models\Operator(); + $operatorModel->updatePassword($params); + $operatorModel->activateByOperator($operatorId); + + $pageParams['SUCCESS_CODE'] = \Utils\ErrorCodes::ACCOUNT_ACTIVATED; + } + } + + return parent::applyPageParams($pageParams); + } + + private function validate(string $renewKey): int|false { + if (!$renewKey) { + return \Utils\ErrorCodes::RENEW_KEY_DOES_NOT_EXIST; + } + + $forgotPasswordModel = new \Models\ForgotPassword(); + $forgotPasswordModel->getUnusedByRenewKey($renewKey); + if (!$forgotPasswordModel->loaded()) { + return \Utils\ErrorCodes::RENEW_KEY_IS_NOT_CORRECT; + } + + $currentTime = time(); + $linkTime = strtotime($forgotPasswordModel->created_at); + $lifeTime = $this->f3->get('RENEW_PASSWORD_LINK_TIME'); + + if ($currentTime > $linkTime + $lifeTime) { + return \Utils\ErrorCodes::RENEW_KEY_WAS_EXPIRED; + } + + return false; + } + + private function validatePost(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $newPassword = $params['new-password']; + if (!$newPassword) { + return \Utils\ErrorCodes::NEW_PASSWORD_DOES_NOT_EXIST; + } + + $newPasswordLegth = strlen($newPassword); + $minPasswordLegth = $this->f3->get('MIN_PASSWORD_LENGTH'); + if ($newPasswordLegth < $minPasswordLegth) { + return \Utils\ErrorCodes::PASSWORD_IS_TO_SHORT; + } + + $passwordConfirmation = $params['password-confirmation']; + if (!$passwordConfirmation) { + return \Utils\ErrorCodes::PASSWORD_CONFIRMATION_DOES_NOT_EXIST; + } + + if ($newPassword !== $passwordConfirmation) { + return \Utils\ErrorCodes::PASSWORDS_ARE_NOT_EQUAL; + } + + return false; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Signup.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Signup.php new file mode 100644 index 0000000..9f4524a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/Signup.php @@ -0,0 +1,146 @@ +getAll())) { + $this->f3->error(404); + } + + $pageParams = [ + 'HTML_FILE' => 'signup.html', + 'TIMEZONES' => \Utils\TimeZones::timeZonesList(), + ]; + + if ($this->isPostRequest()) { + $params = $this->f3->get('POST'); + $errorCode = $this->validate($params); + + $pageParams['ERROR_CODE'] = $errorCode; + + if ($errorCode) { + $pageParams['VALUES'] = $params; + } else { + $operatorModel = $this->addUser($params); + + $operatorId = $operatorModel->id; + $apiKey = $this->addDefaultApiKey($operatorId); + $this->addDefaultRules($apiKey); + + //$this->sendActivationEmail($operatorModel); + $pageParams['SUCCESS_CODE'] = \Utils\ErrorCodes::ACCOUNT_CREATED; + } + } + + return parent::applyPageParams($pageParams); + } + + private function addDefaultApiKey($operatorId) { + $data = [ + 'quote' => $this->f3->get('DEFAULT_API_KEY_QUOTE'), + 'operator_id' => $operatorId, + 'skip_enriching_attributes' => \json_encode(array_keys(\Utils\Constants::get('ENRICHING_ATTRIBUTES'))), + 'skip_blacklist_sync' => true, + ]; + + $model = new \Models\ApiKeys(); + + return $model->add($data); + } + + private function addDefaultRules(int $apiKey): void { + $model = new \Models\OperatorsRules(); + $defaultRules = \Utils\Constants::get('DEFAULT_RULES'); + + if (\Utils\Variables::getEmailPhoneAllowed()) { + $defaultRules = array_merge($defaultRules, \Utils\Constants::get('DEFAULT_RULES_EXTENSION')); + } + + foreach ($defaultRules as $key => $value) { + $model->updateRule($key, $value, $apiKey); + } + } + + private function addUser($data) { + $model = new \Models\Operator(); + $model->add($data); + + return $model; + } + + private function sendActivationEmail(\Models\Operator $operatorModel): void { + $url = \Utils\Variables::getSiteWithProtocol(); + + $toName = $operatorModel->firstname; + $toAddress = $operatorModel->email; + $activationKey = $operatorModel->activation_key; + + $subject = $this->f3->get('Signup_activation_email_subject'); + $message = $this->f3->get('Signup_activation_email_body'); + + $activationUrl = sprintf('%s/account-activation/%s', $url, $activationKey); + $message = sprintf($message, $activationUrl); + + \Utils\Mailer::send($toName, $toAddress, $subject, $message); + } + + private function validate(array $params): int|false { + $errorCode = \Utils\Access::CSRFTokenValid($params, $this->f3); + if ($errorCode) { + return $errorCode; + } + + $email = $params['email']; + $password = $params['password']; + + if (!$email) { + return \Utils\ErrorCodes::EMAIL_DOES_NOT_EXIST; + } + + $audit = \Audit::instance(); + if (!$audit->email($email, true)) { + return \Utils\ErrorCodes::EMAIL_IS_NOT_CORRECT; + } + + $operatorsModel = new \Models\Operator(); + $operator = $operatorsModel->getByEmail($email); + if ($operator) { + return \Utils\ErrorCodes::EMAIL_ALREADY_EXIST; + } + + if (!$password) { + return \Utils\ErrorCodes::PASSWORD_DOES_NOT_EXIST; + } + + $passwordLegth = strlen($password); + $minPasswordLegth = $this->f3->get('MIN_PASSWORD_LENGTH'); + if ($passwordLegth < $minPasswordLegth) { + return \Utils\ErrorCodes::PASSWORD_IS_TO_SHORT; + } + + $timezone = $params['timezone'] ?? null; + $timezones = $this->f3->get('timezones'); + if (!$timezone || !array_key_exists($timezone, $timezones)) { + return \Utils\ErrorCodes::TIME_ZONE_DOES_NOT_EXIST; + } + + return false; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Controllers/Pages/index.php @@ -0,0 +1,3 @@ +f3 = \Base::instance(); + + $this->connectToDb(false); + } + + protected function log(string $message): void { + $cronName = get_class($this); + $cronName = substr($cronName, strrpos($cronName, '\\') + 1); + echo sprintf('[%s] %s%s', $cronName, $message, PHP_EOL); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/AbstractQueueCron.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/AbstractQueueCron.php new file mode 100644 index 0000000..cb6e497 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/AbstractQueueCron.php @@ -0,0 +1,83 @@ +log('Start processing queue.'); + + $start = time(); + $success = $failed = []; + + $errors = []; + + do { + $this->log(sprintf('Fetching next batch (%s) in queue.', \Utils\Variables::getAccountOperationQueueBatchSize())); + $items = $this->accountOpQueueModel->getNextBatchInQueue(\Utils\Variables::getAccountOperationQueueBatchSize()); + + if (!count($items)) { + break; + } + + $items = \array_reverse($items); // to use array_pop later. + + $this->accountOpQueueModel->setExecutingForBatch(\array_column($items, 'id')); + + while (time() - $start < \Utils\Constants::get('ACCOUNT_OPERATION_QUEUE_EXECUTE_TIME_SEC')) { + $item = \array_pop($items); // array_pop has O(1) complexity, array_shift has O(n) complexity. + if (!$item) { + break; + } + + try { + $this->processItem($item); + $success[] = $item; + } catch (\Throwable $e) { + $failed[] = $item; + $this->log(sprintf('Queue error %s.', $e->getMessage())); + $errors[] = sprintf('Error on %s: %s. Trace: %s', json_encode($item), $e->getMessage(), $e->getTraceAsString()); + } + } + } while (time() - $start < \Utils\Constants::get('ACCOUNT_OPERATION_QUEUE_EXECUTE_TIME_SEC')); // allow another batch to be fetched if time permits. + + $this->accountOpQueueModel->setCompletedForBatch(\array_column($success, 'id')); + $this->accountOpQueueModel->setFailedForBatch(\array_column($failed, 'id')); + $this->accountOpQueueModel->setWaitingForBatch(\array_column($items, 'id')); // unfinished items back to waiting. + + if (count($errors)) { + $errObj = [ + 'code' => 500, + 'message' => sprintf('Cron %s err', get_class($this)), + //'trace' => implode('; ', $errors), + 'trace' => $errors[0], + 'sql_log' => '', + ]; + \Utils\ErrorHandler::saveErrorInformation($this->f3, $errObj); + } + + $this->log(sprintf( + 'Processed %s items in %s seconds. %s items failed. %s items put back in queue.', + count($success), + time() - $start, + count($failed), + count($items), + )); + } + + abstract protected function processItem(array $item): void; +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/BatchedNewEvents.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/BatchedNewEvents.php new file mode 100644 index 0000000..d02db27 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/BatchedNewEvents.php @@ -0,0 +1,63 @@ +cursorModel = new \Models\Queue\QueueNewEventsCursor(); + $this->eventsModel = new \Models\Events(); + + $actionType = new \Type\QueueAccountOperationActionType(\Type\QueueAccountOperationActionType::CALCULATE_RISK_SCORE); + $this->accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + } + + public function gatherNewEventsBatch(): void { + if (!$this->cursorModel->acquireLock() && !$this->cursorModel->unclog()) { + $this->log('Could not acquire the lock; another cron is probably already working on recently added events.'); + + return; + } + + try { + $cursor = $this->cursorModel->getCursor(); + $next = $this->cursorModel->getNextCursor($cursor, \Utils\Variables::getNewEventsBatchSize()); + + if ($next) { + $accounts = $this->eventsModel->getDistinctAccounts($cursor, $next); + + $this->accountOpQueueModel->addBatch($accounts); + $this->cursorModel->updateCursor($next); + + // Log new events cursor to database. + \Utils\Logger::log('Updated \'last_event_id\' in \'queue_new_events_cursor\' table to ', $next); + + $this->log(sprintf('Added %s accounts to the risk score queue.', count($accounts))); + } else { + $this->log('No new events.'); + } + } catch (\Throwable $e) { + $this->log(sprintf('Batched new events error %s.', $e->getMessage())); + } finally { + $this->cursorModel->releaseLock(); + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/BlacklistQueueHandler.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/BlacklistQueueHandler.php new file mode 100644 index 0000000..cdade4c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/BlacklistQueueHandler.php @@ -0,0 +1,111 @@ +accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + } + + public function processQueue(): void { + if ($this->accountOpQueueModel->isExecuting() && !$this->accountOpQueueModel->unclog()) { + $this->log('Blacklist queue is already being executed by another cron job.'); + + return; + } + + $this->processItems($this->accountOpQueueModel); + } + + protected function processItem(array $item): void { + $fraud = true; + + $dataController = new \Controllers\Admin\User\Data(); + $items = $dataController->setFraudFlag( + $item['event_account'], + $fraud, + $item['key'], + ); + + $model = new \Models\User(); + $username = $model->getUser($item['event_account'], $item['key'])['userid'] ?? ''; + + $logger = new \Log('blacklist.log'); + $logger->write('[BlacklistQueue] ' . $username . ' added to blacklist.'); + + $model = new \Models\ApiKeys(); + $model->getKeyById($item['key']); + + if (!$model->skip_blacklist_sync && $model->token) { + $user = new \Models\User(); + $userEmail = $user->getUser($item['event_account'], $item['key'])['email'] ?? null; + + if ($userEmail !== null) { + $hashes = $this->getHashes($items, $userEmail); + $errorMessage = $this->sendBlacklistReportPostRequest($hashes, $model->token); + if (strlen($errorMessage) > 0) { + // Log error to database + \Utils\Logger::log('Fraud enrichment API curl error', $errorMessage); + $this->log('Fraud enrichment API curl error logged to database.'); + } + } + } + } + + /** + * @param array $items + */ + private function getHashes(array $items, string $userEmail): array { + $userHash = hash('sha256', $userEmail); + + return array_map(function ($item) use ($userHash) { + return [ + 'type' => $item['type'], + 'value' => hash('sha256', $item['value']), + 'id' => $userHash, + ]; + }, $items); + } + + /** + * @param array $hashes + */ + private function sendBlacklistReportPostRequest(array $hashes, string $enrichmentKey): string { + $postFields = [ + 'data' => $hashes, + ]; + $options = [ + 'method' => 'POST', + 'header' => [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $enrichmentKey, + 'User-Agent: ' . $this->f3->get('USER_AGENT'), + ], + 'content' => \json_encode($postFields), + ]; + + /** @var array{request: array, body: string, headers: array, engine: string, cached: bool, error: string} $result */ + $result = \Web::instance()->request( + url: \Utils\Variables::getEnrichtmentApi() . '/global_alert_report', + options: $options, + ); + + return $result['error']; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/DeletionQueueHandler.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/DeletionQueueHandler.php new file mode 100644 index 0000000..0f0669b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/DeletionQueueHandler.php @@ -0,0 +1,38 @@ +accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + } + + public function processQueue(): void { + if ($this->accountOpQueueModel->isExecuting() && !$this->accountOpQueueModel->unclog()) { + $this->log('Deletion queue is already being executed by another cron job.'); + } else { + $this->processItems($this->accountOpQueueModel); + } + } + + protected function processItem(array $item): void { + $user = new \Models\User(); + $user->deleteAllUserData($item['event_account'], $item['key']); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/EnrichmentQueueHandler.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/EnrichmentQueueHandler.php new file mode 100644 index 0000000..a7d21dc --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/EnrichmentQueueHandler.php @@ -0,0 +1,73 @@ +accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + + $this->apiKeysModel = new \Models\ApiKeys(); + $this->controller = new \Controllers\Admin\Enrichment\Data(); + } + + public function processQueue(): void { + if ($this->accountOpQueueModel->isExecuting() && !$this->accountOpQueueModel->unclog()) { + $this->log('Enrchment queue is already being executed by another cron job.'); + } else { + $this->processItems($this->accountOpQueueModel); + } + } + + protected function processItem(array $item): void { + $start = time(); + $apiKey = $item['key']; + $userId = $item['event_account']; + + $subscriptionKey = $this->apiKeysModel->getKeyById($apiKey)->token; + $entities = $this->controller->getNotCheckedEntitiesByUserId($userId, $apiKey); + + // TODO: check key ? + $this->log(sprintf('Items to enrich for account %s: %s.', $userId, json_encode($entities))); + + $summary = []; + $success = 0; + $failed = 0; + + foreach ($entities as $type => $items) { + if (count($items)) { + $summary[$type] = count($items); + } + foreach ($items as $item) { + $result = $this->controller->enrichEntity($type, null, $item, $apiKey, $subscriptionKey); + if (isset($result['ERROR_CODE'])) { + $failed += 1; + } else { + $success += 1; + } + } + } + + // TODO: if failed !== 0 add to queue again? + // TODO: recalculate score after all? + $this->log(sprintf('Enrichment for account %s: %s enriched, %s failed in %s s (%s).', $userId, $success, $failed, time() - $start, json_encode($summary))); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/LogbookRotation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/LogbookRotation.php new file mode 100644 index 0000000..c18efe3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/LogbookRotation.php @@ -0,0 +1,35 @@ +log('Start logbook rotation.'); + + $model = new \Models\ApiKeys(); + $keys = $model->getAllApiKeyIds(); + // rotate events for unauthorized requests + $keys[] = ['id' => null]; + + $model = new \Models\Logbook(); + $cnt = 0; + foreach ($keys as $key) { + $cnt += $model->rotateRequests($key['id']); + } + + $this->log(sprintf('Deleted %s events for %s keys in logbook.', $cnt, count($keys))); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/NotificationsHandler.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/NotificationsHandler.php new file mode 100644 index 0000000..a59d9b4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/NotificationsHandler.php @@ -0,0 +1,85 @@ +notificationPreferencesModel = new \Models\NotificationPreferences(); + } + + public function prepareNotifications(): void { + $timezonesInWindow = $this->getTimeZonesInWindow(self::NOTIFICATION_WINDOW_HOUR_START, self::NOTIFICATION_WINDOW_HOUR_END); + + $operatorsToNotify = $this->notificationPreferencesModel->listOperatorsEligableForUnreviewedItemsReminder($timezonesInWindow); + + foreach ($operatorsToNotify as $operator) { + try { + $this->sendUnreviewedItemsReminderEmail($operator['firstname'] ?? '', $operator['email'], $operator['review_queue_cnt']); + } catch (\Throwable $e) { + $this->log(sprintf('Notification handler error %s.', $e->getMessage())); + } + } + + $count = \count($operatorsToNotify); + + if ($count > 0) { + $this->notificationPreferencesModel->updateLastUnreviewedItemsReminder(\array_column($operatorsToNotify, 'id')); + } + + $this->log(sprintf('Sent %s unreviewed items reminder notifications.', $count)); + } + + /** + * @return string[] Time zones currently in the notification window + */ + private function getTimeZonesInWindow(int $startHour, int $endHour): array { + $timezones = \DateTimeZone::listIdentifiers(); + + return \array_filter($timezones, function ($timezone) use ($startHour, $endHour) { + $date = new \DateTime('now', new \DateTimeZone($timezone)); + $hour = (int) $date->format('H'); + + return $hour >= $startHour && $hour < $endHour; + }); + } + + private function sendUnreviewedItemsReminderEmail(string $recipientFirstName, string $recipientEmail, int $reviewCount): void { + $audit = \Audit::instance(); + if (!$audit->email($recipientEmail, true)) { + $this->log(sprintf('Username `%s` is not email; review count is %s', $recipientEmail, $reviewCount)); + + return; + } + + $subject = $this->f3->get('UnreviewedItemsReminder_email_subject'); + $subject = sprintf($subject, $reviewCount); + + $message = $this->f3->get('UnreviewedItemsReminder_email_body'); + $url = \Utils\Variables::getSiteWithProtocol(); + $message = sprintf($message, $recipientFirstName, $recipientEmail, $reviewCount, $url); + + \Utils\Mailer::send($recipientFirstName, $recipientEmail, $subject, $message); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/QueuesClearer.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/QueuesClearer.php new file mode 100644 index 0000000..d2962a2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/QueuesClearer.php @@ -0,0 +1,38 @@ +clearCompleted($clearBefore); + } + + $this->log(sprintf('Cleared %s completed items.', $clearedCount)); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/RetentionPolicyViolations.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/RetentionPolicyViolations.php new file mode 100644 index 0000000..27e6e5c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/RetentionPolicyViolations.php @@ -0,0 +1,37 @@ +log('Start retention policy violations.'); + + $eventsModel = new \Models\Events(); + $retentionModel = new \Models\RetentionPolicies(); + + $retentionKeys = $retentionModel->getRetentionKeys(); + $cnt = 0; + + foreach ($retentionKeys as $key) { + // insuring clause + if ($key['retention_policy'] > 0) { + $cnt += $eventsModel->retentionDeletion($key['retention_policy'], $key['id']); + } + } + + $this->log(sprintf('Deleted %s events for %s operators due to retention policy violations.', $cnt, count($retentionKeys))); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/RiskScoreQueueHandler.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/RiskScoreQueueHandler.php new file mode 100644 index 0000000..3c7d141 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/RiskScoreQueueHandler.php @@ -0,0 +1,45 @@ +accountOpQueueModel = new \Models\Queue\AccountOperationQueue($actionType); + $this->rulesModel = new \Models\OperatorsRules(); + + $this->rulesController = new \Controllers\Admin\Rules\Data(); + $this->rulesController->buildEvaluationModels(); + } + + public function processQueue(): void { + if ($this->accountOpQueueModel->isExecuting() && !$this->accountOpQueueModel->unclog()) { + $this->log('Risk score queue is already being executed by another cron job.'); + + return; + } + $this->processItems($this->accountOpQueueModel); + } + + protected function processItem(array $item): void { + $this->rulesController->evaluateUser($item['event_account'], $item['key'], true); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/Totals.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/Totals.php new file mode 100644 index 0000000..22c3a22 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/Totals.php @@ -0,0 +1,51 @@ +log('Start totals calculation.'); + $start = time(); + $models = \Utils\Constants::get('REST_TOTALS_MODELS'); + + $actionType = new \Type\QueueAccountOperationActionType(\Type\QueueAccountOperationActionType::CALCULATE_RISK_SCORE); + $queueModel = new \Models\Queue\AccountOperationQueue($actionType); + $keys = $queueModel->getNextBatchKeysInQueue(\Utils\Variables::getAccountOperationQueueBatchSize()); + + $res = []; + + foreach ($models as $name => $modelClass) { + $res[$name] = ['cnt' => 0, 's' => 0]; + $s = time(); + $model = new $modelClass(); + foreach ($keys as $key) { + (new \Models\SessionStat())->updateStats($key); + + $cnt = $model->updateAllTotals($key); + $res[$name]['cnt'] += $cnt; + if (time() - $start > \Utils\Constants::get('ACCOUNT_OPERATION_QUEUE_EXECUTE_TIME_SEC')) { + // TODO: any reason to put the rest keys to queue? + $res[$name]['s'] = time() - $s; + break 2; + } + } + $res[$name]['s'] = time() - $s; + } + + $this->log(sprintf('Updated %s entities for %s keys and %s models in %s seconds.', array_sum(array_column(array_values($res), 'cnt')), count($keys), count($models), time() - $start)); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Crons/index.php @@ -0,0 +1,3 @@ +get('EXTRA_DICT_EN') ?? []); diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminBot.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminBot.php new file mode 100644 index 0000000..8251b58 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminBot.php @@ -0,0 +1,26 @@ + 'A type of the device.', + 'Base_table_column_os_tooltip' => 'An operating system of the device.', + + 'Base_table_column_total_actions_tooltip_ips' => 'The number of device-associated requests made from an IP address.', + 'Base_table_column_total_users_tooltip_ips' => 'The number of users associated with an IP address.', + + 'AdminIps_map_title_tooltip' => 'Information detected by IP addresses associated with the device. The map visualizes the geolocated countries along with the number of events reported for the device.', + 'AdminUsers_table_title_tooltip' => 'A list of users detected as utilizing the device.', + 'AdminEvents_table_title_tooltip' => 'A list of events recorded for the device. The chart shows the number of events reported each day.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminBots.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminBots.php new file mode 100644 index 0000000..da85f17 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminBots.php @@ -0,0 +1,18 @@ + 'The total number of users associated with a device', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminCountry.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminCountry.php new file mode 100644 index 0000000..d4fd9d3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminCountry.php @@ -0,0 +1,21 @@ + 'A list of users detected as making requests from the country.', + 'AdminIps_table_title_tooltip' => 'A list of IP addresses geolocated to the country.', + 'AdminIsps_table_title_tooltip' => 'A list of internet service providers associated with the requests from the country.', + 'AdminEvents_table_title_tooltip' => 'A list of events detected for the country. The chart shows the number of events reported each day.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminDomain.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminDomain.php new file mode 100644 index 0000000..01e7756 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminDomain.php @@ -0,0 +1,24 @@ + 'The number of events reported for a domain.', + 'Base_table_column_total_users_tooltip_ips' => 'The number of users associated with an IP address.', + + 'AdminIps_map_title_tooltip' => 'Information detected by IP addresses assigned to the domain. The map visualizes the geolocated countries along with the number of events reported for the domain.', + 'AdminUsers_table_title_tooltip' => 'A list of users with an email that belongs to the domain.', + 'AdminIsps_table_title_tooltip' => 'A list of internet service providers associated with the domain.', + 'AdminEvents_table_title_tooltip' => 'A list of events recorded for the domain. The chart shows the number of events reported each day.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminIp.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminIp.php new file mode 100644 index 0000000..cbaf4ec --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminIp.php @@ -0,0 +1,20 @@ + 'A list of users associated with the IP address.', + 'AdminDevices_table_title_tooltip' => 'A list of devices associated with the IP address.', + 'AdminEvents_table_title_tooltip' => 'A list of events associated with the IP address. The chart shows the number of events reported each day.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminIsp.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminIsp.php new file mode 100644 index 0000000..5c22ad8 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminIsp.php @@ -0,0 +1,20 @@ + 'Information detected by IP addresses assigned to the ISP. The map visualizes the geolocated countries along with the number of events reported for the ISP.', + 'AdminUsers_table_title_tooltip' => 'A list of users associated with the ISP.', + 'AdminEvents_table_title_tooltip' => 'A list of events recorded for the ISP. The chart shows the number of events reported each day.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminResource.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminResource.php new file mode 100644 index 0000000..6b01b0a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminResource.php @@ -0,0 +1,23 @@ + 'The number of the resource requests made from an IP address.', + 'AdminIps_map_title_tooltip' => 'Information detected by IP addresses from which the resource was requested. The map visualizes the geolocated countries along with the number of the resource-related events.', + 'AdminUsers_table_title_tooltip' => 'A list of users that interacted with the resource.', + 'AdminIsps_table_title_tooltip' => 'A list of internet service providers associated with the resource requests.', + 'AdminDevices_table_title_tooltip' => 'A list of devices from which the resource was requested.', + 'AdminEvents_table_title_tooltip' => 'A list of events recorded for the resource. The chart shows the number of events reported each day.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminUser.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminUser.php new file mode 100644 index 0000000..4fa787b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminUser.php @@ -0,0 +1,22 @@ + 'The number of the user requests made from an IP address.', + 'AdminIps_map_title_tooltip' => 'Information detected by IP addresses associated with the user. The map visualizes geolocated countries along with the number of user-related events.', + 'AdminEvents_table_title_tooltip' => 'A list of the user-related events. The chart shows the number of events reported each day.', + 'AdminIsps_table_title_tooltip' => 'A list of internet service providers associated with the user.', + 'AdminDevices_table_title_tooltip' => 'A list of devices associated with the user.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminUsers.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminUsers.php new file mode 100644 index 0000000..3ac437f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/AdminUsers.php @@ -0,0 +1,17 @@ + 'Re-enrich data', + 'AdminEnrichment_success_message' => 'Data has been successfully enriched.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/Notifications.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/Notifications.php new file mode 100644 index 0000000..485e4a1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/Notifications.php @@ -0,0 +1,19 @@ + 'Reminder: %s items are waiting for your review', + 'UnreviewedItemsReminder_email_body' => 'Hi %s (%s), %s items are waiting for your review. Sign in to review these items. %s', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/ReviewQueue.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/ReviewQueue.php new file mode 100644 index 0000000..53fa4bd --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/ReviewQueue.php @@ -0,0 +1,18 @@ + 'A user can be blacklisted or whitelisted. To put a user on a list, click "Not reviewed" button and then choose an applicable status.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/Totals.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/Totals.php new file mode 100644 index 0000000..ae3b429 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/Totals.php @@ -0,0 +1,18 @@ + 'Totals calculated succesfully.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Additional/index.php @@ -0,0 +1,3 @@ + 'Error %s occurred', + 'error_email_body_template' => ( + '

Error occurred at: %s

+

Message:

%s +

Trace:

%s + ' + ), +]; + +$errors = addExerciseError($errors, 404, 'Page not found'); +$errors = addExerciseError($errors, 500, 'This function does not work right now'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::CSRF_ATTACK_DETECTED, 'We can\'t proceed with this request. Please reload the page and try again'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::EMAIL_DOES_NOT_EXIST, 'Email does not exist'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::EMAIL_IS_NOT_CORRECT, 'Email is incorrect'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::EMAIL_ALREADY_EXIST, 'Email already exists'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::PASSWORD_DOES_NOT_EXIST, 'Password does not exist'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::PASSWORD_IS_TO_SHORT, 'Minimum password length is 8 characters'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::ACCOUNT_CREATED, 'Thanks for your registration. Please login with your new credentials.'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::ACTIVATION_KEY_DOES_NOT_EXIST, 'Activation key does not exist'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::ACTIVATION_KEY_IS_NOT_CORRECT, 'Activation key is incorrect'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::EMAIL_OR_PASSWORD_IS_NOT_CORRECT, 'Error: Permission denied.'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::API_KEY_ID_DOESNT_EXIST, 'API key does not exist'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::API_KEY_WAS_CREATED_FOR_ANOTHER_USER, 'Incorrect Tracking ID'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::OPERATOR_ID_DOES_NOT_EXIST, 'Operator ID does not exist'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::OPERATOR_IS_NOT_A_CO_OWNER, 'Operator is not a co-owner of this Tracking ID'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::UNKNOWN_ENRICHMENT_ATTRIBUTES, 'Unknown event attributes for data enrichment'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::INVALID_API_RESPONSE, 'Unexpected API response'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::FIRST_NAME_DOES_NOT_EXIST, 'First name is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::LAST_NAME_DOES_NOT_EXIST, 'Last name is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::COUNTRY_DOES_NOT_EXIST, 'Country is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::STREET_DOES_NOT_EXIST, 'Street address is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::CITY_DOES_NOT_EXIST, 'City is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::STATE_DOES_NOT_EXIST, 'State is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::ZIP_DOES_NOT_EXIST, 'ZIP is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::TIME_ZONE_DOES_NOT_EXIST, 'Time zone is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::RETENTION_POLICY_DOES_NOT_EXIST, 'Retention policy is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::UNREVIEWED_ITEMS_REMINDER_FREQUENCY_DOES_NOT_EXIST, 'Unreviewed items reminder frequency is a mandatory field'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::CURRENT_PASSWORD_DOES_NOT_EXIST, 'Current password is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::CURRENT_PASSWORD_IS_NOT_CORRECT, 'Current password is incorrect'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::NEW_PASSWORD_DOES_NOT_EXIST, 'New password is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::PASSWORD_CONFIRMATION_DOES_NOT_EXIST, 'Password confirmation is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::PASSWORDS_ARE_NOT_EQUAL, 'New password and password confirmation do not match'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::EMAIL_IS_NOT_NEW, 'The new email address is the same as the current one'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::RENEW_KEY_CREATED, 'We sent you an email with further instructions on how to reset your password'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::RENEW_KEY_IS_NOT_CORRECT, 'Renew key is incorrect ¯\_ (ツ)_/¯'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::RENEW_KEY_DOES_NOT_EXIST, 'Renew key does not exist'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::RENEW_KEY_WAS_EXPIRED, 'Renew key has expired'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::ACCOUNT_ACTIVATED, 'Your password has been successfully changed. Please login with your new credentials and continue using the system.'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::THERE_ARE_NO_EVENTS_YET, 'No events from your application have been received yet'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::THERE_ARE_NO_EVENTS_LAST_24_HOURS, 'There are no events from your application for more than 24 hours'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::OPERATOR_DOES_NOT_HAVE_ACCESS_TO_ACCOUNT, 'Operator does not have access to this account'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::USER_HAS_BEEN_SUCCESSFULLY_ADDED_TO_WATCH_LIST, 'User has been successfully added to the watchlist'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::USER_HAS_BEEN_SUCCESSFULLY_REMOVED_FROM_WATCH_LIST, 'User has been successfully removed from the watchlist'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::USER_FRAUD_FLAG_HAS_BEEN_SET, 'User has been successfully marked as fraud'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::USER_FRAUD_FLAG_HAS_BEEN_UNSET, 'User has been successfully marked as not fraud'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::USER_REVIEWED_FLAG_HAS_BEEN_SET, 'User has been successfully marked as reviewed'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::USER_REVIEWED_FLAG_HAS_BEEN_UNSET, 'User has been successfully marked as not reviewed'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::USER_DELETION_FAILED, 'User deletion was unsuccessful.'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::USER_BLACKLISTING_FAILED, 'User blacklisting was unsuccessful.'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::USER_BLACKLISTING_QUEUED, 'This user and all associated IPs are currently queued for blacklisting.'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::CHANGE_EMAIL_KEY_DOES_NOT_EXIST, 'Change email key does not exist'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::CHANGE_EMAIL_KEY_IS_NOT_CORRECT, 'Change email key is incorrect'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::CHANGE_EMAIL_KEY_WAS_EXPIRED, 'Change email key has expired'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::EMAIL_CHANGED, 'Your email has been successfully changed. Please login with your new credentials and continue using the system.'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::RULES_HAS_BEEN_SUCCESSFULLY_UPDATED, 'Rules have been successfully updated'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::BLACKLIST_THRESHOLD_DOES_NOT_EXIST, 'Blacklist threshold is mandatory fields'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::REVIEW_QUEUE_THRESHOLD_DOES_NOT_EXIST, 'Review queue threshold is mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::BLACKLIST_THRESHOLD_EXCEEDS_REVIEW_QUEUE_THRESHOLD, 'Blacklist threshold must not exceed review queue threshold.'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::REST_API_KEY_DOES_NOT_EXIST, 'API key could not be found in the headers'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::REST_API_KEY_IS_NOT_CORRECT, 'API key is incorrect'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::REST_API_NOT_AUTHORIZED, 'Not authorized to perform this action'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::REST_API_MISSING_PARAMETER, 'Missing required parameter'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::REST_API_VALIDATION_ERROR, 'Validation error'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::REST_API_USER_ALREADY_SCHEDULED_FOR_DELETION, 'User already scheduled for deletion'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::REST_API_USER_SUCCESSFULLY_ADDED_FOR_DELETION, 'User successfully added for deletion'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::ENRICHMENT_API_KEY_DOES_NOT_EXIST, 'Enrichment API key is not set'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::TYPE_DOES_NOT_EXIST, 'Type is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::SEARCH_QUERY_DOES_NOT_EXIST, 'Search query is a mandatory field'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::ENRICHMENT_API_UNKNOWN_ERROR, 'Unknown error occurred while processing your request'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::ENRICHMENT_API_BOGON_IP, 'IP is bogon'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::ENRICHMENT_API_IP_NOT_FOUND, 'IP not found'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::RISK_SCORE_UPDATE_UNKNOWN_ERROR, 'Unknown error occurred while processing your request'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::ENRICHMENT_API_KEY_OVERUSE, 'You\'ve used up your Enrichment API quota. Please update your plan.'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::ENRICHMENT_API_ATTRIBUTE_IS_UNAVAILABLE, 'Enrichment of this data type is not supported in current subscription.'); +$errors = addExerciseError($errors, \Utils\ErrorCodes::ENRICHMENT_API_IS_NOT_AVAILABLE, 'API server is currently unavailable. Please try again later.'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::SUBSCRIPTION_KEY_INVALID_UPDATE, 'Subscription key is not valid, canceling update'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::TOTALS_INVALID_TYPE, 'Invalid entity type was passed for totals calculation'); + +$errors = addExerciseError($errors, \Utils\ErrorCodes::CRON_JOB_MAY_BE_OFF, 'A cron job isn\'t running. Please check the cron job configuration.'); + +$extraErrors = \Base::instance()->get('EXTRA_DICT_EN_ERRORS') ?? []; +foreach ($extraErrors as $errorCode => $desc) { + $errors = addExerciseError($errors, $errorCode, $desc); +} + +return $errors; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/AccountActivation.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/AccountActivation.php new file mode 100644 index 0000000..017a64b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/AccountActivation.php @@ -0,0 +1,20 @@ + 'Account activation', + 'AccountActivation_success_message' => 'Your account has been successfully activated.', + 'AccountActivation_login' => 'Sign in', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/AdminHome.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/AdminHome.php new file mode 100644 index 0000000..9604d4a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/AdminHome.php @@ -0,0 +1,59 @@ + 'Dashboard', + 'AdminHome_header_title' => 'Dashboard', + 'AdminHome_breadcrumb_title' => 'Dashboard', + + 'AdminHome_table_title' => 'Last events', + 'AdminHome_table_column_timestamp' => 'Timestamp', + 'AdminHome_table_column_user_id' => 'User Id', + 'AdminHome_table_column_url' => 'URL', + 'AdminHome_table_column_client_ip' => 'Client IP', + 'AdminHome_table_column_country' => 'Country', + + 'AdminHome_total_events' => 'Events', + 'AdminHome_total_events_tooltip' => 'The number of events during a selected period of time and in total.', + 'AdminHome_total_users' => 'Users', + 'AdminHome_total_users_tooltip' => 'The number of active users during a selected period of time and in total.', + 'AdminHome_total_ips' => 'IP addresses', + 'AdminHome_total_ips_tooltip' => 'The number of active IP addresses during a selected period of time and in total.', + 'AdminHome_total_countries' => 'Countries', + 'AdminHome_total_countries_tooltip' => 'The number of identified countries during a selected period of time and in total.', + 'AdminHome_total_urls' => 'Resources', + 'AdminHome_total_urls_tooltip' => 'The number of requested resources during a selected period of time and in total.', + 'AdminHome_total_users_for_review' => 'Review', + 'AdminHome_total_users_for_review_tooltip' => 'The number of users added to the review queue during a selected period of time and in total.', + 'AdminHome_total_blocked_users' => 'Blacklisted', + 'AdminHome_total_blocked_users_tooltip' => 'The number of blacklisted users during a selected period of time and in total.', + 'AdminHome_view_all' => 'View all', + + 'AdminHome_top10_most_active_users' => 'Activity by users', + 'AdminHome_top10_most_active_users_tooltip' => 'A list of users with the highest quantity of recorded events.', + 'AdminHome_top10_active_countries' => 'Activity by countries', + 'AdminHome_top10_active_countries_tooltip' => 'A list of countries with the highest quantity of recorded users.', + 'AdminHome_top10_active_urls' => 'Activity by resources', + 'AdminHome_top10_active_urls_tooltip' => 'A list of resources with the highest quantity of recorded users.', + 'AdminHome_top10_ips_with_the_most_users' => 'Shared IP addresses', + 'AdminHome_top10_ips_with_the_most_users_tooltip' => 'A list of IP addresses utilized by several users.', + 'AdminHome_top10_users_with_most_login_fail' => 'Account login fail', + 'AdminHome_top10_users_with_most_login_fail_tooltip' => 'A list of users with the highest quantity of failed login attempts.', + 'AdminHome_top10_users_with_the_most_ips' => 'Multiple IP addresses', + 'AdminHome_top10_users_with_the_most_ips_tooltip' => 'A list of users with a high number of IP addresses.', + + 'AdminHome_clock_day_tooltip' => 'The day of year (DOY) is the sequential day number starting with day 1 on January 1st.', + 'AdminHome_clock_time_tooltip' => 'Current time in your application based on timezone settings.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Api.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Api.php new file mode 100644 index 0000000..547956e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Api.php @@ -0,0 +1,105 @@ + 'Tracking ID', + 'AdminApi_breadcrumb_title' => 'Api', + + 'AdminApi_table_title_tooltip' => 'Use the Tracking ID to access the API. Include it in the HTTP header when sending event information to the endpoint, as shown in the examples below.', + + 'AdminApi_http_endpoint' => 'Tracking code', + 'AdminApi_server_language' => 'Server language', + 'AdminApi_http_endpoint_tooltip' => [ + 'title' => 'This container holds your tracking code and is used to collect and process user data.', + 'items' => [ + 'Choose your server language (PHP, Python, Node.js, Ruby, cURL).', + 'Replace the placeholders in the code with your specific values.', + 'Paste the completed code on every page of your website or app that you want to track. This code should be included for logged-in users.', + 'Data will appear in reports within approximately one minute.', + ], + ], + + 'AdminApi_table_column_sensor_key' => 'Tracking ID', + 'AdminApi_table_column_created_at' => 'Created at', + + 'AdminApi_table_column_action' => 'Action', + 'AdminApi_table_column_action_tooltip' => 'To renew the Tracking ID value, click the "Reset" button. Note that this action cancels the validity of the previously used key.', + + 'AdminApi_table_button_reset' => 'Reset', + 'AdminApi_reset_success_message' => 'The Tracking ID has been reset successfully.', + + 'AdminApi_data_enrichment_title' => 'Data enrichment', + 'AdminApi_data_enrichment_title_tooltip' => 'Choose the components of event information to enhance by additionally applying internal, external, and open-sourced data.', + 'AdminApi_data_enrichment_save_button' => 'Save', + 'AdminApi_data_enrichment_attributes' => [ + 'domain' => 'Domain enrichment', + 'email' => 'Email enrichment', + 'ip' => 'IP address enrichment', + 'ua' => 'User agent enrichment', + 'phone' => 'Phone enrichment', + ], + 'AdminApi_data_enrichment_success_message' => 'Enrichment settings have been updated successfully.', + + 'AdminApi_form_title' => 'Enrichment key', + 'AdminApi_form_title_tooltip' => 'Enrichment key enables access to enrichment.', + 'AdminApi_form_button_save' => 'Save', + 'AdminApi_form_field_token_label' => 'Enrichment key', + 'AdminApi_form_field_token_placeholder' => 'TIR:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=', + 'AdminApi_current_token_tooltip' => 'Current key: ', + 'AdminApi_form_confirmationMessage' => 'You can use a limited version of tirreno without a paid subscription or choose to enrich IP data. To learn about enrichment plans and obtain a subscription key, please visit: https://www.tirreno.com/pricing/', + + 'AdminApi_token_management_title' => 'Enrichment subscription management', + 'AdminApi_token_management_title_tooltip' => 'Usage statistics and subscription key management', + 'AdminApi_token_management_plan_col' => 'Plan', + 'AdminApi_token_management_subscription_status_col' => 'Status', + 'AdminApi_token_management_last_period_usage_col' => 'Current usage', + 'AdminApi_token_management_next_billed_col' => 'Next billed at', + 'AdminApi_token_management_update_payment_action' => 'Update card', + 'AdminApi_token_management_update_payment_button' => 'Update', + 'AdminApi_token_management_reset_token_button' => 'Reset', + + 'AdminApi_exchange_blacklist_title' => 'Data exchange', + 'AdminApi_exchange_blacklist_title_tooltip' => 'Enable data exchange to participate in the formation and benefit from the utilization of the network alert list.', + 'AdminApi_exchange_blacklist_warning' => 'Please note that changing this parameter will only affect newly added items.', + 'AdminApi_exchange_blacklist_label' => 'Blacklisted items', + 'AdminApi_exchange_blacklist_save_button' => 'Save', + 'AdminApi_exchange_blacklist_success_message' => 'Data exchange parameter has been updated successfully.', + 'AdminApi_update_token_success_message' => 'Enrichment key has been updated successfully.', + + 'AdminApi_data_alert_list_exchange' => 'Antifraud network exchange', + + 'AdminApi_shared_keys_title' => 'Share access', + 'AdminApi_shared_keys_delete' => '[ x ]', + 'AdminApi_shared_keys_title_tooltip' => 'Manage operators that can use this console. To share access, start by sending an invitation email.', + 'AdminApi_shared_keys_empty' => 'You are not sharing your access with anyone else.', + + 'AdminApi_add_co_owner_form_email' => 'Email', + 'AdminApi_add_co_owner_form_invite_button' => 'Invite', + 'AdminApi_add_co_owner_success_message' => 'Invitation to share access has been sent successfully.', + + 'AdminApi_invitation_email_subject' => 'Invitation', + 'AdminApi_invitation_email_body' => '%s has invited you to collaborate. You can accept this invitation by setting the password for your account or decline the invitation by ignoring this email. %s This invitation will expire in 24 hours.', + + 'AdminApi_remove_co_owner_success_message' => 'Co-owner has been removed successfully.', + 'AdminApi_remove_co_owner_error_message' => 'An error occured while removing co-owner.', + + 'AdminApi_manual_enrichment_form_title' => 'Manual data enrichment', + 'AdminApi_manual_enrichment_form_confirmationMessage' => 'Identifies and sends data that was previously unenriched for re-enrichment in the background, ensuring all records are complete and accurate.', + 'AdminApi_manual_enrichment_form_button_submit' => 'Preview', + 'AdminApi_manual_enrichment_success_message' => 'Enrichment process started.', + + 'AdminApi_manual_enrichment_popup_header' => 'Manual data enrichment', + 'AdminApi_manual_enrichment_popup_submit_button' => 'Start enrichment', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/BaseTable.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/BaseTable.php new file mode 100644 index 0000000..d791a70 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/BaseTable.php @@ -0,0 +1,210 @@ + 'Signup date', + 'Base_table_column_user_registered_tooltip' => 'The date of user account creation in a client platform. If not set, the date of the first recorded user event.', + 'Base_table_column_userid' => 'User ID', + 'Base_table_column_userid_tooltip' => 'User identifier provided by a client platform.', + 'Base_table_column_user_firstname' => 'First name', + 'Base_table_column_user_firstname_tooltip' => 'A user’s first name.', + 'Base_table_column_user_lastname' => 'Last name', + 'Base_table_column_user_lastname_tooltip' => 'A user’s last name.', + 'Base_table_column_user_risk_score_and_email' => 'Trust score & email', + 'Base_table_column_user_risk_score_and_email_tooltip' => 'Displays two values. The trust score on the left side is a calculated per-user value. It ranges from 0 (lowest trust) to 99 (highest trust). The value on the right side is a user email provided by a client platform.', + 'Base_table_column_session' => 'Session', + 'Base_table_column_session_tooltip' => 'Session parameters.', + 'Base_table_column_user_email' => 'Email', + 'Base_table_column_user_email_tooltip' => 'A user’s email address.', + + 'Base_table_column_last_action_timestamp' => 'Timestamp', + 'Base_table_column_last_action_timestamp_tooltip' => 'The date and time of an event.', + 'Base_table_column_url' => 'URL', + 'Base_table_column_url_tooltip' => 'A reference to a resource requested on a client platform.', + 'Base_table_column_event_type' => 'Event type', + 'Base_table_column_event_type_tooltip' => 'A type of action performed.', + 'Base_table_column_suspicious_url' => 'Suspicious', + 'Base_table_column_suspicious_url_tooltip_resources' => 'URL matches with your suspicious words list.', + + 'Base_table_column_total_users' => 'Users', + 'Base_table_column_total_users_tooltip_ips' => 'The total number of users associated with an IP address.', + 'Base_table_column_total_users_tooltip_countries' => 'The number of users with IP addresses geolocated to a country over the specified and preceding period.', + 'Base_table_column_total_users_tooltip_domains' => 'The total number of users with email addresses belonging to a domain.', + 'Base_table_column_total_users_tooltip_isps' => 'The number of ISP-associated users over the specified and preceding period.', + 'Base_table_column_total_users_tooltip_resources' => 'The number of users that requested a resource over the specified and preceding period.', + 'Base_table_column_total_users_tooltip_phones' => 'The total number of users associated with a phone number.', + 'Base_table_column_total_countries' => 'Countries', + 'Base_table_column_total_countries_tooltip_resources' => 'The number of countries from which a resource was requested over the specified and preceding period.', + + 'Base_table_column_total_ips' => 'IPs', + 'Base_table_column_total_ips_tooltip_resources' => 'The number of unique IP addresses from which a resource was requested over the specified and preceding period.', + 'Base_table_column_total_ips_tooltip_countries' => 'The number of IP addresses geolocated to a country over the specified and preceding period.', + 'Base_table_column_total_ips_tooltip_isps' => 'The number of ISP-belonging IP addresses over the specified and preceding period.', + + 'Base_table_column_total_actions' => 'Events', + 'Base_table_column_total_actions_tooltip_ips' => 'The number of requests from an IP address over the specified and preceding period.', + 'Base_table_column_total_actions_tooltip_users' => 'The total number of requests made by a user.', + 'Base_table_column_total_actions_tooltip_isps' => 'The number of ISP-related events over the specified and preceding period.', + 'Base_table_column_total_actions_tooltip_resources' => 'The number of requests made to a resource over the specified and preceding period.', + 'Base_table_column_total_actions_tooltip_countries' => 'The number of requests made from a country over the specified and preceding period.', + 'Base_table_column_total_actions_tooltip_devices' => 'The total number of requests made from this country.', + + 'Base_table_column_ip' => 'IP', + 'Base_table_column_ip_tooltip' => 'An IP address associated with an event. Note that anonymizing services can hide the real IP address.', + 'Base_table_column_ip_type' => 'IP type', + 'Base_table_column_ip_type_tooltip' => 'A type of IP address. Non-residential addresses are considered a warning signal.', + 'Base_table_column_ip_spamlist' => 'Spam list', + 'Base_table_column_ip_spamlist_tooltip' => 'Someone may have utilized this IP address to exhibit unwanted activity before at other web services.', + 'Base_table_column_ip_blacklist' => 'Blacklisted', + 'Base_table_column_ip_blacklist_tooltip' => 'Whether IP addresses are in the blacklist.', + 'Base_table_column_ip_shared' => 'Shared IP', + 'Base_table_column_ip_shared_tooltip' => 'Multiple users detected on the same IP address.', + 'Base_table_column_ip_apple_relay' => 'Apple relay', + 'Base_table_column_ip_apple_relay_tooltip' => 'IP addresses belong to iCloud Private Relay, part of an iCloud+ subscription.', + 'Base_table_column_ip_tor' => 'TOR', + 'Base_table_column_ip_tor_tooltip' => 'IP addresses are assigned to The Onion Router network. Very few people use TOR, mainly used for anonymization and accessing censored resources.', + 'Base_table_column_ip_vpn' => 'VPN', + 'Base_table_column_ip_vpn_tooltip' => 'IP addresses are used to hide user\'s real location or to bypass regional blocking.', + 'Base_table_column_ip_datacenter' => 'Datacenter', + 'Base_table_column_ip_datacenter_tooltip' => 'IP addresses belong to ISP datacenter, which highly suggests the use of a VPN, script, or privacy software.', + 'Base_table_column_ip_country_tooltip' => 'Country identified by IP address.', + + 'Base_table_column_country' => 'Country', + 'Base_table_column_country_tooltip' => 'An originating country of user request, geolocated by IP address.', + 'Base_table_column_country_tooltip_countries' => 'A country name.', + 'Base_table_column_country_code' => 'Code', + 'Base_table_column_country_code_tooltip' => 'A two-letter country code.', + + 'Base_table_column_code' => 'Code', + 'Base_table_column_code_tooltip' => 'An HTTP response status code.', + + 'Base_table_column_lastseen' => 'Last seen', + 'Base_table_column_lastseen_tooltip' => 'The date and time of the last event.', + 'Base_table_column_lastseen_tooltip_users' => 'The date and time of the last user request.', + + 'Base_table_column_added_to_review' => 'Added to review', + 'Base_table_column_added_to_review_tooltip' => 'The date and time of directing user to review queue.', + + 'Base_table_column_asn' => 'ASN', + 'Base_table_column_asn_tooltip' => 'An autonomous system number. Identifies a detected network.', + 'Base_table_column_netname' => 'Network operator', + 'Base_table_column_netname_tooltip' => 'An organization that provides network services.', + 'Base_table_column_device_name' => 'Type', + 'Base_table_column_device_name_tooltip' => 'A device type.', + 'Base_table_column_device_lang' => 'Language', + 'Base_table_column_device_lang_tooltip' => 'A detected preferred language.', + 'Base_table_column_device_created' => 'First seen', + 'Base_table_column_device_created_tooltip' => 'The date of device registration', + 'Base_table_column_modified' => 'Modified', + 'Base_table_column_modified_tooltip' => 'Whether a user agent string is identified as altered by a user.', + + 'Base_table_column_browser' => 'Browser', + 'Base_table_column_browser_tooltip' => 'An identified browser.', + 'Base_table_column_os' => 'OS', + 'Base_table_column_os_tooltip' => 'An operating system of a device.', + 'Base_table_column_device_id' => 'ID', + 'Base_table_column_device_id_tooltip' => 'A device identifier.', + 'Base_table_column_device' => 'Device', + 'Base_table_column_device_tooltip' => 'A device type and operating system, based on a user agent.', + + 'Base_table_column_email_spamlist' => 'Spam list', + 'Base_table_column_email_spamlist_tooltip' => 'Whether an email address is on a spam list.', + 'Base_table_column_email_blacklist' => 'Blacklisted', + 'Base_table_column_email_blacklist_tooltip' => 'Whether an email address is on a blacklist.', + 'Base_table_column_email_reputation' => 'Reputation', + 'Base_table_column_email_reputation_tooltip' => 'An email’s trust level, evaluated based on all available signals. The higher the number of warning signals, the lower the reputation of an email address.', + 'Base_table_column_email_free_provider' => 'Free provider', + 'Base_table_column_email_free_provider_tooltip' => 'Whether an email address belongs to a provider that offers free accounts.', + 'Base_table_column_email_noprofiles' => 'No profiles', + 'Base_table_column_email_noprofiles_tooltip' => 'The absence of online profiles associated with an email address may be a sign of a throwaway email. An email address with a good reputation is normally connected to digital platforms.', + 'Base_table_column_email_nodatabreach' => 'No breach', + 'Base_table_column_email_nodatabreach_tooltip' => 'The absence of a history of data breaches may be a sign of a throwaway email address.', + 'Base_table_column_email_total_breaches' => 'Total breaches', + 'Base_table_column_email_total_breaches_tooltip' => 'Number of data breaches where email has been compromised.', + 'Base_table_column_email_data_lacking' => 'Data lacking', + 'Base_table_column_email_data_lacking_tooltip' => 'The absence of a history of data breaches or online profiles may be a sign of a throwaway email address.', + 'Base_table_column_email_disposable' => 'Disposable', + 'Base_table_column_email_disposable_tooltip' => 'An email address is provided by a mail server with a low reputation.', + 'Base_table_column_email_earliest_breach' => 'Earliest breach', + 'Base_table_column_email_earliest_breach_tooltip' => 'The absence of a history of data breaches may be a sign of a throwaway email address.', + + 'Base_table_column_user_phone' => 'Phone', + 'Base_table_column_user_phone_tooltip' => 'A user’s phone number.', + + 'Base_table_column_phone_country_tooltip' => 'Country identified by a phone number.', + 'Base_table_column_phone_carrier_name' => 'Carrier', + 'Base_table_column_phone_carrier_name_tooltip' => 'A telecommunications company name.', + 'Base_table_column_phone_type' => 'Type', + 'Base_table_column_phone_type_tooltip' => 'A type of the phone service.', + 'Base_table_column_phone_blacklist' => 'Blacklisted', + 'Base_table_column_phone_blacklist_tooltip' => 'Whether a phone number is on a blacklist.', + 'Base_table_column_phone_invalid' => 'Invalid', + 'Base_table_column_phone_invalid_tooltip' => 'Whether a phone number format is invalid.', + + 'Base_table_column_isps_name' => 'Network operator', + 'Base_table_column_isps_name_tooltip' => 'A company name.', + + 'Base_table_column_domain' => 'Domain', + 'Base_table_column_domain_tooltip' => 'A domain of email address(es).', + 'Base_table_column_free_email_provider' => 'Free', + 'Base_table_column_free_email_provider_tooltip' => 'Whether a domain belongs to a provider that offers free accounts.', + 'Base_table_column_tranco_rank' => 'Ranking', + 'Base_table_column_tranco_rank_tooltip' => 'Popularity of a domain name.', + 'Base_table_column_unavailable' => 'Unavailable', + 'Base_table_column_unavailable_tooltip' => 'Whether a domain’s website is inactive. This can be a sign of a fake mailbox.', + 'Base_table_column_disposable' => 'Disposable', + 'Base_table_column_disposable_tooltip' => 'Whether a domain is identified as associated with throwaway email addresses.', + 'Base_table_column_domain_registered' => 'Creation', + 'Base_table_column_domain_registered_tooltip' => 'The date of a domain creation.', + 'Base_table_column_domain_expires' => 'Expires on', + 'Base_table_column_domain_expires_tooltip' => 'The date of a domain expiration.', + 'Base_table_column_domain_total_accounts' => 'Total accounts', + 'Base_table_column_domain_total_accounts_tooltip' => 'The total number of users that have an email with current domain.', + 'Base_table_column_domain_spamlist' => 'Spam list', + 'Base_table_column_domain_spamlist_tooltip' => 'Whether a domain name is on a spam list.', + + 'Base_table_column_reviewed_status' => 'Review status', + 'Base_table_column_reviewed_status_short' => 'Status', + 'Base_table_column_reviewed_status_tooltip' => 'A user can be blacklisted or whitelisted. For putting a user on a list, open a user page (by clicking on a table row) and then use the corresponding button in the upper-right corner of the interface.', + 'Base_table_column_action_buttons' => 'Review status', + 'Base_table_column_total_fraud_users' => 'Blacklisted', + 'Base_table_column_total_fraud_users_tooltip' => 'The total number of blacklisted users.', + 'Base_table_column_latest_decision' => 'Latest decision', + 'Base_table_column_latest_decision_tooltip' => 'The date of the last user review.', + + 'Base_table_column_isps_description' => 'Description', + 'Base_table_column_isps_description_tooltip' => 'An ISP description.', + + 'Base_table_column_global_alert' => 'Global alert', + 'Base_table_column_global_alert_tooltip' => 'Is on alert list.', + + 'Base_table_column_error_type' => 'Status', + 'Base_table_column_error_type_tooltip' => 'A request processing status.', + 'Base_table_column_error_text' => 'Warning message', + 'Base_table_column_error_text_tooltip' => 'An error message returned by an unsuccessful request processing.', + 'Base_table_column_local_timestamp' => 'Local timestamp', + 'Base_table_column_local_timestamp_tooltip' => 'Server time when API request was received.', + + 'Base_table_column_audit_trail_created' => 'Date', + 'Base_table_column_audit_trail_created_tooltip' => 'The date the field was changed.', + 'Base_table_column_audit_trail_field' => 'Field', + 'Base_table_column_audit_trail_field_tooltip' => 'The name of the field that has been changed.', + 'Base_table_column_audit_trail_old_value' => 'Old value', + 'Base_table_column_audit_trail_old_value_tooltip' => 'Previous value of the field.', + 'Base_table_column_audit_trail_new_value' => 'New value', + 'Base_table_column_audit_trail_new_value_tooltip' => 'Updated value of the field.', + 'Base_table_column_audit_trail_parent' => 'Parent ID', + 'Base_table_column_audit_trail_parent_tooltip' => 'ID of the parent record related to the field change.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Blacklist.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Blacklist.php new file mode 100644 index 0000000..bbb4f12 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Blacklist.php @@ -0,0 +1,31 @@ + 'Blacklist', + 'AdminBlacklist_breadcrumb_title' => 'Blacklist', + 'AdminBlacklist_search_placeholder' => 'User Name, Identity and Added', + 'AdminBlacklist_entity_type_search_placeholder' => '+ Add another entity type', + 'AdminBlacklst_table_title' => 'Blacklist', + 'AdminBlacklst_table_title_tooltip' => 'This page lists user identities that were added to a blacklist during a selected period of time. The chart visualizes the daily number of blacklisted identities. To open a page with extended user analytics, click on a table row.', + 'Base_table_column_blacklist_added' => 'Added', + 'Base_table_column_blacklist_added_tooltip' => 'The date and time an identity was blacklisted.', + 'Base_table_column_blacklist_type' => 'Type', + 'Base_table_column_blacklist_type_tooltip' => 'A type of identity.', + 'Base_table_column_blacklist_value' => 'Identity', + 'Base_table_column_blacklist_value_tooltip' => 'A blacklisted identity.', + 'Base_table_column_blacklist_actions' => 'Action', + 'Base_table_column_blacklist_actions_tooltip' => 'Remove identity from a blacklist.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Bot.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Bot.php new file mode 100644 index 0000000..6702905 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Bot.php @@ -0,0 +1,19 @@ + 'Bot', + 'AdminBot_breadcrumb_title' => 'Bot', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Bots.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Bots.php new file mode 100644 index 0000000..9969377 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Bots.php @@ -0,0 +1,22 @@ + 'Bots', + 'AdminBots_breadcrumb_title' => 'Bots', + 'AdminBots_search_placeholder' => 'OS or User-Agent', + 'AdminBots_table_title' => 'Bots', + 'AdminBots_table_title_tooltip' => 'This page provides device information identified based on the requests’ user agents. The data is shown for a selected period of time. To view more information about a device, click on a table row. The chart illustrates the daily number of active devices, categorized as desktop or mobile, with bots being distinguished in a separate category.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/ChangeEmail.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/ChangeEmail.php new file mode 100644 index 0000000..0a9bad9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/ChangeEmail.php @@ -0,0 +1,21 @@ + 'Change Email', + 'ChangeEmail_page_title' => 'Change Email', + 'ChangeEmail_renew_email_subject' => 'Change your email address', + 'ChangeEmail_renew_email_body' => 'Thanks for your request. We have prepared a link where you can change your email. %s Please ignore this message if you don\'t want to change email address associated with your account.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Countries.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Countries.php new file mode 100644 index 0000000..896f20c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Countries.php @@ -0,0 +1,23 @@ + 'Countries', + 'AdminCountries_breadcrumb_title' => 'Countries', + 'AdminCountries_search_placeholder' => 'Country', + + 'AdminCountries_map_title' => 'Countries', + 'AdminCountries_map_title_tooltip' => 'This page outputs location-related information, as identified by IP addresses. The data is shown for a selected period of time. To open a page with detailed information on a particular country, click on a table row. The map visualizes the geolocated countries and the number of users from each country.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Country.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Country.php new file mode 100644 index 0000000..2e2f59d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Country.php @@ -0,0 +1,27 @@ + 'Country', + 'AdminCountry_breadcrumb_title' => 'Country', + + 'AdminCountry_counters_total_users' => 'User count', + 'AdminCountry_counters_total_ips' => 'IP count', + 'AdminCountry_counters_total_events' => 'Event count', + + 'AdminCountry_counters_total_users_tooltip' => 'The number of users detected as making requests from the country.', + 'AdminCountry_counters_total_ips_tooltip' => 'The number of IP addresses geolocated to the country.', + 'AdminCountry_counters_total_events_tooltip' => 'The number of events reported for the country.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Devices.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Devices.php new file mode 100644 index 0000000..051bd01 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Devices.php @@ -0,0 +1,23 @@ + 'Devices', + 'AdminDevices_breadcrumb_title' => 'Devices', + 'AdminDevices_search_placeholder' => 'Device Type, OS, Browser or User Agent', + + 'AdminDevices_table_title' => 'User agent', + 'AdminDevices_table_title_tooltip' => 'This page provides device information identified based on the requests’ user agents. The data is shown for a selected period of time. To view more information about a device, click on a table row. The chart illustrates the daily number of active devices, categorized as desktop or mobile, with bots being distinguished in a separate category.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Domain.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Domain.php new file mode 100644 index 0000000..5f3493d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Domain.php @@ -0,0 +1,39 @@ + 'Domain', + 'AdminDomain_breadcrumb_title' => 'Domain', + 'AdminDomain_table_title' => 'Domains', + + 'AdminDomain_registration_date' => 'Creation date', + 'AdminDomain_registration_date_tooltip' => 'The date of the domain creation.', + 'AdminDomain_expiration_date' => 'Expiration date', + 'AdminDomain_expiration_date_tooltip' => 'The date of the domain expiration.', + 'AdminDomain_counters_accounts' => 'User count', + 'AdminDomain_counters_accounts_tooltip' => 'The number of users with email addresses belonging to the domain.', + 'AdminDomain_counters_free_email_provider' => 'Free provider', + 'AdminDomain_counters_free_email_provider_tooltip' => 'Whether the domain belongs to a provider that offers free accounts.', + 'AdminDomain_counters_tranco_rank' => 'Web ranking', + 'AdminDomain_counters_tranco_rank_tooltip' => 'Popularity of the domain name.', + 'AdminDomain_counters_unavailable' => 'Unavailable', + 'AdminDomain_counters_unavailable_tooltip' => 'Whether the domain’s website is inactive. This can be a sign of a fake mailbox.', + 'AdminDomain_counters_disposable' => 'Disposable', + 'AdminDomain_counters_disposable_tooltip' => 'Whether a domain is identified as associated with throwaway email addresses.', + 'AdminDomain_counters_fraud' => 'Blacklisted', + 'AdminDomain_counters_fraud_tooltip' => 'The number of blacklisted users with email at this domain.', + 'AdminDomains_table_same_ip' => 'Domain neighbours', + 'AdminDomains_table_same_ip_tooltip' => 'Domain hosting on the same IP address as current domain.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Domains.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Domains.php new file mode 100644 index 0000000..987116f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Domains.php @@ -0,0 +1,23 @@ + 'Domains', + 'AdminDomains_breadcrumb_title' => 'Domains', + 'AdminDomains_search_placeholder' => 'Domain or Created', + + 'AdminDomains_table_title' => 'Domains', + 'AdminDomains_table_title_tooltip' => 'This page provides analytics grouped by a domain of email addresses. The data is shown for a selected period of time. The chart displays the daily number of unique and newly reported domains. To open a page with more information about a particular email domain, click on a table row.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Email.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Email.php new file mode 100644 index 0000000..185ce4d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Email.php @@ -0,0 +1,24 @@ + 'Email', + 'AdminEmail_breadcrumb_title' => 'Email', + 'AdminEmail_reputation' => 'Reputation', + 'AdminEmail_noprofiles' => 'No profiles', + 'AdminEmail_nobreach' => 'No breach', + 'AdminEmail_free_provider' => 'Free provider', + 'AdminEmail_disposable' => 'Disposable', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Emails.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Emails.php new file mode 100644 index 0000000..841ad51 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Emails.php @@ -0,0 +1,22 @@ + 'Emails', + 'AdminEmails_breadcrumb_title' => 'Emails', + 'AdminEmails_table_title' => 'Emails', + 'AdminEmails_table_title_tooltip' => 'A list of email addresses linked to the user.', + 'AdminEmails_search_placeholder' => 'Search placeholder for emails', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Events.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Events.php new file mode 100644 index 0000000..a200945 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Events.php @@ -0,0 +1,25 @@ + 'Events', + 'AdminEvents_breadcrumb_title' => 'Events', + 'AdminEvents_search_placeholder' => 'User ID, Timestamp, IP, HTTP Code', + 'AdminEvents_event_type_search_placeholder' => '+ Add another event type', + 'AdminEvents_device_type_search_placeholder' => '+ Add another device type', + + 'AdminEvents_table_title' => 'Events', + 'AdminEvents_table_title_tooltip' => 'This page lists events recorded during a selected period of time. To see extended event details, click on a table row. The chart shows the number of events recorded each day.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/FieldAuditTrail.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/FieldAuditTrail.php new file mode 100644 index 0000000..ebc56c1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/FieldAuditTrail.php @@ -0,0 +1,19 @@ + 'Field audit trail', + 'AdminFieldAuditTrail_table_title_tooltip' => 'Track modifications by users to important fields, including what changed and when.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/ForgotPassword.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/ForgotPassword.php new file mode 100644 index 0000000..fb364d1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/ForgotPassword.php @@ -0,0 +1,27 @@ + 'Reset your password', + 'ForgotPassword_form_title' => 'Reset your password', + 'ForgotPassword_form_email_label' => 'Email address', + 'ForgotPassword_form_reset_button' => 'Reset', + 'ForgotPassword_form_create_account_link' => 'Create an account', + 'ForgotPassword_form_login_link' => 'Sign in', + 'ForgotPassword_form_renew_button' => 'Reset', + + 'ForgotPassowrd_renew_password_subject' => 'Reset your password', + 'ForgotPassowrd_renew_password_body' => 'Thanks for your request. We have prepared a link where you can reset your password. %s Please ignore this message if you didn\'t reset password.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Ip.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Ip.php new file mode 100644 index 0000000..213aa05 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Ip.php @@ -0,0 +1,36 @@ + 'IP', + 'AdminIp_breadcrumb_title' => 'IP', + 'AdminIp_ips_table_by_cidr_title' => 'Nearby IP addresses', + + 'AdminIp_counters_country' => 'Country', + 'AdminIp_counters_country_tooltip' => 'A country geolocated by the IP address.', + 'AdminIp_counters_asn' => 'ASN', + 'AdminIp_counters_blocklist' => 'Spam list', + 'AdminIp_counters_blocklist_tooltip' => 'Someone may have utilized this IP address to exhibit unwanted activity before at other web services.', + 'AdminIp_counters_blacklist' => 'Blacklisted', + 'AdminIp_counters_blacklist_tooltip' => 'Whether this IP address is in the blacklist.', + 'AdminIp_counters_datacenter' => 'Datacenter', + 'AdminIp_counters_datacenter_tooltip' => 'This IP address belongs to ISP datacenter, which highly suggests the use of a VPN, script, or privacy software.', + 'AdminIp_counters_vpn' => 'VPN', + 'AdminIp_counters_vpn_tooltip' => 'This IP address is used to hide user\'s real location or to bypass regional blocking.', + 'AdminIp_counters_tor' => 'TOR', + 'AdminIp_counters_tor_tooltip' => 'IP address is assigned to The Onion Router network. Very few people use TOR, mainly used for anonymization and accessing censored resources.', + 'AdminIp_counters_apple_relay' => 'Apple Relay', + 'AdminIp_counters_apple_relay_tooltip' => 'IP address belongs to iCloud Private Relay, part of an iCloud+ subscription.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Ips.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Ips.php new file mode 100644 index 0000000..e626fcf --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Ips.php @@ -0,0 +1,26 @@ + 'IPs', + 'AdminIps_breadcrumb_title' => 'IPs', + 'AdminIps_search_placeholder' => 'IP, ASN, Country or Network operator', + 'AdminUsers_ip_type_search_placeholder' => '+ Add another IP type', + + 'AdminIps_table_title' => 'IP addresses', + 'AdminIps_table_title_tooltip' => 'This page outputs information grouped by IP address. The data is shown for a selected period of time. To open a page with more details, click on a table row. The chart shows the number of the residential (considered safe) and non-residential (considered a warning signal) IP addresses from which the requests were made each day.', + + 'AdminIps_map_title' => 'Locations & connections', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Isp.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Isp.php new file mode 100644 index 0000000..c3eb7a9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Isp.php @@ -0,0 +1,31 @@ + 'ISP', + 'AdminIsp_breadcrumb_title' => 'ISP', + 'AdminIsp_table_title' => 'ISPs', + + 'AdminIsp_asn' => 'ASN', + + 'AdminIsp_counters_total_ips' => 'IP count', + 'AdminIsp_counters_total_ips_tooltip' => 'The number of reported IP addresses assigned to the ISP.', + 'AdminIsp_counters_visits' => 'Event count', + 'AdminIsp_counters_visits_tooltip' => 'The number of events reported for the ISP.', + 'AdminIsp_counters_accounts' => 'User count', + 'AdminIsp_counters_accounts_tooltip' => 'The number of users reported for the ISP.', + 'AdminIsp_counters_fraud' => 'Blacklisted', + 'AdminIsp_counters_fraud_tooltip' => 'The number of blacklisted users belonging to the ISP.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Isps.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Isps.php new file mode 100644 index 0000000..173d264 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Isps.php @@ -0,0 +1,22 @@ + 'ISPs', + 'AdminIsps_breadcrumb_title' => 'ISPs', + 'AdminIsps_search_placeholder' => 'ASN, Network operator or Description', + 'AdminIsps_table_title' => 'ISPs', + 'AdminIsps_table_title_tooltip' => 'This page displays analytics grouped by an internet service provider (ISP), as identified by IP address. The data is shown for a selected period of time. The chart displays the daily number of unique and newly reported active ISPs. To open a page with more details on a particular ISP, click on a table row.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Logbook.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Logbook.php new file mode 100644 index 0000000..e567422 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Logbook.php @@ -0,0 +1,24 @@ + 'Logbook', + 'AdminLogbook_breadcrumb_title' => 'Logbook', + 'AdminLogbook_table_title' => 'API requests', + 'AdminLogbook_table_title_tooltip' => 'This page lists the processing statuses of the recent API requests for troubleshooting purposes.', + 'AdminLogbook_search_placeholder' => 'Raw POST data, IP, Error, Timestamp', + 'AdminLogbook_column_ip' => 'Source IP', + 'AdminLogbook_column_ip_tooltip' => 'An IP address originated the request to /sensor/.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Login.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Login.php new file mode 100644 index 0000000..98879be --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Login.php @@ -0,0 +1,23 @@ + 'Log in', + 'Login_form_email_label' => 'Email address', + 'Login_form_password_label' => 'Password', + 'Login_form_signin_button' => 'Log in', + 'Login_form_create_account_link' => 'Create your account', + 'Login_form_forgot_password_link' => 'Forgot password?', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Logout.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Logout.php new file mode 100644 index 0000000..d4af124 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Logout.php @@ -0,0 +1,21 @@ + 'Sign out', + 'Logout_form_cancel_link' => 'Stay signed in', + 'Logout_text' => 'Are you sure that you want to sign out?', + 'Logout_form_submit' => 'Sign out', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/ManualCheck.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/ManualCheck.php new file mode 100644 index 0000000..0d33c6d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/ManualCheck.php @@ -0,0 +1,49 @@ + 'Manual check', + + 'AdminManualCheck_form_title' => 'Manual check', + 'AdminManualCheck_form_field_type_label' => 'Type', + 'AdminManualCheck_form_types' => [ + 'ip' => 'IP', + 'email' => 'Email', + 'domain' => 'Domain', + 'phone' => 'Phone', + ], + 'AdminManualCheck_form_field_search_query_label' => 'Search query', + 'AdminManualCheck_form_button_search' => 'Search', + + 'AdminManualCheck_result_title' => '%s result', + + 'AdminManualCheck_key_overwrites' => [ + 'ip' => 'IP', + 'email' => 'Email', + 'domain' => 'Domain', + 'phone' => 'Phone', + 'geo_ip' => 'Geo IP', + 'geo_html' => 'Geo HTML', + 'iso_country_code' => 'ISO country code', + 'asn' => 'ASN', + 'tor' => 'TOR', + 'vpn' => 'VPN', + 'mx_record' => 'MX record', + 'domains_count' => 'Domains hosting', + 'cidr' => 'CIDR', + ], + + 'AdminManualCheck_history_title' => 'History', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/PasswordRecovering.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/PasswordRecovering.php new file mode 100644 index 0000000..b2baa5f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/PasswordRecovering.php @@ -0,0 +1,22 @@ + 'Renew password', + 'PasswordRecovering_form_title' => 'Renew password', + 'PasswordRecovering_form_newPassword_label' => 'New Password', + 'PasswordRecovering_form_confirmPassword_label' => 'Confirm Password', + 'PasswordRecovering_form_renew_button' => 'Renew', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Phone.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Phone.php new file mode 100644 index 0000000..037d0cd --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Phone.php @@ -0,0 +1,23 @@ + 'Phone', + 'AdminPhone_breadcrumb_title' => 'Phone', + 'AdminPhone_type' => 'Type', + 'AdminPhone_profiles' => 'Profiles', + 'AdminPhone_carrier' => 'Carrier', + 'AdminPhone_blocklist' => 'Spam list', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Phones.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Phones.php new file mode 100644 index 0000000..15cce78 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Phones.php @@ -0,0 +1,22 @@ + 'Phones', + 'AdminPhones_breadcrumb_title' => 'Phones', + 'AdminPhones_table_title' => 'Phones', + 'AdminPhones_table_title_tooltip' => 'A list of phone numbers linked to the user.', + 'AdminPhones_search_placeholder' => 'Search placeholder for phones', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Profile.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Profile.php new file mode 100644 index 0000000..016b5ec --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Profile.php @@ -0,0 +1,43 @@ + 'Profile', + 'AdminProfile_breadcrumb_title' => 'Profile', + 'AdminProfile_success_message' => 'Profile has been successfully updated.', + + 'AdminProfile_form_title' => 'Profile', + 'AdminProfile_form_title_tooltip' => 'Update your profile information.', + 'AdminProfile_form_button_save' => 'Update', + + 'AdminProfile_form_field_firstname_label' => 'First name', + 'AdminProfile_form_field_firstname_placeholder' => 'John', + 'AdminProfile_form_field_lastname_label' => 'Last name', + 'AdminProfile_form_field_lastname_placeholder' => 'Smith', + 'AdminProfile_form_field_country_label' => 'Country', + 'AdminProfile_form_field_country_placeholder' => 'Country', + 'AdminProfile_form_field_street_label' => 'Street', + 'AdminProfile_form_field_street_placeholder' => 'Street address', + 'AdminProfile_form_field_city_label' => 'City', + 'AdminProfile_form_field_city_placeholder' => 'City', + 'AdminProfile_form_field_state_label' => 'State', + 'AdminProfile_form_field_state_placeholder' => 'State', + 'AdminProfile_form_field_zip_label' => 'Postcode', + 'AdminProfile_form_field_zip_placeholder' => 'Postal code', + 'AdminProfile_form_field_company_label' => 'Company', + 'AdminProfile_form_field_company_placeholder' => 'Company name', + 'AdminProfile_form_field_vat_label' => 'VAT', + 'AdminProfile_form_field_vat_placeholder' => 'VAT number', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Resource.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Resource.php new file mode 100644 index 0000000..0effe5c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Resource.php @@ -0,0 +1,29 @@ + 'Resource', + 'AdminResource_breadcrumb_title' => 'Resource', + + 'AdminResource_counters_total_users' => 'User count', + 'AdminResource_counters_total_countries' => 'Country count', + 'AdminResource_counters_total_ips' => 'IP count', + 'AdminResource_counters_total_events' => 'Event count', + + 'AdminResource_counters_total_users_tooltip' => 'The number of users that interacted with the resource.', + 'AdminResource_counters_total_countries_tooltip' => 'The number of countries from which the resource was requested.', + 'AdminResource_counters_total_ips_tooltip' => 'The number of IP addresses from which the resource was requested.', + 'AdminResource_counters_total_events_tooltip' => 'The number of events reported for the resource.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Resources.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Resources.php new file mode 100644 index 0000000..515d9b6 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Resources.php @@ -0,0 +1,22 @@ + 'Resources', + 'AdminResources_breadcrumb_title' => 'Resources', + 'AdminResources_table_title' => 'Resources', + 'AdminResources_table_title_tooltip' => 'This page allows reviewing user activity grouped by a requested resource. The data is shown for a selected period of time. To open a page with extended information on a resource, click on a table row. The chart presents HTTP response status codes user requests resulted in. Namely, it displays the number of OK (200), Not Found (404), and Forbidden (403) with Internal Server Error (500) responses returned each day.', + 'AdminResources_search_placeholder' => 'URL or Title', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/RetentionPolicy.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/RetentionPolicy.php new file mode 100644 index 0000000..246084c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/RetentionPolicy.php @@ -0,0 +1,24 @@ + 'Data retention', + 'AdminRetentionPolicy_form_title_tooltip' => 'Configure the maximum duration of the recorded information storage.', + 'AdminRetentionPolicy_form_button_save' => 'Update', + + 'AdminRetentionPolicy_form_field_policy_label' => 'Retention period', + 'AdminRetentionPolicy_form_field_policy_warning' => 'Caution! Reducing the data retention period will result in the removal of all data belonging to users who haven’t logged in beyond the updated retention period.', + 'AdminRetentionPolicy_changeTimeZone_success_message' => 'Data retention period has been changed successfully.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/ReviewQueue.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/ReviewQueue.php new file mode 100644 index 0000000..44abe92 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/ReviewQueue.php @@ -0,0 +1,24 @@ + 'Review queue', + 'AdminReviewQueue_breadcrumb_title' => 'Review queue', + + 'AdminReviewQueue_search_placeholder' => 'User Name, Last seen and Signup date', + + 'AdminReviewQueue_table_title' => 'Users for review', + 'AdminReviewQueue_table_title_tooltip' => 'This page lists users with low trust scores and facilitates their removal from the queue by performing a review process. To open a page with more information on a user, click on an email address. The chart shows the number of users added to the review queue each day during a selected period of time.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Rules.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Rules.php new file mode 100644 index 0000000..5de6fac --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Rules.php @@ -0,0 +1,55 @@ + 'Rules', + 'AdminRules_breadcrumb_title' => 'Rules', + 'AdminRules_search_placeholder' => 'Rule Code, Type or Description', + + 'AdminRules_table_title' => 'Rules', + 'AdminRules_table_title_tooltip' => 'This page lists conditions (rules) that can be utilized by the rules engine, which is responsible for the user trust score calculations. The page also provides a way for manually triggering a rule and getting a list of users matching it.', + + 'AdminRules_table_header_code' => 'Code', + 'AdminRules_table_header_code_tooltip' => 'A rule’s code identifier.', + 'AdminRules_table_header_group' => 'Type', + 'AdminRules_table_header_group_tooltip' => 'A group to which a rule belongs.', + 'AdminRules_table_header_description' => 'Description', + 'AdminRules_table_header_description_tooltip' => 'Description of a rule.', + 'AdminRules_table_header_proportion' => 'Match rate', + 'AdminRules_table_header_proportion_tooltip' => 'The proportion between users matching the rule and all users.', + 'AdminRules_table_header_weight' => 'Weight', + 'AdminRules_table_header_weight_tooltip' => 'To enable the processing of a rule by the rules engine, set the weight value. The higher the rule’s weight, the more it influences a calculated user trust score. To save an adjusted weight value, click the red button shown on the right side.', + 'AdminRules_table_header_users' => 'Action', + 'AdminRules_table_header_users_tooltip' => 'Get a list of users matching the rule by clicking a green button; the result will be shown below the rule’s definition. When a weight value is changed, this column outputs a red button for saving an adjusted value.', + + 'AdminRules_weight_minus20' => 'Positive', + 'AdminRules_weight_0' => 'None', + 'AdminRules_weight_10' => 'Medium', + 'AdminRules_weight_20' => 'High', + 'AdminRules_weight_70' => 'Extreme', + + 'AdminRules_reload_rules' => 'Refresh', + 'AdminRules_reload_rules_warning' => 'Click to upload new local rules.', + + 'AdminThresholdValues_form_title' => 'Thresholds settings', + 'AdminThresholdValues_form_title_tooltip' => 'Manage and set thresholds for review queue and automated user blacklisting.', + 'AdminThresholdValues_form_field_warning' => 'Set the threshold for user scores to trigger manual review or automatic blacklisting. Use auto-blacklisting with caution and only when truly necessary, as it could negatively impact the user experience due to potential misconfigurations.', + 'AdminThresholdValues_form_field_blacklist_threshold_label' => 'Auto-block', + 'AdminThresholdValues_form_field_review_queue_threshold_label' => 'Manual review queue', + 'AdminThresholdValues_form_button_save' => 'Update', + 'AdminThresholdValues_form_value_prefix' => 'Score below', + 'AdminThresholdValues_form_value_zero_prefix' => 'Score is', + 'AdminThresholdValues_update_success_message' => 'Thresholds updated successfully', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Settings.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Settings.php new file mode 100644 index 0000000..b9cd0ad --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Settings.php @@ -0,0 +1,69 @@ + 'Settings', + 'AdminSettings_breadcrumb_title' => 'Settings', + + 'AdminSettings_changePassword_form_title' => 'Password', + 'AdminSettings_changePassword_form_title_tooltip' => 'Change your account password here. Use a strong password to prevent unauthorized access.', + 'AdminSettings_changePassword_form_field_currentPassword_label' => 'Current password', + 'AdminSettings_changePassword_form_field_currentPassword_placeholder' => 'Enter current password', + 'AdminSettings_changePassword_form_field_newPassword_label' => 'New password', + 'AdminSettings_changePassword_form_field_newPassword_placeholder' => 'Enter new password', + 'AdminSettings_changePassword_form_field_passwordConfirmation_label' => 'Confirm new password', + 'AdminSettings_changePassword_form_field_passwordConfirmation_placeholder' => 'Re-enter new password', + 'AdminSettings_changePassword_form_button_save' => 'Save', + 'AdminSettings_changePassword_success_message' => 'Your password has been successfully changed.', + + 'AdminSettings_changeEmail_form_title' => 'Change email address', + 'AdminSettings_changeEmail_form_title_tooltip' => 'Change the email address for your account here. A message with instructions on how to complete the change will be sent to the new email address.', + 'AdminSettings_changeEmail_form_field_email_label' => 'Email address', + 'AdminSettings_changeEmail_form_field_email_placeholder' => 'New email address', + 'AdminSettings_changeEmail_form_button_save' => 'Change email', + 'AdminSettings_changeEmail_success_message' => 'We have emailed you instructions on how to complete your email address change.', + 'AdminSettings_changeEmail_approval_pending' => 'Confirm your new email address (%s) in order to complete your email address change.', + + 'AdminSettings_form_closeAccount_title' => 'Delete account', + 'AdminSettings_form_closeAccount_confirmationMessage' => 'If you wish to permanently delete this account and all its associated data, including but not limited to users, IP addresses and events, click the button below.', + 'AdminSettings_closeAccount_form_button_save' => 'Delete this account', + 'AdminSettings_closeAccount_success_message' => 'Your account has been successfully deleted and you are unable to use it anymore.', + + 'AdminSettings_checkUpdates_form_title' => 'Check for updates', + 'AdminSettings_form_checkUpdates_confirmationMessage' => 'Periodically, tirreno releases updates which can include application updates and important security patches.', + 'AdminSettings_form_checkUpdates_currentVerision' => 'Current version: ', + 'AdminSettings_checkUpdates_form_button' => 'Check', + + 'AdminSettings_notificationPreferences_title' => 'Review queue notifications', + 'AdminSettings_notificationPreferences_title_tooltip' => 'Select how frequently email notifications should be sent.', + 'AdminSettings_notificationPreferences_reviewReminderFrequency_label' => 'Sending', + 'AdminSettings_notificationPreferences_reviewReminderFrequency_options' => [ + 'daily' => 'Daily', + 'weekly' => 'Weekly', + 'off' => 'Off', + ], + 'AdminSettings_notificationPreferences_button_save' => 'Save', + 'AdminSettings_notificationPreferences_success_message' => 'Your notification preferences have been successfully updated.', + + 'AdminSettings_delete_account_warning_message_par1' => 'Please note that if you choose to delete your account, you will immediately lose access, and your data will be permanently deleted, ' + . 'as outlined in our terms of service. We are unable to offer pro-rata refunds for any remaining subscription period.', + 'AdminSettings_delete_account_warning_message_par2' => 'Alternatively, if you wish to pause your subscription without permanently deleting your account, you can cancel it instead. ' + . 'Upon cancellation, you will immediately lose access, but we will securely store your account data for one year before automatic deletion. ' + . 'You can reactivate your account at any time within one year of cancellation.', + + + 'AdminSettings_submit_account_deletion_button' => 'Confirm account deletion', + 'AdminSettings_account_deletion_warning_header' => 'Permanent account deletion', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Signup.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Signup.php new file mode 100644 index 0000000..c3f9ef1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Signup.php @@ -0,0 +1,27 @@ + 'Create an account', + 'Signup_form_title' => 'Create an account', + 'Signup_form_email_label' => 'Email address', + 'Signup_form_password_label' => 'Password', + 'Signup_form_signin_button' => 'Continue', + 'Signup_form_create_account_question' => 'Already have an account?', + 'Signup_form_create_account_link' => 'Log in', + + 'Signup_activation_email_subject' => 'Activate your acount', + 'Signup_activation_email_body' => 'Thanks for your registration. Please activate your account and start using the system. %s', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/TimeZone.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/TimeZone.php new file mode 100644 index 0000000..08340fa --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/TimeZone.php @@ -0,0 +1,23 @@ + 'Time zone', + 'AdminTimeZone_form_title_tooltip' => 'Select the server’s time zone.', + 'AdminTimeZone_form_button_save' => 'Update', + + 'AdminTimeZone_form_field_timezone_label' => 'City', + 'AdminTimeZone_changeTimeZone_success_message' => 'Time zone has been successfully updated.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/User.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/User.php new file mode 100644 index 0000000..990421a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/User.php @@ -0,0 +1,50 @@ + 'Events', + 'AdminUser_breadcrumb_title' => 'Events', + + 'AdminUser_widgets_id' => 'User', + 'AdminUser_widgets_id_tooltip' => 'Basic user account information.', + 'AdminUser_widgets_ips_warning' => 'IP addresses', + 'AdminUser_widgets_ips_warning_tooltip' => 'A list of warning signals based on IP addresses linked to the user.', + 'AdminUser_widgets_email' => 'Email', + 'AdminUser_widgets_email_tooltip' => 'A list of warning signals based on email addresses linked to the user.', + 'AdminUser_widgets_domain' => 'Domain', + 'AdminUser_widgets_domain_tooltip' => 'A list of warning signals based on email domains linked to the user.', + 'AdminUser_widgets_phone' => 'Phone', + 'AdminUser_widgets_phone_tooltip' => 'Phone.', + + 'AdminUser_counters_total_new_devices' => 'New devices per day', + 'AdminUser_counters_total_new_devices_tooltip' => 'Total new devices over user\'s sessions per day.', + 'AdminUser_counters_total_new_ips' => 'New IPs per day', + 'AdminUser_counters_total_new_ips_tooltip' => 'Total new IPs over user\'s sessions per day.', + 'AdminUser_counters_total_events_max' => 'Events per session', + 'AdminUser_counters_total_events_max_tooltip' => 'Average total events over user\'s sessions per day.', + 'AdminUser_counters_total_sessions' => 'Sessions per day', + 'AdminUser_counters_total_sessions_tooltip' => 'Total user\'s sessions per day.', + + 'AdminUser_recalculate_risk_score_success_message' => 'User trust score was successfully recalculated.', + 'AdminUser_recalculate_risk_score_tooltip' => 'Recalculate trust score', + + 'AdminUser_remove_user_button' => 'Delete user', + 'AdminUser_scheduled_for_removal' => 'All information related to this user is scheduled for removal.', + + 'AdminUser_review_comment_placeholder' => 'There is no review for this user.', + + 'AdminPayload_table_title' => 'Payload', + 'AdminPayload_table_title_tooltip' => 'Payload', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Users.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Users.php new file mode 100644 index 0000000..586f15c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Users.php @@ -0,0 +1,30 @@ + 'Users', + 'AdminUsers_breadcrumb_title' => 'Users', + 'AdminUsers_search_placeholder' => 'User Name, ID and Signup Date', + 'AdminUsers_rules_search_placeholder' => '+ Add another rule', + 'AdminUsers_scores_range_search_placeholder' => '+ Add another score', + + 'AdminUsers_table_title' => 'Users', + 'AdminUsers_table_title_tooltip' => 'This page outputs basic information about users, including their trust scores. The data is shown for a selected period of time. To open a page with extended analytics, click on a table row. The chart displays the daily number of new visitors, grouped by their trust scores values.', + + 'AdminUsers_add_to_watchlist' => 'Add to Watchlist', + 'AdminUsers_remove_from_watchlist' => 'Remove from Watchlist', + 'AdminUsers_remove_reviewed_flag' => 'Not reviewed', + 'AdminUsers_auto_blocked' => 'Auto-blocked', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Watchlist.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Watchlist.php new file mode 100644 index 0000000..355130d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/Watchlist.php @@ -0,0 +1,19 @@ + 'Watchlist', + 'AdminWatchlist_breadcrumb_title' => 'Watchlist', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Pages/index.php @@ -0,0 +1,3 @@ + 'Event report', + 'Details_event_time' => 'Timestamp', + 'Details_event_type' => 'Event type', + 'Details_event_http_method' => 'HTTP method', + 'Details_event_http_code' => 'HTTP code', + 'Details_user_count' => 'User count', + 'Details_event_count' => 'Event count', + 'Details_payload' => 'Payload', + + //User + 'Details_user_id' => 'Trust score & email', + 'Details_accounttitle' => 'User ID', + 'Details_full_name' => 'Name', + 'Details_firstname' => 'First name', + 'Details_lastname' => 'Last name', + 'Details_reviewed_status' => 'Review status', + 'Details_latest_decision' => 'Latest decision', + 'Details_score_details' => 'User score details', + + //Phone + 'Phone_details_placeholder' => 'Phone details', + 'Details_phone' => 'Phone', + 'Details_phone_national' => 'National format', + 'Details_phone_country' => 'Country', + 'Details_carrier_name' => 'Carrier', + 'Details_phone_type' => 'Type', + 'Details_phone_invalid' => 'Invalid', + 'Details_phone_fraud_detected' => 'Blacklisted', + 'Details_phone_alert_list' => 'Global alert', + 'Details_shared_users' => 'Users with the same phone', + + //Email + 'Email_details_placeholder' => 'Email details', + 'Details_email' => 'Email', + 'Details_reputation' => 'Reputation', + 'Details_no_profiles' => 'No profiles', + 'Details_free_provider' => 'Free provider', + 'Details_no_data_breach' => 'No breach', + 'Details_data_breaches' => 'Total breaches', + 'Details_domain' => 'Domain', + 'Details_domain_unavailable' => 'Unavailable', + 'Details_tranco_rank' => 'Web ranking', + 'Details_blockdomain' => 'Spam list', + 'Details_disposable_domains' => 'Disposable', + 'Details_domain_creation_date' => 'Creation date', + 'Details_domain_expiration_date' => 'Expires on', + 'Details_blockemails' => 'Spam list', + 'Details_email_fraud_detected' => 'Blacklisted', + 'Details_email_earliest_breach' => 'Earliest breach', + 'Details_email_alert_list' => 'Global alert', + 'Details_domain_contact_email' => 'Domain contact email', + 'Details_earliest_breach' => 'Earliest breach', + 'Details_mx_record' => 'MX record missing', + 'Details_domain_return_code' => 'Domain return code', + 'Details_closest_snapshot' => 'Oldest mention', + + //Resource + 'Details_url' => 'URL', + 'Details_query' => 'Query', + 'Details_referer' => 'Referer', + + //IP + 'Details_ip' => 'IP address', + 'Details_cidr' => 'CIDR', + 'Details_country' => 'Country', + 'Details_netname' => 'Network operator', + 'Details_ip_type' => 'IP type', + 'Details_ip_spamlist' => 'Spam list', + 'Details_asn' => 'ASN', + 'Details_ip_alert_list' => 'Global alert', + + //Device + 'Device_details_placeholder' => 'Device details', + 'Details_device_id' => 'Device ID', + 'Details_device' => 'Device', + 'Details_ua_modified' => 'User agent modified', + 'Details_browser_name' => 'Browser', + 'Details_lang' => 'Language', + 'Details_os_name' => 'OS', + 'Details_ua' => 'User agent', + 'Details_device_created' => 'First seen', + + //Logbook + 'Details_local_timestamp' => 'Local timestamp', + 'Details_error_type' => 'Status', + 'Details_error_text' => 'Warning message', + 'Details_request' => 'Raw POST data', + 'Details_source_ip' => 'Source IP', + + //Enrichment + 'Details_calculation' => 'The list of entities and the number of records that require enrichment. Billing will be calculated based on API consumption rates.', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/LeftMenu.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/LeftMenu.php new file mode 100644 index 0000000..a0aeb9b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/LeftMenu.php @@ -0,0 +1,38 @@ + 'IP addresses', + 'LeftMenu_home_link' => 'Dashboard', + 'LeftMenu_users_link' => 'Users', + 'LeftMenu_not_reviewed_users_link' => 'Review queue', + 'LeftMenu_countries_link' => 'Countries', + 'LeftMenu_resources_link' => 'Resources', + 'LeftMenu_api_keys_link' => 'API', + 'LeftMenu_all_events_link' => 'Events', + 'LeftMenu_settings_link' => 'Settings', + 'LeftMenu_logout_link' => 'Sign out', + 'LeftMenu_watchlist_link' => 'Watchlist', + 'LeftMenu_emails_link' => 'Emails', + 'LeftMenu_phones_link' => 'Phones', + 'LeftMenu_manual_check_link' => 'Manual check', + 'LeftMenu_rules_link' => 'Rules', + 'LeftMenu_devices_link' => 'Devices', + 'LeftMenu_bots_link' => 'Bots', + 'LeftMenu_isps_link' => 'Networks', + 'LeftMenu_domains_link' => 'Domains', + 'LeftMenu_blacklist_link' => 'Blacklist', + 'LeftMenu_logbook_link' => 'Logbook', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/TimeZones.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/TimeZones.php new file mode 100644 index 0000000..92c5031 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/TimeZones.php @@ -0,0 +1,438 @@ + [ + 'Pacific/Midway' => 'Midway (UTC-11:00)', + 'Pacific/Niue' => 'Niue (UTC-11:00)', + 'Pacific/Pago_Pago' => 'Pago Pago (UTC-11:00)', + 'America/Adak' => 'Adak (UTC-10:00)', + 'Pacific/Honolulu' => 'Honolulu (UTC-10:00)', + 'Pacific/Johnston' => 'Johnston (UTC-10:00)', + 'Pacific/Rarotonga' => 'Rarotonga (UTC-10:00)', + 'Pacific/Tahiti' => 'Tahiti (UTC-10:00)', + 'Pacific/Marquesas' => 'Marquesas (UTC-09:30)', + 'America/Anchorage' => 'Anchorage (UTC-09:00)', + 'Pacific/Gambier' => 'Gambier (UTC-09:00)', + 'America/Juneau' => 'Juneau (UTC-09:00)', + 'America/Nome' => 'Nome (UTC-09:00)', + 'America/Sitka' => 'Sitka (UTC-09:00)', + 'America/Yakutat' => 'Yakutat (UTC-09:00)', + 'America/Dawson' => 'Dawson (UTC-08:00)', + 'America/Los_Angeles' => 'Los Angeles (UTC-08:00)', + 'America/Metlakatla' => 'Metlakatla (UTC-08:00)', + 'Pacific/Pitcairn' => 'Pitcairn (UTC-08:00)', + 'America/Santa_Isabel' => 'Santa Isabel (UTC-08:00)', + 'America/Tijuana' => 'Tijuana (UTC-08:00)', + 'America/Vancouver' => 'Vancouver (UTC-08:00)', + 'America/Whitehorse' => 'Whitehorse (UTC-08:00)', + 'America/Boise' => 'Boise (UTC-07:00)', + 'America/Cambridge_Bay' => 'Cambridge Bay (UTC-07:00)', + 'America/Chihuahua' => 'Chihuahua (UTC-07:00)', + 'America/Creston' => 'Creston (UTC-07:00)', + 'America/Dawson_Creek' => 'Dawson Creek (UTC-07:00)', + 'America/Denver' => 'Denver (UTC-07:00)', + 'America/Edmonton' => 'Edmonton (UTC-07:00)', + 'America/Hermosillo' => 'Hermosillo (UTC-07:00)', + 'America/Inuvik' => 'Inuvik (UTC-07:00)', + 'America/Mazatlan' => 'Mazatlan (UTC-07:00)', + 'America/Ojinaga' => 'Ojinaga (UTC-07:00)', + 'America/Phoenix' => 'Phoenix (UTC-07:00)', + 'America/Shiprock' => 'Shiprock (UTC-07:00)', + 'America/Yellowknife' => 'Yellowknife (UTC-07:00)', + 'America/Bahia_Banderas' => 'Bahia Banderas (UTC-06:00)', + 'America/Belize' => 'Belize (UTC-06:00)', + 'America/North_Dakota/Beulah' => 'Beulah (UTC-06:00)', + 'America/Cancun' => 'Cancun (UTC-06:00)', + 'America/North_Dakota/Center' => 'Center (UTC-06:00)', + 'America/Chicago' => 'Chicago (UTC-06:00)', + 'America/Costa_Rica' => 'Costa Rica (UTC-06:00)', + 'Pacific/Easter' => 'Easter (UTC-06:00)', + 'America/El_Salvador' => 'El Salvador (UTC-06:00)', + 'Pacific/Galapagos' => 'Galapagos (UTC-06:00)', + 'America/Guatemala' => 'Guatemala (UTC-06:00)', + 'America/Indiana/Knox' => 'Knox (UTC-06:00)', + 'America/Managua' => 'Managua (UTC-06:00)', + 'America/Matamoros' => 'Matamoros (UTC-06:00)', + 'America/Menominee' => 'Menominee (UTC-06:00)', + 'America/Merida' => 'Merida (UTC-06:00)', + 'America/Mexico_City' => 'Mexico City (UTC-06:00)', + 'America/Monterrey' => 'Monterrey (UTC-06:00)', + 'America/North_Dakota/New_Salem' => 'New Salem (UTC-06:00)', + 'America/Rainy_River' => 'Rainy River (UTC-06:00)', + 'America/Rankin_Inlet' => 'Rankin Inlet (UTC-06:00)', + 'America/Regina' => 'Regina (UTC-06:00)', + 'America/Resolute' => 'Resolute (UTC-06:00)', + 'America/Swift_Current' => 'Swift Current (UTC-06:00)', + 'America/Tegucigalpa' => 'Tegucigalpa (UTC-06:00)', + 'America/Indiana/Tell_City' => 'Tell City (UTC-06:00)', + 'America/Winnipeg' => 'Winnipeg (UTC-06:00)', + 'America/Atikokan' => 'Atikokan (UTC-05:00)', + 'America/Bogota' => 'Bogota (UTC-05:00)', + 'America/Cayman' => 'Cayman (UTC-05:00)', + 'America/Detroit' => 'Detroit (UTC-05:00)', + 'America/Grand_Turk' => 'Grand Turk (UTC-05:00)', + 'America/Guayaquil' => 'Guayaquil (UTC-05:00)', + 'America/Havana' => 'Havana (UTC-05:00)', + 'America/Indiana/Indianapolis' => 'Indianapolis (UTC-05:00)', + 'America/Iqaluit' => 'Iqaluit (UTC-05:00)', + 'America/Jamaica' => 'Jamaica (UTC-05:00)', + 'America/Lima' => 'Lima (UTC-05:00)', + 'America/Kentucky/Louisville' => 'Louisville (UTC-05:00)', + 'America/Indiana/Marengo' => 'Marengo (UTC-05:00)', + 'America/Kentucky/Monticello' => 'Monticello (UTC-05:00)', + 'America/Montreal' => 'Montreal (UTC-05:00)', + 'America/Nassau' => 'Nassau (UTC-05:00)', + 'America/New_York' => 'New York (UTC-05:00)', + 'America/Nipigon' => 'Nipigon (UTC-05:00)', + 'America/Panama' => 'Panama (UTC-05:00)', + 'America/Pangnirtung' => 'Pangnirtung (UTC-05:00)', + 'America/Indiana/Petersburg' => 'Petersburg (UTC-05:00)', + 'America/Port-au-Prince' => 'Port-au-Prince (UTC-05:00)', + 'America/Thunder_Bay' => 'Thunder Bay (UTC-05:00)', + 'America/Toronto' => 'Toronto (UTC-05:00)', + 'America/Indiana/Vevay' => 'Vevay (UTC-05:00)', + 'America/Indiana/Vincennes' => 'Vincennes (UTC-05:00)', + 'America/Indiana/Winamac' => 'Winamac (UTC-05:00)', + 'America/Caracas' => 'Caracas (UTC-04:30)', + 'America/Anguilla' => 'Anguilla (UTC-04:00)', + 'America/Antigua' => 'Antigua (UTC-04:00)', + 'America/Aruba' => 'Aruba (UTC-04:00)', + 'America/Asuncion' => 'Asuncion (UTC-04:00)', + 'America/Barbados' => 'Barbados (UTC-04:00)', + 'Atlantic/Bermuda' => 'Bermuda (UTC-04:00)', + 'America/Blanc-Sablon' => 'Blanc-Sablon (UTC-04:00)', + 'America/Boa_Vista' => 'Boa Vista (UTC-04:00)', + 'America/Campo_Grande' => 'Campo Grande (UTC-04:00)', + 'America/Cuiaba' => 'Cuiaba (UTC-04:00)', + 'America/Curacao' => 'Curacao (UTC-04:00)', + 'America/Dominica' => 'Dominica (UTC-04:00)', + 'America/Eirunepe' => 'Eirunepe (UTC-04:00)', + 'America/Glace_Bay' => 'Glace Bay (UTC-04:00)', + 'America/Goose_Bay' => 'Goose Bay (UTC-04:00)', + 'America/Grenada' => 'Grenada (UTC-04:00)', + 'America/Guadeloupe' => 'Guadeloupe (UTC-04:00)', + 'America/Guyana' => 'Guyana (UTC-04:00)', + 'America/Halifax' => 'Halifax (UTC-04:00)', + 'America/Kralendijk' => 'Kralendijk (UTC-04:00)', + 'America/La_Paz' => 'La Paz (UTC-04:00)', + 'America/Lower_Princes' => 'Lower Princes (UTC-04:00)', + 'America/Manaus' => 'Manaus (UTC-04:00)', + 'America/Marigot' => 'Marigot (UTC-04:00)', + 'America/Martinique' => 'Martinique (UTC-04:00)', + 'America/Moncton' => 'Moncton (UTC-04:00)', + 'America/Montserrat' => 'Montserrat (UTC-04:00)', + 'Antarctica/Palmer' => 'Palmer (UTC-04:00)', + 'America/Port_of_Spain' => 'Port of Spain (UTC-04:00)', + 'America/Porto_Velho' => 'Porto Velho (UTC-04:00)', + 'America/Puerto_Rico' => 'Puerto Rico (UTC-04:00)', + 'America/Rio_Branco' => 'Rio Branco (UTC-04:00)', + 'America/Santiago' => 'Santiago (UTC-04:00)', + 'America/Santo_Domingo' => 'Santo Domingo (UTC-04:00)', + 'America/St_Barthelemy' => 'St. Barthelemy (UTC-04:00)', + 'America/St_Kitts' => 'St. Kitts (UTC-04:00)', + 'America/St_Lucia' => 'St. Lucia (UTC-04:00)', + 'America/St_Thomas' => 'St. Thomas (UTC-04:00)', + 'America/St_Vincent' => 'St. Vincent (UTC-04:00)', + 'America/Thule' => 'Thule (UTC-04:00)', + 'America/Tortola' => 'Tortola (UTC-04:00)', + 'America/St_Johns' => 'St. Johns (UTC-03:30)', + 'America/Araguaina' => 'Araguaina (UTC-03:00)', + 'America/Bahia' => 'Bahia (UTC-03:00)', + 'America/Belem' => 'Belem (UTC-03:00)', + 'America/Argentina/Buenos_Aires' => 'Buenos Aires (UTC-03:00)', + 'America/Argentina/Catamarca' => 'Catamarca (UTC-03:00)', + 'America/Cayenne' => 'Cayenne (UTC-03:00)', + 'America/Argentina/Cordoba' => 'Cordoba (UTC-03:00)', + 'America/Fortaleza' => 'Fortaleza (UTC-03:00)', + 'America/Godthab' => 'Godthab (UTC-03:00)', + 'America/Argentina/Jujuy' => 'Jujuy (UTC-03:00)', + 'America/Argentina/La_Rioja' => 'La Rioja (UTC-03:00)', + 'America/Maceio' => 'Maceio (UTC-03:00)', + 'America/Argentina/Mendoza' => 'Mendoza (UTC-03:00)', + 'America/Miquelon' => 'Miquelon (UTC-03:00)', + 'America/Montevideo' => 'Montevideo (UTC-03:00)', + 'America/Paramaribo' => 'Paramaribo (UTC-03:00)', + 'America/Recife' => 'Recife (UTC-03:00)', + 'America/Argentina/Rio_Gallegos' => 'Rio Gallegos (UTC-03:00)', + 'Antarctica/Rothera' => 'Rothera (UTC-03:00)', + 'America/Argentina/Salta' => 'Salta (UTC-03:00)', + 'America/Argentina/San_Juan' => 'San Juan (UTC-03:00)', + 'America/Argentina/San_Luis' => 'San Luis (UTC-03:00)', + 'America/Santarem' => 'Santarem (UTC-03:00)', + 'America/Sao_Paulo' => 'Sao Paulo (UTC-03:00)', + 'Atlantic/Stanley' => 'Stanley (UTC-03:00)', + 'America/Argentina/Tucuman' => 'Tucuman (UTC-03:00)', + 'America/Argentina/Ushuaia' => 'Ushuaia (UTC-03:00)', + 'America/Noronha' => 'Noronha (UTC-02:00)', + 'Atlantic/South_Georgia' => 'South Georgia (UTC-02:00)', + 'Atlantic/Azores' => 'Azores (UTC-01:00)', + 'Atlantic/Cape_Verde' => 'Cape Verde (UTC-01:00)', + 'America/Scoresbysund' => 'Scoresbysund (UTC-01:00)', + 'Africa/Abidjan' => 'Abidjan (UTC+00:00)', + 'Africa/Accra' => 'Accra (UTC+00:00)', + 'Africa/Bamako' => 'Bamako (UTC+00:00)', + 'Africa/Banjul' => 'Banjul (UTC+00:00)', + 'Africa/Bissau' => 'Bissau (UTC+00:00)', + 'Atlantic/Canary' => 'Canary (UTC+00:00)', + 'Africa/Casablanca' => 'Casablanca (UTC+00:00)', + 'Africa/Conakry' => 'Conakry (UTC+00:00)', + 'Africa/Dakar' => 'Dakar (UTC+00:00)', + 'America/Danmarkshavn' => 'Danmarkshavn (UTC+00:00)', + 'Europe/Dublin' => 'Dublin (UTC+00:00)', + 'Africa/El_Aaiun' => 'El Aaiun (UTC+00:00)', + 'Atlantic/Faroe' => 'Faroe (UTC+00:00)', + 'Africa/Freetown' => 'Freetown (UTC+00:00)', + 'Europe/Guernsey' => 'Guernsey (UTC+00:00)', + 'Europe/Isle_of_Man' => 'Isle of Man (UTC+00:00)', + 'Europe/Jersey' => 'Jersey (UTC+00:00)', + 'Europe/Lisbon' => 'Lisbon (UTC+00:00)', + 'Africa/Lome' => 'Lome (UTC+00:00)', + 'Europe/London' => 'London (UTC+00:00)', + 'Atlantic/Madeira' => 'Madeira (UTC+00:00)', + 'Africa/Monrovia' => 'Monrovia (UTC+00:00)', + 'Africa/Nouakchott' => 'Nouakchott (UTC+00:00)', + 'Africa/Ouagadougou' => 'Ouagadougou (UTC+00:00)', + 'Atlantic/Reykjavik' => 'Reykjavik (UTC+00:00)', + 'Africa/Sao_Tome' => 'Sao Tome (UTC+00:00)', + 'Atlantic/St_Helena' => 'St. Helena (UTC+00:00)', + 'UTC' => 'UTC (UTC+00:00)', + 'Africa/Algiers' => 'Algiers (UTC+01:00)', + 'Europe/Amsterdam' => 'Amsterdam (UTC+01:00)', + 'Europe/Andorra' => 'Andorra (UTC+01:00)', + 'Africa/Bangui' => 'Bangui (UTC+01:00)', + 'Europe/Belgrade' => 'Belgrade (UTC+01:00)', + 'Europe/Berlin' => 'Berlin (UTC+01:00)', + 'Europe/Bratislava' => 'Bratislava (UTC+01:00)', + 'Africa/Brazzaville' => 'Brazzaville (UTC+01:00)', + 'Europe/Brussels' => 'Brussels (UTC+01:00)', + 'Europe/Budapest' => 'Budapest (UTC+01:00)', + 'Europe/Busingen' => 'Busingen (UTC+01:00)', + 'Africa/Ceuta' => 'Ceuta (UTC+01:00)', + 'Europe/Copenhagen' => 'Copenhagen (UTC+01:00)', + 'Africa/Douala' => 'Douala (UTC+01:00)', + 'Europe/Gibraltar' => 'Gibraltar (UTC+01:00)', + 'Africa/Kinshasa' => 'Kinshasa (UTC+01:00)', + 'Africa/Lagos' => 'Lagos (UTC+01:00)', + 'Africa/Libreville' => 'Libreville (UTC+01:00)', + 'Europe/Ljubljana' => 'Ljubljana (UTC+01:00)', + 'Arctic/Longyearbyen' => 'Longyearbyen (UTC+01:00)', + 'Africa/Luanda' => 'Luanda (UTC+01:00)', + 'Europe/Luxembourg' => 'Luxembourg (UTC+01:00)', + 'Europe/Madrid' => 'Madrid (UTC+01:00)', + 'Africa/Malabo' => 'Malabo (UTC+01:00)', + 'Europe/Malta' => 'Malta (UTC+01:00)', + 'Europe/Monaco' => 'Monaco (UTC+01:00)', + 'Africa/Ndjamena' => 'Ndjamena (UTC+01:00)', + 'Africa/Niamey' => 'Niamey (UTC+01:00)', + 'Europe/Oslo' => 'Oslo (UTC+01:00)', + 'Europe/Paris' => 'Paris (UTC+01:00)', + 'Europe/Podgorica' => 'Podgorica (UTC+01:00)', + 'Africa/Porto-Novo' => 'Porto-Novo (UTC+01:00)', + 'Europe/Prague' => 'Prague (UTC+01:00)', + 'Europe/Rome' => 'Rome (UTC+01:00)', + 'Europe/San_Marino' => 'San Marino (UTC+01:00)', + 'Europe/Sarajevo' => 'Sarajevo (UTC+01:00)', + 'Europe/Skopje' => 'Skopje (UTC+01:00)', + 'Europe/Stockholm' => 'Stockholm (UTC+01:00)', + 'Europe/Tirane' => 'Tirane (UTC+01:00)', + 'Africa/Tripoli' => 'Tripoli (UTC+01:00)', + 'Africa/Tunis' => 'Tunis (UTC+01:00)', + 'Europe/Vaduz' => 'Vaduz (UTC+01:00)', + 'Europe/Vatican' => 'Vatican (UTC+01:00)', + 'Europe/Vienna' => 'Vienna (UTC+01:00)', + 'Europe/Warsaw' => 'Warsaw (UTC+01:00)', + 'Africa/Windhoek' => 'Windhoek (UTC+01:00)', + 'Europe/Zagreb' => 'Zagreb (UTC+01:00)', + 'Europe/Zurich' => 'Zurich (UTC+01:00)', + 'Europe/Athens' => 'Athens (UTC+02:00)', + 'Asia/Beirut' => 'Beirut (UTC+02:00)', + 'Africa/Blantyre' => 'Blantyre (UTC+02:00)', + 'Europe/Bucharest' => 'Bucharest (UTC+02:00)', + 'Africa/Bujumbura' => 'Bujumbura (UTC+02:00)', + 'Africa/Cairo' => 'Cairo (UTC+02:00)', + 'Europe/Chisinau' => 'Chisinau (UTC+02:00)', + 'Asia/Damascus' => 'Damascus (UTC+02:00)', + 'Africa/Gaborone' => 'Gaborone (UTC+02:00)', + 'Asia/Gaza' => 'Gaza (UTC+02:00)', + 'Africa/Harare' => 'Harare (UTC+02:00)', + 'Asia/Hebron' => 'Hebron (UTC+02:00)', + 'Europe/Helsinki' => 'Helsinki (UTC+02:00)', + 'Europe/Istanbul' => 'Istanbul (UTC+02:00)', + 'Asia/Jerusalem' => 'Jerusalem (UTC+02:00)', + 'Africa/Johannesburg' => 'Johannesburg (UTC+02:00)', + 'Europe/Kiev' => 'Kiev (UTC+02:00)', + 'Africa/Kigali' => 'Kigali (UTC+02:00)', + 'Africa/Lubumbashi' => 'Lubumbashi (UTC+02:00)', + 'Africa/Lusaka' => 'Lusaka (UTC+02:00)', + 'Africa/Maputo' => 'Maputo (UTC+02:00)', + 'Europe/Mariehamn' => 'Mariehamn (UTC+02:00)', + 'Africa/Maseru' => 'Maseru (UTC+02:00)', + 'Africa/Mbabane' => 'Mbabane (UTC+02:00)', + 'Asia/Nicosia' => 'Nicosia (UTC+02:00)', + 'Europe/Riga' => 'Riga (UTC+02:00)', + 'Europe/Simferopol' => 'Simferopol (UTC+02:00)', + 'Europe/Sofia' => 'Sofia (UTC+02:00)', + 'Europe/Tallinn' => 'Tallinn (UTC+02:00)', + 'Europe/Uzhgorod' => 'Uzhgorod (UTC+02:00)', + 'Europe/Vilnius' => 'Vilnius (UTC+02:00)', + 'Europe/Zaporozhye' => 'Zaporozhye (UTC+02:00)', + 'Africa/Addis_Ababa' => 'Addis Ababa (UTC+03:00)', + 'Asia/Aden' => 'Aden (UTC+03:00)', + 'Asia/Amman' => 'Amman (UTC+03:00)', + 'Indian/Antananarivo' => 'Antananarivo (UTC+03:00)', + 'Africa/Asmara' => 'Asmara (UTC+03:00)', + 'Asia/Baghdad' => 'Baghdad (UTC+03:00)', + 'Asia/Bahrain' => 'Bahrain (UTC+03:00)', + 'Indian/Comoro' => 'Comoro (UTC+03:00)', + 'Africa/Dar_es_Salaam' => 'Dar es Salaam (UTC+03:00)', + 'Africa/Djibouti' => 'Djibouti (UTC+03:00)', + 'Africa/Juba' => 'Juba (UTC+03:00)', + 'Europe/Kaliningrad' => 'Kaliningrad (UTC+03:00)', + 'Africa/Kampala' => 'Kampala (UTC+03:00)', + 'Africa/Khartoum' => 'Khartoum (UTC+03:00)', + 'Asia/Kuwait' => 'Kuwait (UTC+03:00)', + 'Indian/Mayotte' => 'Mayotte (UTC+03:00)', + 'Europe/Minsk' => 'Minsk (UTC+03:00)', + 'Africa/Mogadishu' => 'Mogadishu (UTC+03:00)', + 'Europe/Moscow' => 'Moscow (UTC+03:00)', + 'Africa/Nairobi' => 'Nairobi (UTC+03:00)', + 'Asia/Qatar' => 'Qatar (UTC+03:00)', + 'Asia/Riyadh' => 'Riyadh (UTC+03:00)', + 'Antarctica/Syowa' => 'Syowa (UTC+03:00)', + 'Asia/Tehran' => 'Tehran (UTC+03:30)', + 'Asia/Baku' => 'Baku (UTC+04:00)', + 'Asia/Dubai' => 'Dubai (UTC+04:00)', + 'Indian/Mahe' => 'Mahe (UTC+04:00)', + 'Indian/Mauritius' => 'Mauritius (UTC+04:00)', + 'Asia/Muscat' => 'Muscat (UTC+04:00)', + 'Indian/Reunion' => 'Reunion (UTC+04:00)', + 'Europe/Samara' => 'Samara (UTC+04:00)', + 'Asia/Tbilisi' => 'Tbilisi (UTC+04:00)', + 'Europe/Volgograd' => 'Volgograd (UTC+04:00)', + 'Asia/Yerevan' => 'Yerevan (UTC+04:00)', + 'Asia/Kabul' => 'Kabul (UTC+04:30)', + 'Asia/Aqtau' => 'Aqtau (UTC+05:00)', + 'Asia/Aqtobe' => 'Aqtobe (UTC+05:00)', + 'Asia/Ashgabat' => 'Ashgabat (UTC+05:00)', + 'Asia/Dushanbe' => 'Dushanbe (UTC+05:00)', + 'Asia/Karachi' => 'Karachi (UTC+05:00)', + 'Indian/Kerguelen' => 'Kerguelen (UTC+05:00)', + 'Indian/Maldives' => 'Maldives (UTC+05:00)', + 'Antarctica/Mawson' => 'Mawson (UTC+05:00)', + 'Asia/Oral' => 'Oral (UTC+05:00)', + 'Asia/Samarkand' => 'Samarkand (UTC+05:00)', + 'Asia/Tashkent' => 'Tashkent (UTC+05:00)', + 'Asia/Colombo' => 'Colombo (UTC+05:30)', + 'Asia/Kolkata' => 'Kolkata (UTC+05:30)', + 'Asia/Kathmandu' => 'Kathmandu (UTC+05:45)', + 'Asia/Almaty' => 'Almaty (UTC+06:00)', + 'Asia/Bishkek' => 'Bishkek (UTC+06:00)', + 'Indian/Chagos' => 'Chagos (UTC+06:00)', + 'Asia/Dhaka' => 'Dhaka (UTC+06:00)', + 'Asia/Qyzylorda' => 'Qyzylorda (UTC+06:00)', + 'Asia/Thimphu' => 'Thimphu (UTC+06:00)', + 'Antarctica/Vostok' => 'Vostok (UTC+06:00)', + 'Asia/Yekaterinburg' => 'Yekaterinburg (UTC+06:00)', + 'Indian/Cocos' => 'Cocos (UTC+06:30)', + 'Asia/Rangoon' => 'Rangoon (UTC+06:30)', + 'Asia/Bangkok' => 'Bangkok (UTC+07:00)', + 'Indian/Christmas' => 'Christmas (UTC+07:00)', + 'Antarctica/Davis' => 'Davis (UTC+07:00)', + 'Asia/Ho_Chi_Minh' => 'Ho Chi Minh (UTC+07:00)', + 'Asia/Hovd' => 'Hovd (UTC+07:00)', + 'Asia/Jakarta' => 'Jakarta (UTC+07:00)', + 'Asia/Novokuznetsk' => 'Novokuznetsk (UTC+07:00)', + 'Asia/Novosibirsk' => 'Novosibirsk (UTC+07:00)', + 'Asia/Omsk' => 'Omsk (UTC+07:00)', + 'Asia/Phnom_Penh' => 'Phnom Penh (UTC+07:00)', + 'Asia/Pontianak' => 'Pontianak (UTC+07:00)', + 'Asia/Vientiane' => 'Vientiane (UTC+07:00)', + 'Asia/Brunei' => 'Brunei (UTC+08:00)', + 'Antarctica/Casey' => 'Casey (UTC+08:00)', + 'Asia/Choibalsan' => 'Choibalsan (UTC+08:00)', + 'Asia/Chongqing' => 'Chongqing (UTC+08:00)', + 'Asia/Harbin' => 'Harbin (UTC+08:00)', + 'Asia/Hong_Kong' => 'Hong Kong (UTC+08:00)', + 'Asia/Kashgar' => 'Kashgar (UTC+08:00)', + 'Asia/Krasnoyarsk' => 'Krasnoyarsk (UTC+08:00)', + 'Asia/Kuala_Lumpur' => 'Kuala Lumpur (UTC+08:00)', + 'Asia/Kuching' => 'Kuching (UTC+08:00)', + 'Asia/Macau' => 'Macau (UTC+08:00)', + 'Asia/Makassar' => 'Makassar (UTC+08:00)', + 'Asia/Manila' => 'Manila (UTC+08:00)', + 'Australia/Perth' => 'Perth (UTC+08:00)', + 'Asia/Shanghai' => 'Shanghai (UTC+08:00)', + 'Asia/Singapore' => 'Singapore (UTC+08:00)', + 'Asia/Taipei' => 'Taipei (UTC+08:00)', + 'Asia/Ulaanbaatar' => 'Ulaanbaatar (UTC+08:00)', + 'Asia/Urumqi' => 'Urumqi (UTC+08:00)', + 'Australia/Eucla' => 'Eucla (UTC+08:45)', + 'Asia/Dili' => 'Dili (UTC+09:00)', + 'Asia/Irkutsk' => 'Irkutsk (UTC+09:00)', + 'Asia/Jayapura' => 'Jayapura (UTC+09:00)', + 'Pacific/Palau' => 'Palau (UTC+09:00)', + 'Asia/Pyongyang' => 'Pyongyang (UTC+09:00)', + 'Asia/Seoul' => 'Seoul (UTC+09:00)', + 'Asia/Tokyo' => 'Tokyo (UTC+09:00)', + 'Australia/Adelaide' => 'Adelaide (UTC+09:30)', + 'Australia/Broken_Hill' => 'Broken Hill (UTC+09:30)', + 'Australia/Darwin' => 'Darwin (UTC+09:30)', + 'Australia/Brisbane' => 'Brisbane (UTC+10:00)', + 'Pacific/Chuuk' => 'Chuuk (UTC+10:00)', + 'Australia/Currie' => 'Currie (UTC+10:00)', + 'Antarctica/DumontDUrville' => 'DumontDUrville (UTC+10:00)', + 'Pacific/Guam' => 'Guam (UTC+10:00)', + 'Australia/Hobart' => 'Hobart (UTC+10:00)', + 'Asia/Khandyga' => 'Khandyga (UTC+10:00)', + 'Australia/Lindeman' => 'Lindeman (UTC+10:00)', + 'Australia/Melbourne' => 'Melbourne (UTC+10:00)', + 'Pacific/Port_Moresby' => 'Port Moresby (UTC+10:00)', + 'Pacific/Saipan' => 'Saipan (UTC+10:00)', + 'Australia/Sydney' => 'Sydney (UTC+10:00)', + 'Asia/Yakutsk' => 'Yakutsk (UTC+10:00)', + 'Australia/Lord_Howe' => 'Lord Howe (UTC+10:30)', + 'Pacific/Efate' => 'Efate (UTC+11:00)', + 'Pacific/Guadalcanal' => 'Guadalcanal (UTC+11:00)', + 'Pacific/Kosrae' => 'Kosrae (UTC+11:00)', + 'Antarctica/Macquarie' => 'Macquarie (UTC+11:00)', + 'Pacific/Noumea' => 'Noumea (UTC+11:00)', + 'Pacific/Pohnpei' => 'Pohnpei (UTC+11:00)', + 'Asia/Sakhalin' => 'Sakhalin (UTC+11:00)', + 'Asia/Ust-Nera' => 'Ust-Nera (UTC+11:00)', + 'Asia/Vladivostok' => 'Vladivostok (UTC+11:00)', + 'Pacific/Norfolk' => 'Norfolk (UTC+11:30)', + 'Asia/Anadyr' => 'Anadyr (UTC+12:00)', + 'Pacific/Auckland' => 'Auckland (UTC+12:00)', + 'Pacific/Fiji' => 'Fiji (UTC+12:00)', + 'Pacific/Funafuti' => 'Funafuti (UTC+12:00)', + 'Asia/Kamchatka' => 'Kamchatka (UTC+12:00)', + 'Pacific/Kwajalein' => 'Kwajalein (UTC+12:00)', + 'Asia/Magadan' => 'Magadan (UTC+12:00)', + 'Pacific/Majuro' => 'Majuro (UTC+12:00)', + 'Antarctica/McMurdo' => 'McMurdo (UTC+12:00)', + 'Pacific/Nauru' => 'Nauru (UTC+12:00)', + 'Antarctica/South_Pole' => 'South Pole (UTC+12:00)', + 'Pacific/Tarawa' => 'Tarawa (UTC+12:00)', + 'Pacific/Wake' => 'Wake (UTC+12:00)', + 'Pacific/Wallis' => 'Wallis (UTC+12:00)', + 'Pacific/Chatham' => 'Chatham (UTC+12:45)', + 'Pacific/Apia' => 'Apia (UTC+13:00)', + 'Pacific/Enderbury' => 'Enderbury (UTC+13:00)', + 'Pacific/Fakaofo' => 'Fakaofo (UTC+13:00)', + 'Pacific/Tongatapu' => 'Tongatapu (UTC+13:00)', + 'Pacific/Kiritimati' => 'Kiritimati (UTC+14:00)', + ], +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/TopTen.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/TopTen.php new file mode 100644 index 0000000..0fd5533 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/TopTen.php @@ -0,0 +1,25 @@ + 'IP', + 'Top10_ips' => 'IPs', + 'Top10_user' => 'User', + 'Top10_users' => 'Users', + 'Top10_events' => 'Events', + 'Top10_country' => 'Country', + 'Top10_resource' => 'Resource', + 'Top10_attempts' => 'Attempts', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/UserDetails.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/UserDetails.php new file mode 100644 index 0000000..59ff511 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/UserDetails.php @@ -0,0 +1,36 @@ + 'Failed login', + 'UserDetails_failed_login_count_tooltip' => 'Failed login', + 'UserDetails_password_reset_count' => 'Password reset', + 'UserDetails_password_reset_count_tooltip' => 'Password reset', + 'UserDetails_auth_error_count' => 'HTTP error', + 'UserDetails_auth_error_count_tooltip' => 'HTTP error', + 'UserDetails_off_hours_login_count' => 'Off-hours login', + 'UserDetails_off_hours_login_count_tooltip' => 'Off-hours login', + 'UserDetails_device_count' => 'Median events', + 'UserDetails_device_count_tooltip' => 'Median events', + 'UserDetails_ip_count' => 'Login attempts', + 'UserDetails_ip_count_tooltip' => 'Login attempts', + 'UserDetails_session_count' => 'Sessions', + 'UserDetails_session_count_tooltip' => 'Sessions', + + 'UserDetails_day_card_title' => 'Today\'s activity', + 'UserDetails_day_card_title_tooltip' => 'Today since midnight.', + 'UserDetails_week_card_title' => 'Average daily activity', + 'UserDetails_week_card_title_tooltip' => 'Median number of user events per day (last 7 days).', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/Welcome.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/Welcome.php new file mode 100644 index 0000000..628cc30 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/Welcome.php @@ -0,0 +1,21 @@ + 'Hello, ', + 'Welcome_hello_without_name' => 'Hello', + 'Welcome_how_are_you_doing' => 'I hope you are having a great day!', + 'Welcome_global_search_placeholder' => 'Search User ID, Last name, IP, ASN', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Dictionary/en/Parts/index.php @@ -0,0 +1,3 @@ + $apiKey, + ':user_url' => \Utils\Variables::getSiteWithProtocol() . '/id/', + ]; + + $query = ( + 'SELECT + -- create user url + :user_url || event_account.id AS internal_url, + -- -event_account.id, + event_account.userid, + event_account.created, + -- event_account.key, + event_account.lastip, + event_account.lastemail, + event_account.lastphone, + event_account.lastseen, + event_account.fullname, + event_account.firstname, + event_account.lastname, + -- event_account.is_important, + event_account.total_visit, + event_account.total_country, + event_account.total_ip, + event_account.total_device, + event_account.total_shared_ip, + event_account.total_shared_phone, + event_account.score_updated_at, + event_account.score, + event_account.score_details, + event_account.reviewed, + event_account.fraud, + event_account.latest_decision + + FROM event_account' + ); + + $where = ' WHERE key = :api_key'; + + if ($userId !== null) { + $params[':user_id'] = $userId; + $where .= ' AND event_account.id = :user_id'; + } + + $query .= $where; + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Api/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Api/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Api/index.php @@ -0,0 +1,3 @@ +load($filters); + } + + public function getSharedApiKeyOperators(int $operatorId): array { + $params = [ + ':creator' => $operatorId, + ]; + + $query = ( + 'SELECT + dshb_operators.id, + dshb_operators.email, + dshb_operators.is_active + FROM + dshb_api + + JOIN dshb_api_co_owners + ON dshb_api.id = dshb_api_co_owners.api + + JOIN dshb_operators + ON dshb_api_co_owners.operator = dshb_operators.id + + WHERE + dshb_api.creator = :creator;' + ); + + return $this->execQuery($query, $params); + } + + public function create(int $operator, int $api): void { + $this->operator = $operator; + $this->api = $api; + + $this->save(); + } + + public function deleteCoOwnership(): void { + if ($this->loaded()) { + $this->erase(); + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ApiKeys.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ApiKeys.php new file mode 100644 index 0000000..d68c0d0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ApiKeys.php @@ -0,0 +1,207 @@ +quote = $quote; + $this->creator = $operatorId; + $this->key = $this->getHash($uuid); + + if (array_key_exists('skip_enriching_attributes', $data)) { + $this->skip_enriching_attributes = $data['skip_enriching_attributes']; + } + if (array_key_exists('skip_blacklist_sync', $data)) { + $this->skip_blacklist_sync = $data['skip_blacklist_sync']; + } + + $this->save(); + + return (int) $this->id; + } + + public function getKeys(int $operatorId): array { + $filters = [ + 'creator=?', $operatorId, + ]; + + return $this->find($filters); + } + + public function getKey(int $operatorId): ?ApiKeys { + $keys = $this->getKeys($operatorId); + + return $keys[0] ?? null; + } + + public function resetKey(int $keyId, int $operatorId): void { + $this->getByKeyAndOperatorId($keyId, $operatorId); + + if ($this->loaded()) { + $uuid = sprintf('%s%s%s', $keyId, $operatorId, time()); + + $this->key = $this->getHash($uuid); + $this->save(); + } + } + + public function getByKeyAndOperatorId(int $keyId, int $operatorId): self|null|false { + $filters = [ + 'id=? AND creator=?', $keyId, $operatorId, + ]; + + return $this->load($filters); + } + + public function getKeyIdByHash(string $hash): self|null|false { + $filters = [ + 'key=?', $hash, + ]; + + return $this->load($filters); + } + + public function getKeyById(int $keyId): self|null|false { + $filters = [ + 'id=?', $keyId, + ]; + + return $this->load($filters); + } + + public function getTimezoneByKeyId(int $keyId): string { + $params = [ + ':api_key' => $keyId, + ]; + + $query = ( + 'SELECT + dshb_operators.timezone + FROM + dshb_api + JOIN dshb_operators + ON dshb_operators.id = dshb_api.creator + WHERE + dshb_api.id = :api_key' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['timezone'] ?? 'UTC'; + } + + public function getSkipEnrichingAttributes(int $keyId): array { + $params = [ + ':api_key' => $keyId, + ]; + + $query = ( + 'SELECT + dshb_api.skip_enriching_attributes + FROM dshb_api + WHERE + dshb_api.id = :api_key' + ); + + $results = $this->execQuery($query, $params); + + if (!count($results)) { + return []; + } + + $results = json_decode($results[0]['skip_enriching_attributes']); + + if (!\Utils\Variables::getEmailPhoneAllowed()) { + if (!in_array('email', $results, true)) { + $results[] = 'email'; + } + if (!in_array('phone', $results, true)) { + $results[] = 'phone'; + } + if (!in_array('domain', $results, true)) { + $results[] = 'domain'; + } + } + + return $results; + } + + public function enrichableAttributes(int $keyId): array { + $skipAttributes = $this->getSkipEnrichingAttributes($keyId); + $attributes = \Utils\Constants::get('ENRICHING_ATTRIBUTES'); + $attributes = array_diff_key($attributes, array_flip($skipAttributes)); + + return $attributes; + } + + public function attributeIsEnrichable(string $attr, int $keyId): bool { + return array_key_exists($attr, $this->enrichableAttributes($keyId)); + } + + public function getAllApiKeyIds(): array { + $query = 'SELECT id from dshb_api'; + return $this->execQuery($query, null); + } + + public function updateSkipEnrichingAttributes(array $attributes): void { + if ($this->loaded()) { + $attributes = \array_values($attributes); + $this->skip_enriching_attributes = \json_encode($attributes); + $this->save(); + } + } + + public function updateSkipBlacklistSynchronisation(bool $skip): void { + if ($this->loaded()) { + $this->skip_blacklist_sync = $skip; + $this->save(); + } + } + + public function updateRetentionPolicy(int $policyInWeeks): void { + if ($this->loaded()) { + $this->retention_policy = $policyInWeeks; + $this->save(); + } + } + + public function updateBlacklistThreshold(int $value): void { + if ($this->loaded()) { + $this->blacklist_threshold = $value; + $this->save(); + } + } + + public function updateReviewQueueThreshold(int $value): void { + if ($this->loaded()) { + $this->review_queue_threshold = $value; + $this->save(); + } + } + + public function updateInternalToken(string $apiToken): void { + if ($this->loaded()) { + $this->token = $apiToken; + $this->save(); + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/BaseSql.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/BaseSql.php new file mode 100644 index 0000000..f6dafc0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/BaseSql.php @@ -0,0 +1,76 @@ +f3 = \Base::instance(); + + if ($this->DB_TABLE_NAME) { + $DB = $this->getDatabaseConnection(); + parent::__construct($DB, $this->DB_TABLE_NAME, $this->DB_TABLE_FIELDS, $this->DB_TABLE_TTL); + } + } + + private function getDatabaseConnection(): ?\DB\SQL { + return $this->f3->get('API_DATABASE'); + } + + public function getHash(string $string): string { + $iterations = 1000; + $salt = $this->f3->get('SALT'); + + return hash_pbkdf2('sha256', $string, $salt, $iterations, 32); + } + + public function getPseudoRandomString(int $length = 32): string { + $bytes = \openssl_random_pseudo_bytes($length / 2); + + return \bin2hex($bytes); + } + + public function printLog(): void { + echo $this->f3->get('API_DATABASE')->log(); + } + + public function getArrayPlaceholders(array $ids, string $postfix = ''): array { + $params = []; + $placeHolders = []; + + $postfix = $postfix !== '' ? '_' . $postfix : ''; + + foreach ($ids as $i => $id) { + $key = sprintf(':item_id_%s%s', $i, $postfix); + $placeHolders[] = $key; + $params[$key] = $id; + } + + $placeHolders = implode(', ', $placeHolders); + + return [$params, $placeHolders]; + } + + public function execQuery(string $query, ?array $params): array|int|null { + return $this->getDatabaseConnection()->exec($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/BlacklistItems.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/BlacklistItems.php new file mode 100644 index 0000000..6b9d90c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/BlacklistItems.php @@ -0,0 +1,109 @@ + $accountId, + ':api_key' => $apiKey, + ]; + + $query = (" + SELECT + event_ip.id, + event_ip.ip AS value, + 'ip' AS type, + :account_id::bigint AS account_id + FROM event_ip + WHERE + (id, key) IN ( + SELECT ip, key + FROM event + WHERE + account = :account_id + AND key = :api_key)"); + + return $this->execQuery($query, $params); + } + + public function getEmailsRelatedToAccountWithinOperator(int $accountId, int $apiKey): array { + $params = [ + ':account_id' => $accountId, + ':api_key' => $apiKey, + ]; + + $query = (" + SELECT + id, + email AS value, + 'email' AS type, + account_id + FROM + event_email + WHERE + key = :api_key + AND account_id = :account_id + "); + + return $this->execQuery($query, $params); + } + + public function getPhonesRelatedToAccountWithinOperator(int $accountId, int $apiKey): array { + $params = [ + ':account_id' => $accountId, + ':api_key' => $apiKey, + ]; + + $query = (" + SELECT + id, + phone_number AS value, + 'phone' AS type, + account_id + FROM + event_phone + WHERE + key = :api_key + AND account_id = :account_id + "); + + return $this->execQuery($query, $params); + } + + public function searchBlacklistedItem(string $value, int $apiKey): ?bool { + $query = ''; + $params = [ + ':value' => $value, + ':api_key' => $apiKey, + ]; + + $query = (' + SELECT 1 + FROM event_account + WHERE + userid = :value AND + fraud IS TRUE AND + key = :api_key + LIMIT 1'); + + $results = $this->execQuery($query, $params); + + return (bool) count($results); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Bot.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Bot.php new file mode 100644 index 0000000..f85e766 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Bot.php @@ -0,0 +1,99 @@ + $apiKey, + ':ua_id' => $subjectId, + ]; + + $results = $this->execQuery($query, $params); + + return count($results) > 0; + } + + public function getFullBotInfoById(int $uaId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':ua_id' => $uaId, + ]; + + $query = ( + 'SELECT + event_ua_parsed.id, + event_ua_parsed.device, + event_ua_parsed.device AS title, + event_ua_parsed.browser_name, + event_ua_parsed.browser_version, + event_ua_parsed.os_name, + event_ua_parsed.os_version, + event_ua_parsed.ua, + event_ua_parsed.modified, + event_ua_parsed.checked + FROM + event_ua_parsed + + WHERE + event_ua_parsed.key = :api_key AND + event_ua_parsed.id = :ua_id' + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function extractById(int $entityId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':id' => $entityId, + ]; + + $query = ( + "SELECT + COALESCE(event_ua_parsed.ua, '') AS value + + FROM + event_ua_parsed + + WHERE + event_ua_parsed.key = :api_key + AND event_ua_parsed.id = :id + + LIMIT 1" + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ChangeEmail.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ChangeEmail.php new file mode 100644 index 0000000..ff4be27 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ChangeEmail.php @@ -0,0 +1,57 @@ +getUnusedKeyByOperatorId($operatorId); + + if ($record) { + $this->status = 'invalidated'; + $this->save(); + } + + $this->reset(); + $this->renew_key = $this->getPseudoRandomString(32); + $this->operator_id = $operatorId; + $this->email = $email; + $this->status = 'unused'; + + $this->save(); + } + + public function getUnusedKeyByOperatorId(int $operatorId): self|null|false { + return $this->load( + ['"operator_id"=? AND "status"=?', $operatorId, 'unused'], + ); + } + + public function getByRenewKey(string $key): self|null|false { + return $this->load( + ['"renew_key"=? AND "status"=?', $key, 'unused'], + ); + } + + public function deactivate(): void { + if ($this->loaded()) { + $this->status = 'used'; + + $this->save(); + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Base.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Base.php new file mode 100644 index 0000000..6ce40a2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Base.php @@ -0,0 +1,163 @@ + $ts, + $field1 => $item[$field1], + $field2 => 0, + ]; + + if ($field3) { + $data0[$ts][$field3] = 0; + } + } + + $iters = count($data2); + + for ($i = 0; $i < $iters; ++$i) { + $item = $data2[$i]; + $ts = $item['ts']; + + if (!array_key_exists($ts, $data0)) { + $data0[$ts] = [ + 'ts' => $ts, + $field1 => 0, + $field2 => 0, + ]; + + if ($field3) { + $data0[$ts][$field3] = 0; + } + } + + $data0[$ts][$field2] = $item[$field2]; + } + + $iters = count($data3); + + for ($i = 0; $i < $iters; ++$i) { + $item = $data3[$i]; + $ts = $item['ts']; + + if (!array_key_exists($ts, $data0)) { + $data0[$ts] = [ + 'ts' => $ts, + $field1 => 0, + $field2 => 0, + $field3 => 0, + ]; + } + + $data0[$ts][$field3] = $item[$field3]; + } + + // TODO: tmp order troubles fix + usort($data0, function ($a, $b) { + return $a['ts'] - $b['ts']; + }); + + return $data0; + } + + protected function addEmptyDays(array $params): array { + $cnt = count($params); + $data = array_fill(0, $cnt, []); + + $request = $this->f3->get('REQUEST'); + $step = \Utils\Constants::get('CHART_RESOLUTION')[$this->getResolution($request)]; + // use offset shift because $startTs/$endTs compared with shifted ['ts'] + $offset = \Utils\TimeZones::getCurrentOperatorOffset(); + $dateRange = $this->getDatesRange($request, $offset); + + if (!$dateRange) { + $now = time() + $offset; + $week = 7 * 24 * 60 * 60; + if (count($params[0]) === 0) { + $dateRange = [ + 'endDate' => date('Y-m-d H:i:s', $now), + 'startDate' => date('Y-m-d 00:00:01', $now - $week), + ]; + } else { + $firstTs = ($now - $params[0][0] < $week) ? $now - $week : $params[0][0]; + $dateRange = [ + 'endDate' => date('Y-m-d H:i:s', $now), + 'startDate' => date('Y-m-d 00:00:01', $firstTs), + ]; + } + } + + $endTs = strtotime($dateRange['endDate']); + $startTs = strtotime($dateRange['startDate']); + + $endTs = $endTs - ($endTs % $step); + $startTs = $startTs - ($startTs % $step); + + $ox = $params[0]; + + while ($endTs >= $startTs) { + $itemIdx = array_search($startTs, $ox); + + $data[0][] = $startTs; + + for ($i = 1; $i < $cnt; ++$i) { + $data[$i][] = ($itemIdx !== false) ? $params[$i][$itemIdx] : 0; + } + + $startTs += $step; + } + + return $data; + } + + protected function execute(string $query, int $apiKey): array { + $request = $this->f3->get('REQUEST'); + + // do not use offset because :start_time/:end_time compared with UTC db timestamps + $dateRange = $this->getDatesRange($request); + + // Search request does not contain daterange param + if (!$dateRange) { + $dateRange = [ + 'endDate' => date('Y-m-d H:i:s'), + 'startDate' => date('Y-m-d H:i:s', 0), + ]; + } + + $offset = \Utils\TimeZones::getCurrentOperatorOffset(); + + $params = [ + ':api_key' => $apiKey, + ':end_time' => $dateRange['endDate'], + ':start_time' => $dateRange['startDate'], + ':resolution' => $this->getResolution($request), + ':offset' => strval($offset), // str for postgres + ]; + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/BaseEventsCount.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/BaseEventsCount.php new file mode 100644 index 0000000..5313cbe --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/BaseEventsCount.php @@ -0,0 +1,107 @@ +alertTypesParams, $this->alertFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('ALERT_EVENT_TYPES'), 'alert'); + [$this->editTypesParams, $this->editFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('EDITING_EVENT_TYPES'), 'edit'); + [$this->normalTypesParams, $this->normalFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('NORMAL_EVENT_TYPES'), 'normal'); + } + + public function getData(int $apiKey): array { + $itemsByDate = []; + $items = $this->getCounts($apiKey); + + foreach ($items as $item) { + $itemsByDate[$item['ts']] = [ + $item['event_normal_type_count'], + $item['event_editing_type_count'], + $item['event_alert_type_count'], + ]; + } + $request = $this->f3->get('REQUEST'); + // use offset shift because $startTs/$endTs compared with shifted ['ts'] + $offset = \Utils\TimeZones::getCurrentOperatorOffset(); + $datesRange = $this->getLatestNDatesRange(180, $offset); + $endTs = strtotime($datesRange['endDate']); + $startTs = strtotime($datesRange['startDate']); + $step = \Utils\Constants::get('CHART_RESOLUTION')[$this->getResolution($request)]; + + $endTs = $endTs - ($endTs % $step); + $startTs = $startTs - ($startTs % $step); + + while ($endTs >= $startTs) { + if (!isset($itemsByDate[$startTs])) { + $itemsByDate[$startTs] = [null, null, null]; + } + + $startTs += $step; + } + + ksort($itemsByDate); + + $ox = []; + $l1 = []; + $l2 = []; + $l3 = []; + + foreach ($itemsByDate as $key => $value) { + $ox[] = $key; + $l1[] = $value[0]; + $l2[] = $value[1]; + $l3[] = $value[2]; + } + + return [$ox, $l1, $l2, $l3]; + } + + protected function executeOnRangeById(string $query, int $apiKey): array { + $request = $this->f3->get('REQUEST'); + // do not use offset because :start_time/:end_time compared with UTC event.time + $dateRange = $this->getLatestNDatesRange(180); + $offset = \Utils\TimeZones::getCurrentOperatorOffset(); + + $params = [ + ':api_key' => $apiKey, + ':end_time' => $dateRange['endDate'], + ':start_time' => $dateRange['startDate'], + ':resolution' => $this->getResolution($request), + ':id' => $request['id'], + ':offset' => strval($offset), // str for postgres + ]; + + $params = array_merge($params, $this->alertTypesParams); + $params = array_merge($params, $this->editTypesParams); + $params = array_merge($params, $this->normalTypesParams); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Blacklist.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Blacklist.php new file mode 100644 index 0000000..8ae59c2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Blacklist.php @@ -0,0 +1,96 @@ +getFirstLine($apiKey); + + $timestamps = array_column($data, 'ts'); + $line1 = array_column($data, 'ts_new_records'); + + return $this->addEmptyDays([$timestamps, $line1]); + } + + private function getFirstLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, tbl.created + :offset))::bigint AS ts, + COUNT(*) AS ts_new_records + FROM ( + SELECT DISTINCT + blacklist.accountid, + blacklist.created, + extra.type, + CASE extra.type + WHEN \'ip\' THEN blacklist.ip + WHEN \'email\' THEN blacklist.email + WHEN \'phone\' THEN blacklist.phone + END AS value + + FROM + ( + SELECT + event_account.id AS accountid, + event_account.latest_decision AS created, + CASE WHEN event_ip.fraud_detected THEN split_part(event_ip.ip::text, \'/\', 1) ELSE NULL END AS ip, + event_ip.fraud_detected AS ip_fraud, + CASE WHEN event_email.fraud_detected THEN event_email.email ELSE NULL END AS email, + event_email.fraud_detected AS email_fraud, + CASE WHEN event_phone.fraud_detected THEN event_phone.phone_number ELSE NULL END AS phone, + event_phone.fraud_detected AS phone_fraud + FROM event + + LEFT JOIN event_account + ON event_account.id = event.account + + LEFT JOIN event_ip + ON event_ip.id = event.ip + + LEFT JOIN event_email + ON event_email.id = event.email + + LEFT JOIN event_phone + ON event_phone.id = event.phone + + WHERE + event_account.key = :api_key AND + event_account.fraud IS TRUE AND + event_account.latest_decision >= :start_time AND + event_account.latest_decision <= :end_time AND + ( + event_email.fraud_detected IS TRUE OR + event_ip.fraud_detected IS TRUE OR + event_phone.fraud_detected IS TRUE + ) + ) AS blacklist, + LATERAL ( + VALUES + (CASE WHEN ip_fraud = true THEN \'ip\' END), + (CASE WHEN email_fraud = true THEN \'email\' END), + (CASE WHEN phone_fraud = true THEN \'phone\' END) + ) AS extra(type) + + WHERE + extra.type IS NOT NULL + ) AS tbl + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Bot.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Bot.php new file mode 100644 index 0000000..c1e9619 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Bot.php @@ -0,0 +1,48 @@ +normalFlatIds}) THEN TRUE END) AS event_normal_type_count, + COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count, + COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count + + FROM + event + + INNER JOIN event_device + ON (event.device = event_device.id) + + INNER JOIN event_ua_parsed + ON (event_device.user_agent = event_ua_parsed.id) + + WHERE + event_ua_parsed.id = :id AND + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts" + ); + + return $this->executeOnRangeById($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Bots.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Bots.php new file mode 100644 index 0000000..e55fe8e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Bots.php @@ -0,0 +1,56 @@ +getFirstLine($apiKey); + + $ox = array_column($data, 'ts'); + $l1 = array_column($data, 'bot_count'); + + return $this->addEmptyDays([$ox, $l1]); + } + + private function getFirstLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts, + COUNT(DISTINCT ( + CASE WHEN event_ua_parsed.modified IS TRUE THEN event.device END) + ) AS bot_count + FROM + event + + INNER JOIN event_device + ON(event.device = event_device.id) + INNER JOIN event_ua_parsed + ON(event_device.user_agent=event_ua_parsed.id) + + WHERE + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Country.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Country.php new file mode 100644 index 0000000..b17320f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Country.php @@ -0,0 +1,48 @@ +normalFlatIds}) THEN TRUE END) AS event_normal_type_count, + COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count, + COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count + + FROM + event + + INNER JOIN event_ip + ON (event.ip = event_ip.id) + + INNER JOIN countries + ON (event_ip.country = countries.id) + + WHERE + countries.id = :id AND + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts" + ); + + return $this->executeOnRangeById($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Domain.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Domain.php new file mode 100644 index 0000000..bbcc573 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Domain.php @@ -0,0 +1,45 @@ +normalFlatIds}) THEN TRUE END) AS event_normal_type_count, + COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count, + COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count + + FROM + event + + INNER JOIN event_email + ON (event.email = event_email.id) + + WHERE + event_email.domain = :id AND + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts" + ); + + return $this->executeOnRangeById($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Domains.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Domains.php new file mode 100644 index 0000000..8cc3cee --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Domains.php @@ -0,0 +1,80 @@ +getFirstLine($apiKey); + + $field2 = 'ts_new_domains'; + $data2 = $this->getSecondLine($apiKey); + + $data0 = $this->concatDataLines($data1, $field1, $data2, $field2); + + $indexedData = array_values($data0); + $timestamps = array_column($indexedData, 'ts'); + $line1 = array_column($indexedData, $field1); + $line2 = array_column($indexedData, $field2); + + return $this->addEmptyDays([$timestamps, $line1, $line2]); + } + + private function getFirstLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts, + COUNT(DISTINCT event_email.domain) AS unique_domains_count + FROM + event + + INNER JOIN event_email + ON (event.email = event_email.id) + + WHERE + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey); + } + + private function getSecondLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event_domain.created + :offset))::bigint AS ts, + COUNT(event_domain.id) AS ts_new_domains + FROM + event_domain + + WHERE + event_domain.key = :api_key AND + event_domain.created >= :start_time AND + event_domain.created <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey, false); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Emails.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Emails.php new file mode 100644 index 0000000..b8a7694 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Emails.php @@ -0,0 +1,56 @@ +getFirstLine($apiKey); + + $timestamps = array_column($data, 'ts'); + $line1 = array_column($data, 'email_count'); + $line2 = array_column($data, 'blockemails_count'); + + return $this->addEmptyDays([$timestamps, $line1, $line2]); + } + + private function getFirstLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts, + COUNT(DISTINCT event.email) AS email_count, + COUNT(DISTINCT ( + CASE WHEN event_email.blockemails IS TRUE THEN event.email END) + ) AS blockemails_count + FROM + event + + INNER JOIN event_email + ON (event.email = event_email.id) + + WHERE + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Events.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Events.php new file mode 100644 index 0000000..131ca7b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Events.php @@ -0,0 +1,83 @@ +getFirstLine($apiKey); + + $timestamps = array_column($data, 'ts'); + $line1 = array_column($data, 'event_normal_type_count'); + $line2 = array_column($data, 'event_editing_type_count'); + $line3 = array_column($data, 'event_alert_type_count'); + $line4 = array_column($data, 'unauthorized_event_count'); + + return $this->addEmptyDays([$timestamps, $line1, $line2, $line3, $line4]); + } + + private function getFirstLine(int $apiKey): array { + $request = $this->f3->get('REQUEST'); + $dateRange = $this->getDatesRange($request); + if (!$dateRange) { + $dateRange = [ + 'endDate' => date('Y-m-d H:i:s'), + 'startDate' => date('Y-m-d H:i:s', 0), + ]; + } + $offset = \Utils\TimeZones::getCurrentOperatorOffset(); + [$alertTypesParams, $alertFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('ALERT_EVENT_TYPES'), 'alert'); + [$editTypesParams, $editFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('EDITING_EVENT_TYPES'), 'edit'); + [$normalTypesParams, $normalFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('NORMAL_EVENT_TYPES'), 'normal'); + $params = [ + ':api_key' => $apiKey, + ':end_time' => $dateRange['endDate'], + ':start_time' => $dateRange['startDate'], + ':resolution' => $this->getResolution($request), + ':offset' => strval($offset), + ':unauth' => \Utils\Constants::get('UNAUTHORIZED_USERID'), + ]; + $params = array_merge($params, $alertTypesParams); + $params = array_merge($params, $editTypesParams); + $params = array_merge($params, $normalTypesParams); + + $query = ( + "SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts, + COUNT(CASE WHEN event.type IN ({$normalFlatIds}) THEN TRUE END) AS event_normal_type_count, + COUNT(CASE WHEN event.type IN ({$editFlatIds}) THEN TRUE END) AS event_editing_type_count, + COUNT(CASE WHEN event.type IN ({$alertFlatIds}) THEN TRUE END) AS event_alert_type_count, + COUNT(CASE WHEN event_account.userid = :unauth THEN TRUE END) AS unauthorized_event_count + + FROM + event + + LEFT JOIN event_account + ON event.account = event_account.id + + WHERE + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Ip.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Ip.php new file mode 100644 index 0000000..0ab0159 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Ip.php @@ -0,0 +1,42 @@ +normalFlatIds}) THEN TRUE END) AS event_normal_type_count, + COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count, + COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count + + FROM + event + + WHERE + event.ip = :id AND + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts" + ); + + return $this->executeOnRangeById($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Ips.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Ips.php new file mode 100644 index 0000000..4b75ce4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Ips.php @@ -0,0 +1,84 @@ +getFirstLine($apiKey); + + $timestamps = array_column($data, 'ts'); + $line1 = array_column($data, 'residence_ip_count'); + $line2 = array_column($data, 'total_privacy'); + $line3 = array_column($data, 'suspicious_ip_count'); + + return $this->addEmptyDays([$timestamps, $line1, $line2, $line3]); + } + + private function getFirstLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts, + COUNT(DISTINCT event.ip) AS unique_ip_count, + + COUNT(DISTINCT + CASE + WHEN event_ip.data_center IS TRUE OR + event_ip.tor IS TRUE OR + event_ip.vpn IS TRUE + THEN event.ip + ELSE NULL + END + ) AS total_privacy, + + COUNT(DISTINCT event.ip) - COUNT(DISTINCT + CASE + WHEN event_ip.data_center IS TRUE OR + event_ip.tor IS TRUE OR + event_ip.vpn IS TRUE + THEN event.ip + ELSE NULL + END + ) AS residence_ip_count, + + COUNT(DISTINCT + CASE + WHEN event_ip.blocklist IS TRUE OR + event_ip.fraud_detected IS TRUE + THEN event.ip + ELSE NULL + END + ) AS suspicious_ip_count + + FROM + event + + INNER JOIN event_ip + ON (event.ip = event_ip.id) + + WHERE + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Isp.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Isp.php new file mode 100644 index 0000000..2604f29 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Isp.php @@ -0,0 +1,48 @@ +normalFlatIds}) THEN TRUE END) AS event_normal_type_count, + COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count, + COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count + + FROM + event + + INNER JOIN event_ip + ON (event.ip = event_ip.id) + + LEFT JOIN event_isp + ON (event_ip.isp = event_isp.id) + + WHERE + event_isp.id = :id AND + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts" + ); + + return $this->executeOnRangeById($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Isps.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Isps.php new file mode 100644 index 0000000..d9061b1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Isps.php @@ -0,0 +1,83 @@ +getFirstLine($apiKey); + + $field2 = 'ts_new_isps'; + $data2 = $this->getSecondLine($apiKey); + + $data0 = $this->concatDataLines($data1, $field1, $data2, $field2); + $indexedData = array_values($data0); + + $timestamps = array_column($indexedData, 'ts'); + $line1 = array_column($indexedData, $field1); + $line2 = array_column($indexedData, $field2); + + return $this->addEmptyDays([$timestamps, $line1, $line2]); + } + + private function getFirstLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts, + COUNT(DISTINCT event_isp.id) AS unique_isps_count + FROM + event + + INNER JOIN event_ip + ON (event.ip = event_ip.id) + + LEFT JOIN event_isp + ON (event_ip.isp = event_isp.id) + + WHERE + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey); + } + + private function getSecondLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event_isp.created + :offset))::bigint AS ts, + COUNT(event_isp.id) AS ts_new_isps + FROM + event_isp + + WHERE + event_isp.key = :api_key AND + event_isp.created >= :start_time AND + event_isp.created <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey, false); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Logbook.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Logbook.php new file mode 100644 index 0000000..4284e61 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Logbook.php @@ -0,0 +1,82 @@ +getFirstLine($apiKey); + + $timestamps = array_column($data, 'ts'); + $line1 = array_column($data, 'event_normal_type_count'); + $line2 = array_column($data, 'event_issued_type_count'); + $line3 = array_column($data, 'event_failed_type_count'); + + return $this->addEmptyDays([$timestamps, $line1, $line2, $line3]); + } + + private function getFirstLine(int $apiKey): array { + $request = $this->f3->get('REQUEST'); + $dateRange = $this->getDatesRange($request); + if (!$dateRange) { + $dateRange = [ + 'endDate' => date('Y-m-d H:i:s'), + 'startDate' => date('Y-m-d H:i:s', 0), + ]; + } + + //$dateRange['endDate'] = \Utils\TimeZones::localizeForActiveOperator($dateRange['endDate']); + //$dateRange['startDate'] = \Utils\TimeZones::localizeForActiveOperator($dateRange['startDate']); + + [$failedTypesParams, $failedFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('FAILED_LOGBOOK_EVENT_TYPES'), 'failed'); + [$issuedTypesParams, $issuedFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('ISSUED_LOGBOOK_EVENT_TYPES'), 'issued'); + [$normalTypesParams, $normalFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('NORMAL_LOGBOOK_EVENT_TYPES'), 'normal'); + $params = [ + ':api_key' => $apiKey, + ':end_time' => $dateRange['endDate'], + ':start_time' => $dateRange['startDate'], + ':resolution' => $this->getResolution($request), + ]; + $params = array_merge($params, $failedTypesParams); + $params = array_merge($params, $issuedTypesParams); + $params = array_merge($params, $normalTypesParams); + + $query = ( + "SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event_logbook.started))::bigint AS ts, + COUNT(CASE WHEN event_error_type.value IN ({$normalFlatIds}) THEN TRUE END) AS event_normal_type_count, + COUNT(CASE WHEN event_error_type.value IN ({$issuedFlatIds}) THEN TRUE END) AS event_issued_type_count, + COUNT(CASE WHEN event_error_type.value IN ({$failedFlatIds}) THEN TRUE END) AS event_failed_type_count + + FROM + event_logbook + + LEFT JOIN event_error_type + ON event_logbook.error_type = event_error_type.id + + WHERE + event_logbook.key = :api_key AND + event_logbook.started >= :start_time AND + event_logbook.started <= :end_time + + GROUP BY ts + ORDER BY ts" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Phones.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Phones.php new file mode 100644 index 0000000..e358c00 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Phones.php @@ -0,0 +1,49 @@ +getFirstLine($apiKey); + + $timestamps = array_column($data, 'ts'); + $line1 = array_column($data, 'phone_count'); + + return $this->addEmptyDays([$timestamps, $line1]); + } + + private function getFirstLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event_phone.lastseen + :offset))::bigint AS ts, + COUNT(*) AS phone_count + FROM + event_phone + + WHERE + event_phone.key = :api_key AND + event_phone.lastseen >= :start_time AND + event_phone.lastseen <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Resource.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Resource.php new file mode 100644 index 0000000..b56a363 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Resource.php @@ -0,0 +1,42 @@ +normalFlatIds}) THEN TRUE END) AS event_normal_type_count, + COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count, + COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count + + FROM + event + + WHERE + event.url = :id AND + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts" + ); + + return $this->executeOnRangeById($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Resources.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Resources.php new file mode 100644 index 0000000..c420bd9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Resources.php @@ -0,0 +1,64 @@ +getFirstLine($apiKey); + + $timestamps = array_column($data, 'ts'); + $line1 = array_column($data, 'count_200'); + $line2 = array_column($data, 'count_404'); + $line3 = array_column($data, 'count_500'); + + return $this->addEmptyDays([$timestamps, $line1, $line2, $line3]); + } + + private function getFirstLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts, + COUNT(DISTINCT event.id) AS url_count, + + COUNT(DISTINCT ( + CASE WHEN event.http_code=200 OR event.http_code IS NULL THEN event.id END) + ) AS count_200, + + COUNT(DISTINCT ( + CASE WHEN event.http_code = 404 THEN event.id END) + ) AS count_404, + + COUNT(DISTINCT ( + CASE WHEN event.http_code IN(403, 500) THEN event.id END) + ) AS count_500 + + FROM + event + + WHERE + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/ReviewQueue.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/ReviewQueue.php new file mode 100644 index 0000000..0896260 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/ReviewQueue.php @@ -0,0 +1,106 @@ +getFirstLine($apiKey); + + $field2 = 'ts_new_added_to_review'; + $data2 = $this->getSecondLine($apiKey); + + $field3 = 'ts_new_users_blacklisted'; + $data3 = $this->getThirdLine($apiKey); + + $data0 = $this->concatDataLines($data1, $field1, $data2, $field2, $data3, $field3); + + $indexedData = array_values($data0); + $timestamps = array_column($indexedData, 'ts'); + $line1 = array_column($indexedData, $field1); + $line2 = array_column($indexedData, $field2); + $line3 = array_column($indexedData, $field3); + + return $this->addEmptyDays([$timestamps, $line1, $line2, $line3]); + } + + private function getFirstLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event_account.latest_decision + :offset))::bigint AS ts, + COUNT(event_account.id) as ts_new_users_whitelisted + FROM + event_account + + WHERE + event_account.key = :api_key AND + event_account.fraud IS FALSE AND + event_account.latest_decision >= :start_time AND + event_account.latest_decision <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey, false); + } + + private function getSecondLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event_account.added_to_review + :offset))::bigint AS ts, + COUNT(event_account.id) AS ts_new_added_to_review + FROM + event_account + + WHERE + event_account.key = :api_key AND + event_account.added_to_review IS NOT NULL AND + event_account.added_to_review >= :start_time AND + event_account.added_to_review <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey, false); + } + + private function getThirdLine(int $apiKey): array { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event_account.latest_decision + :offset))::bigint AS ts, + COUNT(event_account.id) as ts_new_users_blacklisted + FROM + event_account + + WHERE + event_account.key = :api_key AND + event_account.fraud IS TRUE AND + event_account.latest_decision >= :start_time AND + event_account.latest_decision <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->execute($query, $apiKey, false); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/SessionStat.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/SessionStat.php new file mode 100644 index 0000000..289c7d8 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/SessionStat.php @@ -0,0 +1,150 @@ +getCounts($apiKey); + + foreach ($items as $item) { + $itemsByDate[$item['ts']] = [ + $item['new_device_count_sum'], + $item['new_ip_count_sum'], + $item['event_session_cnt'], + $item['event_count_max'], + ]; + } + + $request = $this->f3->get('REQUEST'); + // use offset shift because $startTs/$endTs compared with shifted ['ts'] + $offset = \Utils\TimeZones::getCurrentOperatorOffset(); + $datesRange = $this->getLatestNDatesRange(14, $offset); + $endTs = strtotime($datesRange['endDate']); + $startTs = strtotime($datesRange['startDate']); + $step = \Utils\Constants::get('CHART_RESOLUTION')[$this->getResolution($request)]; + + $endTs = $endTs - ($endTs % $step); + $startTs = $startTs - ($startTs % $step); + + $midTs = $startTs + $step * 7; + + foreach ($itemsByDate as $ts => $value) { + if ($ts <= $midTs) { + $newTs = $ts + $step * 7; + if (!array_key_exists($newTs, $itemsByDate)) { + $itemsByDate[$newTs] = [ + 0, 0, 0, 0, + $value[0], $value[1], $value[2], $value[3], + ]; + } else { + $itemsByDate[$newTs][] = $value[0]; + $itemsByDate[$newTs][] = $value[1]; + $itemsByDate[$newTs][] = $value[2]; + $itemsByDate[$newTs][] = $value[3]; + } + } + } + + while ($endTs >= $startTs) { + if (!isset($itemsByDate[$startTs]) && $startTs > $midTs) { + $itemsByDate[$startTs] = [0, 0, 0, 0, 0, 0, 0, 0]; + } elseif (isset($itemsByDate[$startTs]) && count($itemsByDate[$startTs]) === 4) { + $itemsByDate[$startTs][] = 0; + $itemsByDate[$startTs][] = 0; + $itemsByDate[$startTs][] = 0; + $itemsByDate[$startTs][] = 0; + } + + $startTs += $step; + } + + foreach (array_keys($itemsByDate) as $key) { + if ($key <= $midTs) { + unset($itemsByDate[$key]); + } + } + + ksort($itemsByDate); + + $result = [array_keys($itemsByDate)]; + + for ($i = 0; $i < 8; ++$i) { + $result[] = array_column($itemsByDate, $i); + } + + return $result; + } + + protected function executeOnRangeById(string $query, int $apiKey): array { + $request = $this->f3->get('REQUEST'); + // do not use offset because :start_time/:end_time compared with UTC event.time + $dateRange = $this->getLatestNDatesRange(14); + $offset = \Utils\TimeZones::getCurrentOperatorOffset(); + + $params = [ + ':api_key' => $apiKey, + ':end_time' => $dateRange['endDate'], + ':start_time' => $dateRange['startDate'], + //':resolution' => $this->getResolution($request), + ':resolution' => 60 * 60 * 24, + ':id' => $request['id'], + ':offset' => strval($offset), // str for postgres + ]; + + return $this->execQuery($query, $params); + } + + private function getCounts(int $apiKey): array { + $query = ( + 'SELECT + ((EXTRACT(EPOCH FROM event_session.created)::bigint + :offset::bigint) / :resolution) * :resolution as ts, + + COUNT(event_session.id) AS event_session_cnt, + MAX(event_session_stat.event_count) AS event_count_max, + FLOOR(AVG(event_session_stat.event_count))::int AS event_count_avg, + MAX(event_session_stat.device_count) AS device_count_max, + FLOOR(AVG(event_session_stat.device_count))::int AS device_count_avg, + MAX(event_session_stat.ip_count) AS ip_count_max, + FLOOR(AVG(event_session_stat.ip_count))::int AS ip_count_avg, + MAX(event_session_stat.country_count) AS country_count_max, + FLOOR(AVG(event_session_stat.country_count))::int AS country_count_avg, + SUM(event_session_stat.new_ip_count) AS new_ip_count_sum, + SUM(event_session_stat.new_device_count) AS new_device_count_sum, + jsonb_agg(event_session_stat.event_types) AS event_types, + jsonb_agg(event_session_stat.http_codes) AS http_codes + + FROM + event_session + + LEFT JOIN event_session_stat + ON event_session.id = event_session_stat.session_id + + WHERE + event_session.account_id = :id AND + event_session.key = :api_key AND + event_session.created >= :start_time AND + event_session.created <= :end_time + + GROUP BY ts + ORDER BY ts' + ); + + return $this->executeOnRangeById($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/User.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/User.php new file mode 100644 index 0000000..2bcecac --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/User.php @@ -0,0 +1,45 @@ +normalFlatIds}) THEN TRUE END) AS event_normal_type_count, + COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count, + COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count + + FROM + event + + INNER JOIN event_account + ON (event.account = event_account.id) + + WHERE + event_account.id = :id AND + event.key = :api_key AND + event.time >= :start_time AND + event.time <= :end_time + + GROUP BY ts + ORDER BY ts" + ); + + return $this->executeOnRangeById($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Users.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Users.php new file mode 100644 index 0000000..cc1535f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Users.php @@ -0,0 +1,89 @@ +getFirstLine($apiKey); + $iters = count($data1); + + for ($i = 0; $i < $iters; ++$i) { + $item = $data1[$i]; + $ts = $item['ts']; + $score = $item['score']; + + if (!isset($data0[$ts])) { + $data0[$ts] = [ + 'ts' => $ts, + 'ts_new_users_with_trust_score_high' => 0, + 'ts_new_users_with_trust_score_medium' => 0, + 'ts_new_users_with_trust_score_low' => 0, + ]; + } + + $inf = \Utils\Constants::get('USER_HIGH_SCORE_INF'); + if ($score >= \Utils\Constants::get('USER_HIGH_SCORE_INF')) { + ++$data0[$ts]['ts_new_users_with_trust_score_high']; + } + + $inf = \Utils\Constants::get('USER_MEDIUM_SCORE_INF'); + $sup = \Utils\Constants::get('USER_MEDIUM_SCORE_SUP'); + if ($score >= $inf && $score < $sup) { + ++$data0[$ts]['ts_new_users_with_trust_score_medium']; + } + + $inf = \Utils\Constants::get('USER_LOW_SCORE_INF'); + $sup = \Utils\Constants::get('USER_LOW_SCORE_SUP'); + if ($score >= $inf && $score < $sup) { + ++$data0[$ts]['ts_new_users_with_trust_score_low']; + } + } + + $indexedData = array_values($data0); + $timestamps = array_column($indexedData, 'ts'); + $line1 = array_column($indexedData, 'ts_new_users_with_trust_score_high'); + $line2 = array_column($indexedData, 'ts_new_users_with_trust_score_medium'); + $line3 = array_column($indexedData, 'ts_new_users_with_trust_score_low'); + + return $this->addEmptyDays([$timestamps, $line1, $line2, $line3]); + } + + private function getFirstLine(int $apiKey) { + $query = ( + 'SELECT + EXTRACT(EPOCH FROM date_trunc(:resolution, event_account.created + :offset))::bigint AS ts, + event_account.id, + event_account.score + FROM + event_account + + WHERE + event_account.key = :api_key AND + event_account.created >= :start_time AND + event_account.created <= :end_time + + GROUP BY ts, event_account.id + ORDER BY ts' + ); + + return $this->execute($query, $apiKey); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Watchlist.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Watchlist.php new file mode 100644 index 0000000..044b085 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/Watchlist.php @@ -0,0 +1,61 @@ +getRequestParams($apiKey); + $params[':users_ids'] = $this->userIds; + + $query = ( + "SELECT + TEXT(date_trunc('day', event.time)) AS day, + COUNT(event.id) AS event_count + FROM + event + + INNER JOIN event_account + ON (event.account = event_account.id) + + INNER JOIN event_url + ON (event.url = event_url.id) + + INNER JOIN event_ip + ON (event.ip = event_ip.id) + + INNER JOIN countries + ON (event_ip.country = countries.id) + + WHERE + event.key = :api_key + %s + + GROUP BY day + ORDER BY day" + ); + //$request = $this->f3->get('REQUEST'); + //$dateRange = $this->getDatesRange($request); + + return $this->execQuery($query, $params); + } + + public function setUsersIds(array $userIds): void { + $this->userIds = $userIds; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Chart/index.php @@ -0,0 +1,3 @@ +getArrayPlaceholders($accountIds); + $params[':api_key'] = $apiKey; + + return [$params, $placeHolders]; + } + + protected function groupRecordsByAccount(array $records): array { + $recordsByAccount = []; + $iters = count($records); + + for ($i = 0; $i < $iters; ++$i) { + $item = $records[$i]; + $accountId = $item['accountid']; + + if (!isset($recordsByAccount[$accountId])) { + $recordsByAccount[$accountId] = []; + } + + $recordsByAccount[$accountId][] = $item; + } + + return $recordsByAccount; + } + + protected function getUniqueArray(array $array): array { + return array_values(array_unique($array)); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Data.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Data.php new file mode 100644 index 0000000..6b65c78 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Data.php @@ -0,0 +1,353 @@ +userModel = new User(); + $this->ipModel = new Ip(); + $this->deviceModel = new Device(); + $this->emailModel = new Email(); + $this->phoneModel = new Phone(); + $this->eventModel = new Event(); + $this->sessionModel = new Session(); + $this->keyModel = new \Models\ApiKeys(); + + $this->suspiciousWordsUrl = \Utils\WordsLists\Url::getWords(); + $this->suspiciousWordsUserAgent = \Utils\WordsLists\UserAgent::getWords(); + $this->suspiciousWordsEmail = \Utils\WordsLists\Email::getWords(); + } + + public function getContext(array $accountIds, int $apiKey): array { + $userDetails = $this->userModel->getContext($accountIds, $apiKey); + $ipDetails = $this->ipModel->getContext($accountIds, $apiKey); + $deviceDetails = $this->deviceModel->getContext($accountIds, $apiKey); + $emailDetails = $this->emailModel->getContext($accountIds, $apiKey); + $phoneDetails = $this->phoneModel->getContext($accountIds, $apiKey); + $eventDetails = $this->eventModel->getContext($accountIds, $apiKey); + //$domainDetails = $this->domainModel->getContext($accountIds, $apiKey); + + $timezoneName = $this->keyModel->getTimezoneByKeyId($apiKey); + $utcTime = new \DateTime('now', new \DateTimeZone('UTC')); + $timezone = new \DateTimeZone($timezoneName); + $offsetInSeconds = $timezone->getOffset($utcTime); + + // get only suspicious sessions + $sessionDetails = $this->sessionModel->getContext($accountIds, $apiKey, $offsetInSeconds); + + //Extend user details context + foreach ($userDetails as $userId => $user) { + $user['le_exists'] = ($user['le_email'] ?? null) !== null; + $user['le_email'] = $user['le_email'] ?? ''; + $user['le_local_part'] = explode('@', $user['le_email'])[0] ?? ''; + $user['le_domain_part'] = explode('@', $user['le_email'])[1] ?? ''; + + $userId = $user['ea_id']; + $ip = $ipDetails[$userId] ?? []; + $device = $deviceDetails[$userId] ?? []; + $email = $emailDetails[$userId] ?? []; + $phone = $phoneDetails[$userId] ?? []; + $events = $eventDetails[$userId] ?? []; + $session = $sessionDetails[$userId] ?? []; + + $user['eip_ip_id'] = $ip['eip_ip_id'] ?? []; + $user['eip_ip'] = $ip['eip_ip'] ?? []; + $user['eip_cidr'] = $ip['eip_cidr'] ?? []; + $user['eip_country_id'] = $ip['eip_country_id'] ?? []; + $user['eip_data_center'] = $ip['eip_data_center'] ?? []; + $user['eip_tor'] = $ip['eip_tor'] ?? []; + $user['eip_vpn'] = $ip['eip_vpn'] ?? []; + $user['eip_relay'] = $ip['eip_relay'] ?? []; + $user['eip_starlink'] = $ip['eip_starlink'] ?? []; + $user['eip_total_visit'] = $ip['eip_total_visit'] ?? []; + $user['eip_blocklist'] = $ip['eip_blocklist'] ?? []; + $user['eip_shared'] = $ip['eip_shared'] ?? []; + //$user['eip_domains'] = $ip['eip_domains'] ?? []; + $user['eip_country_id'] = $ip['eip_country_id'] ?? []; + $user['eip_fraud_detected'] = $ip['eip_fraud_detected'] ?? []; + $user['eip_alert_list'] = $ip['eip_alert_list'] ?? []; + $user['eip_domains_count_len'] = $ip['eip_domains_count_len'] ?? []; + + $user['eup_device'] = $device['eup_device'] ?? []; + $user['eup_device_id'] = $device['eup_device_id'] ?? []; + $user['eup_browser_name'] = $device['eup_browser_name'] ?? []; + $user['eup_browser_version'] = $device['eup_browser_version'] ?? []; + $user['eup_os_name'] = $device['eup_os_name'] ?? []; + $user['eup_lang'] = $device['eup_lang'] ?? []; + $user['eup_ua'] = $device['eup_ua'] ?? []; + // $user['eup_lastseen'] = $device['eup_lastseen'] ?? []; + // $user['eup_created'] = $device['eup_created'] ?? []; + + $user['ee_email'] = $email['ee_email'] ?? []; + $user['ee_earliest_breach'] = $email['ee_earliest_breach'] ?? []; + + $user['ep_phone_number'] = $phone['ep_phone_number'] ?? []; + $user['ep_shared'] = $phone['ep_shared'] ?? []; + $user['ep_type'] = $phone['ep_type'] ?? []; + + $user['event_ip'] = $events['event_ip'] ?? []; + $user['event_url_string'] = $events['event_url_string'] ?? []; + $user['event_empty_referer'] = $events['event_empty_referer'] ?? []; + $user['event_device'] = $events['event_device'] ?? []; + $user['event_type'] = $events['event_type'] ?? []; + $user['event_http_method'] = $events['event_http_method'] ?? []; + $user['event_http_code'] = $events['event_http_code'] ?? []; + $user['event_device_created'] = $events['event_device_created'] ?? []; + $user['event_device_lastseen'] = $events['event_device_lastseen'] ?? []; + + $user['event_session_multiple_country'] = $session[0]['event_session_multiple_country'] ?? false; + $user['event_session_multiple_ip'] = $session[0]['event_session_multiple_ip'] ?? false; + $user['event_session_multiple_device'] = $session[0]['event_session_multiple_device'] ?? false; + $user['event_session_night_time'] = $session[0]['event_session_night_time'] ?? false; + + //Extra params for rules + $user = $this->extendParams($user); + + $userDetails[$userId] = $this->extendEventParams($user); + } + + return $userDetails; + } + + private function extendParams(array $record): array { + //$record['timezone'] + + $localPartLen = strlen($record['le_local_part']); + $domainPartLen = strlen($record['le_domain_part']); + $fullName = $this->getUserFullName($record); + + $record['le_local_part_len'] = $localPartLen; + $record['ea_fullname_has_numbers'] = preg_match('~[0-9]+~', $fullName) > 0; + $record['ea_fullname_has_spaces_hyphens'] = preg_match('~[\-\s]~', $fullName) > 0; + $record['ea_days_since_account_creation'] = $this->getDaysSinceAccountCreation($record); + $record['ea_days_since_last_visit'] = $this->getDaysSinceLastVisit($record); + + //$record['le_has_no_profiles'] = $record['le_profiles'] === 0; + $record['le_has_no_data_breaches'] = $record['le_data_breach'] === false; + $record['le_has_suspicious_str'] = $this->checkEmailForSuspiciousString($record); + $record['le_has_numeric_only_local_part'] = preg_match('/^[0-9]+$/', $record['le_local_part']) > 0; + $record['le_email_has_consec_s_chars'] = preg_match('/[^a-zA-Z0-9]{2,}/', $record['le_local_part']) > 0; + $record['le_email_has_consec_nums'] = preg_match('/\d{2}/', $record['le_local_part']) > 0; + $record['le_email_has_no_digits'] = !preg_match('/\d/', $record['le_local_part']); + $record['le_email_has_vowels'] = preg_match('/[aeoui]/i', $record['le_local_part']) > 0; + $record['le_email_has_consonants'] = preg_match('/[bcdfghjklmnpqrstvwxyz]/i', $record['le_local_part']) > 0; + + $record['le_with_long_local_part_length'] = $localPartLen > \Utils\Constants::get('RULE_EMAIL_MAXIMUM_LOCAL_PART_LENGTH'); + $record['le_with_long_domain_length'] = $domainPartLen > \Utils\Constants::get('RULE_EMAIL_MAXIMUM_DOMAIN_LENGTH'); + $record['le_email_in_blockemails'] = $record['le_blockemails'] ?? false; + $record['le_is_invalid'] = $record['le_exists'] && filter_var($record['le_email'], FILTER_VALIDATE_EMAIL) === false; + + $record['le_appears_on_alert_list'] = $record['le_alert_list'] ?? false; + + $record['ld_is_disposable'] = $record['ld_disposable_domains'] ?? false; + $record['ld_days_since_domain_creation'] = $this->getDaysSinceDomainCreation($record); + $record['ld_domain_free_email_provider'] = $record['ld_free_email_provider'] ?? false; + $record['ld_from_blockdomains'] = $record['ld_blockdomains'] ?? false; + $record['ld_domain_without_mx_record'] = $record['ld_mx_record'] === false; + $record['ld_website_is_disabled'] = $record['ld_disabled'] ?? false; + $record['ld_tranco_rank'] = $record['ld_tranco_rank'] ?? -1; + + $record['lp_invalid_phone'] = $record['lp_invalid'] === true; + $record['ep_shared_phone'] = (bool) count(array_filter($record['ep_shared'], static function ($item) { + return $item !== null && $item > 1; + })); + + $daysSinceBreaches = array_map(function ($item) { + return $this->getDaysTillToday($item); + }, $record['ee_earliest_breach']); + + $record['ee_days_since_first_breach'] = count($daysSinceBreaches) ? max($daysSinceBreaches) : -1; + + $onlyNonResidentialParams = !(bool) count(array_filter(array_merge( + $record['eip_fraud_detected'], + $record['eip_blocklist'], + $record['eip_tor'], + $record['eip_starlink'], + $record['eip_relay'], + $record['eip_vpn'], + $record['eip_data_center'], + ), static function ($value): bool { + return $value === true; + })); + $record['eip_only_residential'] = $onlyNonResidentialParams && !in_array(0, $record['eip_country_id']); + $record['eip_has_fraud'] = in_array(true, $record['eip_fraud_detected']); + $record['eip_unique_cidrs'] = count(array_unique($record['eip_cidr'])); + $record['lp_fraud_detected'] = $record['lp_fraud_detected'] ?? false; + $record['le_fraud_detected'] = $record['le_fraud_detected'] ?? false; + + $record['eup_has_rare_browser'] = (bool) count(array_diff($record['eup_browser_name'], array_keys(\Utils\Constants::get('RULE_REGULAR_BROWSER_NAMES')))); + $record['eup_has_rare_os'] = (bool) count(array_diff($record['eup_os_name'], \Utils\Constants::get('RULE_REGULAR_OS_NAMES'))); + $record['eup_device_count'] = count($record['eup_device']); + + $record['eup_vulnerable_ua'] = false; + + if (count($this->suspiciousWordsUserAgent)) { + foreach ($record['eup_ua'] as $url) { + foreach ($this->suspiciousWordsUserAgent as $sub) { + if (stripos($url, $sub) !== false) { + $record['eup_vulnerable_ua'] = true; + break 2; + } + } + } + } + + return $record; + } + + private function extendEventParams(array $record): array { + // Remove null values specifically + $eventTypeFiltered = $this->filterStringNum($record['event_type']); + $eventHttpCodeFiltered = $this->filterStringNum($record['event_http_code']); + + $eventTypeCount = array_count_values($eventTypeFiltered); + + //$accountLoginFailId = \Utils\Constants::get('ACCOUNT_LOGIN_FAIL_EVENT_TYPE_ID'); + $accountEmailChangeId = \Utils\Constants::get('ACCOUNT_EMAIL_CHANGE_EVENT_TYPE_ID'); + $accountPwdChangeId = \Utils\Constants::get('ACCOUNT_PASSWORD_CHANGE_EVENT_TYPE_ID'); + + //$record['event_failed_login_attempts'] = $eventTypeCount[$accountLoginFailId] ?? 0; + $record['event_email_changed'] = array_key_exists($accountEmailChangeId, $eventTypeCount); + $record['event_password_changed'] = array_key_exists($accountPwdChangeId, $eventTypeCount); + + $record['event_http_method_head'] = in_array(\Utils\Constants::get('EVENT_REQUEST_TYPE_HEAD'), $record['event_http_method']); + + $record['event_empty_referer'] = in_array(true, $record['event_empty_referer'], true); + + $clientErrors = 0; + $serverErrors = 0; + $successEvents = 0; + foreach ($eventHttpCodeFiltered as $code) { + if (is_int($code) && $code >= 400 && $code < 500) { + ++$clientErrors; + } elseif (is_int($code) && $code >= 500 && $code < 600) { + ++$serverErrors; + } elseif (is_int($code) && $code >= 200 && $code < 300) { + ++$successEvents; + } + } + + $record['event_multiple_5xx_http'] = $serverErrors; + $record['event_multiple_4xx_http'] = $clientErrors; + + $record['event_2xx_http'] = (bool) $successEvents; + + $record['event_vulnerable_url'] = false; + + if (count($this->suspiciousWordsUrl)) { + foreach ($record['event_url_string'] as $url) { + foreach ($this->suspiciousWordsUrl as $sub) { + if (stripos($url, $sub) !== false) { + $record['event_vulnerable_url'] = true; + break 2; + } + } + } + } + + return $record; + } + + private function getDaysSinceDomainCreation(array $params): int { + $dt1 = date('Y-m-d'); + $dt2 = $params['ld_creation_date']; + + return $this->getDaysDiff($dt1, $dt2); + } + + private function getDaysSinceAccountCreation(array $params): int { + $dt1 = date('Y-m-d'); + $dt2 = $params['ea_created'] ?? null; + + return $this->getDaysDiff($dt1, $dt2); + } + + private function getDaysSinceLastVisit(array $params): int { + $dt1 = date('Y-m-d'); + $dt2 = $params['ea_lastseen'] ?? null; + + return $this->getDaysDiff($dt1, $dt2); + } + + private function getDaysTillToday(?string $dt2): int { + $diff = -1; + + if ($dt2 !== null) { + $dt1 = date('Y-m-d'); + $dt1 = new \DateTime($dt1); + $dt2 = new \DateTime($dt2); + $diff = $dt1->diff($dt2)->format('%a'); + } + + return $diff; + } + + private function getDaysDiff(?string $dt1, ?string $dt2): int { + $diff = -1; + + if ($dt2) { + $dt1 = new \DateTime($dt1); + $dt2 = new \DateTime($dt2); + $diff = $dt1->diff($dt2)->format('%a'); + } + + return $diff; + } + + private function getUserFullName(array $record): string { + $name = []; + $fName = $record['ea_firstname'] ?? ''; + if ($fName) { + $name[] = $fName; + } + + $lName = $record['ea_lastname'] ?? ''; + if ($lName) { + $name[] = $lName; + } + + return trim(join(' ', $name)); + } + + private function checkEmailForSuspiciousString(array $record): bool { + foreach ($this->suspiciousWordsEmail as $sub) { + if (stripos($record['le_email'], $sub) !== false) { + return true; + } + } + + return false; + } + + private function filterStringNum(array $record): array { + return array_filter($record, static function ($value): bool { + return is_string($value) || is_int($value); + }); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Device.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Device.php new file mode 100644 index 0000000..5606aa9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Device.php @@ -0,0 +1,70 @@ +getDetails($accountIds, $apiKey); + $recordByAccount = $this->groupRecordsByAccount($record); + + foreach ($recordByAccount as $key => $value) { + $recordByAccount[$key] = [ + 'eup_device' => array_column($value, 'eup_device'), + 'eup_device_id' => array_column($value, 'eup_device_id'), + 'eup_browser_name' => array_column($value, 'eup_browser_name'), + 'eup_browser_version' => array_column($value, 'eup_browser_version'), + 'eup_os_name' => array_column($value, 'eup_os_name'), + 'eup_lang' => array_column($value, 'eup_lang'), + 'eup_ua' => array_column($value, 'eup_ua'), + // 'eup_lastseen' => array_column($value, 'eup_lastseen'), + // 'eup_created' => array_column($value, 'eup_created'), + ]; + } + + return $recordByAccount; + } + + protected function getDetails(array $accountIds, int $apiKey): array { + [$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey); + + $query = ( + "SELECT + event_device.account_id AS accountid, + event_device.id AS eup_device_id, + event_ua_parsed.device AS eup_device, + event_ua_parsed.browser_name AS eup_browser_name, + event_ua_parsed.browser_version AS eup_browser_version, + event_ua_parsed.os_name AS eup_os_name, + event_ua_parsed.ua AS eup_ua, + -- event_device.lastseen AS eup_lastseen, + -- event_device.created AS eup_created, + event_device.lang AS eup_lang + + FROM + event_device + + INNER JOIN event_ua_parsed + ON(event_device.user_agent=event_ua_parsed.id) + + WHERE + event_device.key = :api_key + AND event_ua_parsed.checked = true + AND event_device.account_id IN ({$placeHolders})" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Domain.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Domain.php new file mode 100644 index 0000000..6a0d880 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Domain.php @@ -0,0 +1,65 @@ +getDetails($accountIds, $apiKey); + $recordsByAccount = $this->groupRecordsByAccount($records); + + foreach ($recordsByAccount as $key => $value) { + $recordsByAccount[$key] = [ + 'ed_domain' => $this->getUniqueArray(array_column($value, 'ed_domain')), + 'ed_blockdomains' => $this->getUniqueArray(array_column($value, 'ed_blockdomains')), + 'ed_disposable_domains' => $this->getUniqueArray(array_column($value, 'ed_disposable_domains')), + 'ed_free_email_provider' => $this->getUniqueArray(array_column($value, 'ed_free_email_provider')), + 'ed_creation_date' => $this->getUniqueArray(array_column($value, 'ed_creation_date')), + 'ed_disabled' => $this->getUniqueArray(array_column($value, 'ed_disabled')), + 'ed_mx_record' => $this->getUniqueArray(array_column($value, 'ed_mx_record')), + ]; + } + + return $recordsByAccount; + } + + protected function getDetails(array $accountIds, int $apiKey): array { + [$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey); + + $query = ( + "SELECT + event_email.account_id AS accountid, + event_domain.domain AS ed_domain, + event_domain.blockdomains AS ed_blockdomains, + event_domain.disposable_domains AS ed_disposable_domains, + event_domain.free_email_provider AS ed_free_email_provider, + event_domain.creation_date AS ed_creation_date, + event_domain.disabled AS ed_disabled, + event_domain.mx_record AS ed_mx_record + + FROM + event_domain + + INNER JOIN event_email + ON event_domain.id = event_email.domain + + WHERE + event_email.key = :api_key + AND event_email.account_id IN ({$placeHolders})" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Email.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Email.php new file mode 100644 index 0000000..9892b63 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Email.php @@ -0,0 +1,52 @@ +getDetails($accountIds, $apiKey); + $recordsByAccount = $this->groupRecordsByAccount($records); + + foreach ($recordsByAccount as $key => $value) { + $recordsByAccount[$key] = [ + 'ee_email' => $this->getUniqueArray(array_column($value, 'ee_email')), + 'ee_earliest_breach' => $this->getUniqueArray(array_column($value, 'ee_earliest_breach')), + ]; + } + + return $recordsByAccount; + } + + protected function getDetails(array $accountIds, int $apiKey): array { + [$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey); + + $query = ( + "SELECT + event_email.account_id AS accountid, + event_email.email AS ee_email, + event_email.earliest_breach AS ee_earliest_breach + FROM + event_email + + WHERE + event_email.key = :api_key + AND event_email.checked = 'True' + AND event_email.account_id IN ({$placeHolders})" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Event.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Event.php new file mode 100644 index 0000000..bf8f5bb --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Event.php @@ -0,0 +1,84 @@ +getDetails($accountIds, $apiKey); + $recordsByAccount = $this->groupRecordsByAccount($records); + + foreach ($recordsByAccount as $key => $value) { + $recordsByAccount[$key] = [ + 'event_ip' => array_column($value, 'event_ip'), + 'event_url_string' => array_column($value, 'event_url_string'), + 'event_empty_referer' => array_column($value, 'event_empty_referer'), + 'event_device' => array_column($value, 'event_device'), + 'event_type' => array_column($value, 'event_type'), + 'event_http_code' => array_column($value, 'event_http_code'), + 'event_device_created' => array_column($value, 'event_device_created'), + 'event_device_lastseen' => array_column($value, 'event_device_lastseen'), + 'event_http_method' => array_column($value, 'event_http_method'), + ]; + } + + return $recordsByAccount; + } + + protected function getDetails(array $accountIds, int $apiKey): array { + [$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey); + $contextLimit = \Utils\Constants::get('RULE_EVENT_CONTEXT_LIMIT'); + + $query = ( + "WITH ranked_events AS ( + SELECT + event.account AS accountid, + event.id AS event_id, + event.ip AS event_ip, + event_url.url AS event_url_string, + event_referer.referer AS event_referer_string, + event.device AS event_device, + event.time AS event_time, + event.type AS event_type, + event.http_code AS event_http_code, + event.http_method AS event_http_method, + ROW_NUMBER() OVER (PARTITION BY event.account ORDER BY event.time DESC) AS rn + FROM event + LEFT JOIN event_url ON event_url.id = event.url + LEFT JOIN event_referer ON event_referer.id = event.referer + WHERE event.key = :api_key + AND event.account IN ({$placeHolders}) + ) + SELECT + accountid, + event_ip, + event_url_string, + (event_referer_string IS NULL OR event_referer_string = '') AS event_empty_referer, + event_device, + ed.created AS event_device_created, + ed.lastseen AS event_device_lastseen, + event_type, + event_http_code, + event_http_method + FROM ranked_events + LEFT JOIN event_device AS ed + ON ranked_events.event_device = ed.id + WHERE rn <= {$contextLimit} + ORDER BY event_time DESC;" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Ip.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Ip.php new file mode 100644 index 0000000..366c9d9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Ip.php @@ -0,0 +1,91 @@ +getDetails($accountIds, $apiKey); + $recordsByAccount = $this->groupRecordsByAccount($records); + + foreach ($recordsByAccount as $key => $value) { + $recordsByAccount[$key] = [ + 'eip_ip_id' => array_column($value, 'eip_ip_id'), + 'eip_ip' => array_column($value, 'eip_ip'), + 'eip_cidr' => array_column($value, 'eip_cidr'), + 'eip_data_center' => array_column($value, 'eip_data_center'), + 'eip_tor' => array_column($value, 'eip_tor'), + 'eip_vpn' => array_column($value, 'eip_vpn'), + 'eip_relay' => array_column($value, 'eip_relay'), + 'eip_starlink' => array_column($value, 'eip_starlink'), + 'eip_total_visit' => array_column($value, 'eip_total_visit'), + 'eip_blocklist' => array_column($value, 'eip_blocklist'), + 'eip_shared' => array_column($value, 'eip_shared'), + //'eip_domains' => $this->getUniqueArray(array_column($value, 'eip_domains')), + 'eip_domains_count_len' => array_column($value, 'eip_domains_count_len'), + 'eip_country_id' => array_column($value, 'eip_country_id'), + 'eip_fraud_detected' => array_column($value, 'eip_fraud_detected'), + 'eip_alert_list' => array_column($value, 'eip_alert_list'), + ]; + } + + return $recordsByAccount; + } + + protected function getDetails(array $accountIds, int $apiKey): array { + [$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey); + + $query = ( + "SELECT DISTINCT + event.account AS accountid, + + event_ip.id AS eip_ip_id, + event_ip.ip AS eip_ip, + event_ip.cidr::text AS eip_cidr, + event_ip.country AS eip_country_id, + event_ip.data_center AS eip_data_center, + event_ip.tor AS eip_tor, + event_ip.vpn AS eip_vpn, + event_ip.relay AS eip_relay, + event_ip.starlink AS eip_starlink, + event_ip.total_visit AS eip_total_visit, + event_ip.blocklist AS eip_blocklist, + event_ip.shared AS eip_shared, + -- event_ip.domains_count AS eip_domains, + json_array_length(event_ip.domains_count::json) AS eip_domains_count_len, + event_ip.fraud_detected AS eip_fraud_detected, + event_ip.alert_list AS eip_alert_list + + FROM + event_ip + + INNER JOIN event + ON (event_ip.id = event.ip) + + WHERE + event_ip.key = :api_key + AND event_ip.checked = 'True' + AND event.account IN ({$placeHolders}) + + -- ORDER BY event_ip.id DESC" + ); + + if (count($accountIds) === 1) { + $query .= ' LIMIT 100 OFFSET 0'; + } + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Phone.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Phone.php new file mode 100644 index 0000000..0113309 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Phone.php @@ -0,0 +1,79 @@ +getDetails($accountIds, $apiKey); + $recordsByAccount = $this->groupRecordsByAccount($records); + + foreach ($recordsByAccount as $key => $value) { + $recordsByAccount[$key] = [ + //'ep_calling_country_code' => $this->getUniqueArray(array_column($value, 'ep_calling_country_code')), + //'ep_carrier_name' => $this->getUniqueArray(array_column($value, 'ep_carrier_name')), + //'ep_checked' => $this->getUniqueArray(array_column($value, 'ep_checked')), + //'ep_country_code' => $this->getUniqueArray(array_column($value, 'ep_country_code')), + //'ep_created' => $this->getUniqueArray(array_column($value, 'ep_created')), + //'ep_lastseen' => $this->getUniqueArray(array_column($value, 'ep_lastseen')), + //'ep_mobile_country_code' => $this->getUniqueArray(array_column($value, 'ep_mobile_country_code')), + //'ep_mobile_network_code' => $this->getUniqueArray(array_column($value, 'ep_mobile_network_code')), + //'ep_national_format' => $this->getUniqueArray(array_column($value, 'ep_national_format')), + 'ep_phone_number' => $this->getUniqueArray(array_column($value, 'ep_phone_number')), + 'ep_shared' => $this->getUniqueArray(array_column($value, 'ep_shared')), + 'ep_type' => $this->getUniqueArray(array_column($value, 'ep_type')), + //'ep_invalid' => $this->getUniqueArray(array_column($value, 'ep_invalid')), + //'ep_validation_errors' => $this->getUniqueArray(array_column($value, 'ep_validation_errors')), + //'ep_alert_list' => $this->getUniqueArray(array_column($value, 'ep_alert_list')), + ]; + } + + return $recordsByAccount; + } + + protected function getDetails(array $accountIds, int $apiKey): array { + [$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey); + + $query = ( + "SELECT + event_phone.account_id AS accountid, + + -- event_phone.calling_country_code AS ep_calling_country_code, + -- event_phone.carrier_name AS ep_carrier_name, + -- event_phone.checked AS ep_checked, + -- event_phone.country_code AS ep_country_code, + -- event_phone.created AS ep_created, + -- event_phone.lastseen AS ep_lastseen, + -- event_phone.mobile_country_code AS ep_mobile_country_code, + -- event_phone.mobile_network_code AS ep_mobile_network_code, + -- event_phone.national_format AS ep_national_format, + event_phone.phone_number AS ep_phone_number, + event_phone.shared AS ep_shared, + event_phone.type AS ep_type + -- event_phone.invalid AS ep_invalid, + -- event_phone.validation_errors AS ep_validation_errors, + -- event_phone.alert_list AS ep_alert_list + + FROM + event_phone + + WHERE + event_phone.key = :api_key + AND event_phone.account_id IN ({$placeHolders})" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Session.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Session.php new file mode 100644 index 0000000..d4a6215 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/Session.php @@ -0,0 +1,71 @@ +getDetails($accountIds, $apiKey, $timezoneOffset); + // one record per account + $recordsByAccount = $this->groupRecordsByAccount($records); + + return $recordsByAccount; + } + + protected function getDetails(array $accountIds, int $apiKey, int $timezoneOffset = 0): array { + [$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey); + + $params[':night_start'] = gmdate('H:i:s', \Utils\Constants::get('NIGHT_RANGE_SECONDS_START') - $timezoneOffset); + $params[':night_end'] = gmdate('H:i:s', \Utils\Constants::get('NIGHT_RANGE_SECONDS_END') - $timezoneOffset); + + // boolean logic for defining time ranges overlap + $query = ( + "SELECT + event_session.account_id AS accountid, + BOOL_OR(event_session.total_country > 1) AS event_session_multiple_country, + BOOL_OR(event_session.total_ip > 1) AS event_session_multiple_ip, + BOOL_OR(event_session.total_device > 1) AS event_session_multiple_device, + BOOL_OR( + (event_session.lastseen - event_session.created) > INTERVAL '1 day' OR + ( + CASE WHEN :night_start::time < :night_end::time + THEN + (event_session.lastseen::time >= :night_start::time AND event_session.lastseen::time <= :night_end::time) OR + (event_session.created::time >= :night_start::time AND event_session.created::time <= :night_end::time) OR + ( + CASE WHEN event_session.lastseen::time > event_session.created::time + THEN + event_session.total_visit > 1 AND :night_start::time >= event_session.created::time AND :night_start::time <= event_session.lastseen::time + ELSE + event_session.total_visit > 1 AND (:night_start::time >= event_session.created::time OR :night_start::time <= event_session.lastseen::time) + END + ) + ELSE + event_session.lastseen::time >= :night_start::time OR event_session.lastseen::time <= :night_end::time OR + event_session.created::time >= :night_start::time OR event_session.created::time <= :night_end::time OR + event_session.lastseen::time < event_session.created::time + END + )) AS event_session_night_time + FROM + event_session + WHERE + event_session.key = :api_key AND + event_session.account_id IN ({$placeHolders}) + GROUP BY event_session.account_id" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/User.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/User.php new file mode 100644 index 0000000..2261bbd --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/User.php @@ -0,0 +1,118 @@ +getDetails($accountIds, $apiKey); + + $this->calculateEmailReputationForContext($results); + + $recordsByAccount = []; + foreach ($results as $item) { + $recordsByAccount[$item['ea_id']] = $item; + } + + return $recordsByAccount; + } + + protected function getDetails(array $accountIds, int $apiKey): array { + [$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey); + + $query = ( + "SELECT + event_account.id AS ea_id, + event_account.userid AS ea_userid, + event_account.created AS ea_created, + event_account.lastseen AS ea_lastseen, + event_account.total_visit AS ea_total_visit, + event_account.total_country AS ea_total_country, + event_account.total_ip AS ea_total_ip, + event_account.total_device AS ea_total_device, + event_account.firstname AS ea_firstname, + event_account.lastname AS ea_lastname, + + event_email.email AS ee_email, + event_email.blockemails AS ee_blockemails, + event_email.data_breach AS ee_data_breach, + -- event_email.profiles AS ee_profiles, + event_email.checked AS ee_checked, + + event_domain.discovery_date AS ed_discovery_date, + event_domain.blockdomains AS ed_blockdomains, + event_domain.disposable_domains AS ed_disposable_domains, + -- event_domain.total_account AS ed_total_account, + event_domain.free_email_provider AS ed_free_provider, + event_domain.tranco_rank AS ed_tranco_rank, + event_domain.creation_date AS ed_creation_date, + event_domain.expiration_date AS ed_expiration_date, + event_domain.return_code AS ed_return_code, + event_domain.closest_snapshot AS ed_closest_snapshot, + event_domain.mx_record AS ed_mx_record, + + lastemail_record.email AS le_email, + lastemail_record.blockemails AS le_blockemails, + lastemail_record.data_breach AS le_data_breach, + -- lastemail_record.profiles AS le_profiles, + lastemail_record.checked AS le_checked, + lastemail_record.fraud_detected AS le_fraud_detected, + lastemail_record.alert_list AS le_alert_list, + + lastdomain_record.disposable_domains AS ld_disposable_domains, + lastdomain_record.free_email_provider AS ld_free_email_provider, + lastdomain_record.blockdomains AS ld_blockdomains, + lastdomain_record.mx_record AS ld_mx_record, + lastdomain_record.disabled AS ld_disabled, + lastdomain_record.creation_date AS ld_creation_date, + lastdomain_record.tranco_rank AS ld_tranco_rank, + + lastphone_record.phone_number AS lp_phone_number, + lastphone_record.country_code AS lp_country_code, + lastphone_record.invalid AS lp_invalid, + lastphone_record.fraud_detected AS lp_fraud_detected, + lastphone_record.alert_list AS lp_alert_list + + FROM + event_account + + LEFT JOIN event_phone + ON (event_account.id = event_phone.account_id) + + LEFT JOIN event_email + ON event_account.id = event_email.account_id + + LEFT JOIN event_domain + ON event_email.domain = event_domain.id + + LEFT JOIN event_email AS lastemail_record + ON event_account.lastemail = lastemail_record.id + + LEFT JOIN event_phone AS lastphone_record + ON event_account.lastphone = lastphone_record.id + + LEFT JOIN event_domain AS lastdomain_record + ON lastemail_record.domain = lastdomain_record.id + + WHERE + event_account.key = :api_key + AND event_account.id IN ({$placeHolders})" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Context/index.php @@ -0,0 +1,3 @@ + $apiKey, + ':country_id' => $countryId, + ]; + + $query = ( + 'SELECT + countries.iso, + countries.value + + FROM + event_country + + INNER JOIN countries + ON (event_country.country = countries.id) + + WHERE + event_country.key = :api_key + AND event_country.country = :country_id' + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function getCountryIdByIso(string $countryIso): int { + $params = [ + ':country_iso' => $countryIso, + ]; + + $query = ( + 'SELECT + countries.id + + FROM + countries + + WHERE + countries.iso = :country_iso' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['id'] ?? 0; + } + + public function checkAccess(int $subjectId, int $apiKey): bool { + $params = [ + ':api_key' => $apiKey, + ':country_id' => $subjectId, + ]; + + $query = ( + 'SELECT + event_country.country + + FROM + event_country + + WHERE + event_country.key = :api_key + AND event_country.country = :country_id' + ); + + $results = $this->execQuery($query, $params); + + return count($results) > 0; + } + + public function insertRecord(array $data, int $apiKey): int { + $params = [ + ':key' => $apiKey, + ':country' => $data['id'], + ':lastseen' => $data['lastseen'], + ':updated' => $data['lastseen'], + ]; + + $query = ( + 'INSERT INTO event_country ( + key, country, lastseen, updated + ) VALUES ( + :key, :country, :lastseen, :updated + ) ON CONFLICT (country, key) DO UPDATE SET + lastseen = EXCLUDED.lastseen + RETURNING id' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['id']; + } + + public function getTimeFrameTotal(array $ids, string $startDate, string $endDate, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + $params[':start_date'] = $startDate; + $params[':end_date'] = $endDate; + + $query = ( + "SELECT + event_ip.country AS id, + COUNT(*) AS cnt + FROM event + INNER JOIN event_ip + ON event.ip = event_ip.id + WHERE + event_ip.country IN ({$flatIds}) AND + event.key = :key AND + event.time > :start_date AND + event.time < :end_date + GROUP BY event_ip.country" + ); + + $totalVisit = $this->execQuery($query, $params); + + $query = ( + "SELECT + event_ip.country AS id, + COUNT(DISTINCT(event.account)) AS cnt + FROM event + INNER JOIN event_ip + ON event.ip = event_ip.id + WHERE + event_ip.country IN ({$flatIds}) AND + event.key = :key AND + event.time > :start_date AND + event.time < :end_date + GROUP BY event_ip.country" + ); + + $totalAccount = $this->execQuery($query, $params); + + $query = ( + "SELECT + event_ip.country AS id, + COUNT(*) AS cnt + FROM event_ip + WHERE + event_ip.country IN ({$flatIds}) AND + event_ip.key = :key AND + event_ip.lastseen > :start_date AND + event_ip.lastseen < :end_date + GROUP BY event_ip.country" + ); + + $totalIp = $this->execQuery($query, $params); + + $result = []; + + foreach ($ids as $id) { + $result[$id] = ['total_visit' => 0, 'total_account' => 0, 'total_ip' => 0]; + } + + foreach ($totalVisit as $rec) { + $result[$rec['id']]['total_visit'] = $rec['cnt']; + } + + foreach ($totalAccount as $rec) { + $result[$rec['id']]['total_account'] = $rec['cnt']; + } + + foreach ($totalIp as $rec) { + $result[$rec['id']]['total_ip'] = $rec['cnt']; + } + + return $result; + } + + public function updateTotalsByEntityIds(array $ids, int $apiKey, bool $force = false): void { + if (!count($ids)) { + return; + } + + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + $extraClause = $force ? '' : ' AND event_country.lastseen >= event_country.updated'; + + $query = ( + "UPDATE event_country + SET + total_visit = COALESCE(sub.total_visit, 0), + total_account = COALESCE(sub.total_account, 0), + total_ip = COALESCE(sub.total_ip, 0), + updated = date_trunc('milliseconds', now()) + FROM ( + SELECT + event_ip.country, + COUNT(*) AS total_visit, + COUNT(DISTINCT event.account) AS total_account, + COUNT(DISTINCT event.ip) AS total_ip + FROM event + JOIN event_ip ON event.ip = event_ip.id + WHERE + event_ip.country IN ($flatIds) AND + event.key = :key + GROUP BY event_ip.country + ) AS sub + RIGHT JOIN countries sub_country ON sub.country = sub_country.id + WHERE + event_country.country = sub_country.id AND + event_country.country IN ($flatIds) AND + event_country.key = :key + $extraClause" + ); + + $this->execQuery($query, $params); + } + + public function updateAllTotals(int $apiKey): int { + $params = [ + ':key' => $apiKey, + ]; + $query = ( + 'UPDATE event_country + SET + total_visit = COALESCE(sub.total_visit, 0), + total_account = COALESCE(sub.total_account, 0), + total_ip = COALESCE(sub.total_ip, 0), + updated = date_trunc(\'milliseconds\', now()) + FROM ( + SELECT + event_ip.country, + COUNT(*) AS total_visit, + COUNT(DISTINCT event.account) AS total_account, + COUNT(DISTINCT event.ip) AS total_ip + FROM event + JOIN event_ip ON event.ip = event_ip.id AND event.key = event_ip.key + WHERE event.key = :key + GROUP BY event_ip.country + ) AS sub + RIGHT JOIN countries sub_country ON sub.country = sub_country.id + WHERE + event_country.key = :key AND + event_country.country = sub_country.id AND + event_country.lastseen >= event_country.updated' + ); + + return $this->execQuery($query, $params); + } + + public function refreshTotals(array $res, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders(array_column($res, 'id')); + $params[':key'] = $apiKey; + $query = ( + "SELECT + country AS id, + total_ip, + total_visit, + total_account + FROM event_country + WHERE country IN ({$flatIds}) AND key = :key" + ); + + $result = $this->execQuery($query, $params); + + $indexedResult = []; + foreach ($result as $item) { + $indexedResult[$item['id']] = $item; + } + + foreach ($res as $idx => $item) { + $item['total_ip'] = $indexedResult[$item['id']]['total_ip']; + $item['total_visit'] = $indexedResult[$item['id']]['total_visit']; + $item['total_account'] = $indexedResult[$item['id']]['total_account']; + $res[$idx] = $item; + } + + return $res; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Dashboard.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Dashboard.php new file mode 100644 index 0000000..f33229d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Dashboard.php @@ -0,0 +1,159 @@ +getTotal($query, $field, $dateRange, $apiKey); + } + + public function getTotalUsersForReview(?array $dateRange, int $apiKey): int { + $query = ( + 'SELECT + COUNT(event_account.id) + + FROM + event_account + + WHERE + event_account.key = :api_key AND + event_account.fraud IS NULL AND + event_account.added_to_review IS NOT NULL' + ); + + $field = 'event_account.added_to_review'; + + return $this->getTotal($query, $field, $dateRange, $apiKey); + } + + public function getTotalEvents(?array $dateRange, int $apiKey): int { + $query = ( + 'SELECT + COUNT(*) + + FROM + event + + WHERE + event.key = :api_key' + ); + + $field = 'event.time'; + + return $this->getTotal($query, $field, $dateRange, $apiKey); + } + + public function getTotalResources(?array $dateRange, int $apiKey): int { + $query = ( + 'SELECT + COUNT(*) + + FROM + event_url + + WHERE + event_url.key = :api_key' + ); + + $field = 'event_url.lastseen'; + + return $this->getTotal($query, $field, $dateRange, $apiKey); + } + + public function getTotalCountries(?array $dateRange, int $apiKey): int { + $query = ( + 'SELECT + COUNT(event_country.id) + + FROM + event_country + + WHERE + event_country.key = :api_key' + ); + + $field = 'event_country.lastseen'; + + return $this->getTotal($query, $field, $dateRange, $apiKey); + } + + public function getTotalIps(?array $dateRange, int $apiKey): int { + $query = ( + 'SELECT + COUNT (*) + + FROM + event_ip + + WHERE + event_ip.key = :api_key' + ); + + $field = 'event_ip.lastseen'; + + return $this->getTotal($query, $field, $dateRange, $apiKey); + } + + public function getTotalUsers(?array $dateRange, int $apiKey): int { + $query = ( + 'SELECT + COUNT (*) + + FROM + event_account + + WHERE + event_account.key = :api_key' + ); + + $field = 'event_account.lastseen'; + + return $this->getTotal($query, $field, $dateRange, $apiKey); + } + + private function getTotal(string $query, string $dateField, ?array $dateRange, int $apiKey): int { + $params = [ + ':api_key' => $apiKey, + ]; + + if ($dateRange) { + $params[':end_time'] = $dateRange['endDate']; + $params[':start_time'] = $dateRange['startDate']; + + $query .= " AND {$dateField} >= :start_time AND {$dateField} <= :end_time"; + } + + $results = $this->execQuery($query, $params); + + return $results[0]['count'] ?? 0; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Device.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Device.php new file mode 100644 index 0000000..da8a17a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Device.php @@ -0,0 +1,187 @@ + $apiKey, + ':device_id' => $subjectId, + ]; + + $results = $this->execQuery($query, $params); + + return count($results) > 0; + } + + public function getFullDeviceInfoById(int $deviceId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':device_id' => $deviceId, + ]; + + $query = ( + 'SELECT + event_device.id, + event_device.lang, + event_device.created, + event_device.user_agent AS ua_id, + event_ua_parsed.device, + event_ua_parsed.browser_name, + event_ua_parsed.browser_version, + event_ua_parsed.os_name, + event_ua_parsed.os_version, + event_ua_parsed.ua, + event_ua_parsed.checked, + event_ua_parsed.modified + FROM + event_device + LEFT JOIN event_ua_parsed + ON (event_device.user_agent = event_ua_parsed.id) + + WHERE + event_device.key = :api_key AND + event_device.id = :device_id' + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function extractById(int $entityId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':id' => $entityId, + ]; + + $query = ( + "SELECT + COALESCE(event_ua_parsed.ua, '') AS value + FROM + event_ua_parsed + WHERE + event_ua_parsed.key = :api_key AND + event_ua_parsed.id = :id + LIMIT 1" + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function updateAllTotals(int $apiKey): int { + $params = [ + ':key' => $apiKey, + ]; + + $query = ( + 'UPDATE event_device + SET + total_visit = COALESCE(sub.total_visit, 0), + updated = date_trunc(\'milliseconds\', now()) + FROM ( + SELECT + event.device, + COUNT(*) AS total_visit + FROM event + WHERE + event.key = :key + GROUP BY event.device + ) AS sub + RIGHT JOIN event_device sub_device ON sub.device = sub_device.id + WHERE + event_device.id = sub_device.id AND + event_device.key = :key AND + event_device.lastseen >= event_device.updated' + ); + + return $this->execQuery($query, $params); + } + + public function countNotChecked(int $apiKey): int { + $params = [ + ':key' => $apiKey, + ]; + + $query = ( + 'SELECT + COUNT(*) AS count + FROM event_ua_parsed + WHERE + event_ua_parsed.key = :key AND + event_ua_parsed.checked IS FALSE' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['count'] ?? 0; + } + + public function notCheckedExists(int $apiKey): bool { + $params = [ + ':key' => $apiKey, + ]; + + $query = ( + 'SELECT 1 + FROM event_ua_parsed + WHERE + event_ua_parsed.key = :key AND + event_ua_parsed.checked IS FALSE + LIMIT 1' + ); + + $results = $this->execQuery($query, $params); + + return (bool) count($results); + } + + public function notCheckedForUserId(int $userId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':user_id' => $userId, + ]; + + $query = ( + 'SELECT DISTINCT + event_ua_parsed.id + FROM event_device + LEFT JOIN event_ua_parsed ON event_device.user_agent = event_ua_parsed.id + WHERE + event_device.account_id = :user_id AND + event_device.key = :api_key AND + event_ua_parsed.checked IS FALSE' + ); + + return array_column($this->execQuery($query, $params), 'id'); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Domain.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Domain.php new file mode 100644 index 0000000..60867cf --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Domain.php @@ -0,0 +1,305 @@ + $apiKey, + ':domain_id' => $subjectId, + ]; + + $query = ( + 'SELECT + event_domain.id + + FROM + event_domain + + WHERE + event_domain.key = :api_key + AND event_domain.id = :domain_id' + ); + + $results = $this->execQuery($query, $params); + + return count($results) > 0; + } + + public function getFullDomainInfoById(int $domainId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':domain_id' => $domainId, + ]; + + $query = ( + 'SELECT + event_domain.id, + event_domain.domain, + event_domain.total_account, + event_domain.lastseen, + event_domain.creation_date, + event_domain.expiration_date, + event_domain.disabled, + event_domain.disposable_domains, + event_domain.free_email_provider, + event_domain.tranco_rank, + event_domain.checked, + ( + SELECT COUNT(*) + FROM event_email + WHERE + event_email.domain = event_domain.id AND + event_email.key = :api_key AND + event_email.fraud_detected IS TRUE + ) AS fraud + + FROM + event_domain + + WHERE + event_domain.key = :api_key + AND event_domain.id = :domain_id + + GROUP BY + event_domain.id' + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function extractById(int $entityId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':id' => $entityId, + ]; + + $query = ( + "SELECT + COALESCE(event_domain.domain, '') AS value + + FROM + event_domain + + WHERE + event_domain.key = :api_key + AND event_domain.id = :id + + LIMIT 1" + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function getTimeFrameTotal(array $ids, string $startDate, string $endDate, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + $params[':start_date'] = $startDate; + $params[':end_date'] = $endDate; + + $query = ( + "SELECT + event_email.domain AS id, + COUNT(DISTINCT(event.account)) AS cnt + FROM event + LEFT JOIN event_email + ON event.email = event_email.id + WHERE + event_email.domain IN ({$flatIds}) AND + event.key = :key AND + event_email.lastseen > :start_date AND + event_email.lastseen < :end_date + GROUP BY event_email.domain" + ); + + $totalAccount = $this->execQuery($query, $params); + + $result = []; + + foreach ($ids as $id) { + $result[$id] = ['total_account' => 0]; + } + + foreach ($totalAccount as $rec) { + $result[$rec['id']]['total_account'] = $rec['cnt']; + } + + return $result; + } + + public function updateTotalsByEntityIds(array $ids, int $apiKey, bool $force = false): void { + if (!count($ids)) { + return; + } + + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + $extraClause = $force ? '' : ' AND event_domain.lastseen >= event_domain.updated'; + + $query = ( + "UPDATE event_domain + SET + total_visit = COALESCE(sub.total_visit, 0), + total_account = COALESCE(sub.total_account, 0), + updated = date_trunc('milliseconds', now()) + FROM ( + SELECT + event_email.domain, + COUNT(*) AS total_visit, + COUNT(DISTINCT account) AS total_account + FROM event + LEFT JOIN event_email + ON event.email = event_email.id + WHERE + event_email.domain IN ($flatIds) AND + event.key = :key + GROUP BY event_email.domain + ) AS sub + RIGHT JOIN event_domain sub_domain ON sub.domain = sub_domain.id + WHERE + event_domain.id = sub_domain.id AND + event_domain.id IN ($flatIds) AND + event_domain.key = :key + $extraClause" + ); + + $this->execQuery($query, $params); + } + + public function updateAllTotals(int $apiKey): int { + $params = [ + ':key' => $apiKey, + ]; + + $query = ( + 'UPDATE event_domain + SET + total_visit = COALESCE(sub.total_visit, 0), + total_account = COALESCE(sub.total_account, 0), + updated = date_trunc(\'milliseconds\', now()) + FROM ( + SELECT + event_email.domain, + COUNT(*) AS total_visit, + COUNT(DISTINCT account) AS total_account + FROM event + LEFT JOIN event_email ON event.email = event_email.id + WHERE + event.key = :key + GROUP BY event_email.domain + ) AS sub + RIGHT JOIN event_domain sub_domain ON sub.domain = sub_domain.id + WHERE + event_domain.id = sub_domain.id AND + event_domain.key = :key AND + event_domain.lastseen >= event_domain.updated' + ); + + return $this->execQuery($query, $params); + } + + public function refreshTotals(array $res, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders(array_column($res, 'id')); + $params[':key'] = $apiKey; + $query = ( + "SELECT + id, + total_visit, + total_account + FROM event_domain + WHERE id IN ({$flatIds}) AND key = :key" + ); + + $result = $this->execQuery($query, $params); + + $indexedResult = []; + foreach ($result as $item) { + $indexedResult[$item['id']] = $item; + } + + foreach ($res as $idx => $item) { + $item['total_visit'] = $indexedResult[$item['id']]['total_visit']; + $item['total_account'] = $indexedResult[$item['id']]['total_account']; + $res[$idx] = $item; + } + + return $res; + } + + public function countNotChecked(int $apiKey): int { + $params = [ + ':key' => $apiKey, + ]; + + $query = ( + 'SELECT + COUNT(*) AS count + FROM event_domain + WHERE + event_domain.key = :key AND + event_domain.checked IS FALSE' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['count'] ?? 0; + } + + public function notCheckedExists(int $apiKey): bool { + $params = [ + ':key' => $apiKey, + ]; + + $query = ( + 'SELECT 1 + FROM event_domain + WHERE + event_domain.key = :key AND + event_domain.checked IS FALSE + LIMIT 1' + ); + + $results = $this->execQuery($query, $params); + + return (bool) count($results); + } + + public function notCheckedForUserId(int $userId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':user_id' => $userId, + ]; + + $query = ( + 'SELECT DISTINCT + event_domain.id + FROM event_email + LEFT JOIN event_domain ON event_email.domain = event_domain.id + WHERE + event_email.account_id = :user_id AND + event_domain.key = :api_key AND + event_domain.checked IS FALSE' + ); + + return array_column($this->execQuery($query, $params), 'id'); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Email.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Email.php new file mode 100644 index 0000000..dcff374 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Email.php @@ -0,0 +1,252 @@ + $apiKey, + ':id' => $id, + ]; + + $query = ( + 'SELECT + event_email.id AS email_id, + event_email.email, + event_email.lastseen AS email_lastseen, + event_email.created AS email_created, + event_email.data_breach, + event_email.data_breaches, + event_email.earliest_breach, + event_email.profiles, + event_email.blockemails, + event_email.domain_contact_email, + event_email.fraud_detected, + event_email.checked, + -- event_email.alert_list, + + event_domain.id AS domain_id, + event_domain.domain, + event_domain.blockdomains, + event_domain.disposable_domains, + event_domain.total_visit, + event_domain.total_account, + event_domain.lastseen AS domain_lastseen, + event_domain.created AS domain_created, + event_domain.free_email_provider, + event_domain.tranco_rank, + event_domain.creation_date, + event_domain.expiration_date, + event_domain.return_code, + event_domain.closest_snapshot, + event_domain.mx_record, + event_domain.disabled + + FROM + event_email + LEFT JOIN event_domain + ON event_email.domain = event_domain.id + + WHERE + event_email.id = :id AND + event_email.key = :api_key' + ); + + $results = $this->execQuery($query, $params); + + $this->calculateEmailReputation($results); + + return $results[0] ?? []; + } + + public function getIdByValue(string $email, int $apiKey): ?int { + $query = ( + 'SELECT + event_email.id + FROM + event_email + WHERE + event_email.key = :api_key + AND event_email.email = :email_value' + ); + + $params = [ + ':email_value' => $email, + ':api_key' => $apiKey, + ]; + + $results = $this->execQuery($query, $params); + + return $results[0]['id'] ?? null; + } + + public function getSeenInLastDay( + bool $includeAlertListed = false, + bool $includeWithoutHash = false, + bool $includeWithBlacklistSyncSkipped = false, + ): array { + $params = [ + ':includeAlertListed' => $includeAlertListed, + ':includeWithoutHash' => $includeWithoutHash, + ':includeWithBlacklistSyncSkipped' => $includeWithBlacklistSyncSkipped, + ]; + + $query = ( + 'SELECT + event_email.key, + event_email.email, + event_email.hash + FROM + event_email + JOIN + event_account ON event_email.account_id = event_account.id + JOIN + dshb_api ON event_account.key = dshb_api.id + WHERE + event_email.lastseen >= CURRENT_DATE - 1 + AND (:includeAlertListed = TRUE OR event_email.alert_list != TRUE OR event_email.alert_list IS NULL) + AND (:includeWithoutHash = TRUE OR event_email.hash IS NOT NULL) + AND (:includeWithBlacklistSyncSkipped = TRUE OR dshb_api.skip_blacklist_sync != TRUE)' + ); + + return $this->execQuery($query, $params); + } + + public function updateAlertListedByHashes(array $hashes, bool $alertListed, int $apiKey): void { + [$params, $placeHolders] = $this->getArrayPlaceholders($hashes); + $params[':alertListed'] = $alertListed; + $params[':key'] = $apiKey; + + $query = ( + "UPDATE event_email + SET alert_list = :alertListed + WHERE + key = :key AND + hash IN ({$placeHolders})" + ); + + $this->execQuery($query, $params); + } + + public function updateFraudFlag(array $ids, bool $fraud, int $apiKey): void { + if (!count($ids)) { + return; + } + + [$params, $placeHolders] = $this->getArrayPlaceholders($ids); + + $params[':fraud'] = $fraud; + $params[':api_key'] = $apiKey; + + $query = ( + "UPDATE event_email + SET fraud_detected = :fraud + + WHERE + key = :api_key + AND id IN ({$placeHolders})" + ); + + $this->execQuery($query, $params); + } + + public function extractById(int $entityId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':id' => $entityId, + ]; + + $query = ( + "SELECT + COALESCE(event_email.email, '') AS value, + event_email.hash AS hash + + FROM + event_email + + WHERE + event_email.key = :api_key + AND event_email.id = :id + + LIMIT 1" + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function countNotChecked(int $apiKey): int { + $params = [ + ':key' => $apiKey, + ]; + + $query = ( + 'SELECT + COUNT(*) AS count + FROM event_email + WHERE + event_email.key = :key AND + event_email.checked IS FALSE' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['count'] ?? 0; + } + + public function notCheckedExists(int $apiKey): bool { + $params = [ + ':key' => $apiKey, + ]; + + $query = ( + 'SELECT 1 + FROM event_email + WHERE + event_email.key = :key AND + event_email.checked IS FALSE + LIMIT 1' + ); + + $results = $this->execQuery($query, $params); + + return (bool) count($results); + } + + public function notCheckedForUserId(int $userId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':user_id' => $userId, + ]; + + $query = ( + 'SELECT DISTINCT + event_email.id + FROM event_email + WHERE + event_email.account_id = :user_id AND + event_email.key = :api_key AND + event_email.checked IS FALSE' + ); + + return array_column($this->execQuery($query, $params), 'id'); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Base.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Base.php new file mode 100644 index 0000000..a7d978d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Base.php @@ -0,0 +1,90 @@ + $value) { + $modifiedArray[':' . $key] = $value; + } + + return $modifiedArray; + } + + public function slimIds(array $ids): array { + $filtered = array_filter($ids, static function ($value): bool { + return $value !== null; + }); + + return array_unique($filtered); + } + + public function updateStringByPlaceholders(array $placeholders): string { + $transformed = array_map(static function ($item): string { + $key = ltrim($item, ':'); + + return "{$key} = {$item}"; + }, $placeholders); + return implode(', ', $transformed); + } + + public function validateIP(string $ip): bool { + return filter_var($ip, FILTER_VALIDATE_IP) !== false; + } + + // Validate date + public function validateDate(string $date, string $format = 'Y-m-d'): bool { + $d = \DateTime::createFromFormat($format, $date); + + return $d && $d->format($format) === $date; + } + + public function validateDates(array $dates): bool { + foreach ($dates as $date) { + if ($date !== null && !$this->validateDate($date)) { + return false; + } + } + + return true; + } + + public function validateCidr(string $cidr): bool { + $parts = explode('/', $cidr); + if (count($parts) !== 2) { + return false; + } + + $ip = $parts[0]; + $netmask = intval($parts[1]); + + if ($netmask < 0) { + return false; + } + + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return $netmask <= 32; + } + + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return $netmask <= 128; + } + + return false; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Device.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Device.php new file mode 100644 index 0000000..f5a648a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Device.php @@ -0,0 +1,69 @@ +ua = $data['ua']; + $this->device = $data['device']; + $this->browser_name = $data['browser_name']; + $this->browser_version = $data['browser_version']; + $this->os_name = $data['os_name']; + $this->os_version = $data['os_version']; + $this->modified = $data['modified']; + } + + public function prepareUpdate(): array { + $params = $this->queryParams(); + unset($params[':ua']); + + $placeholders = array_keys($params); + $updateString = $this->updateStringByPlaceholders($placeholders); + + return [$params, $updateString]; + } + + public function updateEntityInDb(int $entityId, int $apiKey): void { + // total_visit and total_account should remain still + [$params, $updateString] = $this->prepareUpdate(); + + $params['entity_id'] = $entityId; + $params['key'] = $apiKey; + + $query = (" + UPDATE event_ua_parsed + SET {$updateString} + WHERE + event_ua_parsed.id = :entity_id AND + event_ua_parsed.key = :key + "); + + $model = new \Models\Device(); + $model->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/DomainFound.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/DomainFound.php new file mode 100644 index 0000000..295f5ea --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/DomainFound.php @@ -0,0 +1,96 @@ +domain = $data['domain']; + $this->blockdomains = $data['blockdomains']; + $this->disposable_domains = $data['disposable_domains']; + $this->free_email_provider = $data['free_email_provider']; + $this->creation_date = $data['creation_date']; + $this->expiration_date = $data['expiration_date']; + $this->return_code = $data['return_code']; + $this->disabled = $data['disabled']; + $this->closest_snapshot = $data['closest_snapshot']; + $this->mx_record = $data['mx_record']; + $this->ip = $data['ip']; + $this->geo_ip = $data['geo_ip']; + $this->geo_html = $data['geo_html']; + $this->web_server = $data['web_server']; + $this->hostname = $data['hostname']; + $this->emails = $data['emails']; + $this->phone = $data['phone']; + $this->discovery_date = $data['discovery_date']; + $this->tranco_rank = $data['tranco_rank']; + + $dates = [$this->creation_date, $this->expiration_date, $this->closest_snapshot, $this->discovery_date]; + + if (($this->ip && !$this->validateIP($this->ip)) || !$this->validateDates($dates)) { + throw new \Exception('Validation failed'); + } + } + + public function prepareUpdate(): array { + $params = $this->queryParams(); + unset($params[':domain']); + + $placeholders = array_keys($params); + $updateString = $this->updateStringByPlaceholders($placeholders); + + return [$params, $updateString]; + } + + public function updateEntityInDb(int $entityId, int $apiKey): void { + // total_visit and total_account should remain still + [$params, $updateString] = $this->prepareUpdate(); + + $params['entity_id'] = $entityId; + $params['key'] = $apiKey; + + $query = (" + UPDATE event_domain + SET {$updateString} + WHERE + event_domain.id = :entity_id AND + event_domain.key = :key + "); + + $model = new \Models\Domain(); + $model->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/DomainNotFound.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/DomainNotFound.php new file mode 100644 index 0000000..8386b39 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/DomainNotFound.php @@ -0,0 +1,79 @@ +domain = $data['domain']; + $this->blockdomains = $data['blockdomains']; + $this->disposable_domains = $data['disposable_domains']; + $this->free_email_provider = $data['free_email_provider']; + $this->creation_date = $data['creation_date']; + $this->expiration_date = $data['expiration_date']; + $this->return_code = $data['return_code']; + $this->disabled = $data['disabled']; + $this->closest_snapshot = $data['closest_snapshot']; + $this->mx_record = $data['mx_record']; + + if (!$this->validateDates([$this->creation_date, $this->expiration_date, $this->closest_snapshot])) { + throw new \Exception('Validation failed'); + } + } + + public function prepareUpdate(): array { + $params = $this->queryParams(); + unset($params[':domain']); + + $placeholders = array_keys($params); + $updateString = $this->updateStringByPlaceholders($placeholders); + + return [$params, $updateString]; + } + + public function updateEntityInDb(int $entityId, int $apiKey): void { + // total_visit and total_account should remain still + [$params, $updateString] = $this->prepareUpdate(); + + $params['entity_id'] = $entityId; + $params['key'] = $apiKey; + + $query = (" + UPDATE event_domain + SET {$updateString} + WHERE + event_domain.id = :entity_id AND + event_domain.key = :key + "); + + $model = new \Models\Domain(); + $model->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Email.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Email.php new file mode 100644 index 0000000..f92be85 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Email.php @@ -0,0 +1,84 @@ +email = $data['email']; + $this->blockemails = $data['blockemails']; + $this->data_breach = $data['data_breach']; + $this->data_breaches = $data['data_breaches']; + $this->earliest_breach = $data['earliest_breach']; + $this->profiles = $data['profiles']; + $this->domain_contact_email = $data['domain_contact_email']; + $this->domain = $data['domain']; + $this->alert_list = $data['alert_list']; + + if (!$this->validateDates([$this->earliest_breach])) { + throw new \Exception('Validation failed'); + } + } + + public function prepareUpdate(): array { + $params = $this->queryParams(); + unset($params[':email']); + // ! + unset($params[':domain']); + + // if new alert_list is null -- don't override + if ($params[':alert_list'] === null) { + unset($params[':alert_list']); + } + + $placeholders = array_keys($params); + $updateString = $this->updateStringByPlaceholders($placeholders); + + return [$params, $updateString]; + } + + public function updateEntityInDb(int $entityId, int $apiKey): void { + [$params, $updateString] = $this->prepareUpdate(); + + $params['entity_id'] = $entityId; + $params['key'] = $apiKey; + + // other params will stay still + $query = (" + UPDATE event_email + SET {$updateString} + WHERE + event_email.id = :entity_id AND + event_email.key = :key + "); + + $model = new \Models\Device(); + $model->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Ip.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Ip.php new file mode 100644 index 0000000..179bc55 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Ip.php @@ -0,0 +1,155 @@ +ip = $data['ip']; + $this->country = $data['country']; + $this->asn = $data['asn']; + $this->name = $data['name']; + $this->hosting = $data['hosting']; + $this->vpn = $data['vpn']; + $this->tor = $data['tor']; + $this->relay = $data['relay']; + $this->starlink = $data['starlink']; + $this->description = $data['description']; + $this->blocklist = $data['blocklist']; + $this->domains_count = $data['domains_count']; + $this->cidr = $data['cidr']; + $this->alert_list = $data['alert_list']; + + if (!$this->validateIP($this->ip) || !$this->validateCIDR($this->cidr)) { + throw new \Exception('Validation failed'); + } + } + + public function prepareUpdate(): array { + $params = $this->queryParams(); + unset($params[':ip']); + + $params[':domains_count'] = json_encode($params[':domains_count']); + $params[':data_center'] = $params[':hosting']; + unset($params[':hosting']); + + // if new alert_list is null -- don't override + if ($params[':alert_list'] === null) { + unset($params[':alert_list']); + } + + // set $params[':isp'] later + + unset($params[':asn']); + unset($params[':name']); + unset($params[':description']); + $placeholders = array_keys($params); + $updateString = $this->updateStringByPlaceholders($placeholders); + + return [$params, $updateString]; + } + + // TODO: update countries table counters + public function updateEntityInDb(int $entityId, int $apiKey): void { + $ipModel = new \Models\Ip(); + + $previousIpData = $ipModel->getFullIpInfoById($entityId); + $previousIspId = count($previousIpData) ? $previousIpData['ispid'] : null; + $previousCountryId = count($previousIpData) ? $previousIpData['country_id'] : 0; + // get current isp id + $this->name = $this->asn !== null ? $this->name : 'N/A'; + $this->asn = $this->asn !== null ? $this->asn : 64496; + $ispModel = new \Models\Isp(); + $newIspId = $ispModel->getIdByAsn($this->asn, $apiKey); + + $newIspData = [ + 'asn' => $this->asn, + 'name' => $this->name, + 'description' => $this->description, + ]; + $newIspModel = new \Models\Enrichment\Isp(); + $newIspModel->init($newIspData); + + // new isp is not in db + if ($newIspId === null) { + $newIspData['lastseen'] = $previousIpData['lastseen']; + $newIspData['created'] = $previousIpData['created']; + $newIspId = $ispModel->insertRecord($newIspData, $apiKey); + } else { + $newIspModel->updateEntityInDb($newIspId, $apiKey); + } + + $this->isp = $newIspId; + + $countryModel = new \Models\Country(); + $newCountryId = $countryModel->getCountryIdByIso($this->country); + + $countryRecord = $countryModel->getCountryById($newCountryId, $apiKey); + if (!count($countryRecord)) { + $newCountryData = [ + 'id' => $newCountryId, + 'lastseen' => $previousIpData['lastseen'], + 'created' => $previousIpData['created'], + ]; + $countryModel->insertRecord($newCountryData, $apiKey); + } + + // total_visit and total_account should remain still + [$params, $updateString] = $this->prepareUpdate(); + + $params[':country'] = $newCountryId; + $params[':entity_id'] = $entityId; + $params[':key'] = $apiKey; + + $query = (" + UPDATE event_ip + SET {$updateString} + WHERE + event_ip.id = :entity_id AND + event_ip.key = :key + "); + + $model = new \Models\Ip(); + $model->execQuery($query, $params); + + // update totals only after event_ip update! + $ispIds = $this->slimIds([$previousIspId, $newIspId]); + $ispModel->updateTotalsByEntityIds($ispIds, $apiKey, true); + + $countryIds = $this->slimIds([$previousCountryId, $newCountryId]); + $countryModel->updateTotalsByEntityIds($countryIds, $apiKey, true); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Isp.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Isp.php new file mode 100644 index 0000000..51ea3da --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/Isp.php @@ -0,0 +1,59 @@ +asn = $data['asn']; + $this->name = $data['name']; + $this->description = $data['description']; + } + + public function prepareUpdate(): array { + $params = $this->queryParams(); + unset($params[':asn']); + + $placeholders = array_keys($params); + $updateString = $this->updateStringByPlaceholders($placeholders); + + return [$params, $updateString]; + } + + public function updateEntityInDb(int $entityId, int $apiKey): void { + [$params, $updateString] = $this->prepareUpdate(); + + $params[':entity_id'] = $entityId; + $params[':key'] = $apiKey; + + $query = (" + UPDATE event_isp + SET {$updateString} + WHERE + event_isp.id = :entity_id AND + event_isp.key = :key + "); + + $model = new \Models\Isp(); + $model->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/LocalhostIp.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/LocalhostIp.php new file mode 100644 index 0000000..a71f3b7 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/LocalhostIp.php @@ -0,0 +1,119 @@ +ip = $data['value']; + + if (!$this->validateIP($this->ip) || $data['error'] !== \Utils\Constants::get('ENRICHMENT_IP_IS_BOGON')) { + throw new \Exception('Validation failed'); + } + } + + public function prepareUpdate(): array { + $params = $this->queryParams(); + unset($params[':ip']); + + // set $params[':isp'] later + unset($params[':asn']); + unset($params[':name']); + unset($params[':description']); + $placeholders = array_keys($params); + $updateString = $this->updateStringByPlaceholders($placeholders); + + return [$params, $updateString]; + } + + // TODO: update countries table counters + public function updateEntityInDb(int $entityId, int $apiKey): void { + $ipModel = new \Models\Ip(); + + $previousIpData = $ipModel->getFullIpInfoById($entityId); + $previousIspId = count($previousIpData) ? $previousIpData['ispid'] : null; + $previousCountryId = count($previousIpData) ? $previousIpData['country_id'] : 0; + // get current isp id + $ispModel = new \Models\Isp(); + $newIspId = $ispModel->getIdByAsn($this->asn, $apiKey); + + $newIspData = [ + 'asn' => $this->asn, + 'name' => $this->name, + 'description' => $this->description, + ]; + $newIspModel = new \Models\Enrichment\Isp(); + $newIspModel->init($newIspData); + + // new isp is not in db + if ($newIspId === null) { + $newIspData['lastseen'] = $previousIpData['lastseen']; + $newIspId = $ispModel->insertRecord($newIspData, $apiKey); + } else { + $newIspModel->updateEntityInDb($newIspId, $apiKey); + } + + $this->isp = $newIspId; + + $countryModel = new \Models\Country(); + $newCountryId = $countryModel->getCountryIdByIso($this->country); + + $countryRecord = $countryModel->getCountryById($newCountryId, $apiKey); + if (!count($countryRecord)) { + $newCountryData = [ + 'id' => $newCountryId, + 'lastseen' => $previousIpData['lastseen'], + ]; + $countryModel->insertRecord($newCountryData, $apiKey); + } + + // total_visit and total_account should remain still + [$params, $updateString] = $this->prepareUpdate(); + + $params[':country'] = $newCountryId; + $params[':entity_id'] = $entityId; + $params[':key'] = $apiKey; + + $query = (" + UPDATE event_ip + SET {$updateString} + WHERE + event_ip.id = :entity_id AND + event_ip.key = :key + "); + + $model = new \Models\Ip(); + $model->execQuery($query, $params); + + // update totals only after event_ip update! + $ispIds = $this->slimIds([$previousIspId, $newIspId]); + $ispModel->updateTotalsByEntityIds($ispIds, $apiKey, true); + + $countryIds = $this->slimIds([$previousCountryId, $newCountryId]); + $countryModel->updateTotalsByEntityIds($countryIds, $apiKey, true); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/PhoneInvalid.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/PhoneInvalid.php new file mode 100644 index 0000000..c06ecdf --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/PhoneInvalid.php @@ -0,0 +1,70 @@ +phone_number = $data['phone_number']; + $this->invalid = $data['invalid']; + $this->validation_errors = $data['validation_error']; + $this->country_code = 0; + + if (!$this->invalid) { + throw new \Exception('Validation failed'); + } + } + + public function prepareUpdate(): array { + $params = $this->queryParams(); + unset($params[':phone_number']); + + $params[':validation_errors'] = json_encode($params[':validation_errors']); + + $placeholders = array_keys($params); + $updateString = $this->updateStringByPlaceholders($placeholders); + + return [$params, $updateString]; + } + + public function updateEntityInDb(int $entityId, int $apiKey): void { + [$params, $updateString] = $this->prepareUpdate(); + + $params['entity_id'] = $entityId; + $params['key'] = $apiKey; + + // other params will stay still + $query = (" + UPDATE event_phone + SET {$updateString} + WHERE + event_phone.id = :entity_id AND + event_phone.key = :key + "); + + $model = new \Models\Phone(); + $model->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/PhoneValid.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/PhoneValid.php new file mode 100644 index 0000000..2161972 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/PhoneValid.php @@ -0,0 +1,93 @@ +phone_number = $data['phone_number']; + $this->profiles = $data['profiles']; + $this->iso_country_code = $data['iso_country_code']; + $this->calling_country_code = $data['calling_country_code']; + $this->national_format = $data['national_format']; + $this->invalid = $data['invalid']; + $this->validation_errors = $data['validation_error']; + $this->carrier_name = $data['carrier_name']; + $this->type = $data['type']; + $this->alert_list = $data['alert_list']; + + if ($this->invalid || $this->validation_errors !== null) { + throw new \Exception('Validation failed'); + } + } + + public function prepareUpdate(): array { + $params = $this->queryParams(); + unset($params[':phone_number']); + + $params[':validation_errors'] = json_encode($params[':validation_errors']); + + // if new alert_list is null -- don't override + if ($params[':alert_list'] === null) { + unset($params[':alert_list']); + } + + $placeholders = array_keys($params); + $updateString = $this->updateStringByPlaceholders($placeholders); + + return [$params, $updateString]; + } + + public function updateEntityInDb(int $entityId, int $apiKey): void { + $this->country_code = 0; + + if ($this->iso_country_code !== null) { + $countryModel = new \Models\Country(); + $this->country_code = $countryModel->getCountryIdByIso($this->iso_country_code); + } + + [$params, $updateString] = $this->prepareUpdate(); + + $params['entity_id'] = $entityId; + $params['key'] = $apiKey; + + $query = (" + UPDATE event_phone + SET {$updateString} + WHERE + event_phone.id = :entity_id AND + event_phone.key = :key + "); + + $model = new \Models\Phone(); + $model->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Enrichment/index.php @@ -0,0 +1,3 @@ + $apiKey, + ]; + + $query = ( + 'SELECT + event.id, + event.time + FROM + event + WHERE + event.key = :api_key + + ORDER BY id DESC + LIMIT 1 OFFSET 0' + ); + + return $this->execQuery($query, $params); + } + + public function getEventDetails(int $id, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':id' => $id, + ]; + + $query = ( + 'SELECT + event.id, + event.time AS event_time, + event.http_code AS event_http_code, + event.type AS event_type_id, + + event_ip.ip, + event_ip.cidr, + event_ip.data_center, + event_ip.vpn, + event_ip.tor, + event_ip.relay, + event_ip.starlink, + event_ip.id AS ipId, + event_ip.blocklist AS blocklist, + event_ip.alert_list AS ip_alert_list, + event_ip.shared AS ip_users, + event_ip.total_visit AS ip_events, + event_ip.checked, + event_ip.fraud_detected AS fraud_detected, + + event_isp.asn, + event_isp.description, + event_isp.id AS ispId, + event_isp.name AS netname, + + event_url.url, + event_url.title, + event_url_query.query, + event_url.id AS url_id, + + event_referer.referer, + + event_account.score, + event_account.score_updated_at, + event_account.fraud, + event_account.reviewed, + event_account.added_to_review, + event_account.firstname, + event_account.lastname, + event_account.is_important, + event_account.latest_decision, + event_account.id AS accountid, + event_account.userid AS accounttitle, + + event_phone.phone_number AS phonenumber, + event_phone.type AS phone_type, + event_phone.carrier_name, + event_phone.profiles AS phone_profiles, + event_phone.invalid AS phone_invalid, + event_phone.shared AS phone_users, + event_phone.alert_list AS phone_alert_list, + event_phone.fraud_detected AS phone_fraud_detected, + phone_countries.id AS phone_country_id, + phone_countries.iso AS phone_country_iso, + phone_countries.value AS phone_full_country, + + event_email.email, + event_email.profiles, + event_email.data_breach, + event_email.data_breaches, + event_email.earliest_breach AS email_earliest_breach, + event_email.blockemails AS blockemails, + event_email.alert_list AS email_alert_list, + event_email.fraud_detected AS email_fraud_detected, + + current_email.email AS current_email, + + event_domain.id AS domainId, + event_domain.domain, + event_domain.tranco_rank, + event_domain.blockdomains, + event_domain.disposable_domains, + event_domain.free_email_provider, + event_domain.creation_date AS domain_creation_date, + event_domain.disabled AS domain_disabled, + event_domain.expiration_date AS domain_expiration_date, + event_domain.return_code AS domain_return_code, + + ip_countries.id AS country_id, + ip_countries.id AS ip_country_id, + ip_countries.iso AS ip_country_iso, + ip_countries.value AS ip_full_country, + + event_device.lang, + event_device.user_agent AS deviceId, + event_device.created AS device_created, + + event_ua_parsed.ua, + event_ua_parsed.os_name, + event_ua_parsed.os_version, + event_ua_parsed.browser_name, + event_ua_parsed.browser_version, + event_ua_parsed.device AS device_name, + event_ua_parsed.modified AS ua_modified, + + event_type.name AS event_type_name, + event_type.value AS event_type, + + event_payload.payload AS event_payload, + + event_http_method.name AS event_http_method_name + + FROM + event + + INNER JOIN event_account + ON (event.account = event_account.id) + + INNER JOIN event_url + ON (event.url = event_url.id) + + LEFT JOIN event_referer + ON (event.referer = event_referer.id) + + FULL OUTER JOIN event_url_query + ON (event.query = event_url_query.id) + + INNER JOIN event_device + ON (event.device = event_device.id) + + INNER JOIN event_type + ON (event.type = event_type.id) + + INNER JOIN event_ua_parsed + ON (event_device.user_agent = event_ua_parsed.id) + + INNER JOIN event_ip + ON (event.ip = event_ip.id) + + LEFT JOIN event_isp + ON (event_ip.isp = event_isp.id) + + INNER JOIN countries AS ip_countries + ON (event_ip.country = ip_countries.id) + + LEFT JOIN event_email + ON (event.email = event_email.id) + + LEFT JOIN event_email AS current_email + ON (event_account.lastemail = current_email.id) + + LEFT JOIN event_domain + ON (event_email.domain = event_domain.id) + + LEFT JOIN event_phone + ON (event_account.lastphone = event_phone.id) + + LEFT JOIN countries AS phone_countries + ON (phone_countries.id = event_phone.country_code) + + LEFT JOIN event_http_method + ON (event.http_method = event_http_method.id) + + LEFT JOIN event_payload + ON (event.payload = event_payload.id) + + WHERE + event.key = :api_key + AND event.id = :id + LIMIT 1' + ); + + $results = $this->execQuery($query, $params); + + $this->calculateIpType($results); + $this->calculateEmailReputation($results); + //$this->translateTimeZones($results, ['event_time', 'domain_creation_date']); + + if (count($results)) { + $results = $results[0]; + + $spamlist = $results['ip_type'] === 'Spam list'; + $results['spamlist'] = $spamlist; + + $model = new \Models\User(); + $results['score_details'] = $model->getApplicableRulesByAccountId($results['accountid'], $apiKey, true); + $results['score_calculated'] = $results['score'] !== null; + } + + return $results; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/EventType.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/EventType.php new file mode 100644 index 0000000..c633346 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/EventType.php @@ -0,0 +1,13 @@ +execQuery($query, null); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Events.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Events.php new file mode 100644 index 0000000..0878567 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Events.php @@ -0,0 +1,286 @@ + $after, + ':next_cursor' => $to, + ]; + + $query = ( + 'SELECT DISTINCT + event.account AS "accountId", + event.key + FROM + event + JOIN + event_account ON event.account = event_account.id + WHERE + event.id > :cursor + AND event.id <= :next_cursor' + ); + + return $this->execQuery($query, $params); + } + + private function getEvents(array $queryParams): array { + $request = $this->f3->get('REQUEST'); + $dateRange = $this->getDatesRange($request); + $data = $this->getData($queryParams, $dateRange); + $total = $this->getTotal($queryParams, $dateRange); + + return [ + 'draw' => $request['draw'] ?? 1, + 'recordsTotal' => $total, + 'recordsFiltered' => $total, + 'data' => $data, + ]; + } + + public function getEventsByUser(int $userId, int $apiKey): array { + $params = [ + ':user_id' => $userId, + ':api_key' => $apiKey, + ]; + + return $this->getEvents($params); + } + + private function getData(array $params, ?array $dateRange): array { + $query = ( + 'SELECT + event.id, + event.time, + event.http_code, + + event_ip.ip, + event_ip.data_center, + -- event_ip.asn, + event_ip.vpn, + event_ip.tor, + event_ip.relay, + event_ip.starlink, + event_ip.id AS ipId, + -- event_ip.description, + -- event_ip.name as netname, + event_ip.blocklist, + event_ip.fraud_detected, + event_ip.checked, + + event_isp.asn, + event_isp.description, + event_isp.name AS netname, + + event_url.url, + event_url.id as url_id, + event_url_query.query, + event_url.title, + event_referer.referer, + -- event_account.is_important, + event_account.id AS accountid, + event_account.userid AS accounttitle, + event_account.score, + + countries.id AS country_id, + countries.iso AS country_iso, + countries.value AS full_country, + event_device.id AS deviceId, + event_ua_parsed.device, + event_ua_parsed.ua, + event_ua_parsed.browser_name, + event_ua_parsed.browser_version, + event_ua_parsed.os_name, + event_ua_parsed.os_version, + event_ua_parsed.ua, + event_type.value AS event_type, + event_type.name AS event_type_name + FROM + event + INNER JOIN event_account + ON (event.account = event_account.id) + + INNER JOIN event_url + ON (event.url = event_url.id) + LEFT JOIN event_referer + ON (event.referer = event_referer.id) + -- FULL OUTER JOIN event_url_query + LEFT JOIN event_url_query + ON (event.query = event_url_query.id) + INNER JOIN event_device + ON (event.device = event_device.id) + INNER JOIN event_type + ON (event.type = event_type.id) + INNER JOIN event_ua_parsed + ON (event_device.user_agent = event_ua_parsed.id) + INNER JOIN event_ip + ON (event.ip = event_ip.id) + LEFT JOIN event_isp + ON (event_ip.isp = event_isp.id) + INNER JOIN countries + ON (event_ip.country = countries.id) + + WHERE + event.account = :user_id AND + event.key = :api_key + %s + + ORDER BY + event.time DESC' + ); + + $this->applyDateRange($query, $params, $dateRange); + $this->applyLimit($query, $params); + + $results = $this->execQuery($query, $params); + $this->calculateIpType($results); + + return $results; + } + + private function getTotal(array $params, ?array $dateRange): int { + $query = ( + 'SELECT + COUNT(event.id) + + FROM + event + INNER JOIN event_account + ON (event.account = event_account.id) + + INNER JOIN event_url + ON (event.url = event_url.id) + FULL OUTER JOIN event_url_query + ON (event.query = event_url_query.id) + INNER JOIN event_device + ON (event.device = event_device.id) + INNER JOIN event_ua_parsed + ON (event_device.user_agent = event_ua_parsed.id) + INNER JOIN event_ip + ON (event.ip = event_ip.id) + INNER JOIN countries + ON (event_ip.country = countries.id) + + WHERE + event.account = :user_id AND + event.key = :api_key + %s' + ); + + $this->applyDateRange($query, $params, $dateRange); + + $results = $this->execQuery($query, $params); + + return $results[0]['count']; + } + + private function applyDateRange(string &$query, array &$params, ?array $dateRange = null): void { + $searchConditions = ''; + if ($dateRange) { + $searchConditions = ( + 'AND event.time >= :start_time + AND event.time <= :end_time' + ); + + $params[':end_time'] = $dateRange['endDate']; + $params[':start_time'] = $dateRange['startDate']; + } + + $query = sprintf($query, $searchConditions); + } + + protected function applyLimit(string &$query, array &$queryParams): void { + $request = $this->f3->get('REQUEST'); + + $start = $request['start'] ?? null; + $length = $request['length'] ?? null; + + if (isset($start) && isset($length)) { + $query .= ' LIMIT :length OFFSET :start'; + + $queryParams[':start'] = $start; + $queryParams[':length'] = $length; + } + } + + public function uniqueEntitesByUserId(int $userId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':user_id' => $userId, + ]; + $query = ( + 'SELECT DISTINCT + event.ip, + event_ip.isp, + event_ip.country, + event.url, + event_email.domain, + event_phone.phone_number + FROM + event + LEFT JOIN event_ip + ON event.ip = event_ip.id + LEFT JOIN event_email + ON event.email = event_email.id + LEFT JOIN event_phone + ON event.phone = event_phone.id + LEFT JOIN event_country + ON event_ip.country = event_country.country AND event_ip.key = event_country.key + + WHERE + event.key = :api_key AND + event.account = :user_id' + ); + + $results = $this->execQuery($query, $params); + + return [ + 'ip_ids' => array_unique(array_column($results, 'ip')), + 'isp_ids' => array_unique(array_column($results, 'isp')), + 'country_ids' => array_unique(array_column($results, 'country')), + 'url_ids' => array_unique(array_column($results, 'url')), + 'domain_ids' => array_unique(array_column($results, 'domain')), + 'phone_numbers' => array_unique(array_column($results, 'phone_number')), + ]; + } + + public function retentionDeletion(int $weeks, int $apiKey): int { + // insuring clause + if ($weeks < 1) { + return 0; + } + + $params = [ + ':api_key' => $apiKey, + ':weeks' => $weeks, + ]; + + $query = ( + 'DELETE FROM event + WHERE + event.key = :api_key AND + (EXTRACT(EPOCH FROM (NOW() - event.time)) / (60 * 60 * 24 * 7)) >= :weeks' + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/FieldAuditTrail.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/FieldAuditTrail.php new file mode 100644 index 0000000..f160aaf --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/FieldAuditTrail.php @@ -0,0 +1,72 @@ + $eventId, + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT + event_field_audit_trail.field_id, + event_field_audit_trail.field_name, + event_field_audit_trail.old_value, + event_field_audit_trail.new_value, + event_field_audit_trail.parent_id, + event_field_audit_trail.parent_name + FROM + event_field_audit_trail + WHERE + event_field_audit_trail.event_id = :event_id AND + event_field_audit_trail.key = :api_key + + ORDER BY id DESC' + ); + + return $this->execQuery($query, $params); + } + + public function getByUserId(int $userId, int $apiKey): array { + $params = [ + ':user_id' => $userId, + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT + event_field_audit_trail.field_id, + event_field_audit_trail.field_name, + event_field_audit_trail.old_value, + event_field_audit_trail.new_value, + event_field_audit_trail.parent_id, + event_field_audit_trail.parent_name + FROM + event_field_audit_trail + WHERE + event_field_audit_trail.account_id = :user_id AND + event_field_audit_trail.key = :api_key + + ORDER BY id DESC' + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ForgotPassword.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ForgotPassword.php new file mode 100644 index 0000000..7bd28fb --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ForgotPassword.php @@ -0,0 +1,56 @@ +getUnusedKeyByOperatorId($operatorId); + + if ($record) { + $this->status = 'invalidated'; + $this->save(); + } + + $this->reset(); + $this->renew_key = $this->getPseudoRandomString(32); + $this->operator_id = $operatorId; + $this->status = 'unused'; + + $this->save(); + } + + private function getUnusedKeyByOperatorId(int $operatorId): self|null|false { + return $this->load( + ['"operator_id"=? AND "status"=?', $operatorId, 'unused'], + ); + } + + public function getUnusedByRenewKey(string $key): self|null|false { + return $this->load( + ['"renew_key"=? AND "status"=?', $key, 'unused'], + ); + } + + public function deactivate(): void { + if ($this->loaded()) { + $this->status = 'used'; + + $this->save(); + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Base/Grid.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Base/Grid.php new file mode 100644 index 0000000..89bea97 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Base/Grid.php @@ -0,0 +1,78 @@ +setIds($ids, $idsParams); + + $draw = $this->f3->get('REQUEST.draw'); + + $draw = $draw ?? 1; + $data = $this->getData(); + $total = $this->getTotal(); + + $params = $this->f3->get('GET'); + $dateRange = $this->getDatesRange($params); + + return [ + 'data' => $data, + 'draw' => $draw, + 'recordsTotal' => $total, + 'recordsFiltered' => $total, + 'dateRange' => $dateRange, + ]; + } + + public function setIds(?string $ids, array $idsParams): void { + $this->query->setIds($ids, $idsParams); + } + + protected function getData(): array { + [$query, $params] = $this->query->getData(); + + $results = $this->execQuery($query, $params); + + $this->convertTimeToUserTimezone($results); + $this->calculateCustomParams($results); + + return $results; + } + + protected function getTotal(): int { + [$query, $params] = $this->query->getTotal(); + + $results = $this->execQuery($query, $params); + + return $results[0]['count']; + } + + protected function convertTimeToUserTimezone(array &$result): void { + $this->translateTimeZones($result); + } + + protected function calculateCustomParams(array &$result): void { + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Base/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Base/Ids.php new file mode 100644 index 0000000..b3a4dc0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Base/Ids.php @@ -0,0 +1,37 @@ +apiKey = $apiKey; + } + + public function execute(string $query, array $params): array { + $params[':api_key'] = $this->apiKey; + + $data = $this->execQuery($query, $params); + $results = array_column($data, 'itemid'); + + return count($results) ? $results : [-1]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Base/Query.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Base/Query.php new file mode 100644 index 0000000..a69f1a3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Base/Query.php @@ -0,0 +1,127 @@ +f3 = \Base::instance(); + $this->apiKey = $apiKey; + } + + public function setIds(?string $ids, array $idsParams): void { + $this->ids = $ids; + $this->idsParams = $idsParams; + if (count($this->idsParams)) { + $this->itemKey = array_keys($this->idsParams)[0]; + $this->itemId = $this->idsParams[$this->itemKey]; + } + } + + protected function applyOrder(string &$query): void { + $request = $this->f3->get('REQUEST'); + + $order = $request['order'] ?? []; + $columns = $request['columns'] ?? []; + + $orderCondition = $this->defaultOrder; + + if (count($order) && count($columns)) { + $orderClauses = []; + foreach ($order as $orderData) { + $sortDirection = $orderData['dir'] === 'asc' ? 'ASC' : 'DESC'; + $columnIndex = $orderData['column']; + $sortColumn = $columns[$columnIndex]['data']; + if (in_array($sortColumn, $this->allowedColumns)) { + $orderClauses[] = sprintf('%s %s', $sortColumn, $sortDirection); + } + } + + if (count($orderClauses)) { + $orderCondition = implode(', ', $orderClauses); + } + } + + if ($orderCondition) { + $query .= sprintf(' ORDER BY %s', $orderCondition); + } + } + + protected function applyDateRange(string &$query, array &$queryParams): void { + $params = $this->f3->get('GET'); + $dateRange = $this->getDatesRange($params); + + if ($dateRange) { + $searchConditions = ( + "AND {$this->dateRangeField} >= :start_time + AND {$this->dateRangeField} <= :end_time + %s" + ); + + $query = sprintf($query, $searchConditions); + $queryParams[':end_time'] = $dateRange['endDate']; + $queryParams[':start_time'] = $dateRange['startDate']; + } + } + + protected function applyLimit(string &$query, array &$queryParams): void { + $request = $this->f3->get('REQUEST'); + + $start = $request['start'] ?? null; + $length = $request['length'] ?? null; + + if (isset($start) && isset($length)) { + $query .= ' LIMIT :length OFFSET :start'; + + $queryParams[':start'] = $start; + $queryParams[':length'] = $length; + } + } + + protected function getQueryParams(): array { + return [':api_key' => $this->apiKey]; + } + + public function injectIdQuery(string $field, &$params): string { + $idsQuery = $this->ids; + if ($idsQuery === null || $idsQuery === '') { + return ''; + } + $idsParams = $this->idsParams; + + foreach ($idsParams as $key => $value) { + if (!array_key_exists($key, $params) || $params[$key] === null) { + $params[$key] = $value; + } + } + + return " AND $field IN ($idsQuery)"; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Base/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Base/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Base/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getAllBlacklistedItems(): array { + return $this->getGrid(); + } + + protected function convertTimeToUserTimezone(array &$result): void { + $fields = ['created', 'score_updated_at']; + + $this->translateTimeZones($result, $fields); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Blacklist/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Blacklist/Ids.php new file mode 100644 index 0000000..99794ed --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Blacklist/Ids.php @@ -0,0 +1,19 @@ +getQueryParams(); + + $query = (" + SELECT DISTINCT + blacklist.is_important, + blacklist.accountid, + blacklist.accounttitle, + blacklist.created, + blacklist.score_updated_at, + blacklist.score, + blacklist.account_email AS email, + extra.type, + TRUE AS fraud, + CASE extra.type + WHEN 'ip' THEN blacklist.ip + WHEN 'email' THEN blacklist.email + WHEN 'phone' THEN blacklist.phone + END AS value, + CASE extra.type + WHEN 'ip' THEN blacklist.ip_id + WHEN 'email' THEN blacklist.email_id + WHEN 'phone' THEN blacklist.phone_id + END AS entity_id + + FROM + ( + SELECT + event_account.is_important, + event_account.id AS accountid, + event_account.userid AS accounttitle, + event_account.latest_decision AS created, + event_account.score_updated_at, + event_account.score, + + account_email.email AS account_email, + + CASE WHEN event_ip.fraud_detected THEN split_part(event_ip.ip::text, '/', 1) ELSE NULL END AS ip, + CASE WHEN event_ip.fraud_detected THEN event_ip.id ELSE NULL END AS ip_id, + event_ip.fraud_detected AS ip_fraud, + + CASE WHEN event_email.fraud_detected THEN event_email.email ELSE NULL END AS email, + CASE WHEN event_email.fraud_detected THEN event_email.id ELSE NULL END AS email_id, + event_email.fraud_detected AS email_fraud, + + CASE WHEN event_phone.fraud_detected THEN event_phone.phone_number ELSE NULL END AS phone, + CASE WHEN event_phone.fraud_detected THEN event_phone.id ELSE NULL END AS phone_id, + event_phone.fraud_detected AS phone_fraud + + FROM event + + LEFT JOIN event_account + ON event_account.id = event.account + + LEFT JOIN event_email AS account_email + ON event_account.lastemail = account_email.id + + LEFT JOIN event_ip + ON event_ip.id = event.ip + + LEFT JOIN event_email + ON event_email.id = event.email + + LEFT JOIN event_phone + ON event_phone.id = event.phone + + WHERE + event_account.key = :api_key AND + event_account.fraud IS TRUE AND + ( + event_email.fraud_detected IS TRUE OR + event_ip.fraud_detected IS TRUE OR + event_phone.fraud_detected IS TRUE + ) + ) AS blacklist, + LATERAL ( + VALUES + (CASE WHEN ip_fraud = true THEN 'ip' END), + (CASE WHEN email_fraud = true THEN 'email' END), + (CASE WHEN phone_fraud = true THEN 'phone' END) + ) AS extra(type) + + WHERE + extra.type IS NOT NULL + %s + "); + + $this->applySearch($query, $queryParams); + $this->applyEntityTypes($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = (" + SELECT COUNT(*) + FROM ( + SELECT DISTINCT + blacklist.accountid, + blacklist.accounttitle, + blacklist.created, + extra.type, + CASE extra.type + WHEN 'ip' THEN blacklist.ip + WHEN 'email' THEN blacklist.email + WHEN 'phone' THEN blacklist.phone + END AS value + + FROM + ( + SELECT + event_account.id AS accountid, + event_account.userid AS accounttitle, + event_account.latest_decision AS created, + CASE WHEN event_ip.fraud_detected THEN split_part(event_ip.ip::text, '/', 1) ELSE NULL END AS ip, + event_ip.fraud_detected AS ip_fraud, + CASE WHEN event_email.fraud_detected THEN event_email.email ELSE NULL END AS email, + event_email.fraud_detected AS email_fraud, + CASE WHEN event_phone.fraud_detected THEN event_phone.phone_number ELSE NULL END AS phone, + event_phone.fraud_detected AS phone_fraud + FROM event + + LEFT JOIN event_account + ON event_account.id = event.account + + LEFT JOIN event_ip + ON event_ip.id = event.ip + + LEFT JOIN event_email + ON event_email.id = event.email + + LEFT JOIN event_phone + ON event_phone.id = event.phone + + WHERE + event_account.key = :api_key AND + event_account.fraud IS TRUE AND + ( + event_email.fraud_detected IS TRUE OR + event_ip.fraud_detected IS TRUE OR + event_phone.fraud_detected IS TRUE + ) + ) AS blacklist, + LATERAL ( + VALUES + (CASE WHEN ip_fraud = true THEN 'ip' END), + (CASE WHEN email_fraud = true THEN 'email' END), + (CASE WHEN phone_fraud = true THEN 'phone' END) + ) AS extra(type) + + WHERE + extra.type IS NOT NULL + %s + ) AS tbl + "); + + $this->applySearch($query, $queryParams); + $this->applyEntityTypes($query, $queryParams); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + $this->applyDateRange($query, $queryParams); + + $searchConditions = ''; + $search = $this->f3->get('REQUEST.search'); + + if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') { + $searchConditions .= ( + " AND ( + LOWER(blacklist.accounttitle) LIKE LOWER(:search_value) OR + LOWER(extra.type) LIKE LOWER(:search_value) OR + LOWER(CASE extra.type + WHEN 'ip' THEN blacklist.ip + WHEN 'email' THEN blacklist.email + WHEN 'phone' THEN blacklist.phone + END) LIKE LOWER(:search_value) OR + TO_CHAR(blacklist.created::timestamp without time zone, 'dd/mm/yyyy hh24:mi:ss') LIKE :search_value + )" + ); + + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + + //Add search into request + $query = sprintf($query, $searchConditions . ' %s'); + } + + private function applyEntityTypes(string &$query, array &$queryParams): void { + $searchCondition = ''; + + $entityTypeIds = $this->f3->get('REQUEST.entityTypeIds'); + if ($entityTypeIds !== null && count($entityTypeIds)) { + $clauses = []; + + foreach ($entityTypeIds as $key => $entityTypeId) { + $clauses[] = 'extra.type = :entity_type_' . $key; + $queryParams[':entity_type_' . $key] = strtolower(\Utils\Constants::get('ENTITY_TYPES')[$entityTypeId]); + } + + $searchCondition = ' AND (' . implode(' OR ', $clauses) . ')'; + } + + $query = sprintf($query, $searchCondition); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Blacklist/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Blacklist/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Blacklist/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getAllBots(): array { + return $this->getGrid(); + } + + protected function calculateCustomParams(array &$result): void { + $this->applyDeviceParams($result); + } + + protected function convertTimeToUserTimezone(array &$result): void { + $fields = ['lastseen']; + + $this->translateTimeZones($result, $fields); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Bots/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Bots/Ids.php new file mode 100644 index 0000000..fb749c6 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Bots/Ids.php @@ -0,0 +1,19 @@ +getQueryParams(); + + $query = ( + 'SELECT + event_ua_parsed.id, + event_ua_parsed.device, + event_ua_parsed.browser_name, + event_ua_parsed.browser_version, + event_ua_parsed.os_name, + event_ua_parsed.os_version, + event_ua_parsed.ua, + event_ua_parsed.modified, + ed.lastseen + + FROM + event_ua_parsed + + LEFT JOIN ( + SELECT + user_agent, + MAX(lastseen) AS lastseen + FROM event_device + WHERE key = :api_key + GROUP BY user_agent + ) AS ed + ON event_ua_parsed.id = ed.user_agent + + WHERE + event_ua_parsed.key = :api_key AND + event_ua_parsed.modified IS TRUE + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT(*) + + FROM + event_ua_parsed + + LEFT JOIN ( + SELECT + user_agent, + MAX(lastseen) AS lastseen + FROM event_device + WHERE key = :api_key + GROUP BY user_agent + ) AS ed + ON event_ua_parsed.id = ed.user_agent + + WHERE + event_ua_parsed.key = :api_key AND + event_ua_parsed.modified IS TRUE + %s' + ); + + $this->applySearch($query, $queryParams); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + $this->applyDateRange($query, $queryParams); + + $searchConditions = ''; + $search = $this->f3->get('REQUEST.search'); + + if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') { + $searchConditions = ( + ' AND + ( + event_ua_parsed.device LIKE LOWER(:search_value) OR + event_ua_parsed.browser_name LIKE LOWER(:search_value) OR + event_ua_parsed.os_name LIKE LOWER(:search_value) OR + event_ua_parsed.ua LIKE LOWER(:search_value) + )' + ); + + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + + //Add search into request + $query = sprintf($query, $searchConditions); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Bots/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Bots/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Bots/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getAllCountries(): array { + return $this->getGrid(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Countries/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Countries/Ids.php new file mode 100644 index 0000000..929ed49 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Countries/Ids.php @@ -0,0 +1,19 @@ +getQueryParams(); + + $query = ( + 'SELECT + countries.iso AS country_iso, + countries.id AS country_id, + countries.value AS full_country, + countries.id, + + event_country.total_visit, + event_country.total_account, + event_country.total_ip + + + FROM + event_country + + LEFT JOIN countries + ON event_country.country = countries.id + + WHERE + event_country.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyOrder($query); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT(event_country.id) + + FROM + event_country + + INNER JOIN countries + ON event_country.country = countries.id + + WHERE + event_country.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + //Add dates into request + $this->applyDateRange($query, $queryParams); + + $search = $this->f3->get('REQUEST.search'); + $searchConditions = ''; + + if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') { + $searchConditions .= ( + ' AND + ( + LOWER(countries.value) LIKE LOWER(:search_value) + OR LOWER(countries.iso) LIKE LOWER(:search_value) + )' + ); + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + + //Add search and ids into request + $query = sprintf($query, $searchConditions); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Countries/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Countries/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Countries/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getDevicesByIpId(int $ipId): array { + $params = [':ip_id' => $ipId]; + + return $this->getGrid($this->idsModel->getDevicesIdsByIpId(), $params); + } + + public function getDevicesByUserId(int $userId): array { + $params = [':account_id' => $userId]; + + return $this->getGrid($this->idsModel->getDevicesIdsByUserId(), $params); + } + + public function getDevicesByResourceId(int $resourceId): array { + $params = [':resource_id' => $resourceId]; + + return $this->getGrid($this->idsModel->getDevicesIdsByResourceId(), $params); + } + + public function getAllDevices(): array { + return $this->getGrid(); + } + + protected function calculateCustomParams(array &$result): void { + $this->applyDeviceParams($result); + } + + protected function convertTimeToUserTimezone(array &$result): void { + $fields = ['created']; + + $this->translateTimeZones($result, $fields); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Devices/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Devices/Ids.php new file mode 100644 index 0000000..44eae13 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Devices/Ids.php @@ -0,0 +1,51 @@ +getQueryParams(); + + $query = ( + 'SELECT + event_device.id, + event_device.lang, + event_device.created, + event_ua_parsed.device, + event_ua_parsed.browser_name, + event_ua_parsed.browser_version, + event_ua_parsed.os_name, + event_ua_parsed.os_version, + event_ua_parsed.ua, + event_ua_parsed.modified + FROM + event_device + LEFT JOIN event_ua_parsed + ON (event_device.user_agent = event_ua_parsed.id) + + WHERE + event_device.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT(DISTINCT event_device.id) + + FROM + event_device + + WHERE + event_device.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + //$search = $this->f3->get('REQUEST.search'); + $searchConditions = $this->injectIdQuery('event_device.id', $queryParams); + + //Add ids into request + $query = sprintf($query, $searchConditions); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Devices/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Devices/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Devices/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getDomainsBySameIpDomainId(int $domainId): array { + $params = [':domain_id' => $domainId]; + + return $this->getGrid($this->idsModel->getDomainsIdsBySameIpDomainId(), $params); + } + + public function getAllDomains(): array { + return $this->getGrid(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Domains/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Domains/Ids.php new file mode 100644 index 0000000..1b8d2c2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Domains/Ids.php @@ -0,0 +1,38 @@ +getQueryParams(); + + $query = ( + 'SELECT + event_domain.id, + event_domain.domain, + event_domain.ip, + event_domain.total_account, + event_domain.total_visit, + event_domain.disposable_domains, + event_domain.creation_date, + event_domain.disabled, + event_domain.free_email_provider, + event_domain.tranco_rank, + ( + SELECT COUNT(*) + FROM event_email + WHERE + event_email.domain = event_domain.id AND + event_email.key = :api_key AND + event_email.fraud_detected IS TRUE + ) AS fraud + + FROM + event_domain + + WHERE + event_domain.key = :api_key + %s + + GROUP BY + event_domain.id' + ); + + $this->applySearch($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT (event_domain.id) + + FROM + event_domain + + WHERE + event_domain.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + $this->applyDateRange($query, $queryParams); + + $search = $this->f3->get('REQUEST.search'); + $searchConditions = $this->injectIdQuery('event_domain.id', $queryParams); + + if (isset($search) && $search['value'] !== null) { + $searchConditions .= ( + ' AND ( + LOWER(event_domain.domain) LIKE LOWER(:search_value) + OR TEXT(event_domain.creation_date) LIKE LOWER(:search_value) + )' + ); + + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + + //Add search and ids into request + $query = sprintf($query, $searchConditions); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Domains/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Domains/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Domains/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getEmailsByUserId(int $userId): array { + $params = [':account_id' => $userId]; + + return $this->getGrid($this->idsModel->getEmailsIdsByUserId(), $params); + } + + protected function calculateCustomParams(array &$result): void { + $this->calculateEmailReputation($result); + } + + protected function convertTimeToUserTimezone(array &$result): void { + $fields = ['lastseen']; + + $this->translateTimeZones($result, $fields); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Emails/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Emails/Ids.php new file mode 100644 index 0000000..32f5cc0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Emails/Ids.php @@ -0,0 +1,29 @@ +getQueryParams(); + + $query = ( + 'SELECT + event_email.id, + event_email.email, + event_email.data_breach, + event_email.data_breaches, + event_email.fraud_detected, + -- event_email.profiles, + event_email.blockemails, + event_email.lastseen, + event_email.alert_list, + + event_domain.domain, + event_domain.id AS domain_id, + event_domain.free_email_provider, + event_domain.disposable_domains + + FROM + event_email + + LEFT JOIN event_domain + ON (event_email.domain = event_domain.id) + + WHERE + event_email.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT(*) + + FROM + event_email + + WHERE + event_email.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + $search = $this->f3->get('REQUEST.search'); + $searchConditions = $this->injectIdQuery('event_email.id', $queryParams); + + if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') { + $searchConditions .= ( + ' AND + ( + event_email.email LIKE :search_value + OR TEXT(event_email.lastseen) LIKE :search_value + )' + ); + + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + + //Add search and ids into request + $query = sprintf($query, $searchConditions); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Emails/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Emails/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Emails/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getEventsByUserId(int $userId): array { + $ids = ['userId' => $userId]; + + return $this->getGrid(null, $ids); + } + + public function getEventsByIspId(int $ispId): array { + $ids = ['ispId' => $ispId]; + + return $this->getGrid(null, $ids); + } + + public function getEventsByDomainId(int $domainId): array { + $ids = ['domainId' => $domainId]; + + return $this->getGrid(null, $ids); + } + + public function getEventsByDeviceId(int $deviceId): array { + $ids = ['deviceId' => $deviceId]; + + return $this->getGrid(null, $ids); + } + + public function getEventsByResourceId(int $resourceId): array { + $ids = ['resourceId' => $resourceId]; + + return $this->getGrid(null, $ids); + } + + public function getEventsByCountryId(int $countryId): array { + $ids = ['countryId' => $countryId]; + + return $this->getGrid(null, $ids); + } + + public function getEventsByIpId(int $ipId): array { + $ids = ['ipId' => $ipId]; + + return $this->getGrid(null, $ids); + } + + public function getAllEvents() { + return $this->getGrid(); + } + + protected function calculateCustomParams(array &$result): void { + $this->calculateIpType($result); + $this->applyDeviceParams($result); + } + + protected function convertTimeToUserTimezone(array &$result): void { + $fields = ['time', 'lastseen', 'session_max_t', 'session_min_t', 'score_updated_at']; + + $this->translateTimeZones($result, $fields); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Events/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Events/Ids.php new file mode 100644 index 0000000..ecb6e09 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Events/Ids.php @@ -0,0 +1,19 @@ +getQueryParams(); + + $query = ( + 'SELECT + event.id, + event.time, + + event_type.value AS event_type, + event_type.name AS event_type_name, + + event_account.is_important, + event_account.id AS accountid, + event_account.userid AS accounttitle, + event_account.score_updated_at, + event_account.score, + event_account.fraud, + + event_url.url, + event_url.id as url_id, + event_url_query.query, + event_url.title, + + event_ip.ip, + event_ip.data_center, + event_ip.vpn, + event_ip.tor, + event_ip.relay, + event_ip.starlink, + event_ip.blocklist, + event_ip.fraud_detected, + event_ip.checked, + + event_isp.name AS isp_name, + + countries.iso AS country_iso, + countries.id AS country_id, + countries.value AS full_country, + + event_ua_parsed.ua, + event_ua_parsed.device, + event_ua_parsed.os_name, + + event_email.email, + event.http_code, + event.session_id, + event_session.total_visit AS session_cnt, + event_session.lastseen AS session_max_t, + event_session.created AS session_min_t, + event_session.lastseen - event_session.created AS session_duration + + FROM + event + + INNER JOIN event_account + ON (event.account = event_account.id) + + INNER JOIN event_url + ON (event.url = event_url.id) + + FULL OUTER JOIN event_url_query + ON (event.query = event_url_query.id) + + LEFT JOIN event_device + ON (event.device = event_device.id) + + LEFT JOIN event_type + ON (event.type = event_type.id) + + LEFT JOIN event_ua_parsed + ON (event_device.user_agent = event_ua_parsed.id) + + LEFT JOIN event_ip + ON (event.ip = event_ip.id) + + LEFT JOIN event_isp + ON (event_ip.isp = event_isp.id) + + INNER JOIN countries + ON (event_ip.country = countries.id) + + LEFT JOIN event_email + ON (event.email = event_email.id) + + LEFT JOIN event_session + ON (event.time = event_session.lastseen AND event.session_id = event_session.id) + + WHERE + event.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyEventTypes($query, $queryParams); + $this->applyDeviceTypes($query, $queryParams); + $this->applyRules($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $query = null; + $queryParams = $this->getQueryParams(); + + if ($this->itemId !== null) { + switch ($this->itemKey) { + case 'userId': + $query = 'SELECT total_visit AS count FROM event_account WHERE key = :api_key AND id = :item_id'; + break; + case 'ispId': + $query = 'SELECT total_visit AS count FROM event_isp WHERE key = :api_key AND id = :item_id'; + break; + case 'domainId': + $query = 'SELECT total_visit AS count FROM event_domain WHERE key = :api_key AND id = :item_id'; + break; + case 'resourceId': + $query = 'SELECT total_visit AS count FROM event_url WHERE key = :api_key AND id = :item_id'; + break; + case 'countryId': + $query = 'SELECT total_visit AS count FROM event_country WHERE key = :api_key AND country = :item_id'; + break; + case 'ipId': + $query = 'SELECT total_visit AS count FROM event_ip WHERE key = :api_key AND id = :item_id'; + break; + case 'deviceId': + $query = ( + 'SELECT + COUNT(event.id) AS count + FROM event + INNER JOIN event_device + ON (event.device = event_device.id) + WHERE + event_device.key = :api_key AND + event_device.user_agent = :item_id' + ); + break; + } + } + + if (!$query) { + $query = ( + 'SELECT + COUNT(event.id) AS count + + FROM + event + + INNER JOIN event_account + ON (event.account = event_account.id) + + INNER JOIN event_url + ON (event.url = event_url.id) + + INNER JOIN event_ip + ON (event.ip = event_ip.id) + + INNER JOIN countries + ON (event_ip.country = countries.id) + + LEFT JOIN event_email + ON (event.email = event_email.id) + + LEFT JOIN event_device + ON (event.device = event_device.id) + + LEFT JOIN event_type + ON (event.type = event_type.id) + + LEFT JOIN event_ua_parsed + ON (event_device.user_agent = event_ua_parsed.id) + + WHERE + event.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyEventTypes($query, $queryParams); + $this->applyDeviceTypes($query, $queryParams); + $this->applyRules($query, $queryParams); + } + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + //Add dates into request + $this->applyDateRange($query, $queryParams); + + //Apply itemId into request + $this->applyRelatedToIdSearchConitions($query); + + $searchConditions = ''; + $search = $this->f3->get('REQUEST.search'); + + if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') { + //TODO: user isIp function here + if (filter_var($search['value'], FILTER_VALIDATE_IP) !== false) { + $searchConditions .= ( + ' AND + ( + event_ip.ip = :search_value + )' + ); + + $queryParams[':search_value'] = $search['value']; + } else { + // https://stackoverflow.com/a/63701098 + $searchConditions .= ( + " AND + ( + LOWER(event_email.email) LIKE LOWER(:search_value) OR + LOWER(event_account.userid) LIKE LOWER(:search_value) OR + event.http_code::text LIKE LOWER(:search_value) OR + + CASE WHEN event.http_code >= 400 THEN + CONCAT('error ', event.http_code) + ELSE + '' END LIKE LOWER(:search_value) OR + + TO_CHAR(event.time::timestamp without time zone, 'dd/mm/yyyy hh24:mi:ss') LIKE :search_value + )" + ); + + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + } + + //Add search and ids into request + $query = sprintf($query, $searchConditions); + } + + protected function getQueryParams(): array { + $params = [':api_key' => $this->apiKey]; + if ($this->itemId !== null) { + $params[':item_id'] = $this->itemId; + } + + return $params; + } + + private function applyRelatedToIdSearchConitions(string &$query): void { + $searchConditions = null; + + if ($this->itemId !== null) { + switch ($this->itemKey) { + case 'userId': + $searchConditions = ' AND event.account = :item_id %s'; + break; + case 'ispId': + $searchConditions = ' AND event_isp.id = :item_id %s'; + break; + case 'domainId': + $searchConditions = ' AND event_email.domain = :item_id %s'; + break; + case 'resourceId': + $searchConditions = ' AND event.url = :item_id %s'; + break; + case 'countryId': + $searchConditions = ' AND countries.id = :item_id %s'; + break; + case 'ipId': + $searchConditions = ' AND event_ip.id = :item_id %s'; + break; + case 'deviceId': + $searchConditions = ' AND event_ua_parsed.id = :item_id %s'; + break; + } + } + + //Add search and ids into request + if ($searchConditions !== null) { + $query = sprintf($query, $searchConditions); + } + } + + private function applyEventTypes(string &$query, array &$queryParams): void { + $eventTypeIds = $this->f3->get('REQUEST.eventTypeIds'); + if ($eventTypeIds === null || !count($eventTypeIds)) { + return; + } + + $clauses = []; + foreach ($eventTypeIds as $key => $eventTypeId) { + $clauses[] = 'event.type = :event_type_id_' . $key; + $queryParams[':event_type_id_' . $key] = $eventTypeId; + } + + $query .= ' AND (' . implode(' OR ', $clauses) . ')'; + } + + private function applyDeviceTypes(string &$query, array &$queryParams): void { + $deviceTypes = $this->f3->get('REQUEST.deviceTypes'); + if ($deviceTypes === null || !count($deviceTypes)) { + return; + } + + $clauses = []; + foreach ($deviceTypes as $key => $deviceType) { + if ($deviceType === 'other') { + $placeholders = []; + + foreach (\Utils\Constants::get('DEVICE_TYPES') as $device) { + if ($device !== 'unknown' && $device !== 'other') { + $param = ':device_exclude_' . $device; + $placeholders[] = $param; + $queryParams[$param] = $device; + } + } + + $params = implode(', ', $placeholders); + + $clauses[] = '(event_ua_parsed.device NOT IN (' . $params . ') AND event_ua_parsed.device IS NOT NULL)'; + } elseif ($deviceType === 'unknown') { + $clauses[] = 'event_ua_parsed.device IS NULL'; + } else { + $param = ':device_' . $key; + $clauses[] = 'event_ua_parsed.device = ' . $param; + $queryParams[$param] = $deviceType; + } + } + + $query .= ' AND (' . implode(' OR ', $clauses) . ')'; + } + + private function applyRules(string &$query, array &$queryParams): void { + $ruleUids = $this->f3->get('REQUEST.ruleUids'); + if ($ruleUids === null) { + return; + } + + $uids = []; + foreach ($ruleUids as $ruleUid) { + $uids[] = ['uid' => $ruleUid]; + } + + $query .= ' AND score_details @> :rules_uids::jsonb'; + $queryParams[':rules_uids'] = json_encode($uids); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Events/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Events/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Events/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getIpsByUserId(int $userId): array { + $params = [':account_id' => $userId]; + + return $this->getGrid($this->idsModel->getIpsIdsByUserId(), $params); + } + + public function getIpsByIspId(int $ispId): array { + $params = [':isp_id' => $ispId]; + + return $this->getGrid($this->idsModel->getIpsIdsByIspId(), $params); + } + + public function getIpsByDomainId($domainId) { + $params = [':domain_id' => $domainId]; + + return $this->getGrid($this->idsModel->getIpsIdsByDomainId(), $params); + } + + public function getIpsByCountryId(int $countryId): array { + $params = [':country_id' => $countryId]; + + return $this->getGrid($this->idsModel->getIpsIdsByCountryId(), $params); + } + + public function getIpsByDeviceId(int $deviceId): array { + $params = [':device_id' => $deviceId]; + + return $this->getGrid($this->idsModel->getIpsIdsByDeviceId(), $params); + } + + public function getIpsByResourceId(int $resourceId): array { + $params = [':resource_id' => $resourceId]; + + return $this->getGrid($this->idsModel->getIpsIdsByResourceId(), $params); + } + + public function getAllIps() { + return $this->getGrid(); + } + + protected function calculateCustomParams(array &$result): void { + $this->calculateIpType($result); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Ips/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Ips/Ids.php new file mode 100644 index 0000000..03b379a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Ips/Ids.php @@ -0,0 +1,88 @@ +getQueryParams(); + + $query = ( + 'SELECT + event_ip.id, + event_ip.ip, + event_ip.fraud_detected, + event_ip.alert_list, + event_ip.data_center, + event_ip.vpn, + event_ip.tor, + event_ip.relay, + event_ip.blocklist, + event_ip.starlink, + event_ip.shared AS total_account, + event_ip.total_visit, + event_ip.checked, + + event_ip.lastseen AS lastseen, + + event_isp.name AS netname, + event_isp.description, + event_isp.asn, + + countries.id AS country_id, + countries.iso AS country_iso, + countries.value AS full_country + + FROM + event_ip + + LEFT JOIN countries + ON (event_ip.country = countries.id) + + LEFT JOIN event_isp + ON (event_ip.isp = event_isp.id) + + WHERE + event_ip.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyIpTypes($query); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT (DISTINCT event_ip.ip) + + FROM + event_ip + + LEFT JOIN countries + ON (event_ip.country = countries.id) + + LEFT JOIN event_isp + ON (event_ip.isp = event_isp.id) + + WHERE + event_ip.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyIpTypes($query); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + $this->applyDateRange($query, $queryParams); + + $search = $this->f3->get('REQUEST.search'); + $searchConditions = $this->injectIdQuery('event_ip.id', $queryParams); + + if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') { + $searchConditions .= ( + ' AND + ( + TEXT(event_ip.ip) LIKE LOWER(:search_value) + OR LOWER(event_isp.asn::text) LIKE LOWER(:search_value) + OR LOWER(event_isp.name) LIKE LOWER(:search_value) + OR LOWER(countries.value) LIKE LOWER(:search_value) + OR LOWER(countries.iso) LIKE LOWER(:search_value) + )' + ); + + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + + //Add search and ids into request + $query = sprintf($query, $searchConditions); + } + + private function applyIpTypes(string &$query): void { + $ipTypeIds = $this->f3->get('REQUEST.ipTypeIds'); + if ($ipTypeIds === null) { + return; + } + + foreach ($ipTypeIds as $ipTypeId) { + switch ($ipTypeId) { + case 0: + $query .= ' AND fraud_detected IS TRUE '; + break; + case 1: + $query .= ' AND blocklist IS TRUE '; + break; + case 2: + $query .= ' AND countries.id = 0 AND event_ip.checked IS TRUE '; + break; + case 3: + $query .= ' AND tor IS TRUE '; + break; + case 4: + $query .= ' AND starlink IS TRUE '; + break; + case 5: + $query .= ' AND relay IS TRUE '; + break; + case 6: + $query .= ' AND vpn IS TRUE '; + break; + case 7: + $query .= ' AND data_center IS TRUE '; + break; + case 8: + $query .= ' AND (event_ip.checked IS FALSE OR event_ip.checked IS NULL) '; + break; + case 9: + $query .= ' AND (tor IS FALSE AND vpn IS FALSE AND relay IS FALSE AND data_center IS FALSE AND event_ip.checked IS TRUE) '; + break; + } + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Ips/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Ips/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Ips/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getIspsByUserId(int $userId): array { + $params = [':account_id' => $userId]; + + return $this->getGrid($this->idsModel->getIspsIdsByUserId(), $params); + } + + public function getIspsByDomainId(int $domainId): array { + $params = [':domain_id' => $domainId]; + + return $this->getGrid($this->idsModel->getIspsIdsByDomainId(), $params); + } + + public function getIspsByCountryId(int $countryId): array { + $params = [':country_id' => $countryId]; + + return $this->getGrid($this->idsModel->getIspsIdsByCountryId(), $params); + } + + public function getIspsByResourceId(int $resourceId): array { + $params = [':resource_id' => $resourceId]; + + return $this->getGrid($this->idsModel->getIspsIdsByResourceId(), $params); + } + + public function getAllIsps(): array { + return $this->getGrid(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Isps/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Isps/Ids.php new file mode 100644 index 0000000..0e4a506 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Isps/Ids.php @@ -0,0 +1,70 @@ +getQueryParams(); + + $query = ( + 'SELECT + event_isp.id, + event_isp.asn, + event_isp.name, + -- event_isp.description, + event_isp.total_visit, + event_isp.total_account, + ( + SELECT COUNT(DISTINCT event.account) + FROM event + LEFT JOIN event_ip ON event.ip = event_ip.id + LEFT JOIN event_account ON event.account = event_account.id + WHERE + event_ip.isp = event_isp.id AND + event.key = :api_key AND + event_account.fraud IS TRUE + ) AS fraud, + ( + SELECT + COUNT ( DISTINCT eip.ip ) + + FROM + event_ip AS eip + + WHERE + eip.isp = event_isp.id + AND eip.key = event_isp.key + AND eip.isp IS NOT NULL + + ) AS total_ip + FROM + event_isp + + WHERE + event_isp.key = :api_key + %s + + GROUP BY + event_isp.id' + ); + + $this->applySearch($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT (event_isp.id) + + FROM + event_isp + + WHERE + event_isp.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + $this->applyDateRange($query, $queryParams); + + $search = $this->f3->get('REQUEST.search'); + $searchConditions = $this->injectIdQuery('event_isp.id', $queryParams); + + if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') { + $searchConditions .= ( + ' AND + ( + LOWER(event_isp.asn::text) LIKE LOWER(:search_value) + OR LOWER(event_isp.name) LIKE LOWER(:search_value) + OR LOWER(event_isp.description) LIKE LOWER(:search_value) + )' + ); + + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + + //Add search and ids into request + $query = sprintf($query, $searchConditions); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Isps/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Isps/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Isps/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getAllLogbookEvents() { + return $this->getGrid(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Logbook/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Logbook/Ids.php new file mode 100644 index 0000000..81f7f09 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Logbook/Ids.php @@ -0,0 +1,19 @@ +getQueryParams(); + + $query = ( + 'SELECT + event_logbook.id, + event_logbook.ip, + event_logbook.error_type, + event_logbook.error_text, + event_logbook.raw, + event_logbook.started, + event_error_type.name AS error_name, + event_error_type.value AS error_value + + FROM + event_logbook + + LEFT JOIN event_error_type + ON (event_logbook.error_type = event_error_type.id) + + WHERE + event_logbook.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT(event_logbook.id) AS count + + FROM + event_logbook + + LEFT JOIN event_error_type + ON (event_logbook.error_type = event_error_type.id) + + WHERE + event_logbook.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + //Add dates into request + $this->applyDateRange($query, $queryParams); + + $search = $this->f3->get('REQUEST.search'); + $searchConditions = ''; + + if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') { + $extra = ''; + //TODO: use isIp function here + if (filter_var($search['value'], FILTER_VALIDATE_IP) !== false) { + $extra = ' event_logbook.ip = :search_ip_value OR '; + $queryParams[':search_ip_value'] = $search['value']; + } + + $searchConditions .= ( + " AND + ( + $extra + LOWER(event_logbook.raw::text) LIKE LOWER(:search_value) OR + LOWER(event_logbook.error_text) LIKE LOWER(:search_value) OR + LOWER(event_error_type.name) LIKE LOWER(:search_value) + )" + ); + + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + + //Add search and ids into request + $query = sprintf($query, $searchConditions); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Logbook/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Logbook/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Logbook/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getDataByUserId(int $userId): array { + $params = [':account_id' => $userId]; + + return $this->getGrid($this->idsModel->getDataIdsByUserId(), $params); + } + + protected function convertTimeToUserTimezone(array &$result): void { + $fields = ['created']; + + $this->translateTimeZones($result, $fields); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Payloads/FieldAuditTrail/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Payloads/FieldAuditTrail/Ids.php new file mode 100644 index 0000000..fb35d51 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Payloads/FieldAuditTrail/Ids.php @@ -0,0 +1,29 @@ +getQueryParams(); + + $query = ( + 'SELECT + event_field_audit_trail.id, + event_field_audit_trail.created, + event_field_audit_trail.event_id, + event_field_audit_trail.field_id, + event_field_audit_trail.field_name, + event_field_audit_trail.old_value, + event_field_audit_trail.new_value, + event_field_audit_trail.parent_id, + event_field_audit_trail.parent_name + + FROM + event_field_audit_trail + + WHERE + event_field_audit_trail.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT(*) + + FROM + event_field_audit_trail + + WHERE + event_field_audit_trail.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + $searchConditions = $this->injectIdQuery('event_field_audit_trail.id', $queryParams); + + //Add ids into request + $query = sprintf($query, $searchConditions); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Payloads/FieldAuditTrail/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Payloads/FieldAuditTrail/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Payloads/FieldAuditTrail/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getPhonesByUserId(int $userId): array { + $params = [':account_id' => $userId]; + + return $this->getGrid($this->idsModel->getPhonesIdsByUserId(), $params); + } + + protected function convertTimeToUserTimezone(array &$result): void { + $fields = ['lastseen']; + + $this->translateTimeZones($result, $fields); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Phones/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Phones/Ids.php new file mode 100644 index 0000000..4932526 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Phones/Ids.php @@ -0,0 +1,29 @@ +getQueryParams(); + + $query = ( + 'SELECT + event_phone.id, + event_phone.phone_number as phonenumber, + event_phone.type, + event_phone.carrier_name, + event_phone.lastseen, + event_phone.invalid, + event_phone.shared, + event_phone.alert_list, + event_phone.fraud_detected, + + countries.id AS country_id, + countries.iso AS country_iso, + countries.value AS full_country + + FROM + event_phone + + LEFT JOIN countries + ON (event_phone.country_code = countries.id) + + WHERE + event_phone.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT(*) + + FROM + event_phone + + WHERE + event_phone.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + $search = $this->f3->get('REQUEST.search'); + $searchConditions = $this->injectIdQuery('event_phone.id', $queryParams); + + if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') { + $searchConditions .= ( + ' AND + ( + event_phone.phone_number LIKE :search_value + OR TEXT(event_phone.lastseen) LIKE :search_value + )' + ); + + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + + //Add search and ids into request + $query = sprintf($query, $searchConditions); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Phones/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Phones/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Phones/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getAllResources(): array { + $data = $this->getGrid(); + if (isset($data['data'])) { + $data['data'] = $this->extendWithSuspiciousUrl($data['data']); + } + + return $data; + } + + private function extendWithSuspiciousUrl(array $result): array { + if (is_array($result) && count($result)) { + $suspiciousUrlWords = \Utils\WordsLists\Url::getWords(); + foreach ($result as &$record) { + $record['suspicious'] = $this->isUrlSuspicious($suspiciousUrlWords, $record['url']); + } + unset($record); + } + + return $result; + } + + private function isUrlSuspicious(array $substrings, string $url): bool { + foreach ($substrings as $sub) { + if (stripos($url, $sub) !== false) { + return true; + } + } + + return false; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Resources/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Resources/Ids.php new file mode 100644 index 0000000..89c72f1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Resources/Ids.php @@ -0,0 +1,19 @@ +getQueryParams(); + + $query = ( + 'SELECT + event_url.id, + event_url.id AS url_id, + event_url.key, + event_url.url, + event_url.title, + event_url.http_code, + + event_url.total_visit, + event_url.total_ip, + event_url.total_account, + event_url.total_country + + FROM + event_url + + WHERE + event_url.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT(event_url.id) + + FROM + event_url + + WHERE + event_url.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + $this->applyDateRange($query, $queryParams); + + $search = $this->f3->get('REQUEST.search'); + $searchConditions = $this->injectIdQuery('event_url.id', $queryParams); + + if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') { + $searchConditions .= ( + ' AND + ( + LOWER(event_url.title) LIKE LOWER(:search_value) + OR LOWER(event_url.url) LIKE LOWER(:search_value) + )' + ); + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + + //Add search and ids into request + $query = sprintf($query, $searchConditions); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Resources/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Resources/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Resources/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getAllUnderReviewUsers(): array { + return $this->getGrid(); + } + + public function getTotalUnderReviewUsers(): int { + return $this->getTotal(); + } + + public function getTotalUnderReviewUsersOverall(): int { + [$query, $params] = $this->query->getTotalOverall(); + $results = $this->execQuery($query, $params); + + return $results[0]['count']; + } + + protected function convertTimeToUserTimezone(array &$result): void { + $fields = ['lastseen', 'created', 'score_updated_at', 'added_to_review']; + + $this->translateTimeZones($result, $fields); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/ReviewQueue/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/ReviewQueue/Ids.php new file mode 100644 index 0000000..a42a8b8 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/ReviewQueue/Ids.php @@ -0,0 +1,19 @@ +getQueryParams(); + + $query = ( + 'SELECT + event_account.id AS accountid, + event_account.userid AS accounttitle, + event_account.created AS created, + event_account.is_important, + event_account.score_updated_at, + event_account.score, + event_account.firstname, + event_account.lastname, + event_account.lastseen, + event_account.added_to_review, + + event_email.email + + FROM + event_account + + LEFT JOIN event_email + ON (event_account.lastemail = event_email.id) + + WHERE + event_account.key = :api_key AND + event_account.fraud IS NULL AND + event_account.added_to_review IS NOT NULL + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyRules($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT (event_account.id) + + FROM + event_account + + LEFT JOIN event_email + ON (event_account.lastemail = event_email.id) + + WHERE + event_account.key = :api_key AND + event_account.fraud IS NULL AND + event_account.added_to_review IS NOT NULL + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyRules($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotalOverall(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT(event_account.id) AS count + + FROM + event_account + + WHERE + event_account.key = :api_key AND + event_account.fraud IS NULL AND + event_account.added_to_review IS NOT NULL' + ); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + $this->applyDateRange($query, $queryParams); + + $searchConditions = ''; + $search = $this->f3->get('REQUEST.search'); + + if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') { + $searchConditions .= ( + " AND + ( + LOWER(REPLACE( + COALESCE(event_account.firstname, '') || + COALESCE(event_account.lastname, '') || + COALESCE(event_account.firstname, ''), + ' ', '')) LIKE LOWER(REPLACE(:search_value, ' ', '')) OR + LOWER(event_email.email) LIKE LOWER(:search_value) OR + LOWER(event_account.userid) LIKE LOWER(:search_value) OR + + TO_CHAR(event_account.lastseen::timestamp without time zone, 'dd/mm/yyyy hh24:mi:ss') LIKE :search_value OR + TO_CHAR(event_account.created::timestamp without time zone, 'dd/mm/yyyy hh24:mi:ss') LIKE :search_value + )" + ); + + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + + //Add search and ids into request + $query = sprintf($query, $searchConditions); + } + + private function applyRules(string &$query, array &$queryParams): void { + $ruleUids = $this->f3->get('REQUEST.ruleUids'); + if ($ruleUids === null) { + return; + } + + $uids = []; + foreach ($ruleUids as $ruleUid) { + $uids[] = ['uid' => $ruleUid]; + } + + $query .= ' AND score_details @> :rules_uids::jsonb'; + $queryParams[':rules_uids'] = json_encode($uids); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/ReviewQueue/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/ReviewQueue/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/ReviewQueue/index.php @@ -0,0 +1,3 @@ +apiKey = $apiKey; + $this->idsModel = new Ids($apiKey); + $this->query = new Query($apiKey); + } + + public function getUsersByIpId(int $ipId): array { + $params = [':ip_id' => $ipId]; + + return $this->getGrid($this->idsModel->getUsersIdsByIpId(), $params); + } + + public function getUsersByIspId(int $ispId): array { + $params = [':isp_id' => $ispId]; + + return $this->getGrid($this->idsModel->getUsersIdsByIspId(), $params); + } + + public function getUsersByDomainId(int $domainId): array { + $params = [':domain_id' => $domainId]; + + return $this->getGrid($this->idsModel->getUsersIdsByDomainId(), $params); + } + + public function getUsersByCountryId(int $countryId): array { + $params = [':country_id' => $countryId]; + + return $this->getGrid($this->idsModel->getUsersIdsByCountryId(), $params); + } + + public function getUsersByDeviceId(int $deviceId): array { + $params = [':device_id' => $deviceId]; + + return $this->getGrid($this->idsModel->getUsersIdsByDeviceId(), $params); + } + + public function getUsersByResourceId(int $resourceId): array { + $params = [':resource_id' => $resourceId]; + + return $this->getGrid($this->idsModel->getUsersIdsByResourceId(), $params); + } + + public function getAllUsers(): array { + return $this->getGrid(); + } + + protected function convertTimeToUserTimezone(array &$result): void { + $fields = ['time', 'lastseen', 'latest_decision', 'created', 'score_updated_at']; + + $this->translateTimeZones($result, $fields); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Users/Ids.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Users/Ids.php new file mode 100644 index 0000000..da3074c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Users/Ids.php @@ -0,0 +1,92 @@ +getQueryParams(); + + $query = ( + "SELECT + TEXT(date_trunc('day', event_account.created)::date) AS created_day, + + event_account.id, + event_account.is_important, + event_account.id AS accountid, + event_account.userid AS accounttitle, + event_account.score, + event_account.score_updated_at, + event_account.created, + event_account.fraud, + event_account.reviewed, + event_account.firstname, + event_account.lastname, + event_account.lastseen, + event_account.total_visit, + event_account.total_ip, + event_account.total_device, + event_account.total_country, + event_account.latest_decision, + event_account.added_to_review, + + event_email.email, + event_email.blockemails + + FROM + event_account + + LEFT JOIN event_email + ON (event_account.lastemail = event_email.id) + + WHERE + event_account.key = :api_key + %s" + ); + + $this->applySearch($query, $queryParams); + $this->applyRules($query, $queryParams); + $this->applyScore($query, $queryParams); + $this->applyOrder($query); + $this->applyLimit($query, $queryParams); + + return [$query, $queryParams]; + } + + public function getTotal(): array { + $queryParams = $this->getQueryParams(); + + $query = ( + 'SELECT + COUNT (event_account.id) + + FROM + event_account + + LEFT JOIN event_email + ON (event_account.lastemail = event_email.id) + + WHERE + event_account.key = :api_key + %s' + ); + + $this->applySearch($query, $queryParams); + $this->applyRules($query, $queryParams); + $this->applyScore($query, $queryParams); + + return [$query, $queryParams]; + } + + private function applySearch(string &$query, array &$queryParams): void { + $this->applyDateRange($query, $queryParams); + + $search = $this->f3->get('REQUEST.search'); + $searchConditions = $this->injectIdQuery('event_account.id', $queryParams); + + if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') { + $searchConditions .= ( + " AND + ( + LOWER(REPLACE( + COALESCE(event_account.firstname, '') || + COALESCE(event_account.lastname, '') || + COALESCE(event_account.firstname, ''), + ' ', '')) LIKE LOWER(REPLACE(:search_value, ' ', '')) OR + LOWER(event_email.email) LIKE LOWER(:search_value) OR + LOWER(event_account.userid) LIKE LOWER(:search_value) OR + + TO_CHAR(event_account.created::timestamp without time zone, 'dd/mm/yyyy hh24:mi:ss') LIKE :search_value + )" + ); + + $queryParams[':search_value'] = '%' . $search['value'] . '%'; + } + + //Add search and ids into request + $query = sprintf($query, $searchConditions); + } + + private function applyRules(string &$query, array &$queryParams): void { + $ruleUids = $this->f3->get('REQUEST.ruleUids'); + if ($ruleUids === null) { + return; + } + + $uids = []; + foreach ($ruleUids as $ruleUid) { + $uids[] = ['uid' => $ruleUid]; + } + + $query .= ' AND score_details @> (:rules_uids)::jsonb'; + $queryParams[':rules_uids'] = json_encode($uids); + } + + private function applyScore(string &$query, array &$queryParams): void { + $scoresRanges = $this->f3->get('REQUEST.scoresRange'); + if ($scoresRanges === null) { + return; + } + + $clauses = []; + foreach ($scoresRanges as $key => $scoreBase) { + $clauses[] = sprintf('event_account.score >= :score_base_%s AND event_account.score <= :score_base_%s + 10', $key, $key); + $queryParams[':score_base_' . $key] = intval($scoreBase); + } + + $query .= ' AND (' . implode(' OR ', $clauses) . ')'; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Users/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Users/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Grid/Users/index.php @@ -0,0 +1,3 @@ +getFullIpInfoById($ipId); + + return $info['ip'] ?? null; + } + + public function getIdByValue(string $ip, int $apiKey): ?int { + $params = [ + ':ip_value' => $ip, + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT + event_ip.id + FROM + event_ip + WHERE + event_ip.key = :api_key + AND event_ip.ip = :ip_value' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['id'] ?? null; + } + + public function getFullIpInfoById(int $ipId): array { + $params = [ + ':ipid' => $ipId, + ]; + + $query = ( + 'SELECT + event_ip.id, + event_ip.ip, + event_ip.cidr, + event_ip.lastseen, + event_ip.created, + event_ip.ip AS title, + event_ip.isp AS ispid, + event_ip.data_center, + event_ip.relay, + event_ip.starlink, + event_ip.vpn, + event_ip.tor, + event_ip.fraud_detected, + event_ip.blocklist, + event_ip.checked, + + event_isp.asn, + event_isp.name, + event_isp.description, + + countries.value AS full_country, + countries.id AS country_id, + countries.iso AS country_iso + + FROM + event_ip + + LEFT JOIN event_isp + ON (event_ip.isp = event_isp.id) + + INNER JOIN countries + ON (event_ip.country = countries.id) + + WHERE + event_ip.id = :ipid' + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function checkAccess(int $subjectId, int $apiKey): bool { + $params = [ + ':ip_id' => $subjectId, + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT + event_ip.id + + FROM + event_ip + + WHERE + event_ip.key = :api_key + AND event_ip.id = :ip_id' + ); + + $results = $this->execQuery($query, $params); + + return count($results) > 0; + } + + public function updateFraudFlag(array $ids, bool $fraud, int $apiKey): void { + if (!count($ids)) { + return; + } + + [$params, $placeHolders] = $this->getArrayPlaceholders($ids); + + $params[':fraud'] = $fraud; + $params[':api_key'] = $apiKey; + + $query = ( + "UPDATE event_ip + SET fraud_detected = :fraud + + WHERE + key = :api_key + AND id IN ({$placeHolders})" + ); + + $this->execQuery($query, $params); + } + + public function extractById(int $entityId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':id' => $entityId, + ]; + + $query = ( + "SELECT + split_part(COALESCE(event_ip.ip::text, ''), '/', 1) AS value, + event_ip.hash AS hash + + FROM + event_ip + + WHERE + event_ip.key = :api_key + AND event_ip.id = :id + + LIMIT 1" + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function getTimeFrameTotal(array $ids, string $startDate, string $endDate, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + $params[':start_date'] = $startDate; + $params[':end_date'] = $endDate; + + $query = ( + "SELECT + event.ip AS id, + COUNT(*) AS cnt + FROM event + WHERE + event.ip IN ({$flatIds}) AND + event.key = :key AND + event.time > :start_date AND + event.time < :end_date + GROUP BY event.ip" + ); + + $totalVisit = $this->execQuery($query, $params); + + $result = []; + + foreach ($ids as $id) { + $result[$id] = ['total_visit' => 0]; + } + + foreach ($totalVisit as $rec) { + $result[$rec['id']]['total_visit'] = $rec['cnt']; + } + + return $result; + } + + public function updateTotalsByEntityIds(array $ids, int $apiKey, bool $force = false): void { + if (!count($ids)) { + return; + } + + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + $extraClause = $force ? '' : ' AND event_ip.lastseen >= event_ip.updated'; + + $query = ( + "UPDATE event_ip + SET + total_visit = COALESCE(sub.total_visit, 0), + shared = COALESCE(sub.shared, 1), + updated = date_trunc('milliseconds', now()) + FROM ( + SELECT + event.ip, + COUNT(*) AS total_visit, + COUNT(DISTINCT account) AS shared + FROM event + WHERE + event.ip IN ($flatIds) AND + event.key = :key + GROUP BY event.ip + ) AS sub + RIGHT JOIN event_ip sub_ip ON sub.ip = sub_ip.id + WHERE + event_ip.id = sub_ip.id AND + event_ip.id IN ($flatIds) AND + event_ip.key = :key + $extraClause" + ); + + $this->execQuery($query, $params); + } + + public function updateTotalsByAccountIds(array $ids, int $apiKey): int { + if (!count($ids)) { + return 0; + } + + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + + $idsQuery = ( + "SELECT + DISTINCT event.ip + FROM event + WHERE + event.account IN ($flatIds) AND + event.key = :key" + ); + + $query = ( + "UPDATE event_ip + SET + total_visit = COALESCE(sub.total_visit, 0), + shared = COALESCE(sub.shared, 1), + updated = date_trunc('milliseconds', now()) + FROM ( + SELECT + event.ip, + COUNT(*) AS total_visit, + COUNT(DISTINCT account) AS shared + FROM event + WHERE + event.ip IN ($idsQuery) AND + event.key = :key + GROUP BY event.ip + ) AS sub + RIGHT JOIN event_ip sub_ip ON sub.ip = sub_ip.id + WHERE + event_ip.id = sub.ip AND + event_ip.id IN ($idsQuery) AND + event_ip.key = :key AND + event_ip.lastseen >= event_ip.updated" + ); + + return $this->execQuery($query, $params); + } + + public function refreshTotals(array $res, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders(array_column($res, 'id')); + $params[':key'] = $apiKey; + $query = ( + "SELECT + id, + total_visit, + shared AS total_account + FROM event_ip + WHERE id IN ({$flatIds}) AND key = :key" + ); + + $result = $this->execQuery($query, $params); + $indexedResult = []; + foreach ($result as $item) { + $indexedResult[$item['id']] = $item; + } + + foreach ($res as $idx => $item) { + $item['total_visit'] = $indexedResult[$item['id']]['total_visit']; + $item['total_account'] = $indexedResult[$item['id']]['total_account']; + $res[$idx] = $item; + } + + return $res; + } + + public function countNotChecked(int $apiKey): int { + $params = [ + ':key' => $apiKey, + ]; + + // count only ips appearing in events (not overriden by retention) + $query = ( + 'SELECT + COUNT(DISTINCT event_ip.id) AS count + FROM event + LEFT JOIN event_ip + ON event.ip = event_ip.id + WHERE + event_ip.key = :key AND + event_ip.checked IS FALSE' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['count'] ?? 0; + } + + public function notCheckedExists(int $apiKey): bool { + $params = [ + ':key' => $apiKey, + ]; + + // count only ips appearing in events (not overriden by retention) + $query = ( + 'SELECT 1 + FROM event + LEFT JOIN event_ip + ON event.ip = event_ip.id + WHERE + event_ip.key = :key AND + event_ip.checked IS FALSE + LIMIT 1' + ); + + $results = $this->execQuery($query, $params); + + return (bool) count($results); + } + + public function notCheckedForUserId(int $userId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':user_id' => $userId, + ]; + + $query = ( + 'SELECT DISTINCT + event_ip.id + FROM event + LEFT JOIN event_ip ON event.ip = event_ip.id + WHERE + event.account = :user_id AND + event.key = :api_key AND + event_ip.checked IS FALSE' + ); + + return array_column($this->execQuery($query, $params), 'id'); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Isp.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Isp.php new file mode 100644 index 0000000..7e630d9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Isp.php @@ -0,0 +1,328 @@ + $apiKey, + ':isp_id' => $subjectId, + ]; + + $query = ( + 'SELECT + event_isp.id + + FROM + event_isp + + WHERE + event_isp.key = :api_key + AND event_isp.id = :isp_id' + ); + + $results = $this->execQuery($query, $params); + + return count($results) > 0; + } + + public function getIdByAsn(int $asn, int $apiKey): ?int { + $params = [ + ':api_key' => $apiKey, + ':asn' => $asn, + ]; + + $query = ( + 'SELECT + event_isp.id + FROM event_isp + WHERE + event_isp.key = :api_key AND + event_isp.asn = :asn + LIMIT 1' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['id'] ?? null; + } + + public function getIpCountById(int $ispId, int $apiKey): int { + $params = [ + ':api_key' => $apiKey, + ':ispid' => $ispId, + ]; + + $query = ( + 'SELECT COUNT(*) AS count + FROM event_ip + WHERE + event_ip.isp = :ispid AND + event_ip.key = :api_key' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['count'] ?? 0; + } + + public function getFullIspInfoById(int $ispId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':ispid' => $ispId, + ]; + + $query = ( + 'SELECT + event_isp.asn, + event_isp.name, + event_isp.description, + event_isp.total_visit, + event_isp.total_account, + event_isp.lastseen, + ( + SELECT COUNT(DISTINCT event.account) + FROM event + + LEFT JOIN event_ip + ON event.ip = event_ip.id + + LEFT JOIN event_account + ON event.account = event_account.id + + WHERE + event.key = :api_key AND + event_ip.isp = :ispid AND + event_account.fraud is TRUE + ) AS total_fraud + + FROM + event_isp + + WHERE + event_isp.key = :api_key + AND event_isp.id = :ispid + + GROUP BY + event_isp.id' + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function insertRecord(array $data, int $apiKey): int { + $params = [ + ':key' => $apiKey, + ':asn' => $data['asn'], + ':name' => $data['name'], + ':description' => $data['description'], + ':lastseen' => $data['lastseen'], + ':created' => $data['created'], + ':updated' => $data['lastseen'], + ]; + + $query = ( + 'INSERT INTO event_isp ( + key, asn, name, description, lastseen, created, updated + ) VALUES ( + :key, :asn, :name, :description, :lastseen, :created, :updated + ) ON CONFLICT (key, asn) DO UPDATE SET + name = EXCLUDED.name, description = EXCLUDED.description, lastseen = EXCLUDED.lastseen + RETURNING id' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['id']; + } + + public function getTimeFrameTotal(array $ids, string $startDate, string $endDate, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + $params[':start_date'] = $startDate; + $params[':end_date'] = $endDate; + + $query = ( + "SELECT + event_ip.isp AS id, + COUNT(*) AS cnt + FROM event + INNER JOIN event_ip + ON event.ip = event_ip.id + WHERE + event_ip.isp IN ({$flatIds}) AND + event.key = :key AND + event.time > :start_date AND + event.time < :end_date + GROUP BY event_ip.isp" + ); + + $totalVisit = $this->execQuery($query, $params); + + $query = ( + "SELECT + event_ip.isp AS id, + COUNT(DISTINCT(event.account)) AS cnt + FROM event + INNER JOIN event_ip + ON event.ip = event_ip.id + WHERE + event_ip.isp IN ({$flatIds}) AND + event.key = :key AND + event.time > :start_date AND + event.time < :end_date + GROUP BY event_ip.isp" + ); + + $totalAccount = $this->execQuery($query, $params); + + $query = ( + "SELECT + event_ip.isp AS id, + COUNT(*) AS cnt + FROM event_ip + WHERE + event_ip.isp IN ({$flatIds}) AND + event_ip.key = :key AND + event_ip.lastseen > :start_date AND + event_ip.lastseen < :end_date + GROUP BY event_ip.isp" + ); + + $totalIp = $this->execQuery($query, $params); + + $result = []; + + foreach ($ids as $id) { + $result[$id] = ['total_visit' => 0, 'total_account' => 0, 'total_ip' => 0]; + } + + foreach ($totalVisit as $rec) { + $result[$rec['id']]['total_visit'] = $rec['cnt']; + } + + foreach ($totalAccount as $rec) { + $result[$rec['id']]['total_account'] = $rec['cnt']; + } + + foreach ($totalIp as $rec) { + $result[$rec['id']]['total_ip'] = $rec['cnt']; + } + + return $result; + } + + public function updateTotalsByEntityIds(array $ids, int $apiKey): void { + if (!count($ids)) { + return; + } + + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + + $query = ( + "UPDATE event_isp + SET + total_visit = COALESCE(sub.total_visit, 0), + total_account = COALESCE(sub.total_account, 0), + updated = date_trunc('milliseconds', now()) + FROM ( + SELECT + event_ip.isp, + COUNT(*) AS total_visit, + COUNT(DISTINCT account) AS total_account + FROM event + LEFT JOIN event_ip + ON event.ip = event_ip.id + WHERE + event_ip.isp IN ({$flatIds}) AND + event.key = :key + GROUP BY event_ip.isp + ) AS sub + RIGHT JOIN event_isp sub_isp ON sub.isp = sub_isp.id + WHERE + event_isp.key = :key AND + event_isp.id = sub_isp.id AND + event_isp.id IN ({$flatIds}) AND + event_isp.lastseen >= event_isp.updated" + ); + + $this->execQuery($query, $params); + } + + public function updateAllTotals(int $apiKey): int { + $params = [ + ':key' => $apiKey, + ]; + $query = ( + 'UPDATE event_isp + SET + total_visit = COALESCE(sub.total_visit, 0), + total_account = COALESCE(sub.total_account, 0), + updated = date_trunc(\'milliseconds\', now()) + FROM ( + SELECT + event_ip.isp, + COUNT(*) AS total_visit, + COUNT(DISTINCT account) AS total_account + FROM event + LEFT JOIN event_ip ON event.ip = event_ip.id + WHERE + event.key = :key + GROUP BY event_ip.isp + ) AS sub + RIGHT JOIN event_isp sub_isp ON sub.isp = sub_isp.id + WHERE + event_isp.key = :key AND + event_isp.id = sub_isp.id AND + event_isp.lastseen >= event_isp.updated' + ); + + return $this->execQuery($query, $params); + } + + public function refreshTotals(array $res, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders(array_column($res, 'id')); + $params[':key'] = $apiKey; + $query = ( + "SELECT + id, + total_visit, + total_account + FROM event_isp + WHERE id IN ({$flatIds}) AND key = :key" + ); + + $result = $this->execQuery($query, $params); + + $indexedResult = []; + foreach ($result as $item) { + $indexedResult[$item['id']] = $item; + } + + foreach ($res as $idx => $item) { + $item['total_visit'] = $indexedResult[$item['id']]['total_visit']; + $item['total_account'] = $indexedResult[$item['id']]['total_account']; + $res[$idx] = $item; + } + + return $res; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Log.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Log.php new file mode 100644 index 0000000..0f658e8 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Log.php @@ -0,0 +1,29 @@ +text = json_encode($data); + $this->save(); + } + + public function getAll(): array { + return $this->find(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Logbook.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Logbook.php new file mode 100644 index 0000000..a6e8693 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Logbook.php @@ -0,0 +1,92 @@ + $apiKey, + ':id' => $id, + ]; + + $query = ( + 'SELECT + event_logbook.id, + event_logbook.ip, + event_logbook.raw, + event_logbook.started, + event_logbook.error_text, + event_logbook.error_type, + event_error_type.name AS error_name, + event_error_type.value AS error_value + + FROM + event_logbook + + LEFT JOIN event_error_type + ON (event_logbook.error_type = event_error_type.id) + + WHERE + event_logbook.id = :id AND + event_logbook.key = :api_key + LIMIT 1' + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function rotateRequests(?int $apiKey): int { + $params = [ + ':key' => $apiKey, + ':limit' => \Utils\Variables::getLogbookLimit(), + ]; + + $query = ( + 'SELECT + id + FROM event_logbook + WHERE key = :key + ORDER BY id DESC + LIMIT 1 OFFSET :limit' + ); + + $result = $this->execQuery($query, $params); + + if (!count($result)) { + return 0; + } + + $params = [ + ':id' => $result[0]['id'], + ':key' => $apiKey, + ]; + + $query = ( + 'DELETE FROM event_logbook + WHERE + id < :id AND + key = :key' + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ManualCheckHistory.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ManualCheckHistory.php new file mode 100644 index 0000000..25fcdb3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ManualCheckHistory.php @@ -0,0 +1,30 @@ +find( + ['operator=?', $operatorId], + [ + 'order' => 'created_at DESC', + 'limit' => 15, + ], + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ManualCheckHistoryQuery.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ManualCheckHistoryQuery.php new file mode 100644 index 0000000..dbfc53e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/ManualCheckHistoryQuery.php @@ -0,0 +1,30 @@ +reset(); + + $this->operator = $search['operator']; + $this->type = $search['type']; + $this->search_query = $search['search']; + + $this->save(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Map.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Map.php new file mode 100644 index 0000000..5f7f503 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Map.php @@ -0,0 +1,228 @@ + $apiKey, + ]; + $query = ( + 'SELECT + countries.iso, + countries.value, + countries.id, + COUNT(DISTINCT event.account) AS total_account + + FROM event + + LEFT JOIN event_ip + ON (event.ip = event_ip.id) + + LEFT JOIN countries + ON (event_ip.country = countries.id) + + WHERE + event.key = :api_key' + ); + + if ($dateTo !== null && $dateFrom !== null) { + $params[':date_from'] = $dateFrom; + $params[':date_to'] = $dateTo; + + $query .= ' AND event.time >= :date_from AND event.time <= :date_to'; + } + + $query .= ( + ' GROUP BY + countries.iso, + countries.value, + countries.id' + ); + + return $this->execQuery($query, $params); + } + + public function getCountriesByIspId(int $ispId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':id' => $ispId, + ]; + $query = ( + 'SELECT + countries.iso, + countries.value, + countries.id, + SUM(event_ip.total_visit) AS total_visit + + FROM event_ip + + LEFT JOIN countries + ON (event_ip.country = countries.id) + + WHERE + event_ip.isp = :id AND + event_ip.key = :api_key + + GROUP BY + countries.iso, + countries.value, + countries.id' + ); + + return $this->execQuery($query, $params); + } + + public function getCountriesByDomainId(int $domainId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':id' => $domainId, + ]; + $query = ( + 'SELECT + countries.iso, + countries.value, + countries.id, + COUNT(event.id) AS total_visit + + FROM event + + LEFT JOIN event_ip + ON (event.ip = event_ip.id) + + LEFT JOIN event_email + ON (event.email = event_email.id) + + LEFT JOIN countries + ON (event_ip.country = countries.id) + + WHERE + event_email.domain = :id AND + event_email.key = :api_key + + GROUP BY + countries.iso, + countries.value, + countries.id' + ); + + return $this->execQuery($query, $params); + } + + public function getCountriesByUserId(int $userId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':id' => $userId, + ]; + $query = ( + 'SELECT + countries.iso, + countries.value, + countries.id, + COUNT(event.id) AS total_visit + + FROM event + + LEFT JOIN event_ip + ON (event.ip = event_ip.id) + + LEFT JOIN countries + ON (event_ip.country = countries.id) + + WHERE + event.account = :id AND + event.key = :api_key + + GROUP BY + countries.iso, + countries.value, + countries.id' + ); + + return $this->execQuery($query, $params); + } + + public function getCountriesByResourceId(int $resourceId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':id' => $resourceId, + ]; + $query = ( + 'SELECT + countries.iso, + countries.value, + countries.id, + COUNT(event.id) AS total_visit + + FROM event + + LEFT JOIN event_ip + ON (event.ip = event_ip.id) + + LEFT JOIN countries + ON (event_ip.country = countries.id) + + WHERE + event.url = :id AND + event.key = :api_key + + GROUP BY + countries.iso, + countries.value, + countries.id' + ); + + return $this->execQuery($query, $params); + } + + public function getCountriesByBotId(int $botId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':id' => $botId, + ]; + $query = ( + 'SELECT + countries.iso, + countries.value, + countries.id, + COUNT(event.id) AS total_visit + + FROM event + + LEFT JOIN event_ip + ON (event.ip = event_ip.id) + + LEFT JOIN event_device + ON (event.device = event_device.id) + + LEFT JOIN countries + ON (event_ip.country = countries.id) + + WHERE + event_device.user_agent = :id AND + event_device.key = :api_key + + GROUP BY + countries.iso, + countries.value, + countries.id' + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Message.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Message.php new file mode 100644 index 0000000..08e9bdf --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Message.php @@ -0,0 +1,39 @@ +text = $msg; + + $this->save(); + + return (int) $this->id; + } + + public function getMessage(): self|null|false { + $filters = []; + $options = [ + 'order' => 'id DESC', + 'offset' => 0, + 'limit' => 1, + ]; + + return $this->load($filters, $options); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/NotificationPreferences.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/NotificationPreferences.php new file mode 100644 index 0000000..5eb78d4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/NotificationPreferences.php @@ -0,0 +1,62 @@ +getArrayPlaceholders($timezones); + $params = \array_merge($params, [ + ':daily' => \Type\UnreviewedItemsReminderFrequencyType::Daily, + ':weekly' => \Type\UnreviewedItemsReminderFrequencyType::Weekly, + ':off' => \Type\UnreviewedItemsReminderFrequencyType::Off, + ]); + + $query = \sprintf('SELECT * + FROM dshb_operators + WHERE + timezone IN (%s) + AND unreviewed_items_reminder_freq != :off + AND review_queue_cnt > 0 + AND (last_unreviewed_items_reminder IS NULL + OR (unreviewed_items_reminder_freq = :daily AND last_unreviewed_items_reminder <= NOW() - \'1 day\'::interval) + OR (unreviewed_items_reminder_freq = :weekly AND last_unreviewed_items_reminder <= NOW() - \'7 day\'::interval) + ); + ', $placeHolders); + + return $this->execQuery($query, $params); + } + + /** + * @param int[] $operatorsIds + */ + public function updateLastUnreviewedItemsReminder(array $operatorsIds): void { + [$params, $placeHolders] = $this->getArrayPlaceholders($operatorsIds); + + $query = \sprintf('UPDATE dshb_operators + SET last_unreviewed_items_reminder = NOW() + WHERE "id" IN (%s); + ', $placeHolders); + + $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Operator.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Operator.php new file mode 100644 index 0000000..07c0097 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Operator.php @@ -0,0 +1,203 @@ +password = self::hashPassword($password); + } + + $this->email = $data['email']; + $this->timezone = $data['timezone']; + $this->is_active = 1; + $this->save(); + } + + public function updatePassword(array $data): void { + $operatorId = $data['id']; + $this->getOperatorById($operatorId); + + if ($this->loaded()) { + $password = $data['new-password']; + $this->password = self::hashPassword($password, PASSWORD_DEFAULT); + + $this->save(); + } + } + + public function updateEmail(array $data): void { + $operatorId = $data['id']; + $this->getOperatorById($operatorId); + + if ($this->loaded()) { + $this->email = $data['email']; + + $this->save(); + } + } + + public function updateTimeZone(array $data): void { + $operatorId = $data['id']; + $this->getOperatorById($operatorId); + + if ($this->loaded()) { + $this->timezone = $data['timezone']; + + $this->save(); + } + } + + public function updateNotificationPreferences( + \Type\UnreviewedItemsReminderFrequencyType $unreviewedItemsReminderFrequency, + ): void { + if ($this->loaded()) { + $this->unreviewed_items_reminder_freq = $unreviewedItemsReminderFrequency->value; + + $this->save(); + } + } + + public function updateReviewedQueueCnt(array $data): void { + $operatorId = $data['id']; + $this->getOperatorById($operatorId); + + if ($this->loaded()) { + $this->review_queue_cnt = $data['review_queue_cnt']; + $this->review_queue_updated_at = gmdate('Y-m-d H:i:s'); + + $this->save(); + } + } + + public function updateLastEventTime(array $data): void { + $operatorId = $data['id']; + $this->getOperatorById($operatorId); + + if ($this->loaded()) { + $this->last_event_time = $data['last_event_time']; + + $this->save(); + } + } + + public function closeAccount(): void { + if ($this->loaded()) { + $this->is_closed = 1; + + $this->save(); + } + } + + public function deleteAccount(): void { + if ($this->loaded()) { + $this->erase(); + } + } + + public function removeData(): void { + if ($this->loaded()) { + $params = [ + ':operator_id' => $this->id, + ]; + + # firstly delete all nested data to not break the cascade + $queries = [ + 'DELETE FROM event + WHERE event.key IN (SELECT id FROM dshb_api WHERE creator = :operator_id);', + 'DELETE FROM event_account + WHERE event_account.key IN (SELECT id FROM dshb_api WHERE creator = :operator_id);', + 'DELETE FROM event_ip + WHERE event_ip.key IN (SELECT id FROM dshb_api WHERE creator = :operator_id);', + 'DELETE FROM event_device + WHERE event_device.key IN (SELECT id FROM dshb_api WHERE creator = :operator_id);', + 'DELETE FROM event_email + WHERE event_email.key IN (SELECT id FROM dshb_api WHERE creator = :operator_id);', + ]; + + try { + $this->db->begin(); + $this->db->exec($queries, array_fill(0, 5, $params)); + + $query = 'DELETE FROM dshb_api WHERE creator = :operator_id'; + $this->db->exec($query, $params); + + $this->db->commit(); + } catch (\Exception $e) { + $this->db->rollback(); + error_log($e->getMessage()); + throw $e; + } + } + } + + public function activateByOperator(int $operatorId): void { + $this->getOperatorById($operatorId); + + if ($this->loaded()) { + $this->is_active = 1; + $this->save(); + } + } + + public function getByEmail(string $email): self|null|false { + return $this->load( + ['"email"=?', $email], + ); + } + + public function getActivatedByEmail(string $email): int { + $isActive = 1; + $isClosed = 0; + + $filters = ['LOWER(email)=LOWER(?) AND "is_active"=? AND "is_closed"=?', $email, $isActive, $isClosed]; + $this->load($filters); + + return $this->loaded(); + } + + public function getOperatorById(int $id): self|null|false { + return $this->load( + ['"id"=? AND "is_closed"=?', $id, 0], + ); + } + + public function verifyPassword(string $password): bool { + if (!$this->loaded() || !$this->password) { + return false; + } + + $pepper = \Utils\Variables::getPepper(); + $pepperedPassword = \hash_hmac('sha256', $password, $pepper); + + return \password_verify($pepperedPassword, $this->password); + } + + public function getAll(): array { + return $this->find(null, ['order' => 'email ASC']); + } + + public static function hashPassword(string $password): string { + $pepper = \Utils\Variables::getPepper(); + $pepperedPassword = \hash_hmac('sha256', $password, $pepper); + + return \password_hash($pepperedPassword, PASSWORD_DEFAULT); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/OperatorsRules.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/OperatorsRules.php new file mode 100644 index 0000000..8b5c0e3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/OperatorsRules.php @@ -0,0 +1,159 @@ + $apiKey, + ]; + + $query = ( + 'SELECT + dshb_rules.uid, + dshb_rules.validated, + dshb_rules.name, + dshb_rules.descr, + dshb_rules.attributes, + COALESCE(dshb_operators_rules.value, 0) AS value, + dshb_operators_rules.proportion, + dshb_operators_rules.proportion_updated_at + + FROM + dshb_rules + + LEFT JOIN dshb_operators_rules + ON (dshb_rules.uid = dshb_operators_rules.rule_uid AND dshb_operators_rules.key = :api_key) + + WHERE + dshb_rules.missing IS NOT TRUE AND + dshb_rules.validated IS TRUE' + ); + + $results = $this->execQuery($query, $params); + + $result = []; + foreach ($results as $row) { + $result[$row['uid']] = $row; + } + + // attributes filter applied in controller + return $result; + } + + public function getAllRulesByOperator(int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT + dshb_rules.uid, + dshb_rules.validated, + dshb_rules.missing, + dshb_rules.name, + dshb_rules.descr, + dshb_rules.attributes, + COALESCE(dshb_operators_rules.value, 0) AS value, + dshb_operators_rules.proportion, + dshb_operators_rules.proportion_updated_at + + FROM + dshb_rules + + LEFT JOIN dshb_operators_rules + ON (dshb_rules.uid = dshb_operators_rules.rule_uid AND dshb_operators_rules.key = :api_key)' + ); + + $results = $this->execQuery($query, $params); + + $result = []; + foreach ($results as $row) { + $result[$row['uid']] = $row; + } + + return $result; + } + + public function getRuleWithOperatorValue(string $ruleUid, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':uid' => $ruleUid, + ]; + + $query = ( + 'SELECT + dshb_rules.uid, + dshb_rules.validated, + dshb_rules.name, + dshb_rules.descr, + dshb_rules.attributes, + COALESCE(dshb_operators_rules.value, 0) AS value + + FROM + dshb_rules + + LEFT JOIN dshb_operators_rules + ON (dshb_rules.uid = dshb_operators_rules.rule_uid AND dshb_operators_rules.key = :api_key) + + WHERE + dshb_rules.uid = :uid AND + dshb_rules.missing IS NOT TRUE AND + dshb_rules.validated IS TRUE' + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function updateRule(string $ruleUid, int $score, int $apiKey): void { + $found = $this->load( + ['"key"=? AND "rule_uid"=?', $apiKey, $ruleUid], + ); + + if (!$found) { + $this->key = $apiKey; + $this->rule_uid = $ruleUid; + $this->proportion = null; + } + + $this->value = $score; + // do not change proportion + + $this->save(); + } + + public function updateRuleProportion(string $ruleUid, float $proportion, int $apiKey): void { + $found = $this->load( + ['"key"=? AND "rule_uid"=?', $apiKey, $ruleUid], + ); + + // set value if record is new + if (!$found) { + $this->key = $apiKey; + $this->rule_uid = $ruleUid; + $this->value = 0; + } + + $this->proportion = $proportion; + $this->proportion_updated_at = date('Y-m-d H:i:s'); + + $this->save(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Phone.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Phone.php new file mode 100644 index 0000000..798cd67 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Phone.php @@ -0,0 +1,365 @@ + $apiKey, + ':id' => $id, + ]; + + $query = ( + 'SELECT + event_phone.id, + event_phone.account_id, + event_phone.phone_number, + event_phone.national_format, + event_phone.country_code, + -- event_phone.validation_errors, + -- event_phone.mobile_country_code, + -- event_phone.mobile_network_code, + event_phone.carrier_name, + event_phone.type, + event_phone.lastseen, + event_phone.created, + event_phone.shared, + event_phone.fraud_detected, + -- event_phone.alert_list, + -- event_phone.profiles, + event_phone.iso_country_code, + event_phone.invalid, + event_phone.checked, + countries.id AS country_id, + countries.iso AS country_iso, + countries.value AS full_country + + FROM + event_phone + LEFT JOIN countries + ON (countries.id = event_phone.country_code) + + WHERE + event_phone.id = :id AND + event_phone.key = :api_key' + ); + + $result = $this->execQuery($query, $params); + + if (count($result)) { + $result = $result[0]; + $result['shared_users'] = []; + + if ($result['shared'] > 1) { + $params = [ + ':api_key' => $apiKey, + ':phone_number' => $result['phone_number'], + ':current_account' => $result['account_id'], + ]; + $query = ( + 'SELECT + event_account.score, + event_account.score_updated_at, + event_account.id AS accountid, + event_account.userid AS accounttitle, + event_email.email + + FROM event_account + + LEFT JOIN event_email + ON event_account.lastemail = event_email.id + + WHERE event_account.id IN ( + SELECT event_phone.account_id + FROM event_phone + WHERE + event_phone.phone_number = :phone_number AND + event_phone.key = :api_key AND + event_phone.account_id != :current_account + ) AND + event_account.key = :api_key' + ); + $result['shared_users'] = $this->execQuery($query, $params); + } + } + + return $result; + } + + public function getIdByValue(string $phone, int $apiKey): ?int { + $query = ( + 'SELECT + event_phone.id + FROM + event_phone + WHERE + event_phone.key = :api_key + AND event_phone.phone_number = :phone_value' + ); + + $params = [ + ':phone_value' => $phone, + ':api_key' => $apiKey, + ]; + + $results = $this->execQuery($query, $params); + + return $results[0]['id'] ?? null; + } + + public function updateFraudFlag(array $ids, bool $fraud, int $apiKey): void { + if (!count($ids)) { + return; + } + + [$params, $placeHolders] = $this->getArrayPlaceholders($ids); + + $params[':fraud'] = $fraud; + $params[':api_key'] = $apiKey; + + $query = ( + "UPDATE event_phone + SET fraud_detected = :fraud + + WHERE + key = :api_key + AND id IN ({$placeHolders})" + ); + + $this->execQuery($query, $params); + } + + // phone_number may be null for some events + public function updateTotalsByValues(array $values, int $apiKey): void { + foreach ($values as $value) { + if ($value !== null) { + $this->updateTotals($value, $apiKey); + } + } + } + + public function updateTotals(string $phoneNumber, int $apiKey): void { + $params = [ + ':phone_number' => $phoneNumber, + ':key' => $apiKey, + ]; + + $query = ( + 'SELECT + COUNT(*) AS cnt + FROM + event_phone + WHERE + event_phone.key = :key AND + event_phone.phone_number = :phone_number' + ); + $results = $this->execQuery($query, $params); + + $params[':cnt'] = $results[0]['cnt']; + + $query = ( + 'UPDATE event_phone + SET + shared = :cnt + WHERE + event_phone.key = :key AND + event_phone.phone_number = :phone_number' + ); + $this->execQuery($query, $params); + } + + public function extractById(int $entityId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':id' => $entityId, + ]; + + $query = ( + "SELECT + COALESCE(event_phone.phone_number, '') AS value, + event_phone.hash AS hash + + FROM + event_phone + + WHERE + event_phone.key = :api_key + AND event_phone.id = :id + + LIMIT 1" + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function updateTotalsByAccountIds(array $ids, int $apiKey): int { + if (!count($ids)) { + return 0; + } + + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + + $idsQuery = ( + "SELECT + DISTINCT event_phone.phone_number + FROM event_phone + WHERE + event_phone.account_id IN ($flatIds) AND + event_phone.key = :key" + ); + + $query = ( + "UPDATE event_phone + SET + shared = COALESCE(sub.shared, 1) + FROM ( + SELECT + COUNT(*) AS shared, + event_phone.phone_number + FROM + event_phone + WHERE + event_phone.phone_number IN ($idsQuery) AND + event_phone.key = :key + GROUP BY event_phone.phone_number + ) AS sub + RIGHT JOIN event_phone sub_phone ON sub.phone_number = sub_phone.phone_number + WHERE + event_phone.phone_number = sub_phone.phone_number AND + event_phone.phone_number IN ($idsQuery) AND + event_phone.key = :key" + ); + + return $this->execQuery($query, $params); + } + + public function updateTotalsByEntityIds(array $ids, int $apiKey, bool $force = false): void { + if (!count($ids)) { + return; + } + + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + $extraClause = $force ? '' : ' AND event_phone.lastseen >= event_phone.updated'; + + $query = ( + "UPDATE event_phone + SET + shared = ( + SELECT COUNT(*) + FROM event_phone AS ep + WHERE + ep.phone_number = event_phone.phone_number AND + ep.key = :key + ), + updated = date_trunc('milliseconds', now()) + WHERE + event_phone.id IN ({$flatIds}) AND + event_phone.key = :key + $extraClause" + ); + + $this->execQuery($query, $params); + } + + public function refreshTotals(array $res, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders(array_column($res, 'id')); + $params[':key'] = $apiKey; + $query = ( + "SELECT + id, + shared + FROM event_phone + WHERE id IN ({$flatIds}) AND key = :key" + ); + + $result = $this->execQuery($query, $params); + + $indexedResult = []; + foreach ($result as $item) { + $indexedResult[$item['id']] = $item; + } + + foreach ($res as $idx => $item) { + $item['shared'] = $indexedResult[$item['id']]['shared']; + $res[$idx] = $item; + } + + return $res; + } + + public function countNotChecked(int $apiKey): int { + $params = [ + ':key' => $apiKey, + ]; + + $query = ( + 'SELECT + COUNT(*) AS count + FROM event_phone + WHERE + event_phone.key = :key AND + event_phone.checked IS FALSE' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['count'] ?? 0; + } + + public function notCheckedExists(int $apiKey): bool { + $params = [ + ':key' => $apiKey, + ]; + + $query = ( + 'SELECT 1 + FROM event_phone + WHERE + event_phone.key = :key AND + event_phone.checked IS FALSE + LIMIT 1' + ); + + $results = $this->execQuery($query, $params); + + return (bool) count($results); + } + + public function notCheckedForUserId(int $userId, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':user_id' => $userId, + ]; + + $query = ( + 'SELECT DISTINCT + event_phone.id + FROM event_phone + WHERE + event_phone.account_id = :user_id AND + event_phone.key = :api_key AND + event_phone.checked IS FALSE' + ); + + return array_column($this->execQuery($query, $params), 'id'); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Queue/AccountOperationQueue.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Queue/AccountOperationQueue.php new file mode 100644 index 0000000..f9688ea --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Queue/AccountOperationQueue.php @@ -0,0 +1,390 @@ +actionType = $actionType; + + parent::__construct(); + } + + public function add(int $accountId, int $key): void { + $this->reset(); + $this->event_account = $accountId; + $this->key = $key; + $this->action = $this->actionType->value; + $this->save(); + } + + /** + * @param array{accountId: int, key: int}[] $accounts + */ + public function addBatch(array $accounts): void { + if (count($accounts) === 0) { + return; + } + + $params = [':action' => strval($this->actionType->value)]; + $arrayPlaceholders = []; + $prefix = ''; + foreach ($accounts as $idx => $record) { + $prefix = ":{$idx}_"; + + $params[$prefix . 'idx'] = $idx; + $params[$prefix . 'account_id'] = intval($record['accountId']); + $params[$prefix . 'key'] = intval($record['key']); + $arrayPlaceholders[] = "({$prefix}idx, {$prefix}account_id, {$prefix}key)"; + } + + $strPlaceholders = \implode(', ', $arrayPlaceholders); + + // update waiting records + $query = ( + "UPDATE queue_account_operation + SET + updated = now() + FROM (VALUES $strPlaceholders) AS v(idx, account_id, key) + WHERE + queue_account_operation.event_account = v.account_id::bigint AND + queue_account_operation.key = v.key::bigint AND + queue_account_operation.action = :action AND + queue_account_operation.status = 'waiting' + RETURNING v.idx" + ); + + $results = $this->execQuery($query, $params); + + $updatedIdxs = array_unique(array_column($results, 'idx')); + $notUpdatedIdxs = array_keys(array_diff(array_keys($accounts), $updatedIdxs)); + + if (!count($notUpdatedIdxs)) { + return; + } + + $params = [':action' => strval($this->actionType->value)]; + $arrayPlaceholders = []; + foreach ($notUpdatedIdxs as $idxToInsert) { + $prefix = ":{$idxToInsert}_"; + $record = $accounts[$idxToInsert]; + + $params[$prefix . 'account_id'] = $record['accountId']; + $params[$prefix . 'key'] = $record['key']; + $arrayPlaceholders[] = "({$prefix}account_id, {$prefix}key, :action)"; + } + + $strPlaceholders = \implode(', ', $arrayPlaceholders); + + $query = "INSERT INTO queue_account_operation (event_account, key, action) VALUES {$strPlaceholders} RETURNING id"; + + $result = $this->execQuery($query, $params); + + $msg = sprintf('Adding %s accounts to %s queue -- %s updated, %s inserted', count($accounts), strval($this->actionType->value), count($updatedIdxs), count($result)); + \Utils\Logger::log(null, $msg); + } + + public function addBatchIds(array $accountIds, int $key): void { + $batchSize = \Utils\Variables::getAccountOperationQueueBatchSize(); + + $batch = []; + $cnt = 0; + + foreach ($accountIds as $id) { + $batch[] = [ + 'accountId' => $id, + 'key' => $key, + ]; + $cnt++; + + if ($cnt >= $batchSize) { + $this->addBatch($batch); + $batch = []; + $cnt = 0; + } + } + + if ($cnt) { + $this->addBatch($batch); + } + } + + public function isInQueueStatus(int $accountId, int $key): array { + $this->reset(); + $this->load([ + 'event_account = ? AND key = ? AND action = ? AND status != ?', + $accountId, $key, $this->actionType->value, \Type\QueueAccountOperationStatusType::COMPLETED, + ]); + + return $this->dry() ? [false, null] : [true, $this->status]; + } + + public function isInQueue(int $accountId, int $key): bool { + $this->reset(); + $this->load([ + 'event_account = ? AND key = ? AND action = ? AND status != ?', + $accountId, $key, $this->actionType->value, \Type\QueueAccountOperationStatusType::COMPLETED, + ]); + + return $this->dry() ? false : true; + } + + public function isExecuting(): bool { + $this->load([ + 'action = ? AND status = ?', + $this->actionType->value, \Type\QueueAccountOperationStatusType::EXECUTING, + ]); + + return $this->dry() ? false : true; + } + + public function actionIsInQueueProcessing(int $key): bool { + $this->reset(); + $this->load([ + 'key = ? AND action = ? AND status != ? AND status != ?', + $key, + $this->actionType->value, + \Type\QueueAccountOperationStatusType::COMPLETED, + \Type\QueueAccountOperationStatusType::FAILED, + ]); + + return $this->dry() ? false : true; + } + + public function getNextInQueue(): array|null { + $this->reset(); + $this->creator = 'SELECT creator + FROM dshb_api + WHERE dshb_api.id = queue_account_operation.key'; + $this->load([ + 'action = ? AND status = ?', + $this->actionType->value, \Type\QueueAccountOperationStatusType::WAITING, + ], ['order' => 'created ASC']); + + return $this->dry() ? null : $this->cast(); + } + + public function getNextBatchInQueue(int $batchSize): array { + $params = [ + ':batchSize' => $batchSize, + ':action' => $this->actionType->value, + ':status' => \Type\QueueAccountOperationStatusType::WAITING, + ]; + + $query = (' + SELECT + queue_account_operation.*, + dshb_api.creator + FROM queue_account_operation + JOIN dshb_api + ON dshb_api.id = queue_account_operation.key + WHERE + action = :action + AND status = :status + ORDER BY id ASC + LIMIT :batchSize + '); + + return $this->execQuery($query, $params); + } + + public function getNextBatchKeysInQueue(int $batchSize): array { + $params = [ + ':batchSize' => $batchSize, + ':action' => $this->actionType->value, + ':status' => \Type\QueueAccountOperationStatusType::WAITING, + ]; + + $query = (' + SELECT + DISTINCT key + FROM ( + SELECT + queue_account_operation.id, + queue_account_operation.key + FROM queue_account_operation + WHERE + action = :action + AND status = :status + ORDER BY id ASC + LIMIT :batchSize + ) AS t + '); + + $results = $this->execQuery($query, $params); + + return array_column($results, 'key'); + } + + public function setWaiting(): void { + if ($this->loaded()) { + $now = new \DateTime(); + $this->updated = $now->format(self::DATETIME_FORMAT); + $this->status = \Type\QueueAccountOperationStatusType::WAITING; + $this->save(); + } + } + + /** + * @param int[] $ids + */ + public function setWaitingForBatch(array $ids): void { + $this->setStatusForBatch( + $ids, + new \Type\QueueAccountOperationStatusType(\Type\QueueAccountOperationStatusType::WAITING), + ); + } + + public function setExecuting(): void { + if ($this->loaded()) { + $now = new \DateTime(); + $this->updated = $now->format(self::DATETIME_FORMAT); + $this->status = \Type\QueueAccountOperationStatusType::EXECUTING; + $this->save(); + } + } + + /** + * @param int[] $ids + */ + public function setExecutingForBatch(array $ids): void { + $this->setStatusForBatch( + $ids, + new \Type\QueueAccountOperationStatusType(\Type\QueueAccountOperationStatusType::EXECUTING), + ); + } + + public function setCompleted(): void { + if ($this->loaded()) { + $now = new \DateTime(); + $this->updated = $now->format(self::DATETIME_FORMAT); + $this->status = \Type\QueueAccountOperationStatusType::COMPLETED; + $this->save(); + } + } + + /** + * @param int[] $ids + */ + public function setCompletedForBatch(array $ids): void { + $this->setStatusForBatch( + $ids, + new \Type\QueueAccountOperationStatusType(\Type\QueueAccountOperationStatusType::COMPLETED), + ); + } + + public function setFailed(): void { + if ($this->loaded()) { + $now = new \DateTime(); + $this->updated = $now->format(self::DATETIME_FORMAT); + $this->status = \Type\QueueAccountOperationStatusType::FAILED; + $this->save(); + } + } + + /** + * @param int[] $ids + */ + public function setFailedForBatch(array $ids): void { + $this->setStatusForBatch( + $ids, + new \Type\QueueAccountOperationStatusType(\Type\QueueAccountOperationStatusType::FAILED), + ); + } + + /** + * @param int[] $ids + */ + private function setStatusForBatch(array $ids, \Type\QueueAccountOperationStatusType $status): void { + if (!count($ids)) { + return; + } + + [$params, $placeHolders] = $this->getArrayPlaceholders($ids); + + $params[':status'] = $status->value; + $params[':updated'] = (new \DateTime())->format(self::DATETIME_FORMAT); + + $query = (" + UPDATE queue_account_operation + SET + status = :status, + updated = :updated + WHERE + id IN ({$placeHolders}) + "); + + $this->execQuery($query, $params); + } + + public function removeFromQueue(): void { + if ($this->loaded()) { + $this->erase(); + } + } + + public function unclog(): bool { + if ($this->loaded() && $this->status === \Type\QueueAccountOperationStatusType::EXECUTING) { + $updatedDateTime = new \DateTime($this->updated); + $currentDateTime = new \DateTime(); + + $differenceInSeconds = $currentDateTime->getTimestamp() - $updatedDateTime->getTimestamp(); + $totalMinutes = (int) floor($differenceInSeconds / 60); + + if ($totalMinutes < \Utils\Constants::get('ACCOUNT_OPERATION_QUEUE_AUTO_UNCLOG_AFTER_MINUTES')) { + return false; // Time not elapsed, no need to unclog (yet). + } + + $this->setFailed(); + + $msg = sprintf('Queue failed on unclog (now - updated > 2 hours) on account %d minutes diff %d.', $this->event_account, $totalMinutes); + \Utils\Logger::log(null, $msg); + + return true; // Unclogged queue. + } + + return false; // No need to unclog. + } + + public function clearCompleted(\DateTime $clearBefore): int { + $this->reset(); + + $params = [ + ':daysAgo' => $clearBefore->format(self::DATETIME_FORMAT), + ':status' => \Type\QueueAccountOperationStatusType::COMPLETED, + ]; + + $query = (' + WITH deleted AS + ( + DELETE FROM queue_account_operation + WHERE + status = :status + AND updated < :daysAgo + RETURNING * + ) SELECT count(*) FROM deleted + '); + + $results = $this->execQuery($query, $params); + + return $results[0]['count'] ?? 0; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Queue/QueueNewEventsCursor.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Queue/QueueNewEventsCursor.php new file mode 100644 index 0000000..cf8cbf2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Queue/QueueNewEventsCursor.php @@ -0,0 +1,151 @@ +execQuery($query, null); + + return $results[0]['last_event_id'] ?? 0; + } + + public function getNextCursor(int $currentCursor, int $batchSize = 100): int|null { + $params = [ + ':current_cursor' => $currentCursor, + ':batch_size' => $batchSize, + ]; + + $query = ('WITH numbered_events AS ( + SELECT + id, + ROW_NUMBER() OVER (ORDER BY id) AS rownum + FROM + event + WHERE + event.id > :current_cursor + LIMIT :batch_size + ) + SELECT + id AS next_cursor + FROM + numbered_events + ORDER BY + rownum DESC + LIMIT 1; + '); + + $results = $this->execQuery($query, $params); + + return $results[0]['next_cursor'] ?? null; + } + + public function updateCursor(int $lastEventId): void { + $params = [ + ':last_event_id' => $lastEventId, + ]; + + $query = ('UPDATE + queue_new_events_cursor + SET + last_event_id = :last_event_id; + '); + + $this->execQuery($query, $params); + } + + public function acquireLock(): bool { + $this->obtainLockState(); + + if ($this->locked) { + return false; // lock is busy + } + + if ($this->locked === null) { // insert cursor row and acquire lock + $query = ( + 'INSERT INTO queue_new_events_cursor + (last_event_id, locked) + VALUES + (-1, TRUE)' + ); + } else { // locked is False, acquire lock + $query = ( + 'UPDATE + queue_new_events_cursor + SET + locked = TRUE, + updated = NOW()' + ); + } + + $this->execQuery($query, null); + + return true; // lock acquired + } + + private function obtainLockState(): void { + $query = ('SELECT + locked, + updated + FROM + queue_new_events_cursor; + '); + + $results = $this->execQuery($query, null); + + $this->locked = $results[0]['locked'] ?? null; + $this->updated = $results[0]['updated'] ?? null; + } + + public function releaseLock(): void { + $query = ('UPDATE + queue_new_events_cursor + SET + locked = FALSE, + updated = now(); + '); + $this->execQuery($query, null); + } + + public function unclog(): bool { + $this->obtainLockState(); + + if ($this->locked) { + $updatedDateTime = new \DateTime($this->updated); + $currentDateTime = new \DateTime(); + + $differenceInSeconds = $currentDateTime->getTimestamp() - $updatedDateTime->getTimestamp(); + $totalMinutes = floor($differenceInSeconds / 60); + + if ($totalMinutes < \Utils\Constants::get('ACCOUNT_OPERATION_QUEUE_AUTO_UNCLOG_AFTER_MINUTES')) { + return false; // Time not elapsed, no need to unclog (yet). + } + + $this->releaseLock(); + + return true; // Unclogged cursor. + } + + return false; // No need to unclog. + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Queue/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Queue/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Queue/index.php @@ -0,0 +1,3 @@ + $resourceId, + ]; + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function checkAccess(int $subjectId, int $apiKey): bool { + $params = [ + ':api_key' => $apiKey, + ':resource_id' => $subjectId, + ]; + + $query = ( + 'SELECT + event_url.id + + FROM + event_url + + WHERE + event_url.id = :resource_id + AND event_url.key = :api_key' + ); + + $results = $this->execQuery($query, $params); + + return count($results) > 0; + } + + public function getTimeFrameTotal(array $ids, string $startDate, string $endDate, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + $params[':start_date'] = $startDate; + $params[':end_date'] = $endDate; + + $query = ( + "SELECT + event.url AS id, + COUNT(*) AS cnt + FROM event + WHERE + event.url IN ({$flatIds}) AND + event.key = :key AND + event.time > :start_date AND + event.time < :end_date + GROUP BY event.url" + ); + + $totalVisit = $this->execQuery($query, $params); + + $query = ( + "SELECT + event.url AS id, + COUNT(DISTINCT(event.account)) AS cnt + FROM event + WHERE + event.url IN ({$flatIds}) AND + event.key = :key AND + event.time > :start_date AND + event.time < :end_date + GROUP BY event.url" + ); + + $totalAccount = $this->execQuery($query, $params); + + $query = ( + "SELECT + event.url AS id, + COUNT(DISTINCT(event.ip)) AS cnt + FROM event + WHERE + event.url IN ({$flatIds}) AND + event.key = :key AND + event.time > :start_date AND + event.time < :end_date + GROUP BY event.url" + ); + + $totalIp = $this->execQuery($query, $params); + + $query = ( + "SELECT + event.url AS id, + COUNT(DISTINCT(event_ip.country)) AS cnt + FROM event + INNER JOIN event_ip + ON event.ip = event_ip.id + WHERE + event.url IN ({$flatIds}) AND + event.key = :key AND + event.time > :start_date AND + event.time < :end_date + GROUP BY event.url" + ); + + $totalCountry = $this->execQuery($query, $params); + + $result = []; + + foreach ($ids as $id) { + $result[$id] = ['total_visit' => 0, 'total_account' => 0, 'total_ip' => 0, 'total_country' => 0]; + } + + foreach ($totalVisit as $rec) { + $result[$rec['id']]['total_visit'] = $rec['cnt']; + } + + foreach ($totalAccount as $rec) { + $result[$rec['id']]['total_account'] = $rec['cnt']; + } + + foreach ($totalIp as $rec) { + $result[$rec['id']]['total_ip'] = $rec['cnt']; + } + + foreach ($totalCountry as $rec) { + $result[$rec['id']]['total_country'] = $rec['cnt']; + } + + return $result; + } + + public function updateTotalsByEntityIds(array $ids, int $apiKey, bool $force = false): void { + if (!count($ids)) { + return; + } + + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + $extraClause = $force ? '' : ' AND event_url.lastseen >= event_url.updated'; + + $query = ( + "UPDATE event_url + SET + total_visit = COALESCE(sub.total_visit, 0), + total_account = COALESCE(sub.total_account, 0), + total_ip = COALESCE(sub.total_ip, 0), + total_device = COALESCE(sub.total_device, 0), + total_country = COALESCE(sub.total_country, 0), + updated = date_trunc('milliseconds', now()) + FROM ( + SELECT + event.url, + COUNT(*) AS total_visit, + COUNT(DISTINCT event.account) AS total_account, + COUNT(DISTINCT event.ip) AS total_ip, + COUNT(DISTINCT event.device) AS total_device, + COUNT(DISTINCT event_ip.country) AS total_country + FROM event + LEFT JOIN event_ip ON event.ip = event_ip.id + WHERE + event.url IN ($flatIds) AND + event.key = :key + GROUP BY event.url + ) AS sub + RIGHT JOIN event_url sub_url ON sub.url = sub_url.id + WHERE + event_url.id = sub_url.id AND + event_url.id IN ($flatIds) AND + event_url.key = :key + $extraClause" + ); + + $this->execQuery($query, $params); + } + + public function updateAllTotals(int $apiKey): int { + $params = [ + ':key' => $apiKey, + ]; + + $query = ( + 'UPDATE event_url + SET + total_visit = COALESCE(sub.total_visit, 0), + total_account = COALESCE(sub.total_account, 0), + total_ip = COALESCE(sub.total_ip, 0), + total_device = COALESCE(sub.total_device, 0), + total_country = COALESCE(sub.total_country, 0), + updated = date_trunc(\'milliseconds\', now()) + FROM ( + SELECT + event.url, + COUNT(*) AS total_visit, + COUNT(DISTINCT event.account) AS total_account, + COUNT(DISTINCT event.ip) AS total_ip, + COUNT(DISTINCT event.device) AS total_device, + COUNT(DISTINCT event_ip.country) AS total_country + FROM event + LEFT JOIN event_ip ON event.ip = event_ip.id + WHERE + event.key = :key + GROUP BY event.url + ) AS sub + RIGHT JOIN event_url sub_url ON sub.url = sub_url.id + WHERE + event_url.id = sub_url.id AND + event_url.key = :key AND + event_url.lastseen >= event_url.updated' + ); + + return $this->execQuery($query, $params); + } + + public function refreshTotals(array $res, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders(array_column($res, 'id')); + $params[':key'] = $apiKey; + $query = ( + "SELECT + id, + total_ip, + total_visit, + total_country, + total_account + FROM event_url + WHERE id IN ({$flatIds}) AND key = :key" + ); + + $result = $this->execQuery($query, $params); + + $indexedResult = []; + foreach ($result as $item) { + $indexedResult[$item['id']] = $item; + } + + foreach ($res as $idx => $item) { + $item['total_ip'] = $indexedResult[$item['id']]['total_ip']; + $item['total_visit'] = $indexedResult[$item['id']]['total_visit']; + $item['total_country'] = $indexedResult[$item['id']]['total_country']; + $item['total_account'] = $indexedResult[$item['id']]['total_account']; + $res[$idx] = $item; + } + + return $res; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/RetentionPolicies.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/RetentionPolicies.php new file mode 100644 index 0000000..c632f92 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/RetentionPolicies.php @@ -0,0 +1,34 @@ + 0' + ); + + return $this->execQuery($query, null); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Rules.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Rules.php new file mode 100644 index 0000000..199459c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Rules.php @@ -0,0 +1,97 @@ +execQuery($query, null); + } + + public function addRule(string $uid, string $name, string $descr, array $attr, bool $validated): void { + $params = [ + ':validated' => $validated, + ':uid' => $uid, + ':name' => $name, + ':descr' => $descr, + ':attributes' => json_encode($attr), + ]; + + $query = ( + 'INSERT INTO dshb_rules (uid, name, descr, validated, attributes) + VALUES (:uid, :name, :descr, :validated, :attributes) + ON CONFLICT (uid) DO UPDATE + SET name = EXCLUDED.name, descr = EXCLUDED.descr, validated = EXCLUDED.validated, + attributes = EXCLUDED.attributes, updated = now(), missing = null' + ); + + $this->execQuery($query, $params); + } + + public function setInvalidByUid(string $uid): void { + $params = [ + ':uid' => $uid, + ]; + + $query = ( + 'UPDATE dshb_rules + SET validated = false, updated = now() + WHERE dshb_rules.uid = :uid' + ); + + $this->execQuery($query, $params); + } + + public function setMissingByUid(string $uid): void { + $params = [ + ':uid' => $uid, + ]; + + $query = ( + 'UPDATE dshb_rules + SET missing = true, updated = now() + WHERE dshb_rules.uid = :uid' + ); + + $this->execQuery($query, $params); + } + + public function deleteByUid(string $uid): void { + $params = [ + ':uid' => $uid, + ]; + + $query = ( + 'DELETE FROM dshb_rules WHERE uid = :uid' + ); + + $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Domain.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Domain.php new file mode 100644 index 0000000..3609f2b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Domain.php @@ -0,0 +1,46 @@ + $apiKey, + ':query' => "%{$query}%", + ]; + + $query = ( + "SELECT + event_domain.id AS id, + 'Domain' AS \"groupName\", + 'domain' AS \"entityId\", + event_domain.domain AS value + + FROM + event_domain + + WHERE + event_domain.key = :api_key + AND LOWER(event_domain.domain) LIKE LOWER(:query) + + LIMIT 25 OFFSET 0" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Email.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Email.php new file mode 100644 index 0000000..170ad51 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Email.php @@ -0,0 +1,46 @@ + $apiKey, + ':query' => "%{$query}%", + ]; + + $query = ( + "SELECT + event_email.account_id AS id, + 'Email' AS \"groupName\", + 'id' AS \"entityId\", + event_email.email AS value + + FROM + event_email + + WHERE + event_email.key = :api_key + AND LOWER(event_email.email) LIKE LOWER(:query) + + LIMIT 25 OFFSET 0" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Ip.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Ip.php new file mode 100644 index 0000000..c1f4af7 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Ip.php @@ -0,0 +1,46 @@ + $apiKey, + ':query' => "%{$query}%", + ]; + + $query = ( + "SELECT + event_ip.id AS id, + 'IP' AS \"groupName\", + 'ip' AS \"entityId\", + event_ip.ip AS value + + FROM + event_ip + + WHERE + event_ip.key = :api_key + AND LOWER(TEXT(event_ip.ip)) LIKE LOWER(:query) + + LIMIT 25 OFFSET 0" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Isp.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Isp.php new file mode 100644 index 0000000..6248af1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Isp.php @@ -0,0 +1,46 @@ + $apiKey, + ':query' => "%{$query}%", + ]; + + $query = ( + "SELECT + event_isp.id AS id, + 'ASN' AS \"groupName\", + 'isp' AS \"entityId\", + event_isp.asn::text AS value + + FROM + event_isp + + WHERE + event_isp.key = :api_key + AND LOWER(event_isp.asn::text) LIKE LOWER(:query) + + LIMIT 25 OFFSET 0" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Phone.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Phone.php new file mode 100644 index 0000000..d1aaa5c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/Phone.php @@ -0,0 +1,46 @@ + $apiKey, + ':query' => "%{$query}%", + ]; + + $query = ( + "SELECT + event_phone.account_id AS id, + 'Phone' AS \"groupName\", + 'id' AS \"entityId\", + event_phone.phone_number AS value + + FROM + event_phone + + WHERE + event_phone.key = :api_key + AND LOWER(event_phone.phone_number) LIKE LOWER(:query) + + LIMIT 25 OFFSET 0" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/User.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/User.php new file mode 100644 index 0000000..4e2a6fa --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/User.php @@ -0,0 +1,78 @@ + $apiKey, + ':query' => "%{$query}%", + ]; + + $query = ( + "SELECT + event_account.id AS id, + 'ID' AS \"groupName\", + 'id' AS \"entityId\", + event_account.userid AS value + + FROM + event_account + + WHERE + event_account.key = :api_key + AND LOWER(event_account.userid) LIKE LOWER(:query) + + LIMIT 25 OFFSET 0" + ); + + return $this->execQuery($query, $params); + } + + public function searchByName(string $query, int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ':query' => "%{$query}%", + ]; + + $query = ( + "SELECT + event_account.id AS id, + 'Name' AS \"groupName\", + 'id' AS \"entityId\", + CONCAT_WS(' ', event_account.firstname, + event_account.lastname) AS value + + FROM + event_account + + WHERE + event_account.key = :api_key + AND ( + LOWER(REPLACE(event_account.firstname || event_account.lastname, ' ', '')) + LIKE LOWER(REPLACE(:query, ' ', '')) OR + LOWER(REPLACE(event_account.lastname || event_account.firstname, ' ', '')) + LIKE LOWER(REPLACE(:query, ' ', '')) + ) + + LIMIT 25 OFFSET 0" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Search/index.php @@ -0,0 +1,3 @@ + $apiKey, + ':session_id' => $subjectId, + ]; + + $results = $this->execQuery($query, $params); + + return count($results) > 0; + } + + public function updateTotalsByAccountIds(array $ids, int $apiKey): int { + if (!count($ids)) { + return 0; + } + + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + + $query = ( + "UPDATE event_session + SET + total_visit = COALESCE(sub.total_visit, 0), + total_ip = COALESCE(sub.total_ip, 0), + total_device = COALESCE(sub.total_device, 0), + total_country = COALESCE(sub.total_country, 0), + updated = date_trunc('milliseconds', now()) + FROM ( + SELECT + event.session_id, + COUNT(*) AS total_visit, + COUNT(DISTINCT event.ip) AS total_ip, + COUNT(DISTINCT event.device) AS total_device, + COUNT(DISTINCT event_ip.country) AS total_country + FROM event + LEFT JOIN event_ip + ON event.ip = event_ip.id + WHERE + event.account IN ($flatIds) AND + event.key = :key + GROUP BY event.session_id + ) AS sub + RIGHT JOIN event_session sub_session ON sub.session_id = sub_session.id + WHERE + event_session.id = sub.session_id AND + event_session.account_id IN ($flatIds) AND + event_session.key = :key AND + event_session.lastseen >= event_session.updated" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/SessionStat.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/SessionStat.php new file mode 100644 index 0000000..6de8b18 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/SessionStat.php @@ -0,0 +1,311 @@ +getArrayPlaceholders($accounts); + $params[':key'] = $apiKey; + + $query = (" + SELECT + event_session.id + + FROM + event_session + + LEFT JOIN event_session_stat + ON event_session.id = event_session_stat.session_id + + WHERE + (event_session_stat.completed IS NULL OR + event_session_stat.completed IS FALSE) AND + event_session.account_id IN ({$flatIds}) AND + event_session.key = :key + "); + + + $results = $this->execQuery($query, $params); + + if (!count($results)) { + return null; + } + + $sessionIds = array_column($results, 'id'); + $result = $this->updateStatsByIds($sessionIds, $apiKey); + + $cnt += $result ? $result : 0; + } + + return $cnt; + } + + public function updateStats(int $apiKey): ?int { + $params = [ + ':key' => $apiKey, + ]; + + + // select only that sessions which events have not went under retention + $query = (' + SELECT + COUNT(DISTINCT event.session_id) AS cnt + FROM + event + + LEFT JOIN event_session + ON event_session.id = event.session_id + + LEFT JOIN event_session_stat + ON event_session.id = event_session_stat.session_id + + WHERE + (event_session_stat.completed IS NULL OR + event_session_stat.completed IS FALSE) AND + event_session.key = :key'); + + $results = $this->execQuery($query, $params); + + $total = $results[0] ?? []; + $total = $total['cnt'] ?? 0; + + if (!$total) { + return null; + } + + $query = (' + SELECT + DISTINCT event.session_id AS id + FROM event + + LEFT JOIN event_session + ON event_session.id = event.session_id + + LEFT JOIN event_session_stat + ON event_session.id = event_session_stat.session_id + + WHERE + (event_session_stat.completed IS NULL OR + event_session_stat.completed IS FALSE) AND + event_session.key = :key + + ORDER BY event.session_id DESC + LIMIT :length OFFSET :offset + '); + + $cnt = 0; + $limit = 5000; + + for ($offset = 0; $offset < $total; $offset += $limit) { + $params[':length'] = $limit; + $params[':offset'] = $offset; + $results = $this->execQuery($query, $params); + if (!count($results)) { + continue; + } + + $sessionIds = array_column($results, 'id'); + $results = $this->updateStatsByIds($sessionIds, $apiKey); + $cnt += $results ? $results : 0; + } + + return $cnt; + } + + public function updateStatsByIds(array $sessionIds, int $apiKey): ?int { + $batchSize = 5000; + + $stats = []; + $plcArr = []; + $results = null; + $params = []; + $arrPlaceholders = []; + + foreach (array_chunk($sessionIds, $batchSize) as $ids) { + $stats = []; + $plcArr = []; + $params = [ + ':key' => $apiKey, + ]; + + foreach ($ids as $id) { + $plcArr[] = ':id_' . strval($id); + $params[':id_' . strval($id)] = $id; + } + $idsStr = '(' . implode(', ', $plcArr) . ')'; + + $results = $this->getBaseSessionsData($idsStr, $params); + + if (!count($results)) { + continue; + } + + foreach ($results as $result) { + $id = $result['id']; + $stats[$id] = [ + ':key_' . strval($id) => $apiKey, + ':session_id_' . strval($id) => $result['id'], + ':duration_' . strval($id) => $result['duration'], + ':completed_' . strval($id) => $result['completed'], + ':event_count_' . strval($id) => $result['event_count'], + ':device_count_' . strval($id) => $result['device_count'], + ':ip_count_' . strval($id) => $result['ip_count'], + ':country_count_' . strval($id) => $result['country_count'], + ':new_device_count_' . strval($id) => $result['new_device_count'], + ':new_ip_count_' . strval($id) => $result['new_ip_count'], + ]; + } + + $this->eventColumnStats('type', $idsStr, $params, $stats, ':event_types_'); + $this->eventColumnStats('http_code', $idsStr, $params, $stats); + $this->eventColumnStats('http_method', $idsStr, $params, $stats); + + $arrPlaceholders = []; + $params = []; + + foreach ($stats as $id => $item) { + ksort($item); + $arrPlaceholders[] = '(' . implode(', ', array_keys($item)) . ')'; + foreach ($item as $key => $val) { + $params[$key] = $val; + } + } + + $strPlaceholders = implode(', ', $arrPlaceholders); + + // column names sorted asc + $query = (" + INSERT INTO event_session_stat ( + completed, country_count, device_count, duration, event_count, event_types, + http_codes, http_methods, ip_count, key, new_device_count, new_ip_count, session_id + ) VALUES {$strPlaceholders} + ON CONFLICT (session_id) DO UPDATE SET + updated = NOW(), ip_count = EXCLUDED.ip_count, device_count = EXCLUDED.device_count, + event_count = EXCLUDED.event_count, country_count = EXCLUDED.country_count, + new_ip_count = EXCLUDED.new_ip_count, new_device_count = EXCLUDED.new_device_count, + http_codes = EXCLUDED.http_codes, http_methods = EXCLUDED.http_methods, + event_types = EXCLUDED.event_types, completed = EXCLUDED.completed + "); + + $this->execQuery($query, $params); + } + + return count($sessionIds); + } + + private function getBaseSessionsData(string $idsPlaceholder, array &$params): array { + $query = (" + SELECT + event_session.id AS id, + EXTRACT(EPOCH FROM (event_session.lastseen - event_session.created))::integer AS duration, + ( + EXTRACT(EPOCH FROM(event_session.lastseen - event_session.created))::integer > 14400 OR + EXTRACT(EPOCH FROM(CURRENT_TIMESTAMP))::integer - 3600 > EXTRACT(EPOCH FROM(event_session.lastseen))::integer + )::boolean AS completed, + COUNT(event.id) AS event_count, + COUNT(DISTINCT event.device) AS device_count, + COUNT(DISTINCT event.ip) AS ip_count, + COUNT(DISTINCT event_ip.country) AS country_count, + COUNT(DISTINCT CASE WHEN + event_device.created <= event_session.lastseen AND + event_device.created >= event_session.created + THEN event.device END) AS new_device_count, + COUNT(DISTINCT CASE WHEN + event_ip.created <= event_session.lastseen AND + event_ip.created >= event_session.created + THEN event.ip END) AS new_ip_count + + FROM event_session + + LEFT JOIN event + ON event_session.id = event.session_id + + LEFT JOIN event_device + ON event.device = event_device.id + + LEFT JOIN event_ip + ON event.ip = event_ip.id + + WHERE + event_session.id IN {$idsPlaceholder} AND + event.key = :key + + GROUP BY + event_session.id, + event_session.lastseen, + event_session.created + "); + + return $this->execQuery($query, $params); + } + + private function eventColumnStats(string $column, string $idsPlaceholder, array &$params, array &$stats, ?string $alias = null): void { + if (!in_array($column, ['http_code', 'http_method', 'type'])) { + return; + } + + if (!$alias) { + $alias = ':' . $column . 's_'; + } + + $query = (" + SELECT + event.session_id AS id, + event.{$column} AS value, + COUNT(*) AS cnt + FROM + event + + WHERE + event.session_id IN {$idsPlaceholder} AND + event.key = :key + + GROUP BY + event.session_id, + event.{$column} + "); + + $results = $this->execQuery($query, $params); + + $data = []; + foreach ($results as $result) { + if ($result['value'] !== null) { + if (!array_key_exists($result['id'], $data)) { + $data[$result['id']] = []; + } + $data[$result['id']][$result['value']] = $result['cnt']; + } + } + + foreach ($stats as $id => $item) { + if (array_key_exists($id, $data)) { + $stats[$id][$alias . strval($id)] = json_encode($data[$id], JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE); + } else { + $stats[$id][$alias . strval($id)] = null; + } + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/Base.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/Base.php new file mode 100644 index 0000000..a040fc9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/Base.php @@ -0,0 +1,44 @@ + $apiKey]; + + $endDate = $dateRange['endDate'] ?? null; + $startDate = $dateRange['startDate'] ?? null; + + if ($startDate && $endDate) { + $queryParams[':end_time'] = $dateRange['endDate']; + $queryParams[':start_time'] = $dateRange['startDate']; + } + + return $queryParams; + } + + public function getQueryConditions(?array $dateRange): array { + $conditions = ['event.key = :api_key']; + + $endDate = $dateRange['endDate'] ?? null; + if ($endDate) { + $conditions[] = 'event.time >= :start_time'; + $conditions[] = 'event.time <= :end_time'; + } + + return $conditions; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/CountriesByUsers.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/CountriesByUsers.php new file mode 100644 index 0000000..2e0c1ba --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/CountriesByUsers.php @@ -0,0 +1,57 @@ +getQueryParams($apiKey, $dateRange); + + $queryConditions = $this->getQueryConditions($dateRange); + $queryConditions = join(' AND ', $queryConditions); + + $query = ( + "SELECT + countries.id AS country_id, + countries.iso AS country_iso, + countries.value AS full_country, + COUNT(DISTINCT event.account) AS value + + FROM + event + + INNER JOIN event_ip + ON (event.ip = event_ip.id) + + INNER JOIN countries + ON (event_ip.country = countries.id) + + WHERE + {$queryConditions} + + GROUP BY + countries.id + + ORDER BY + value DESC + + LIMIT 10 OFFSET 0" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/IpsByUsers.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/IpsByUsers.php new file mode 100644 index 0000000..878f6aa --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/IpsByUsers.php @@ -0,0 +1,65 @@ +getQueryParams($apiKey, $dateRange); + + $queryConditions = $this->getQueryConditions($dateRange); + $queryConditions[] = 'event_ip.shared > 1'; + $queryConditions = join(' AND ', $queryConditions); + + $query = ( + "SELECT + MAX(event_ip.ip) AS ip, + MAX(event_ip.id) AS ipid, + MAX(event_ip.shared) AS value, + MAX(event_isp.name) AS isp_name, + + MAX(countries.value) AS full_country, + MAX(countries.id) AS country_id, + MAX(countries.iso) AS country_iso + + FROM + event + + INNER JOIN event_ip + ON (event.ip = event_ip.id) + + INNER JOIN countries + ON (event_ip.country = countries.id) + + INNER JOIN event_isp + ON (event_ip.isp = event_isp.id) + + WHERE + {$queryConditions} + + GROUP BY + event_ip.ip + + ORDER BY + value DESC + + LIMIT 10 OFFSET 0" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/ResourcesByUsers.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/ResourcesByUsers.php new file mode 100644 index 0000000..3ebb88e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/ResourcesByUsers.php @@ -0,0 +1,57 @@ +getQueryParams($apiKey, $dateRange); + + $queryConditions = $this->getQueryConditions($dateRange); + $queryConditions = join(' AND ', $queryConditions); + + $query = ( + "SELECT + event_url.url, + event_url.title, + event_url.id AS url_id, + COUNT(DISTINCT event.account) AS value + + FROM + event + + INNER JOIN event_url + ON (event.url = event_url.id) + + FULL OUTER JOIN event_url_query + ON (event.query = event_url_query.id) + + WHERE + {$queryConditions} + + GROUP BY + event_url.id + + ORDER BY + value DESC + + LIMIT 10 OFFSET 0" + ); + + return $this->execQuery($query, $params); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/UsersByEvents.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/UsersByEvents.php new file mode 100644 index 0000000..986a69d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/UsersByEvents.php @@ -0,0 +1,69 @@ +getQueryParams($apiKey, $dateRange); + + $queryConditions = $this->getQueryConditions($dateRange); + $queryConditions = join(' AND ', $queryConditions); + + $query = ( + "SELECT + event_account.id AS accountid, + event_account.userid AS accounttitle, + event_account.fraud, + event_account.score, + event_account.score_updated_at, + event_email.email, + COUNT(event_account.userid) AS value + + FROM + event + + INNER JOIN event_account + ON (event.account = event_account.id) + + LEFT JOIN event_email + ON (event_account.lastemail = event_email.id) + + WHERE + {$queryConditions} + + GROUP BY + event_account.id, + event_account.userid, + event_email.email + + ORDER BY + value DESC + + LIMIT 10 OFFSET 0" + ); + + $results = $this->execQuery($query, $params); + + foreach ($results as $row) { + $tsColumns = ['score_updated_at']; + \Utils\TimeZones::localizeTimestampsForActiveOperator($tsColumns, $row); + } + + return $results; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/UsersByIps.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/UsersByIps.php new file mode 100644 index 0000000..9d2cae2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/UsersByIps.php @@ -0,0 +1,72 @@ +getQueryParams($apiKey, $dateRange); + + $queryConditions = $this->getQueryConditions($dateRange); + $queryConditions = join(' AND ', $queryConditions); + + $query = ( + "SELECT + event_account.id AS accountid, + event_account.userid AS accounttitle, + event_account.fraud, + event_account.score, + event_account.score_updated_at, + COUNT(DISTINCT event.ip) AS value, + event_email.email + + FROM + event + + INNER JOIN event_account + ON (event.account = event_account.id) + + LEFT JOIN event_email + ON (event_account.lastemail = event_email.id) + + WHERE + {$queryConditions} + + GROUP BY + event_account.id, + event_account.userid, + event_email.email + + HAVING + COUNT(DISTINCT event.ip) > 1 + + ORDER BY + value DESC + + LIMIT 10 OFFSET 0" + ); + + $results = $this->execQuery($query, $params); + + foreach ($results as $row) { + $tsColumns = ['score_updated_at']; + \Utils\TimeZones::localizeTimestampsForActiveOperator($tsColumns, $row); + } + + return $results; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/UsersByLoginFail.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/UsersByLoginFail.php new file mode 100644 index 0000000..72680ea --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/UsersByLoginFail.php @@ -0,0 +1,72 @@ +getQueryParams($apiKey, $dateRange); + + $queryConditions = $this->getQueryConditions($dateRange); + $queryConditions[] = 'event.type = :event_type'; + $queryConditions = join(' AND ', $queryConditions); + + $params[':event_type'] = \Utils\Constants::get('ACCOUNT_LOGIN_FAIL_EVENT_TYPE_ID'); + + $query = ( + "SELECT + event_account.id AS accountid, + event_account.userid AS accounttitle, + event_account.fraud, + event_account.score, + event_account.score_updated_at, + event_email.email, + COUNT(event_account.userid) AS value + + FROM + event + + INNER JOIN event_account + ON (event.account = event_account.id) + + LEFT JOIN event_email + ON (event_account.lastemail = event_email.id) + + WHERE + {$queryConditions} + + GROUP BY + event_account.id, + event_account.userid, + event_email.email + + ORDER BY + value DESC + + LIMIT 10 OFFSET 0" + ); + + $results = $this->execQuery($query, $params); + + foreach ($results as $row) { + $tsColumns = ['score_updated_at']; + \Utils\TimeZones::localizeTimestampsForActiveOperator($tsColumns, $row); + } + + return $results; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/TopTen/index.php @@ -0,0 +1,3 @@ +f3 = $f3; + $db = $this->getDbConnection(\Utils\Variables::getDB()); + $this->f3->set('API_DATABASE', $db); + $this->db = $db; + \DB\SQL\Mapper::__construct($db, $this->DB_TABLE_NAME, $this->DB_TABLE_FIELDS, $this->DB_TABLE_TTL); + $this->db = $db; + $this->createIfNotExists(); + } + + private function getDbConnection(string $url): ?\DB\SQL { + $urlComponents = parse_url($url); + + $host = $urlComponents['host']; + $port = $urlComponents['port']; + $user = $urlComponents['user']; + $pass = $urlComponents['pass']; + $db = ltrim($urlComponents['path'], '/'); + + $dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $host, $port, $db); + $options = [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + ]; + try { + return new \DB\SQL($dsn, $user, $pass, $options); + } catch (\Exception $e) { + throw new \Exception('Failed to establish database connection: ' . $e->getMessage()); + } + } + + public function checkDb(string $service, array $updatesList) { + try { + $this->db->begin(); + foreach ($updatesList as $migration) { + if (!$migration::isApplied($this)) { + $migration::apply($this->db); + $this->add($migration::$version, $service); + } + } + $this->db->commit(); + } catch (\Exception $e) { + $this->db->rollback(); + error_log($e->getMessage()); + throw $e; + } + } + + public function isApplied(string $version, string $name): bool { + $params = [ + ':version' => $version, + ':service' => $name, + ]; + + $query = 'SELECT 1 FROM dshb_updates WHERE version = :version and service = :service LIMIT 1'; + + $results = $this->execQuery($query, $params); + + return (bool) count($results); + } + + public function add(string $version, string $name): void { + $params = [ + ':version' => $version, + ':service' => $name, + ]; + + $query = 'INSERT INTO dshb_updates (service, version) VALUES (:service, :version)'; + + $this->execQuery($query, $params); + } + + private function createIfNotExists(): void { + $query = 'SELECT 1 FROM information_schema.tables WHERE table_name = \'dshb_updates\''; + + if (count($this->execQuery($query, null))) { + return; + } + + $queries = [ + ('CREATE SEQUENCE IF NOT EXISTS dshb_updates_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1'), + ('CREATE TABLE IF NOT EXISTS dshb_updates ( + id bigint NOT NULL DEFAULT nextval(\'dshb_updates_id_seq\'::regclass), + service varchar(30), + version varchar(30), + created timestamp without time zone DEFAULT now() NOT NULL + )'), + 'ALTER SEQUENCE dshb_updates_id_seq OWNED BY dshb_updates.id', + 'ALTER TABLE ONLY dshb_updates ADD CONSTRAINT dshb_updates_service_version_key UNIQUE (service, version)', + 'ALTER TABLE ONLY dshb_updates ADD CONSTRAINT dshb_updates_id_pkey PRIMARY KEY (id)', + ]; + + foreach ($queries as $query) { + $this->execQuery($query, null); + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/User.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/User.php new file mode 100644 index 0000000..9358867 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/User.php @@ -0,0 +1,395 @@ + $userId, + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT + userid + + FROM + event_account + + WHERE + event_account.id = :user_id + AND event_account.key = :api_key' + ); + + $results = $this->execQuery($query, $params); + + return count($results) > 0; + } + + public function checkAccessByExternalId(string $externalUserId, int $apiKey): bool { + $params = [ + ':user_id' => $externalUserId, + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT + userid + + FROM + event_account + + WHERE + event_account.userid = :user_id + AND event_account.key = :api_key + + LIMIT 1' + ); + + $results = $this->execQuery($query, $params); + + return count($results) > 0; + } + + public function getUser(int $userId, int $apiKey): array { + $params = [ + ':user_id' => $userId, + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT + event_account.id AS accountid, + event_account.userid, + event_account.lastseen, + event_account.created, + event_account.firstname, + event_account.lastname, + event_account.score, + event_account.score_details, + event_account.score_updated_at, + event_account.is_important, + event_account.fraud, + event_account.reviewed, + event_account.latest_decision, + event_account.added_to_review, + + event_email.email + + FROM + event_account + + LEFT JOIN event_email + ON (event_account.lastemail = event_email.id) + + WHERE + event_account.id = :user_id + AND event_account.key = :api_key' + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } + + public function deleteAllUserData(int $userId, int $apiKey): void { + $params = [ + ':user_id' => $userId, + ':api_key' => $apiKey, + ]; + + $queries = [ + // Delete all user events. + 'DELETE FROM event + WHERE event.account = :user_id + AND event.key = :api_key;', + // Delete user account. + 'DELETE FROM event_account + WHERE event_account.id = :user_id + AND event_account.key = :api_key;', + // Delete all devices related to user. + 'DELETE FROM event_device + WHERE event_device.account_id = :user_id + AND event_device.key = :api_key;', + // Delete all emails related to user. + 'DELETE FROM event_email + WHERE event_email.account_id = :user_id + AND event_email.key = :api_key;', + // Delete all phones related to user. + 'DELETE FROM event_phone + WHERE event_phone.account_id = :user_id + AND event_phone.key = :api_key;', + // Delete all related sessions + 'DELETE FROM event_session + WHERE event_session.account_id = :user_id + AND event_session.key = :api_key', + ]; + + try { + $model = new \Models\Events(); + $entities = $model->uniqueEntitesByUserId($userId, $apiKey); + + $this->db->begin(); + $this->db->exec($queries, array_fill(0, 6, $params)); + + // force update totals for ips before isps and countries! + $model = new \Models\Ip(); + $model->updateTotalsByEntityIds($entities['ip_ids'], $apiKey, true); + + $model = new \Models\Isp(); + $model->updateTotalsByEntityIds($entities['isp_ids'], $apiKey, true); + + $model = new \Models\Country(); + $model->updateTotalsByEntityIds($entities['country_ids'], $apiKey, true); + + $model = new \Models\Resource(); + $model->updateTotalsByEntityIds($entities['url_ids'], $apiKey, true); + + $model = new \Models\Domain(); + $model->updateTotalsByEntityIds($entities['domain_ids'], $apiKey, true); + + $model = new \Models\Phone(); + $model->updateTotalsByValues($entities['phone_numbers'], $apiKey, true); + + $this->db->commit(); + } catch (\Exception $e) { + $this->db->rollback(); + error_log($e->getMessage()); + throw $e; + } + } + + public function getTimeFrameTotal(array $ids, string $startDate, string $endDate, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + $params[':start_date'] = $startDate; + $params[':end_date'] = $endDate; + + $query = ( + "SELECT + event.account AS id, + COUNT(*) AS cnt + FROM event + WHERE + event.account IN ({$flatIds}) AND + event.key = :key AND + event.time > :start_date AND + event.time < :end_date + GROUP BY event.account" + ); + + $totalVisit = $this->execQuery($query, $params); + + $result = []; + + foreach ($ids as $id) { + $result[$id] = ['total_visit' => 0]; + } + + foreach ($totalVisit as $rec) { + $result[$rec['id']]['total_visit'] = $rec['cnt']; + } + + return $result; + } + + public function getAccountIdByUserId(string $userId, int $apiKey): self|null|false { + $filters = [ + 'key=? AND userid=?', $apiKey, $userId, + ]; + + return $this->load($filters); + } + + public function getApplicableRulesByAccountId(int $id, int $apiKey, bool $all = false): array { + $params = [ + ':account_id' => $id, + ':api_key' => $apiKey, + ]; + + $query = ( + "SELECT + (score_element ->> 'score')::int AS score, + event_account.score AS total_score, + dshb_rules.uid, + dshb_rules.name, + dshb_rules.descr, + dshb_rules.validated + + FROM + event_account + + JOIN jsonb_array_elements(event_account.score_details) AS score_element + ON true + + LEFT JOIN dshb_rules + ON (dshb_rules.uid = (score_element ->> 'uid')::varchar) + + WHERE + event_account.id = :account_id AND + event_account.key = :api_key AND + uid IS NOT NULL" + ); + + if (!$all) { + $query .= ' AND (score_element ->> \'score\')::int != 0'; + } + + $results = $this->execQuery($query, $params); + + usort($results, static function ($a, $b): int { + return $b['score'] <=> $a['score']; + }); + + return $results; + } + + public function updateUserStatus(int $accountId, array $data, int $apiKey): void { + $this->load( + ['id=? AND key=?', $accountId, $apiKey], + ); + + if ($this->loaded()) { + $timestamp = (new \DateTime('now', new \DateTimeZone('UTC')))->format(\Utils\TimeZones::FORMAT); + $this->score = $data['score']; + $this->score_details = $data['details']; + $this->score_updated_at = $timestamp; + $this->score_recalculate = false; + + if ($data['addToReview']) { + $this->added_to_review = $timestamp; + } + + $this->save(); + } + } + + public function updateFraudFlag(array $accountIds, int $apiKey, bool $fraud): void { + if (!count($accountIds)) { + return; + } + + [$params, $placeHolders] = $this->getArrayPlaceholders($accountIds); + + $params[':fraud'] = $fraud; + $params[':api_key'] = $apiKey; + $params[':latest_decision'] = gmdate('Y-m-d H:i:s'); + + $query = ( + "UPDATE event_account + SET fraud = :fraud, latest_decision = :latest_decision + + WHERE + key = :api_key + AND id IN ({$placeHolders})" + ); + + $this->execQuery($query, $params); + } + + public function updateReviewedFlag(int $accountId, int $apiKey, bool $reviewed): void { + $this->load( + ['id=? AND key=?', $accountId, $apiKey], + ); + + if ($this->loaded()) { + //Workaround. Emulate nullable default value + $this->fraud = null; + + $this->reviewed = $reviewed; + + $this->save(); + } + } + + public function updateTotalsByAccountIds(array $ids, int $apiKey): int { + if (!count($ids)) { + return 0; + } + + [$params, $flatIds] = $this->getArrayPlaceholders($ids); + $params[':key'] = $apiKey; + + $query = ( + "UPDATE event_account + SET + total_visit = COALESCE(sub.total_visit, 0), + total_ip = COALESCE(sub.total_ip, 0), + total_device = COALESCE(sub.total_device, 0), + total_country = COALESCE(sub.total_country, 0), + total_shared_ip = COALESCE(sub.total_shared_ips, 0), + total_shared_phone = COALESCE(sub.total_shared_phones, 0), + updated = date_trunc('milliseconds', now()) + FROM ( + SELECT + event.account, + COUNT(*) AS total_visit, + COUNT(DISTINCT event.ip) AS total_ip, + COUNT(DISTINCT event.device) AS total_device, + COUNT(DISTINCT event_ip.country) AS total_country, + COUNT(DISTINCT CASE WHEN event_ip.shared > 1 THEN event.ip ELSE NULL END) AS total_shared_ips, + (SELECT COUNT(*) FROM event_phone WHERE event_phone.account_id = event.account AND event_phone.shared > 1) AS total_shared_phones + FROM event + LEFT JOIN event_ip + ON event_ip.id = event.ip + WHERE event.account IN ($flatIds) + GROUP BY event.account + ) AS sub + RIGHT JOIN event_account sub_account ON sub.account = sub_account.id + WHERE + event_account.id = sub_account.id AND + event_account.id IN ($flatIds) AND + event_account.key = :key AND + event_account.lastseen >= event_account.updated" + ); + + return $this->execQuery($query, $params); + } + + public function refreshTotals(array $res, int $apiKey): array { + [$params, $flatIds] = $this->getArrayPlaceholders(array_column($res, 'id')); + $params[':key'] = $apiKey; + $query = ( + "SELECT + id, + total_ip, + total_visit, + total_device, + total_country + FROM event_account + WHERE id IN ({$flatIds}) AND key = :key" + ); + + $result = $this->execQuery($query, $params); + + $indexedResult = []; + foreach ($result as $item) { + $indexedResult[$item['id']] = $item; + } + + foreach ($res as $idx => $item) { + $item['total_ip'] = $indexedResult[$item['id']]['total_ip']; + $item['total_visit'] = $indexedResult[$item['id']]['total_visit']; + $item['total_device'] = $indexedResult[$item['id']]['total_device']; + $item['total_country'] = $indexedResult[$item['id']]['total_country']; + $res[$idx] = $item; + } + + return $res; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UserDetails/Behaviour.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UserDetails/Behaviour.php new file mode 100644 index 0000000..0979010 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UserDetails/Behaviour.php @@ -0,0 +1,188 @@ + $userId, + ':api_key' => $apiKey, + ':start_ts' => $dateRange['startDate'], + ':end_ts' => $dateRange['endDate'], + ':offset' => $dateRange['offset'], + ':failed_login' => \Utils\Constants::get('ACCOUNT_LOGIN_FAIL_EVENT_TYPE_ID'), + ':success_login' => \Utils\Constants::get('ACCOUNT_LOGIN_EVENT_TYPE_ID'), + ':password_reset' => \Utils\Constants::get('ACCOUNT_PASSWORD_CHANGE_EVENT_TYPE_ID'), + ':seconds_day' => 60 * 60 * 24, + ':night_time_end' => 60 * 60 * 5, + ]; + + $query = ( + 'SELECT + COUNT(CASE WHEN event.type = :failed_login THEN TRUE END) AS failed_login_cnt, + COUNT(CASE WHEN event.type = :password_reset THEN TRUE END) AS password_reset_cnt, + COUNT(CASE WHEN event.http_code > 400 THEN TRUE END) AS auth_error_cnt, + COUNT(CASE WHEN event.type IN (:failed_login, :success_login) THEN TRUE END) AS login_cnt, + COUNT(CASE WHEN + MOD(EXTRACT(EPOCH FROM event.time) + :offset, :seconds_day) < :night_time_end THEN TRUE END + ) AS off_hours_login_cnt, + COUNT(DISTINCT event.device) AS device_cnt, + COUNT(DISTINCT event.ip) AS ip_cnt, + COUNT(DISTINCT event.session_id) AS session_cnt + + FROM + event + + WHERE + event.account = :user_id AND + event.key = :api_key AND + event.time > :start_ts AND + event.time < :end_ts' + ); + + $results = $this->execQuery($query, $params); + + $result = $results[0] ?? []; + + if ($result) { + $params = [ + ':user_id' => $userId, + ':api_key' => $apiKey, + ':start_ts' => $dateRange['startDate'], + ':end_ts' => $dateRange['endDate'], + ]; + + $query = ( + 'SELECT + percentile_disc(0.5) WITHIN GROUP (ORDER BY event_session.total_visit) AS median_event_cnt + FROM + event_session + + WHERE + event_session.account_id = :user_id AND + event_session.key = :api_key AND + event_session.lastseen > :start_ts AND + event_session.lastseen < :end_ts' + ); + + $results = $this->execQuery($query, $params); + + $result['median_event_cnt'] = $results[0]['median_event_cnt'] ?? 0; + } + + return $result; + } + + public function getWeekDetails(int $userId, array $dateRange, int $apiKey): array { + $params = [ + ':user_id' => $userId, + ':api_key' => $apiKey, + ':start_ts' => $dateRange['startDate'], + ':end_ts' => $dateRange['endDate'], + ':offset' => $dateRange['offset'], + ':failed_login' => \Utils\Constants::get('ACCOUNT_LOGIN_FAIL_EVENT_TYPE_ID'), + ':success_login' => \Utils\Constants::get('ACCOUNT_LOGIN_EVENT_TYPE_ID'), + ':password_reset' => \Utils\Constants::get('ACCOUNT_PASSWORD_CHANGE_EVENT_TYPE_ID'), + ':seconds_day' => 60 * 60 * 24, + ':night_time_end' => 60 * 60 * 5, + ]; + + $query = ( + 'WITH daily AS ( + SELECT + EXTRACT(EPOCH FROM date_trunc(\'day\', event.time + :offset))::bigint AS ts, + COUNT(CASE WHEN event.type = :failed_login THEN TRUE END) AS failed_login_cnt, + COUNT(CASE WHEN event.type = :password_reset THEN TRUE END) AS password_reset_cnt, + COUNT(CASE WHEN event.http_code > 400 THEN TRUE END) AS auth_error_cnt, + COUNT(CASE WHEN event.type IN (:failed_login, :success_login) THEN TRUE END) AS login_cnt, + COUNT(CASE WHEN + MOD(EXTRACT(EPOCH FROM event.time + :offset), :seconds_day) < :night_time_end THEN TRUE END + ) AS off_hours_login_cnt, + COUNT(DISTINCT event.device) AS device_cnt, + COUNT(DISTINCT event.ip) AS ip_cnt, + COUNT(DISTINCT event.session_id) AS session_cnt + + FROM + event + + WHERE + event.account = :user_id AND + event.key = :api_key AND + event.time > :start_ts AND + event.time < :end_ts + + GROUP BY ts + ORDER BY ts + ) + SELECT + percentile_disc(0.5) WITHIN GROUP (ORDER BY failed_login_cnt) AS failed_login_cnt, + percentile_disc(0.5) WITHIN GROUP (ORDER BY password_reset_cnt) AS password_reset_cnt, + percentile_disc(0.5) WITHIN GROUP (ORDER BY auth_error_cnt) AS auth_error_cnt, + percentile_disc(0.5) WITHIN GROUP (ORDER BY login_cnt) AS login_cnt, + percentile_disc(0.5) WITHIN GROUP (ORDER BY off_hours_login_cnt) AS off_hours_login_cnt, + percentile_disc(0.5) WITHIN GROUP (ORDER BY device_cnt) AS device_cnt, + percentile_disc(0.5) WITHIN GROUP (ORDER BY ip_cnt) AS ip_cnt, + percentile_disc(0.5) WITHIN GROUP (ORDER BY session_cnt) AS session_cnt + + + FROM daily' + ); + + $results = $this->execQuery($query, $params); + + $result = $results[0] ?? []; + + if ($result) { + $params = [ + ':user_id' => $userId, + ':api_key' => $apiKey, + ':start_ts' => $dateRange['startDate'], + ':end_ts' => $dateRange['endDate'], + ':offset' => $dateRange['offset'], + ]; + + $query = ( + 'WITH daily AS ( + SELECT + EXTRACT(EPOCH FROM date_trunc(\'day\', event_session.lastseen + :offset))::bigint AS ts, + percentile_disc(0.5) WITHIN GROUP (ORDER BY event_session.total_visit) AS median_event_cnt + FROM + event_session + + WHERE + event_session.account_id = :user_id AND + event_session.key = :api_key AND + event_session.lastseen > :start_ts AND + event_session.lastseen < :end_ts + + GROUP BY ts + ORDER BY ts + ) + SELECT + percentile_disc(0.5) WITHIN GROUP (ORDER BY median_event_cnt) AS median_event_cnt + FROM daily' + ); + + $results = $this->execQuery($query, $params); + + $result['median_event_cnt'] = $results[0]['median_event_cnt'] ?? 0; + } + + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UserDetails/Id.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UserDetails/Id.php new file mode 100644 index 0000000..ca5f6b3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UserDetails/Id.php @@ -0,0 +1,88 @@ + $subjectId, + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT + userid + + FROM + event_account + + WHERE + event_account.id = :user_id + AND event_account.key = :api_key' + ); + + $results = $this->execQuery($query, $params); + + return count($results) > 0; + } + + public function getDetails(int $userId, int $apiKey): array { + $params = [ + ':user_id' => $userId, + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT + event_account.userid, + event_account.lastseen, + event_account.created, + event_account.firstname, + event_account.lastname, + event_account.score, + event_account.score_details, + event_account.is_important, + event_account.fraud, + event_account.reviewed, + event_account.latest_decision, + event_account.added_to_review, + + event_email.email + + FROM + event_account + + LEFT JOIN event_email + ON (event_account.lastemail = event_email.id) + + WHERE + event_account.id = :user_id + AND event_account.key = :api_key' + ); + + + $results = $this->execQuery($query, $params); + + $result = $results[0] ?? []; + + $tsColumns = ['created', 'lastseen', 'score_updated_at', 'latest_decision', 'updated', 'added_to_review']; + \Utils\TimeZones::localizeTimestampsForActiveOperator($tsColumns, $result); + + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UserDetails/Ip.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UserDetails/Ip.php new file mode 100644 index 0000000..3a19c27 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UserDetails/Ip.php @@ -0,0 +1,80 @@ +getIpsDetails($userId, $apiKey); + $data = []; + + if (count($details)) { + $data = [ + 'accountid' => $userId, + + 'withdc' => $details['data_center'], + 'withar' => $details['relay'], + // 'withsl' => $details['starlink'], + 'withvpn' => $details['vpn'], + 'withtor' => $details['tor'], + 'sharedips' => $details['shared'] > 1, + 'fraud_detected' => $details['fraud_detected'], + 'spamlist' => $details['blocklist'], + ]; + } + + return $data; + } + + private function getIpsDetails(int $userId, int $apiKey): array { + $params = [ + ':user_id' => $userId, + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT + event.account AS accountid, + BOOL_OR(event_ip.data_center) AS data_center, + BOOL_OR(event_ip.relay) AS relay, + BOOL_OR(event_ip.starlink) AS starlink, + BOOL_OR(event_ip.vpn) AS vpn, + BOOL_OR(event_ip.tor) AS tor, + MAX(event_ip.shared) AS shared, + BOOL_OR(event_ip.fraud_detected) AS fraud_detected, + BOOL_OR(event_ip.blocklist) AS blocklist, + BOOL_OR(event_ip.checked) AS checked + + FROM + event_ip + + INNER JOIN event + ON (event_ip.id = event.ip) + + WHERE + event_ip.key = :api_key AND + event.account = :user_id + GROUP BY event.account' + ); + + $results = $this->execQuery($query, $params); + + return $results[0] ?? []; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UserDetails/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UserDetails/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UserDetails/index.php @@ -0,0 +1,3 @@ + $apiKey, + ]; + + $query = ( + 'SELECT + event_account.id AS accountid, + event_account.userid AS accounttitle, + event_account.lastseen, + event_email.email + + FROM + event_account + + LEFT JOIN event_email + ON event_account.lastemail = event_email.id + + WHERE + event_account.key = :api_key + + ORDER BY event_account.id DESC' + ); + + return $this->execQuery($query, $params); + } + + public function getTotalUsers(int $apiKey): int { + $params = [ + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT + COUNT(event_account.id) + + FROM + event_account + + WHERE + event_account.key = :api_key' + ); + + $results = $this->execQuery($query, $params); + + return $results[0]['count'] ?? 0; + } + + public function notCheckedUsers(int $apiKey): array { + $params = [ + ':api_key' => $apiKey, + ]; + + $query = ( + 'SELECT DISTINCT + event.account AS id + FROM + event + LEFT JOIN event_ip ON event.ip = event_ip.id + WHERE + event.key = :api_key AND + event_ip.checked IS FALSE' + ); + $result = array_column($this->execQuery($query, $params), 'id'); + + // email + domain + $query = ( + 'SELECT DISTINCT + event_email.account_id AS id + FROM + event_email + LEFT JOIN event_domain ON event_email.domain = event_domain.id + WHERE + event_email.key = :api_key AND + (event_email.checked IS FALSE OR event_domain.checked IS FALSE)' + ); + $result = array_merge($result, array_column($this->execQuery($query, $params), 'id')); + + // phone + $query = ( + 'SELECT DISTINCT + event_phone.account_id AS id + FROM + event_phone + WHERE + event_phone.key = :api_key AND + event_phone.checked IS FALSE' + ); + $result = array_merge($result, array_column($this->execQuery($query, $params), 'id')); + + // device + $query = ( + 'SELECT DISTINCT + event_device.account_id AS id + FROM + event_device + LEFT JOIN event_ua_parsed ON event_device.user_agent = event_ua_parsed.id + WHERE + event_device.key = :api_key AND + event_ua_parsed.checked IS FALSE' + ); + $result = array_merge($result, array_column($this->execQuery($query, $params), 'id')); + + return array_unique($result); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UsersStat.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UsersStat.php new file mode 100644 index 0000000..0250c89 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/UsersStat.php @@ -0,0 +1,82 @@ + $apiKey, + ':user_id' => $userId, + ]; + + $query = ( + 'SELECT + event_account.userid, + COUNT(DISTINCT event.id) AS total_visits, + COUNT(DISTINCT event_ip.id) AS total_ips, + COUNT(DISTINCT countries.id) AS total_countries, + COUNT(DISTINCT event_device.id) AS total_devices + + FROM + event + + LEFT JOIN event_account + ON event.account = event_account.id + + LEFT JOIN event_url + ON event.url = event_url.id + + LEFT JOIN event_ip + ON event.ip = event_ip.id + + LEFT JOIN event_device + ON event.device = event_device.id + + LEFT JOIN countries + ON event_ip.country = countries.id + + WHERE + event.key = :api_key + AND event_account.userid = :user_id + %s + + GROUP BY + event_account.userid' + ); + + $this->applyDateRange($query, $params, $dateRange); + + return $this->execQuery($query, $params); + } + + private function applyDateRange(string &$query, array &$params, ?array $dateRange = null): void { + $searchConditions = ''; + + if ($dateRange) { + $searchConditions = ( + 'AND event.time >= :start_time + AND event.time <= :end_time' + ); + + $params[':end_time'] = $dateRange['endDate']; + $params[':start_time'] = $dateRange['startDate']; + } + + $query = sprintf($query, $searchConditions); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Watchlist.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Watchlist.php new file mode 100644 index 0000000..2f06d1d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/Watchlist.php @@ -0,0 +1,50 @@ +getById($accountId, $apiKey); + + if ($this->loaded()) { + $this->is_important = 1; + $this->save(); + } + } + + public function remove(int $accountId, int $apiKey): void { + $this->getById($accountId, $apiKey); + + if ($this->loaded()) { + $this->is_important = 0; + $this->save(); + } + } + + private function getById(int $accountId, int $apiKey): void { + $this->load( + ['id=? AND key=?', $accountId, $apiKey], + ); + } + + public function getUsersByKey(int $apiKey): array { + return $this->find( + ['key=? AND is_important=?', $apiKey, 1], + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Models/index.php @@ -0,0 +1,3 @@ +getCurrentOperatorApiKeyObject(); + + return $key ? $key->id : null; + } + + public function getCurrentOperatorApiKeyString(): ?string { + $key = $this->getCurrentOperatorApiKeyObject(); + + return $key ? $key->key : null; + } + + public function getCurrentOperatorEnrichmentKeyString(): ?string { + $key = $this->getCurrentOperatorApiKeyObject(); + + return $key ? $key->token : null; + } + + public function getOperatorApiKeys(int $operatorId): array { + $model = new \Models\ApiKeys(); + $apiKeys = $model->getKeys($operatorId); + + $isOwner = true; + if (!$apiKeys) { + $coOwnerModel = new \Models\ApiKeyCoOwner(); + $coOwnerModel->getCoOwnership($operatorId); + + if ($coOwnerModel->loaded()) { + $isOwner = false; + $apiKeys[] = $model->getKeyById($coOwnerModel->api); + } + } + + return [$isOwner, $apiKeys]; + } + + // returns \Models\ApiKeys; in test mode returns object + protected function getCurrentOperatorApiKeyObject(): object|null { + $currentOperator = $this->f3->get('CURRENT_USER'); + + if (!$currentOperator) { + return null; + } + + $model = new \Models\ApiKeys(); + + //This key specified in the local configuration file and will not applied to the production environment + $testId = $this->f3->get('TEST_API_KEY_ID'); + if (isset($testId) && $testId !== '') { + return (object) [ + 'id' => $testId, + 'key' => $model->getKeyById($testId)->key, + 'skip_blacklist_sync' => true, + 'token' => $model->getKeyById($testId)->token, + ]; + } + + $operatorId = $currentOperator->id; + $key = $model->getKey($operatorId); + + if (!$key) { // Check if operator is co-owner of another API key when it has no own API key. + $coOwnerModel = new \Models\ApiKeyCoOwner(); + $coOwnerModel->getCoOwnership($operatorId); + + if ($coOwnerModel->loaded()) { + $key = $model->getKeyById($coOwnerModel->api); + } + } + + return $key; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/DateRange.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/DateRange.php new file mode 100644 index 0000000..a83802f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/DateRange.php @@ -0,0 +1,62 @@ + date('Y-m-d H:i:s', strtotime($endDate) + $offset), + 'startDate' => date('Y-m-d H:i:s', strtotime($startDate) + $offset), + ]; + } + + public function getDatesRange(array $request, int $offset = 0): ?array { + $dates = null; + $dateTo = $request['dateTo'] ?? null; + $dateFrom = $request['dateFrom'] ?? null; + $keepDates = $request['keepDates'] ?? null; + + if ($dateTo && $dateFrom) { + $dates = $this->getDatesRangeByGivenDates($dateFrom, $dateTo, $offset); + + $endDate = null; + $startDate = null; + + if ($keepDates) { + $endDate = $dates['endDate']; + $startDate = $dates['startDate']; + } + + $this->f3->set('SESSION.filterEndDate', $endDate); + $this->f3->set('SESSION.filterStartDate', $startDate); + } + + return $dates; + } + + public function getLatestNDatesRange(int $days, int $offset = 0): array { + return [ + 'endDate' => date('Y-m-d 23:59:59', time() + $offset), + 'startDate' => date('Y-m-d 00:00:01', time() - ($days * 24 * 60 * 60) + $offset), + ]; + } + + public function getResolution(array $request): string { + $resolution = $request['resolution'] ?? 'day'; + + return array_key_exists($resolution, \Utils\Constants::get('CHART_RESOLUTION')) ? $resolution : 'day'; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/Db.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/Db.php new file mode 100644 index 0000000..0f21c90 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/Db.php @@ -0,0 +1,114 @@ +f3->get('API_DATABASE'); + + if (!$db) { + $url = \Utils\Variables::getDB(); + + if ($url === null) { + return false; + } + $db = $this->getDbConnection($url); + + if ($keepSessionInDb) { + new \DB\SQL\Session($db, 'dshb_sessions'); + } + + $this->f3->set('API_DATABASE', $db); + } + + return true; + } catch (\Exception $e) { + error_log('Failed to establish database connection: ' . $e->getMessage()); + } + + return false; + } + + private function getDbConnection(string $url): ?\DB\SQL { + $urlComponents = parse_url($url); + + $host = $urlComponents['host']; + $port = $urlComponents['port']; + $user = $urlComponents['user']; + $pass = $urlComponents['pass']; + $db = ltrim($urlComponents['path'], '/'); + + // Include port in DSN if it's set + $dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $host, $port, $db); + $options = [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + ]; + try { + return new \DB\SQL($dsn, $user, $pass, $options); + } catch (\Exception $e) { + throw new \Exception('Failed to establish database connection: ' . $e->getMessage()); + } + } + + private function getOperatorInfoFromF3(): \Models\Operator|false|null { + return $this->f3->get('CURRENT_USER'); + } + + private function getOperatorInfoFromDb(): \Models\Operator|false|null { + $model = new \Models\Operator(); + $loggedInOperatorId = $this->f3->get('SESSION.active_user_id'); + + return $loggedInOperatorId ? $model->getOperatorById($loggedInOperatorId) : null; + } + + public function getLoggedInOperator(): \Models\Operator|false|null { + $testId = $this->f3->get('TEST_API_KEY_ID'); + if ($testId !== null) { + $keyModel = new \Models\ApiKeys(); + $operatorModel = new \Models\Operator(); + $loggedInOperatorId = $keyModel->getKeyById($testId)->creator; + + return $operatorModel->getOperatorById($loggedInOperatorId); + } + + $user = $this->getOperatorInfoFromF3(); + + if (!$user) { + $user = $this->getOperatorInfoFromDb(); + } + + return $user; + } + + public function showForbiddenIfUnlogged(): void { + if (!boolval($this->getLoggedInOperator())) { + $this->f3->error(403); + } + } + + public function redirectIfUnlogged(string $targetPage = '/'): void { + if (!boolval($this->getLoggedInOperator())) { + $this->f3->reroute($targetPage); + } + } + + public function redirectIfLogged(): void { + if (boolval($this->getLoggedInOperator())) { + $this->f3->reroute('/'); + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/Debug.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/Debug.php new file mode 100644 index 0000000..acadfc2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/Debug.php @@ -0,0 +1,27 @@ + 'high', + 1 => 'medium', + 0 => 'low', + default => 'none', + }; + } + + /*if (!$r['profiles'] && !$r['data_breach'] && $r['blockemails']) { + $reputation = 'low'; + } elseif (!$r['profiles'] && $r['data_breach'] && !$reputation) { + $reputation = 'medium'; + } elseif ($r['profiles'] && !$r['data_breach'] && !$reputation) { + $reputation = 'medium'; + } elseif ($r['profiles'] && $r['data_breach'] && !$reputation) { + $reputation = 'high'; + } else { + $reputation = 'none'; + }*/ + + $r[$fieldName] = $reputation; + + $records[$i] = $r; + } + } + + private function calculateEmailReputationForContext(array &$records): void { + $iters = count($records); + + for ($i = 0; $i < $iters; ++$i) { + $r = $records[$i]; + + //$r['profiles'] = $r['ee_profiles'] ?? 0; + $r['data_breach'] = $r['ee_data_breach'] ?? false; + $r['blockemails'] = $r['ee_blockemails'] ?? false; + //$r['disposable_domains'] = $r['ed_disposable_domains'] ?? false; + + $records[$i] = $r; + } + + $fieldName = 'ee_reputation'; + $this->calculateEmailReputation($records, $fieldName); + + for ($i = 0; $i < $iters; ++$i) { + $r = $records[$i]; + + //unset($r['profiles']); + unset($r['data_breach']); + unset($r['blockemails']); + //unset($r['disposable_domains']); + + $records[$i] = $r; + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/Enrichment/Ips.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/Enrichment/Ips.php new file mode 100644 index 0000000..18c6ea0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/Enrichment/Ips.php @@ -0,0 +1,69 @@ +translateTimeZone($row, $attributes, $useMilliseconds); + + return $row; + }, $rows); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/Enrichment/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/Enrichment/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/Enrichment/index.php @@ -0,0 +1,3 @@ +f3->get('CURRENT_USER'); + + if ($currentOperator) { + $apiKey = $this->getCurrentOperatorApiKeyId(); + $messages = \Utils\SystemMessages::get($apiKey); + + $this->f3->set('SYSTEM_MESSAGES', $messages); + + if (count($messages)) { + $m = $messages[0]; + $doRedirect = $this->shouldRedirectToApiKeys($m); + + if ($doRedirect) { + $this->f3->reroute('/api'); + } + } + } + } + + private function shouldRedirectToApiKeys($message): bool { + $route = $this->f3->get('PARAMS.0'); + $allowedPages = [ + '/api', + '/settings', + '/logbook', + ]; + + $allowedPages = array_merge($allowedPages, $this->f3->get('EXTRA_ALLOWED_PAGES') ?? []); + + $isPageAllowed = in_array($route, $allowedPages); + + return !$isPageAllowed && ($message['id'] === \Utils\ErrorCodes::THERE_ARE_NO_EVENTS_YET); + } + + public function isPostRequest(): bool { + return $this->f3->VERB === 'POST'; + } + + /** + * set a new view. + */ + /* TODO: make sure that setView() is not needed + public function setView(BaseView $view) { + $this->response = $view; + }*/ + + /** + * kick start the View, which creates the response + * based on our previously set content data. + * finally echo the response or overwrite this method + * and do something else with it. + */ + public function afterroute(): void { + if (!$this->response) { + trigger_error('No View has been set.'); + } + + $shouldPrintSqlToLog = $this->f3->get('PRINT_SQL_LOG_AFTER_EACH_SCRIPT_CALL'); + + if ($shouldPrintSqlToLog) { + $hive = $this->f3->hive(); + $path = $hive['PATH']; + + $log = $this->f3->get('API_DATABASE')->log(); + if ($log) { + \Utils\Logger::logSql($path, $log); + } + } + + echo $this->response->render(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Traits/index.php @@ -0,0 +1,3 @@ +value = $value; + } + + public function __get($name): int|string { + if ($name === 'value') { + return $this->value; + } + throw new \InvalidArgumentException('Invalid property ' . $name); + } + + public static function getValues(): array { + $reflection = new \ReflectionClass(static::class); + + return array_values($reflection->getConstants()); + } + + public static function from(int|string $value): self { + if (!static::isValid($value)) { + throw new \ValueError(\sprintf('Invalid value for %s: %s', static::class, $value)); + } + + return new static($value); + } + + public static function tryFrom(int|string $value): ?self { + if (!static::isValid($value)) { + return null; + } + + return new static($value); + } + + private static function isValid(int|string $value): bool { + return in_array($value, static::getValues(), true); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Type/QueueAccountOperationActionType.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Type/QueueAccountOperationActionType.php new file mode 100644 index 0000000..a1b8afe --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Type/QueueAccountOperationActionType.php @@ -0,0 +1,25 @@ +isApplied(static::$version, 'core'); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update001.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update001.php new file mode 100644 index 0000000..48f025f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update001.php @@ -0,0 +1,30 @@ +exec($sql); + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update002.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update002.php new file mode 100644 index 0000000..d030aa2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update002.php @@ -0,0 +1,258 @@ + 'E20', + 24 => 'B19', + 19 => 'B04', + 44 => 'E27', + 4 => 'B07', + 1 => 'I01', + 9 => 'E19', + 99 => 'E05', + 82 => 'E10', + 10 => 'D06', + 15 => 'E13', + 2 => 'I06', + 102 => 'E08', + 84 => 'P02', + 23 => 'I08', + 63 => 'D02', + 100 => 'E06', + 7 => 'E12', + 87 => 'B08', + 74 => 'E24', + 20 => 'B05', + 46 => 'C01', + 8 => 'E16', + 3 => 'E01', + 109 => 'B23', + 16 => 'A01', + 25 => 'D07', + 11 => 'B01', + 65 => 'D03', + 67 => 'A03', + 54 => 'C08', + 50 => 'C04', + 17 => 'B02', + 81 => 'D08', + 47 => 'C02', + 62 => 'D01', + 18 => 'B03', + 22 => 'B06', + 14 => 'E11', + 72 => 'A08', + 86 => 'E15', + 35 => 'C16', + 12 => 'I09', + 5 => 'E17', + 6 => 'E02', + 106 => 'R03', + 48 => 'P04', + 104 => 'R01', + 105 => 'R02', + 41 => 'B20', + 45 => 'E28', + 88 => 'B09', + 97 => 'E03', + 77 => 'B12', + 78 => 'D09', + 85 => 'P01', + 94 => 'B16', + 57 => 'I03', + 40 => 'B18', + 58 => 'B21', + 69 => 'A05', + 75 => 'E25', + 89 => 'B10', + 83 => 'E14', + 31 => 'E22', + 80 => 'B14', + 73 => 'E23', + 96 => 'P03', + 68 => 'A04', + 101 => 'E07', + 64 => 'I04', + 26 => 'D04', + 27 => 'D05', + 79 => 'E26', + 95 => 'B13', + 53 => 'C07', + 43 => 'I11', + 37 => 'E29', + 71 => 'A07', + 66 => 'A02', + 93 => 'B15', + 30 => 'E21', + 60 => 'I02', + 59 => 'I05', + 42 => 'B17', + 76 => 'B11', + 38 => 'E30', + 61 => 'I07', + 28 => 'I10', + 39 => 'D10', + 70 => 'A06', + 98 => 'E04', + 21 => 'C11', + 103 => 'E09', + 49 => 'C03', + 52 => 'C06', + 108 => 'I12', + 33 => 'C14', + 55 => 'C09', + 56 => 'C10', + 32 => 'C13', + 36 => 'C12', + 107 => 'B22', + 51 => 'C05', + 34 => 'C15', + 110 => 'B24', + ]; + + public static function apply($db) { + $queries = [ + 'INSERT INTO dshb_rules (id) VALUES (109), (110)', + 'CREATE INDEX event_account_lastseen_key_idx ON event_account USING btree (lastseen, key)', + 'CREATE INDEX event_url_lastseen_key_idx ON event_url USING btree (lastseen, key)', + 'CREATE INDEX event_time_key_idx ON event USING btree (time, key)', + 'CREATE INDEX event_account_latest_decision_key_idx ON event_account USING btree (latest_decision, key)', + 'CREATE INDEX event_country_lastseen_key_idx ON event_country USING btree (lastseen, key)', + 'CREATE INDEX event_ip_lastseen_key_idx ON event_ip USING btree (lastseen, key)', + 'ALTER TABLE dshb_rules ADD COLUMN validated BOOLEAN NOT NULL DEFAULT false', + 'ALTER TABLE dshb_rules ADD COLUMN uid VARCHAR', + 'ALTER TABLE dshb_rules ADD COLUMN name VARCHAR', + 'ALTER TABLE dshb_rules ADD COLUMN descr VARCHAR', + 'ALTER TABLE dshb_rules ADD COLUMN attributes JSONB DEFAULT \'[]\' NOT NULL', + 'ALTER TABLE dshb_rules ADD COLUMN updated TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL', + 'ALTER TABLE dshb_rules ADD COLUMN missing BOOLEAN', + 'DELETE FROM dshb_rules WHERE id IN (13, 90, 91, 92)', + 'UPDATE event_error_type SET name = \'Success\' WHERE id = 0', + 'UPDATE event_error_type SET name = \'Success with warnings\' WHERE id = 1', + ]; + foreach ($queries as $sql) { + $db->exec($sql); + } + + $sql = 'SELECT id FROM dshb_rules'; + $rulesIds = array_column($db->exec($sql), 'id'); + + $rules = self::extendIds($rulesIds); + + // extend rules data + foreach ($rules as $id => $rule) { + $params = [ + ':validated' => true, + ':id' => $id, + ':uid' => $rule['uid'], + ':name' => $rule['name'], + ':descr' => $rule['descr'], + ':attributes' => json_encode($rule['attributes']), + ]; + + $query = ( + 'INSERT INTO dshb_rules (id, uid, name, descr, validated, attributes) + VALUES (:id, :uid, :name, :descr, :validated, :attributes) + ON CONFLICT (id) DO UPDATE + SET uid = EXCLUDED.uid, name = EXCLUDED.name, descr = EXCLUDED.descr, + validated = EXCLUDED.validated, attributes = EXCLUDED.attributes' + ); + + $db->exec($query, $params); + } + + // add uid to dshb_operators_rules + $sql = 'ALTER TABLE dshb_operators_rules ADD COLUMN rule_uid VARCHAR'; + $db->exec($sql); + + foreach ($rulesIds as $id) { + $sql = 'UPDATE dshb_operators_rules SET rule_uid = :uid WHERE rule_id = :id'; + $db->exec($sql, [':id' => $id, ':uid' => self::$rulesMap[$id]]); + } + + // update event_account score details + $sql = 'ALTER TABLE event_account ALTER COLUMN score_details TYPE JSONB USING score_details::jsonb'; + $db->exec($sql); + + $sql = ( + 'UPDATE event_account + SET score_details = ( + SELECT jsonb_agg((elem - \'id\') || jsonb_build_object(\'uid\', dshb_rules.uid)) + FROM jsonb_array_elements(score_details) AS elem + + JOIN dshb_rules + ON (elem->>\'id\')::int = dshb_rules.id + ) + WHERE event_account.score_details IS NOT NULL' + ); + $db->exec($sql); + + // cleanup + $queries = [ + 'ALTER TABLE dshb_operators_rules DROP COLUMN rule_id', + 'ALTER TABLE dshb_rules ALTER COLUMN uid SET NOT NULL', + 'ALTER TABLE dshb_rules ALTER COLUMN name SET NOT NULL', + 'ALTER TABLE dshb_rules ALTER COLUMN descr SET NOT NULL', + 'ALTER TABLE dshb_rules DROP COLUMN id', + 'ALTER TABLE dshb_rules ADD CONSTRAINT dshb_rules_uid_pkey PRIMARY KEY (uid)', + 'ALTER TABLE dshb_operators_rules ADD CONSTRAINT dshb_operators_rules_rule_uid_fkey FOREIGN KEY (rule_uid) REFERENCES dshb_rules(uid) ON DELETE CASCADE', + 'CREATE INDEX event_account_score_details_idx ON event_account USING GIN (score_details)', + ]; + + foreach ($queries as $sql) { + $db->exec($sql); + } + } + + public static function extendIds(array $ids): array { + $results = []; + + $rules = self::getCoreRulesMetadata(); + + foreach ($ids as $id) { + $uid = self::$rulesMap[$id]; + if (isset($rules[$uid])) { + $results[$id] = $rules[$uid]; + } + } + + return $results; + } + + private static function getCoreRulesMetadata(): array { + $rules = \Utils\RulesClasses::getRulesClasses(true); + $out = []; + + foreach ($rules['imported'] as $uid => $cls) { + try { + $out[$uid] = [ + 'uid' => $uid, + 'name' => $cls::NAME, + 'descr' => $cls::DESCRIPTION, + 'attributes' => $cls::ATTRIBUTES, + ]; + } catch (\Throwable $e) { + error_log('Fail on const call: ' . $e->getMessage()); + } + } + + return $out; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update003.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update003.php new file mode 100644 index 0000000..ea81b4d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update003.php @@ -0,0 +1,67 @@ + \Utils\Constants::get('PAGE_ERROR_EVENT_TYPE_ID')]; + + $queries = [ + 'ALTER TABLE event_logbook DROP COLUMN raw_time', + 'ALTER TABLE event_account ADD COLUMN added_to_review TIMESTAMP WITHOUT TIME ZONE DEFAULT NULL', + 'CREATE INDEX event_account_added_to_review_idx ON event_account USING btree (added_to_review)', + ( + 'UPDATE event_account + SET added_to_review = event_account.lastseen + FROM dshb_api + WHERE + event_account.key = dshb_api.id AND + event_account.fraud IS NULL AND + event_account.score <= dshb_api.review_queue_threshold' + ), + ]; + + foreach ($queries as $sql) { + $db->exec($sql); + } + + $sql = 'INSERT INTO event_type (id, value, name) VALUES (:type, \'page_error\', \'Page Error\')'; + $db->exec($sql, $data); + + $queries = [ + 'ALTER TABLE countries RENAME COLUMN id TO iso', + 'ALTER TABLE countries RENAME COLUMN serial TO id', + 'ALTER TABLE countries DROP CONSTRAINT countries_id_pkey', + 'DROP INDEX countries_serial_uidx', + 'ALTER TABLE countries ADD CONSTRAINT countries_id_pkey PRIMARY KEY (id)', + 'CREATE UNIQUE INDEX countries_iso_uidx ON countries USING btree (iso)', + ]; + + foreach ($queries as $sql) { + $db->exec($sql); + } + + $sql = ( + 'UPDATE event + SET type = :type + WHERE http_code >= 400' + ); + + $db->exec($sql, $data); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update004.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update004.php new file mode 100644 index 0000000..f9a3843 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update004.php @@ -0,0 +1,88 @@ + \Utils\Constants::get('FIELD_EDIT_EVENT_TYPE_ID')]; + + $queries = [ + ('CREATE SEQUENCE event_field_audit_trail_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + '), + ('CREATE TABLE event_field_audit_trail ( + id BIGINT NOT NULL DEFAULT nextval(\'event_field_audit_trail_id_seq\'::regclass), + account_id BIGINT NOT NULL, + key smallint NOT NULL, + created timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + event_id BIGINT, + field_id varchar, + field_name varchar, + old_value varchar, + new_value varchar, + parent_id varchar, + parent_name varchar + )'), + 'ALTER SEQUENCE event_field_audit_trail_id_seq OWNED BY event_field_audit_trail.id', + 'CREATE INDEX event_field_audit_trail_account_id_idx ON event_field_audit_trail USING btree (account_id)', + 'CREATE INDEX event_field_audit_trail_key_idx ON event_field_audit_trail USING btree (key)', + 'ALTER TABLE ONLY event_field_audit_trail ADD CONSTRAINT event_field_audit_trail_id_pkey PRIMARY KEY (id)', + ]; + + foreach ($queries as $sql) { + $db->exec($sql); + } + + $sql = 'INSERT INTO event_type (id, value, name) VALUES (:type, \'field_edit\', \'Field Edit\')'; + $db->exec($sql, $data); + + $queries = [ + ('CREATE SEQUENCE event_payload_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + '), + ('CREATE TABLE event_payload ( + id BIGINT NOT NULL DEFAULT nextval(\'event_payload_id_seq\'::regclass), + key smallint NOT NULL, + created timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + payload json + )'), + 'ALTER SEQUENCE event_payload_id_seq OWNED BY event_payload.id', + 'CREATE INDEX event_payload_created_idx ON event_payload USING btree (created)', + 'CREATE INDEX event_payload_key_idx ON event_payload USING btree (key)', + 'ALTER TABLE ONLY event_payload ADD CONSTRAINT event_payload_id_pkey PRIMARY KEY (id)', + 'ALTER TABLE event DROP COLUMN payload', + 'ALTER TABLE event ADD COLUMN payload BIGINT', + 'CREATE INDEX event_payload_idx ON event USING btree (payload)', + 'ALTER TABLE ONLY event ADD CONSTRAINT event_payload_fkey FOREIGN KEY (payload) REFERENCES event_payload(id)', + ]; + + foreach ($queries as $sql) { + $db->exec($sql); + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update005.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update005.php new file mode 100644 index 0000000..e740aa9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/Update005.php @@ -0,0 +1,66 @@ +exec($sql); + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Updates/index.php @@ -0,0 +1,3 @@ +get('SESSION.csrf'); + + if (!isset($token) || $token === '' || !isset($csrf) || $csrf === '' || $token !== $csrf) { + return \Utils\ErrorCodes::CSRF_ATTACK_DETECTED; + } + + return false; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/ApiResponseFormats.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/ApiResponseFormats.php new file mode 100644 index 0000000..345368a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/ApiResponseFormats.php @@ -0,0 +1,109 @@ +exists($f3key)) { + $value = is_array($value) ? array_merge($value, $f3->get($f3key)) : $f3->get($f3key); + } + + return $value; + } + + // TODO: rewrite context so event amount limit will not be needed + public const RULE_EVENT_CONTEXT_LIMIT = 25; + public const RULE_CHECK_USERS_PASSED_TO_CLIENT = 25; + public const RULE_USERS_BATCH_SIZE = 3500; + public const RULE_EMAIL_MAXIMUM_LOCAL_PART_LENGTH = 17; + public const RULE_EMAIL_MAXIMUM_DOMAIN_LENGTH = 22; + public const RULE_MAXIMUM_NUMBER_OF_404_CODES = 4; + public const RULE_MAXIMUM_NUMBER_OF_500_CODES = 4; + public const RULE_MAXIMUM_NUMBER_OF_LOGIN_ATTEMPTS = 3; + public const RULE_LOGIN_ATTEMPTS_WINDOW = 8; + public const RULE_NEW_DEVICE_MAX_AGE_IN_MINUTES = 60 * 3; + public const RULE_REGULAR_OS_NAMES = ['Windows', 'Android', 'Mac', 'iOS']; + public const RULE_REGULAR_BROWSER_NAMES = [ + 'Chrome' => 90, + 'Chrome Mobile' => 90, + 'Firefox' => 78, + 'Opera' => 70, + 'Safari' => 13, + 'Mobile Safari' => 13, + 'Samsung Browser' => 12, + 'Internet Explorer' => 12, + 'Microsoft Edge' => 90, + 'Chrome Mobile iOS' => 90, + 'Android Browser' => 81, + 'Chrome Webview' => 90, + 'Google Search App' => 90, + 'Yandex Browser' => 20, + ]; + + public const DEVICE_TYPES = [ + 'bot', + 'desktop', + 'smartphone', + 'tablet', + 'other', + 'unknown', + ]; + + public const LOGBOOK_LIMIT = 1000; + + public const NIGHT_RANGE_SECONDS_START = 0; // midnight + public const NIGHT_RANGE_SECONDS_END = 18000; // 5 AM + + public const COUNTRY_CODE_NIGERIA = 160; + public const COUNTRY_CODE_INDIA = 104; + public const COUNTRY_CODE_CHINA = 47; + public const COUNTRY_CODE_BRAZIL = 31; + public const COUNTRY_CODE_PAKISTAN = 168; + public const COUNTRY_CODE_INDONESIA = 105; + public const COUNTRY_CODE_VENEZUELA = 243; + public const COUNTRY_CODE_SOUTH_AFRICA = 199; + public const COUNTRY_CODE_PHILIPPINES = 175; + public const COUNTRY_CODE_ROMANIA = 182; + public const COUNTRY_CODE_RUSSIA = 183; + public const COUNTRY_CODE_AUSTRALIA = 14; + public const COUNTRY_CODE_UAE = 236; + public const COUNTRY_CODE_JAPAN = 113; + + public const COUNTRY_CODES_NORTH_AMERICA = [238, 40]; + public const COUNTRY_CODES_EUROPE = [77, 2, 15, 22, 35, 57, 60, 61, 62, 71, 78, 85, 88, 102, 108, 111, 122, 128, 129, 136, 155, 177, 178, 182, 195, 196, 203, 215]; + + public const EVENT_REQUEST_TYPE_HEAD = 3; + + public const ACCOUNT_OPERATION_QUEUE_CLEAR_COMPLETED_AFTER_DAYS = 7; + public const ACCOUNT_OPERATION_QUEUE_AUTO_UNCLOG_AFTER_MINUTES = 60 * 2; + public const ACCOUNT_OPERATION_QUEUE_EXECUTE_TIME_SEC = 60 * 3; + public const ACCOUNT_OPERATION_QUEUE_BATCH_SIZE = 2500; + public const NEW_EVENTS_BATCH_SIZE = 15000; + + public const USER_LOW_SCORE_INF = 0; + public const USER_LOW_SCORE_SUP = 33; + public const USER_MEDIUM_SCORE_INF = 33; + public const USER_MEDIUM_SCORE_SUP = 67; + public const USER_HIGH_SCORE_INF = 67; + + public const UNAUTHORIZED_USERID = 'N/A'; + + public const ENRICHMENT_IP_IS_BOGON = 'IP is bogon'; + public const ENRICHMENT_IP_IS_NOT_FOUND = 'Value is not found'; + + public const MAIL_FROM_NAME = 'Analytics'; + public const MAIL_HOST = 'smtp.eu.mailgun.org'; + public const MAIL_SEND_BIN = '/usr/sbin/sendmail'; + + public const PAGE_TITLE_POSTFIX = '| tirreno'; + + public const PAGE_VIEW_EVENT_TYPE_ID = 1; + public const PAGE_EDIT_EVENT_TYPE_ID = 2; + public const PAGE_DELETE_EVENT_TYPE_ID = 3; + public const PAGE_SEARCH_EVENT_TYPE_ID = 4; + public const ACCOUNT_LOGIN_EVENT_TYPE_ID = 5; + public const ACCOUNT_LOGOUT_EVENT_TYPE_ID = 6; + public const ACCOUNT_LOGIN_FAIL_EVENT_TYPE_ID = 7; + public const ACCOUNT_REGISTRATION_EVENT_TYPE_ID = 8; + public const ACCOUNT_EMAIL_CHANGE_EVENT_TYPE_ID = 9; + public const ACCOUNT_PASSWORD_CHANGE_EVENT_TYPE_ID = 10; + public const ACCOUNT_EDIT_EVENT_TYPE_ID = 11; + public const PAGE_ERROR_EVENT_TYPE_ID = 12; + public const FIELD_EDIT_EVENT_TYPE_ID = 13; + + public const DEFAULT_RULES = [ + // Positive + 'E23' => -20, + 'E24' => -20, + 'E25' => -20, + 'I07' => -20, + 'I08' => -20, + 'I10' => -20, + // Medium + 'B01' => 10, + 'B04' => 10, + 'B05' => 10, + 'B07' => 10, + 'C01' => 10, + 'C02' => 10, + 'C03' => 10, + 'C04' => 10, + 'C05' => 10, + 'C06' => 10, + 'C07' => 10, + 'C08' => 10, + 'C09' => 10, + 'C10' => 10, + 'C11' => 10, + 'D04' => 10, + 'D08' => 10, + 'E06' => 10, + 'E07' => 10, + 'E08' => 10, + //'E18' => 10, + 'E21' => 10, + 'E22' => 10, + 'I05' => 10, + 'I06' => 10, + 'I09' => 10, + // High + 'D01' => 20, + 'D02' => 20, + 'D03' => 20, + 'D05' => 20, + 'D06' => 20, + 'D07' => 20, + 'E03' => 20, + 'E04' => 20, + 'E05' => 20, + 'I02' => 20, + 'I03' => 20, + 'I04' => 20, + 'P03' => 20, + // Extreme + 'B06' => 70, + 'E01' => 70, + 'E19' => 70, + 'I01' => 70, + 'R01' => 70, + 'R02' => 70, + 'R03' => 70, + ]; + + public const DEFAULT_RULES_EXTENSION = [ + // Positive + 'E20' => -20, + // Medium + 'E09' => 10, + 'E10' => 10, + 'E12' => 10, + 'E15' => 10, + 'P01' => 10, + // High + 'E16' => 20, + // Extreme + 'E02' => 70, + 'E11' => 70, + 'E13' => 70, + 'E14' => 70, + 'E17' => 70, + ]; + + public const CHART_MODEL_MAP = [ + 'resources' => \Models\Chart\Resources::class, + 'resource' => \Models\Chart\Resource::class, + 'users' => \Models\Chart\Users::class, + 'user' => \Models\Chart\User::class, + 'isps' => \Models\Chart\Isps::class, + 'isp' => \Models\Chart\Isp::class, + 'ips' => \Models\Chart\Ips::class, + 'ip' => \Models\Chart\Ip::class, + 'domains' => \Models\Chart\Domains::class, + 'domain' => \Models\Chart\Domain::class, + 'bots' => \Models\Chart\Bots::class, + 'bot' => \Models\Chart\Bot::class, + 'events' => \Models\Chart\Events::class, + 'emails' => \Models\Chart\Emails::class, + 'phones' => \Models\Chart\Phones::class, + 'review-queue' => \Models\Chart\ReviewQueue::class, + 'country' => \Models\Chart\Country::class, + 'blacklist' => \Models\Chart\Blacklist::class, + 'logbook' => \Models\Chart\Logbook::class, + 'stats' => \Models\Chart\SessionStat::class, + ]; + + public const LINE_CHARTS = [ + 'ips', + 'users', + 'review-queue', + 'events', + 'phones', + 'emails', + 'resources', + 'bots', + 'isps', + 'domains', + 'blacklist', + 'logbook' + ]; + + public const CHART_RESOLUTION = [ + 'day' => 60 * 60 * 24, + 'hour' => 60 * 60, + 'minute' => 60, + ]; + + public const TOP_TEN_MODELS_MAP = [ + 'mostActiveUsers' => \Models\TopTen\UsersByEvents::class, + 'mostActiveCountries' => \Models\TopTen\CountriesByUsers::class, + 'mostActiveUrls' => \Models\TopTen\ResourcesByUsers::class, + 'ipsWithTheMostUsers' => \Models\TopTen\IpsByUsers::class, + 'usersWithMostLoginFail' => \Models\TopTen\UsersByLoginFail::class, + 'usersWithMostIps' => \Models\TopTen\UsersByIps::class, + ]; + + public const RULES_TOTALS_MODELS = [ + \Models\Phone::class, + \Models\Ip::class, + \Models\Session::class, + \Models\User::class, + ]; + + public const REST_TOTALS_MODELS = [ + 'isp' => \Models\Isp::class, + 'resource' => \Models\Resource::class, + 'domain' => \Models\Domain::class, + 'device' => \Models\Device::class, + 'country' => \Models\Country::class, + ]; + + public const ENRICHING_ATTRIBUTES = [ + 'ip' => \Models\Ip::class, + 'email' => \Models\Email::class, + 'domain' => \Models\Domain::class, + 'phone' => \Models\Phone::class, + //'ua' => \Models\Device::class, + ]; + + public const ADMIN_PAGES = [ + 'AdminIsps', + 'AdminIsp', + 'AdminUsers', + 'AdminUser', + 'AdminIps', + 'AdminIp', + 'AdminDomains', + 'AdminDomain', + 'AdminCountries', + 'AdminCountry', + 'AdminBots', + 'AdminBot', + 'AdminResources', + 'AdminResource', + 'AdminLogbook', + 'AdminHome', + 'AdminApi', + 'AdminReviewQueue', + 'AdminRules', + 'AdminSettings', + 'AdminWatchlist', + 'AdminBlacklist', + 'AdminManualCheck', + 'AdminEvents', + ]; + + public const IP_TYPES = [ + 'Blacklisted', + 'Spam list', + 'Localhost', + 'TOR', + 'Starlink', + 'AppleRelay', + 'VPN', + 'Datacenter', + 'Unknown', + 'Residential', + ]; + + public const ALERT_EVENT_TYPES = [ + self::PAGE_DELETE_EVENT_TYPE_ID, + self::PAGE_ERROR_EVENT_TYPE_ID, + self::ACCOUNT_LOGIN_FAIL_EVENT_TYPE_ID, + self::ACCOUNT_EMAIL_CHANGE_EVENT_TYPE_ID, + self::ACCOUNT_PASSWORD_CHANGE_EVENT_TYPE_ID, + ]; + + public const EDITING_EVENT_TYPES = [ + self::PAGE_EDIT_EVENT_TYPE_ID, + self::ACCOUNT_REGISTRATION_EVENT_TYPE_ID, + self::ACCOUNT_EDIT_EVENT_TYPE_ID, + self::FIELD_EDIT_EVENT_TYPE_ID, + ]; + + public const NORMAL_EVENT_TYPES = [ + self::PAGE_VIEW_EVENT_TYPE_ID, + self::PAGE_SEARCH_EVENT_TYPE_ID, + self::ACCOUNT_LOGIN_EVENT_TYPE_ID, + self::ACCOUNT_LOGOUT_EVENT_TYPE_ID, + ]; + + public const FAILED_LOGBOOK_EVENT_TYPES = [ + 'critical_validation_error', + 'critical_error', + ]; + + public const ISSUED_LOGBOOK_EVENT_TYPES = [ + 'validation_error', + ]; + + public const NORMAL_LOGBOOK_EVENT_TYPES = [ + 'success', + ]; + + public const ENTITY_TYPES = [ + 'IP', + 'Email', + 'Phone', + ]; +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Cron.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Cron.php new file mode 100644 index 0000000..4097a43 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Cron.php @@ -0,0 +1,227 @@ + 0, 'max' => 59], // minute + ['min' => 0, 'max' => 23], // hour + ['min' => 1, 'max' => 31], // day of month + ['min' => 1, 'max' => 12], // month + ['min' => 0, 'max' => 6], // day of week (0 = Sunday) + ]; + public const PATTERN = '/^(\*|\d+)(?:-(\d+))?(?:\/(\d+))?$/'; + + protected $f3; + protected array $jobs = []; + protected array $forceRun = []; + protected bool $runForcedOnly = false; + + public function __construct() { + $this->f3 = \Base::instance(); + $this->f3->route('GET /cron', function (): void { + $this->route(); + }); + } + + public static function parseExpression(string $expression): false|array { + $parts = []; + $expressionParts = preg_split('/\s+/', trim($expression), -1, PREG_SPLIT_NO_EMPTY); + + if (count($expressionParts) !== 5) { + return false; + } + + foreach ($expressionParts as $i => $field) { + $values = []; + // handle lists + $fieldParts = explode(',', $field); + + foreach ($fieldParts as $part) { + if (!preg_match(self::PATTERN, $part, $matches)) { + return false; + } + + $start = $matches[1]; + $end = $matches[2] ?? null; + $step = $matches[3] ?? 1; + + // Convert '*' to start and end values + if ($start === '*') { + $start = self::RANGES[$i]['min']; + $end = self::RANGES[$i]['max']; + } else { + $start = (int) $start; + $end = $end !== null ? (int) $end : $start; + } + $step = (int) $step; + + if ($start > $end || $start < self::RANGES[$i]['min'] || $end > self::RANGES[$i]['max'] || $step < 1) { + return false; + } + + $range = range($start, $end, $step); + $values = array_merge($values, $range); + } + + $parts[$i] = array_unique($values); + sort($parts[$i]); + } + + return $parts; + } + + public static function parseTimestamp(\DateTime $time): array { + return [ + (int) $time->format('i'), // minute + (int) $time->format('H'), // hour + (int) $time->format('d'), // day of month + (int) $time->format('m'), // month + (int) $time->format('w'), // day of week + ]; + } + + public function addJob(string $jobName, string $handler, string $expression): void { + if (!preg_match('/^[\w\-]+$/', $jobName)) { + throw new \Exception('Invalid job name.'); + } + + $this->jobs[$jobName] = [$handler, $expression]; + } + + public function run(\DateTime|null $time = null): void { + if (!$time) { + $time = new \DateTime(); + } + + $toRun = $this->getJobsToRun($time); + if (!count($toRun)) { + echo sprintf('No jobs to run at %s%s', $time->format('Y-m-d H:i:s'), PHP_EOL); + exit; + } + + foreach ($toRun as $jobName) { + $this->execute($jobName); + } + } + + private function route(): void { + if (PHP_SAPI !== 'cli') { + $this->f3->error(404); + + return; + } + + $this->f3->set('ONERROR', \Utils\ErrorHandler::getCronErrorHandler()); + + while (ob_get_level()) { + ob_end_flush(); + } + ob_implicit_flush(1); + + $this->readArguments(); + $this->loadCrons(); + $this->validateForcedJobs(); + $this->run(); + } + + private function readArguments(): void { + $argv = $GLOBALS['argv']; + + foreach ($argv as $position => $argument) { + if ($argument === '--force') { + if (array_key_exists($position + 1, $argv)) { + $this->forceRun[] = $argv[$position + 1]; + } else { + echo 'No job specified to force. Ignoring flag.' . PHP_EOL; + } + } elseif ($argument === '--force-only') { + $this->runForcedOnly = true; + } + } + } + + private function loadCrons(): void { + $this->f3->config('config/crons.ini'); + + $crons = (array) $this->f3->get('crons'); + foreach (array_keys($crons) as $jobName) { + if (substr($jobName, 0, 1) !== '#') { + $cron = $crons[$jobName]; + $this->addJob($jobName, $cron[self::HANDLER], $cron[self::EXPRESSION]); + } + } + } + + private function validateForcedJobs(): void { + $notFound = array_diff($this->forceRun, array_keys($this->jobs)); + foreach ($notFound as $flagArgument) { + echo sprintf('Job not found. Ignoring --force %s flag.%s', $flagArgument, PHP_EOL); + } + + $this->forceRun = array_diff($this->forceRun, $notFound); + } + + public function execute(string $jobName): void { + if (!isset($this->jobs[$jobName])) { + throw new \Exception('Job does not exist.'); + } + + $job = $this->jobs[$jobName]; + $handler = $job[self::HANDLER]; + if (is_string($handler)) { + $handler = $this->f3->grab($handler); + } + if (!is_callable($handler)) { + throw new \Exception('Invalid job handler.'); + } + + call_user_func_array($handler, [$this->f3]); + } + + private function isDue(\DateTime $time, string $expression): bool { + $parts = self::parseExpression($expression); + if (!$parts) { + return false; + } + + foreach (self::parseTimestamp($time) as $i => $k) { + if (!in_array($k, $parts[$i])) { + return false; + } + } + + return true; + } + + private function getJobsToRun(\DateTime $time): array { + if ($this->runForcedOnly) { + return $this->forceRun; + } + + $toRun = array_keys($this->jobs); + $toRun = array_filter($toRun, function ($jobName) use ($time) { + return $this->isDue($time, $this->jobs[$jobName][self::EXPRESSION]); + }); + + return array_unique(array_merge($toRun, $this->forceRun)); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/DictManager.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/DictManager.php new file mode 100644 index 0000000..bbde708 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/DictManager.php @@ -0,0 +1,37 @@ +get('LOCALES'); + $language = $f3->get('LANGUAGE'); + + $path = sprintf('%s%s/Additional/%s.php', $locale, $language, $file); + + $isFileExists = file_exists($path); + + if ($isFileExists) { + $values = include $path; + + foreach ($values as $key => $value) { + $f3->set($key, $value); + } + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/ElapsedDate.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/ElapsedDate.php new file mode 100644 index 0000000..ac04b10 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/ElapsedDate.php @@ -0,0 +1,57 @@ + $secs / 31556926 % 12, + ' week' => $secs / 604800 % 52, + ' day' => $secs / 86400 % 7, + ' hour' => $secs / 3600 % 24, + ' minute' => $secs / 60 % 60, + ' second' => $secs % 60, + ]; + + foreach ($bit as $k => $v) { + if ($v > 1) { + $ret[] = $v . $k . 's'; + } + if ($v === 1) { + $ret[] = $v . $k; + } + } + + array_splice($ret, count($ret) - 1, 0, 'and'); + $ret[] = 'ago.'; + + return join(' ', $ret); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/ErrorCodes.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/ErrorCodes.php new file mode 100644 index 0000000..39dbf29 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/ErrorCodes.php @@ -0,0 +1,133 @@ +get('ERROR.trace'); + $errorTraceArray = preg_split('/$\R?^/m', $errorTraceString); + $maximalStringIndex = 0; + $maximalStringLength = 0; + $iters = count($errorTraceArray); + + for ($i = 0; $i < $iters; ++$i) { + $currentStringLength = strlen($errorTraceArray[$i]); + if ($maximalStringLength < $currentStringLength) { + $maximalStringIndex = $i; + $maximalStringLength = $currentStringLength; + } + } + + if ($iters > 1) { + array_splice($errorTraceArray, $maximalStringIndex, 1); + } + + $iters = count($errorTraceArray); + for ($i = 0; $i < $iters; ++$i) { + $errorTraceArray[$i] = strip_tags($errorTraceArray[$i]); + $errorTraceArray[$i] = str_replace(['>', '<'], ['>', '<'], $errorTraceArray[$i]); + } + + $errorCode = $f3->get('ERROR.code'); + $errorMessage = join(', ', ['ERROR_' . $errorCode, $f3->get('ERROR.text')]); + + return [ + 'ip' => $f3->IP, + 'code' => $errorCode, + 'message' => $errorMessage, + 'trace' => join('
', $errorTraceArray), + 'date' => date('l jS \of F Y h:i:s A'), + 'post' => $f3->get('POST'), + 'get' => $f3->get('GET'), + ]; + } + + public static function saveErrorInformation(\Base $f3, array $errorData): void { + \Utils\Logger::log(null, $errorData['message']); + + $errorTraceArray = explode('
', $errorData['trace']); + $printErrorTraceToLog = $f3->get('PRINT_ERROR_TRACE_TO_LOG'); + if ($printErrorTraceToLog) { + $iters = count($errorTraceArray); + + for ($i = 0; $i < $iters; ++$i) { + \Utils\Logger::log(null, $errorTraceArray[$i]); + } + } + + $db = $f3->get('API_DATABASE'); + if ($db) { + $errorData['sql_log'] = $db->log(); + $logModel = new \Models\Log(); + $logModel->add($errorData); + + \Utils\Logger::log('SQL', $errorData['sql_log']); + } + + if ($errorData['code'] === 500) { + $toName = 'Admin'; + $toAddress = \Utils\Variables::getAdminEmail(); + if ($toAddress === null) { + \Utils\Logger::log('Log mail error', 'ADMIN_EMAIL is not set'); + + return; + } + + $subject = $f3->get('error_email_subject'); + $subject = sprintf($subject, $errorData['code']); + + $currentTime = date('d-m-Y H:i:s'); + $errorMessage = $errorData['message']; + $errorTrace = $errorData['trace']; + + $message = $f3->get('error_email_body_template'); + $message = sprintf($message, $currentTime, $errorMessage, $errorTrace); + + \Utils\Mailer::send($toName, $toAddress, $subject, $message); + } + } + + protected static function getAjaxErrorMessage(array $errorData): string|false { + return json_encode( + [ + 'status' => false, + 'code' => $errorData['code'], + 'message' => sprintf('Request finished with code %s', $errorData['code']), + ], + ); + } + + public static function getOnErrorHandler(): callable { + /** + * Custom onError handler: http://stackoverflow.com/questions/19763414/fat-free-framework-f3-custom-404-page-and-others-errors, https://groups.google.com/forum/#!topic/f3-framework/BOIrLs5_aEA + * We can can use $f3->get('ERROR.text'), and decide which template should be displayed. + * + * @param $f3 + */ + return function (\Base $f3): void { + $hive = $f3->hive(); + $isAjax = $hive['AJAX']; + + $errorData = self::getErrorDetails($f3); + self::saveErrorInformation($f3, $errorData); + + if ($errorData['code'] === 403 && $isAjax) { + echo self::getAjaxErrorMessage($errorData); + + return; + } + + if ($errorData['code'] === 403 && !$isAjax) { + $f3->reroute('/logout'); + + return; + } + + // Add handling 404 error + if ($errorData['code'] === 404) { + } + + if ($isAjax) { + echo self::getAjaxErrorMessage($errorData); + + return; + } + + $response = new \Views\Frontend(); + $pageController = new \Controllers\Pages\Error(); + + $errorData['message'] = 'ERROR_' . $errorData['code']; + unset($errorData['trace']); + $pageParams = $pageController->getPageParams($errorData); + + $response->data = $pageParams; + echo $response->render(); + }; + } + + public static function getCronErrorHandler(): callable { + return function (\Base $f3): void { + $errorData = self::getErrorDetails($f3); + self::saveErrorInformation($f3, $errorData); + }; + } + + public static function exceptionErrorHandler(int $severity, string $message, string $file, int $line): bool { + if (!(error_reporting() & $severity)) { + return false; + } + throw new \ErrorException($message, 0, $severity, $file, $line); + + return true; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/HotDebug.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/HotDebug.php new file mode 100644 index 0000000..2e006af --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/HotDebug.php @@ -0,0 +1,59 @@ +TABLE{border-collapse: collapse;} TH {text-align: right; background-color: lightgrey;} TH, TD{ border: 1px solid #000; padding: 3px;} + + + + + + + + + + + + + + +
Debug
File:%s
Line:%s
Message:
%s
'; + + $bt = debug_backtrace(); + + $caller = $bt[2]; //bt[0] - is \use \Traits\\Debug + + $isVariableRecursive = self::isRecursive($value); + $data = $isVariableRecursive ? var_dump($value) : var_export($value, true); + + $html = sprintf($html, $caller['file'], $caller['line'], $data); + + echo $html; + + if ($shouldExit) { + exit; + } + } + + //https://stackoverflow.com/questions/17181375/test-if-variable-contains-circular-references + private static function isRecursive($array): bool { + $dump = print_r($array, true); + return strpos($dump, '*RECURSION*') !== false; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Logger.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Logger.php new file mode 100644 index 0000000..516a98d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Logger.php @@ -0,0 +1,43 @@ +get('LOG_FILE'); + $logger = new \Log($logFile); + + if (is_array($message)) { + $message = var_export($message, true); + } + + if ($title) { + $message = sprintf('%s:%s%s', $title, PHP_EOL, $message); + } + + $logger->write($message); + } + + public static function logSql(string $title, string $message): void { + $f3 = \Base::instance(); + $logFile = $f3->get('LOG_SQL_FILE'); + $logDelimiter = $f3->get('LOG_DELIMITER'); + + $logger = new \Log($logFile); + $logger->write($title . ':' . PHP_EOL . $message . $logDelimiter); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Mailer.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Mailer.php new file mode 100644 index 0000000..baeee1c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Mailer.php @@ -0,0 +1,134 @@ +get('SEND_EMAIL'); + if (!$canSendEmail) { + return [ + 'success' => true, + 'message' => 'Email will not be sent in development mode', + ]; + } + + $toName = $toName ?? ''; + $data = null; + if (\Utils\Variables::getMailPassword()) { + $data = self::sendByMailgun($toAddress, $toName, $subject, $message); + } + + if ($data === null || !$data['success']) { + $data = self::sendByNativeMail($toAddress, $toName, $subject, $message); + } + + return $data; + } + + private static function sendByMailgun(string $toAddress, string $toName, string $subject, string $message): array { + $f3 = \Base::instance(); + + $fromName = \Utils\Constants::get('MAIL_FROM_NAME'); + $smtpDebug = $f3->get('SMTP_DEBUG'); + $fromAddress = \Utils\Variables::getMailLogin(); + $mailLogin = \Utils\Variables::getMailLogin(); + $mailPassword = \Utils\Variables::getMailPassword(); + + if ($fromAddress === null) { + return [ + 'success' => false, + 'message' => 'Admin email is not set.', + ]; + } + + $mail = new \PHPMailer\PHPMailer\PHPMailer(true); + + try { + //Server settings + $mail->SMTPDebug = $smtpDebug; //Enable verbose debug output + $mail->isSMTP(); //Send using SMTP + $mail->Host = \Utils\Constants::get('MAIL_HOST'); //Set the SMTP server to send through + $mail->SMTPAuth = true; //Enable SMTP authentication + $mail->Username = $mailLogin; //SMTP username + $mail->Password = $mailPassword; //SMTP password + $mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; //Enable implicit TLS encryption + $mail->Port = 587; //TCP port to connect to; use 587 if you have set `SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS` + + //Recipients + $mail->setFrom($fromAddress, $fromName); + $mail->addAddress($toAddress, $toName); //Add a recipient + $mail->addReplyTo($fromAddress, $fromName); + + //Content + $mail->isHTML(false); //Set email format to HTML + $mail->Subject = $subject; + $mail->Body = $message; + + $mail->send(); + + $success = true; + $message = 'Message has been sent'; + } catch (\Exception $e) { + $success = false; + $message = "Message could not be sent. Mailer Error: {$mail->ErrorInfo}"; + } + + return [ + 'success' => $success, + 'message' => $message, + ]; + } + + private static function sendByNativeMail(string $toAddress, string $toName, string $subject, string $message): array { + $sendMailPath = \Utils\Constants::get('MAIL_SEND_BIN'); + + if (!file_exists($sendMailPath) || !is_executable($sendMailPath)) { + return [ + 'success' => false, + 'message' => 'Sendmail is not installed. Cannot send email.', + ]; + } + + $fromName = \Utils\Constants::get('MAIL_FROM_NAME'); + $fromAddress = \Utils\Variables::getMailLogin(); + + if ($fromAddress === null) { + return [ + 'success' => false, + 'message' => 'Admin email is not set.', + ]; + } + + $headers = [ + 'MIME-Version: 1.0', + 'Content-type: text/html; charset=utf-8', + sprintf('From: %s <%s>', $fromName, $fromAddress), + sprintf('Reply-To: %s', $fromAddress), + sprintf('X-Mailer: PHP/%s', phpversion()), + ]; + + $headers = implode("\r\n", $headers); + + $success = mail($toAddress, $subject, $message, $headers); + $message = $success ? 'Message sent' : 'Error occurred'; + + return [ + 'success' => $success, + 'message' => $message, + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Rules.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Rules.php new file mode 100644 index 0000000..17756fe --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Rules.php @@ -0,0 +1,58 @@ +diff($deviceLastseen); + + return abs($interval->days * 24 * 60 + $interval->h * 60 + $interval->i) < \Utils\Constants::get('RULE_NEW_DEVICE_MAX_AGE_IN_MINUTES'); + } + + public static function countryIsNewByIpId(array $params, int $ipId): bool { + $filtered = array_filter($params['eip_country_id'], function ($value) { + return $value !== null; + }); + $countryCounts = array_count_values($filtered); + $ipIdx = array_search($ipId, $params['eip_ip_id']); + $eventIpCountryId = $params['eip_country_id'][$ipIdx]; + $count = $countryCounts[$eventIpCountryId] ?? 0; + + return $count === 1; + } + + public static function cidrIsNewByIpId(array $params, int $ipId): bool { + $filtered = array_filter($params['eip_cidr'], function ($value) { + return $value !== null; + }); + $cidrCounts = array_count_values($filtered); + $ipIdx = array_search($ipId, $params['eip_ip_id']); + $eventIpCidr = $params['eip_cidr'][$ipIdx]; + $count = $cidrCounts[$eventIpCidr] ?? 0; + + return $count === 1; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/RulesClasses.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/RulesClasses.php new file mode 100644 index 0000000..cceb546 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/RulesClasses.php @@ -0,0 +1,184 @@ + 'positive', + 10 => 'medium', + 20 => 'high', + 70 => 'extreme', + 0 => 'none', + ]; + + private const RULES_TYPES = [ + 'A' => 'Account takeover', + 'B' => 'Behaviour', + 'C' => 'Country', + 'D' => 'Device', + 'E' => 'Email', + 'I' => 'IP', + 'R' => 'Reuse', + 'P' => 'Phone', + 'X' => 'Extra', + ]; + + private static string $coreRulesNamespace = '\\Controllers\\Admin\\Rules\\Set'; + private static string $assetsRulesNamespace = '\\ExtendedRules'; + + public static function getRuleClass(?int $value): string { + return self::RULES_WEIGHT[$value ?? 0] ?? 'none'; + } + + public static function getRuleTypeByUid(string $uid): string { + return self::RULES_TYPES[$uid[0]] ?? $uid[0]; + } + + public static function getUserScoreClass(?int $score): array { + $cls = 'empty'; + if ($score === null) { + return ['−', $cls]; + } + + if ($score >= \Utils\Constants::get('USER_LOW_SCORE_INF') && $score < \Utils\Constants::get('USER_LOW_SCORE_SUP')) { + $cls = 'low'; + } + + if ($score >= \Utils\Constants::get('USER_MEDIUM_SCORE_INF') && $score < \Utils\Constants::get('USER_MEDIUM_SCORE_SUP')) { + $cls = 'medium'; + } + + if ($score >= \Utils\Constants::get('USER_HIGH_SCORE_INF')) { + $cls = 'high'; + } + + return [$score, $cls]; + } + + private static function getCoreRulesDir(): string { + return dirname(__DIR__, 1) . '/Controllers/Admin/Rules/Set'; + } + + private static function getAssetsRulesDir(): string { + return dirname(__DIR__, 2) . '/assets/rules'; + } + + public static function getAllRulesObjects(?\Ruler\RuleBuilder $rb): array { + $local = self::getRulesClasses(false); + $core = self::getRulesClasses(true); + + $total = $local['imported'] + $core['imported']; + + foreach ($total as $uid => $cls) { + $total[$uid] = new $cls($rb, []); + } + + return $total; + } + + public static function getSingleRuleObject(string $uid, ?\Ruler\RuleBuilder $rb): ?\Controllers\Admin\Rules\Set\BaseRule { + $obj = null; + $cores = [false, true]; + + foreach ($cores as $core) { + $dir = $core ? self::getCoreRulesDir() : self::getAssetsRulesDir(); + $namespace = $core ? self::$coreRulesNamespace : self::$assetsRulesNamespace; + + $filename = $dir . '/' . $uid . '.php'; + $cls = $namespace . '\\' . $uid; + + try { + self::validateRuleClass($uid, $filename, $cls, $core); + $obj = new $cls($rb, []); + break; + } catch (\Throwable $e) { + error_log('Rule validation failed at file ' . $filename); + } + } + + return $obj; + } + + public static function getRulesClasses(bool $core): array { + $dir = $core ? self::getCoreRulesDir() : self::getAssetsRulesDir(); + $namespace = $core ? self::$coreRulesNamespace : self::$assetsRulesNamespace; + + $out = []; + $failed = []; + $iter = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir)); + $namePattern = $core ? '/^[A-WY-Z][0-9]{2,3}$/' : '/^[A-Z][0-9]{2,3}$/'; + + foreach ($iter as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + $name = null; + try { + $name = basename($file->getFilename(), '.php'); + + if (!preg_match($namePattern, $name)) { + continue; + } + + $filePath = $file->getRealPath(); + + $cls = $namespace . '\\' . $name; + + self::validateRuleClass($name, $filePath, $cls, $core); + + $out[$name] = $cls; + } catch (\Throwable $e) { + $failed[] = $name; + error_log('Fail on require_once: ' . $e->getMessage()); + } + } + } + + return ['imported' => $out, 'failed' => $failed]; + } + + private static function validateRuleClass(string $uid, string $filename, string $classname, bool $core): string { + $reflection = self::validateObject($filename, $classname); + + if (!$core && !str_starts_with($uid, 'X')) { + $parentClassName = $reflection->getParentClass()?->getName(); + if ('\\' . $parentClassName !== self::$coreRulesNamespace . '\\' . $uid) { + throw new \LogicException("Class {$classname} in assets has invalid parent class {$parentClassName}"); + } + } + + return $classname; + } + + private static function validateObject(string $filename, string $classname): \ReflectionClass { + if (!file_exists($filename)) { + throw new \LogicException("File {$filename} doesn't exist."); + } + + require_once $filename; + + if (!class_exists($classname, false)) { + throw new \LogicException("Class {$classname} not found after including {$filename}"); + } + + $reflection = new \ReflectionClass($classname); + $reflectionFileName = $reflection->getFileName(); + + if (realpath($reflectionFileName) !== realpath($filename)) { + throw new \LogicException("Class {$classname} is defined in {$reflectionFileName}, not in {$filename}"); + } + + return $reflection; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/SystemMessages.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/SystemMessages.php new file mode 100644 index 0000000..c6b8453 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/SystemMessages.php @@ -0,0 +1,158 @@ +get($code); + + $time = gmdate('Y-m-d H:i:s'); + \Utils\TimeZones::localizeForActiveOperator($time); + + $m['text'] = $text; + $m['created_at'] = $time; + $m['class'] = 'is-warning'; + } + + $msg[] = $m; + } + } + + return $msg; + } + + private static function getNoEventsMessage(int $apiKey): ?array { + $f3 = \Base::instance(); + $currentOperator = $f3->get('CURRENT_USER'); + + $takeFromCache = self::canTakeLastEventTimeFromCache($currentOperator); + + $lastEventTime = $currentOperator->last_event_time; + + if (!$takeFromCache) { + $model = new \Models\Event(); + $event = $model->getLastEvent($apiKey); + + if (!count($event)) { + return ['id' => \Utils\ErrorCodes::THERE_ARE_NO_EVENTS_YET]; + } + + $lastEventTime = $event[0]['time']; + + $data = [ + 'id' => $currentOperator->id, + 'last_event_time' => $lastEventTime, + ]; + + $model = new \Models\Operator(); + $model->updateLastEventTime($data); + } + + $currentTime = gmdate('Y-m-d H:i:s'); + + $dt1 = new \DateTime($currentTime); + $dt2 = new \DateTime($lastEventTime); + $diff = $dt1->getTimestamp() - $dt2->getTimestamp(); + + $noEventsThreshold = $f3->get('NO_EVENTS_TIME'); + $noEventsLast24Hours = $diff > $noEventsThreshold; + + if ($noEventsLast24Hours) { + return ['id' => \Utils\ErrorCodes::THERE_ARE_NO_EVENTS_LAST_24_HOURS]; + } + + return null; + } + + private static function getOveruseMessage(int $apiKey): ?array { + $model = new \Models\ApiKeys(); + $model->getKeyById($apiKey); + + if ($model->last_call_reached === false) { + return ['id' => \Utils\ErrorCodes::ENRICHMENT_API_KEY_OVERUSE]; + } + + return null; + } + + private static function getInactiveCronMessage(int $apiKey): ?array { + $cursorModel = new \Models\Queue\QueueNewEventsCursor(); + $eventModel = new \Models\Event(); + + if ($cursorModel->getCursor() === 0 && count($eventModel->getLastEvent($apiKey))) { + return ['id' => \Utils\ErrorCodes::CRON_JOB_MAY_BE_OFF]; + } + + return null; + } + + //TODO: think about custom function which receives three params: date1, date2 and diff. + private static function canTakeLastEventTimeFromCache(\Models\Operator $currentOperator): bool { + $f3 = \Base::instance(); + + $diff = PHP_INT_MAX; + $currentTime = gmdate('Y-m-d H:i:s'); + $updatedAt = $currentOperator->last_event_time; + + if ($updatedAt) { + $dt1 = new \DateTime($currentTime); + $dt2 = new \DateTime($updatedAt); + + $diff = $dt1->getTimestamp() - $dt2->getTimestamp(); + } + + $cacheTime = $f3->get('LAST_EVENT_CACHE_TIME'); + + return $cacheTime > $diff; + } + + private static function getCustomErrorMessage(): ?array { + $message = null; + $model = new \Models\Message(); + + $data = $model->getMessage(); + + if ($data) { + $message = [ + 'id' => \Utils\ErrorCodes::CUSTOM_ERROR_FROM_DSHB_MESSAGES, + 'text' => $data->text, + 'created_at' => $data->created_at, + ]; + } + + return $message; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/TimeZones.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/TimeZones.php new file mode 100644 index 0000000..2c0320e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/TimeZones.php @@ -0,0 +1,137 @@ +setTimezone($to); + + return $new->format($format); + } + + public static function localizeForActiveOperator(string &$time, bool $useMilliseconds = false): void { + $f3 = \Base::instance(); + $currentOperator = $f3->get('CURRENT_USER'); + $operatorTimeZone = new \DateTimeZone($currentOperator->timezone ?? self::DEFAULT); + $utc = new \DateTimeZone(self::DEFAULT); + $time = self::localizeTimeStamp($time, $utc, $operatorTimeZone, $useMilliseconds); + } + + public static function localizeTimestampsForActiveOperator(array $keys, array &$data): void { + $f3 = \Base::instance(); + $currentOperator = $f3->get('CURRENT_USER'); + $operatorTimeZone = new \DateTimeZone($currentOperator->timezone ?? self::DEFAULT); + $utc = new \DateTimeZone(self::DEFAULT); + + $ts = array_intersect_key($data, array_flip($keys)); + + foreach ($ts as $key => $t) { + if ($t !== null) { + $data[$key] = self::localizeTimeStamp($t, $utc, $operatorTimeZone, false); + } + } + } + + public static function localizeUnixTimestamps(array &$ts): void { + $f3 = \Base::instance(); + $currentOperator = $f3->get('CURRENT_USER'); + $operatorTimeZone = new \DateTimeZone($currentOperator->timezone ?? self::DEFAULT); + $utcTime = new \DateTime('now', new \DateTimeZone('UTC')); + $offsetInSeconds = $operatorTimeZone->getOffset($utcTime); + + foreach (array_keys($ts) as $idx) { + $ts[$idx] += $offsetInSeconds; + } + } + + public static function getCurrentOperatorOffset(): int { + $f3 = \Base::instance(); + $currentOperator = $f3->get('CURRENT_USER'); + $operatorTimeZone = new \DateTimeZone($currentOperator->timezone ?? self::DEFAULT); + $utcTime = new \DateTime('now', new \DateTimeZone('UTC')); + + return $operatorTimeZone->getOffset($utcTime); + } + + public static function getLastNDaysRange(int $days = 1, int $offset = 0): array { + $now = time(); + $daySeconds = 24 * 60 * 60; + + $date = new \DateTime(); + $date->setTimestamp($now - ($daySeconds * $days) - (($now + $offset) % $daySeconds)); + + $date->setTime(0, 0, 0); + + return [ + 'endDate' => date(self::FORMAT, $now), + 'startDate' => date(self::FORMAT, $date->getTimestamp() - $offset), + 'offset' => $offset, + ]; + } + + public static function getCurDayRange(int $offset = 0): array { + $now = time(); + + $date = new \DateTime(); + $date->setTimestamp($now + $offset); + + $date->setTime(0, 0, 0); + + return [ + 'endDate' => date(self::FORMAT, $now), + 'startDate' => date(self::FORMAT, $date->getTimestamp() - $offset), + 'offset' => $offset, + ]; + } + + public static function getCurWeekRange(int $offset = 0): array { + $now = time(); + + $date = new \DateTime(); + $date->setTimestamp($now + $offset); + + $date->setTime(0, 0, 0); + + $dow = (int) $date->format('N'); + + return [ + 'endDate' => date(self::FORMAT, $now), + 'startDate' => date(self::FORMAT, $date->getTimestamp() - $offset - (($dow - 1) * 24 * 60 * 60)), + 'offset' => $offset, + ]; + } + + public static function timeZonesList(): array { + $timezones = (\Base::instance())->get('timezones'); + $utcTime = new \DateTime('now', new \DateTimeZone('UTC')); + + foreach ($timezones as $key => $value) { + $offset = (new \DateTimeZone($key))->getOffset($utcTime); + $part = ($offset < 0) ? '-' . date('H:i', -$offset) : '+' . date('H:i', $offset); + $timezones[$key] = explode('(', $value)[0] . '(UTC' . $part . ')'; + } + + return $timezones; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Updates.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Updates.php new file mode 100644 index 0000000..95f3558 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Updates.php @@ -0,0 +1,31 @@ +checkDb('core', self::UPDATES_LIST); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Variables.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Variables.php new file mode 100644 index 0000000..992bd56 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/Variables.php @@ -0,0 +1,103 @@ +get('DATABASE_URL'); + } + + public static function getConfigFile(): string { + return getenv('CONFIG_FILE') ?: 'local/config.local.ini'; + } + + public static function getSite(): string { + return getenv('SITE') ?: self::getF3()->get('SITE'); + } + + public static function getAdminEmail(): ?string { + return getenv('ADMIN_EMAIL') ?: self::getF3()->get('ADMIN_EMAIL'); + } + + public static function getMailLogin(): ?string { + return getenv('MAIL_LOGIN') ?: self::getF3()->get('MAIL_LOGIN'); + } + + public static function getMailPassword(): ?string { + return getenv('MAIL_PASS') ?: self::getF3()->get('MAIL_PASS'); + } + + public static function getEnrichtmentApi(): string { + return getenv('ENRICHMENT_API') ?: self::getF3()->get('ENRICHMENT_API'); + } + + public static function getPepper(): string { + return getenv('PEPPER') ?: self::getF3()->get('PEPPER'); + } + + public static function getLogbookLimit(): int { + $value = getenv('LOGBOOK_LIMIT') ?: self::getF3()->get('LOGBOOK_LIMIT') ?: \Utils\Constants::get('LOGBOOK_LIMIT'); + + return is_int($value) ? $value : (ctype_digit($value) ? intval($value) : \Utils\Constants::get('LOGBOOK_LIMIT')); + } + + public static function getForgotPasswordAllowed(): bool { + $variable = getenv('ALLOW_FORGOT_PASSWORD') ?: self::getF3()->get('ALLOW_FORGOT_PASSWORD') ?? 'false'; + + return filter_var($variable, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false; + } + + public static function getEmailPhoneAllowed(): bool { + $variable = getenv('ALLOW_EMAIL_PHONE') ?: self::getF3()->get('ALLOW_EMAIL_PHONE') ?? 'false'; + + return filter_var($variable, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false; + } + + public static function getForceHttps(): bool { + // set 'false' string if FORCE_HTTPS wasn't set due to filter_var() issues + $variable = getenv('FORCE_HTTPS') ?: self::getF3()->get('FORCE_HTTPS') ?? 'false'; + + return filter_var($variable, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? true; + } + + public static function getSiteWithProtocol(): string { + return (self::getForceHttps() ? 'https://' : 'http://') . self::getSite(); + } + + public static function getAccountOperationQueueBatchSize(): int { + return getenv('ACCOUNT_OPERATION_QUEUE_BATCH_SIZE') ? intval(getenv('ACCOUNT_OPERATION_QUEUE_BATCH_SIZE')) : \Utils\Constants::get('ACCOUNT_OPERATION_QUEUE_BATCH_SIZE'); + } + + public static function getNewEventsBatchSize(): int { + return getenv('NEW_EVENTS_BATCH_SIZE') ? intval(getenv('NEW_EVENTS_BATCH_SIZE')) : \Utils\Constants::get('NEW_EVENTS_BATCH_SIZE'); + } + + public static function getRuleUsersBatchSize(): int { + return getenv('RULE_USERS_BATCH_SIZE') ? intval(getenv('RULE_USERS_BATCH_SIZE')) : \Utils\Constants::get('RULE_USERS_BATCH_SIZE'); + } + + public static function completedConfig(): bool { + return + (getenv('SITE') || self::getF3()->get('SITE')) && + (getenv('PEPPER') || self::getF3()->get('PEPPER')) && + (getenv('ENRICHMENT_API') || self::getF3()->get('ENRICHMENT_API')) && + (getenv('DATABASE_URL') || self::getF3()->get('DATABASE_URL')); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/VersionControl.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/VersionControl.php new file mode 100644 index 0000000..1b6191a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/VersionControl.php @@ -0,0 +1,30 @@ +', + '?page=', + 'C:', + 'CAT%20', + 'CdRom0', + 'GLOBALROOT', + 'Inetpub', + 'SELECT%20', + 'alert(', + 'apache', + 'autoload', + 'backdoor', + 'backup', + 'bash_history', + 'c:', + 'cat%20', + 'cgi-bin', + 'cjfuns', + 'cmd=', + 'cmdshell', + 'composer', + 'composer.php', + 'core-plugin', + 'curl', + 'diskVolume', + 'document.cookie', + 'eval(', + 'eval-stdin.php', + 'ftp', + 'gaocc', + 'get.asp', + 'get.cgi', + 'get.php', + 'httpd', + 'id_rsa', + 'include.php', + 'indexes', + 'inetpub', + 'install.php', + 'javascript:', + 'localhost', + 'modext', + 'netcat', + 'nginx', + 'onerror=', + 'onload=', + 'passwd', + 'password.txt', + 'passwords.txt', + 'phpmyadmin', + 'plugins', + 'select%20', + 'sftp.json', + 'sh_history', + 'shadow', + 'ssh', + 'system32', + 'uname', + 'url:file', + 'url:http', + 'vsftpd', + 'web.config', + 'wget', + 'win.ini', + 'wp', + 'wp-config', + 'wwwroot', + ]; +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/WordsLists/UserAgent.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/WordsLists/UserAgent.php new file mode 100644 index 0000000..fe21cf9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Utils/WordsLists/UserAgent.php @@ -0,0 +1,55 @@ +f3 = $f3; + } + + public function render(): string|false|null { + if (!is_array($this->data)) { + $this->data = []; + } + + return null; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Views/Frontend.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Views/Frontend.php new file mode 100644 index 0000000..5630bd4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Views/Frontend.php @@ -0,0 +1,36 @@ +data) { + $this->f3->mset($this->data); + } + + // Use anti-CSRF token in templates. + $this->f3->CSRF = $this->f3->get('SESSION.csrf'); + + $tpl = $this->f3->get('TPL') ?? null; + if ($tpl) { + $tpl::registerExtends(); + } + + return \Template::instance()->render('templates/layout.html'); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Views/Json.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Views/Json.php new file mode 100644 index 0000000..a704cab --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Views/Json.php @@ -0,0 +1,26 @@ +data); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Views/Xml.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Views/Xml.php new file mode 100644 index 0000000..ad77b7e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Views/Xml.php @@ -0,0 +1,24 @@ +data; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Views/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Views/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/app/Views/index.php @@ -0,0 +1,3 @@ +', + '?page=', + 'C:', + 'CAT%20', + 'CdRom0', + 'GLOBALROOT', + 'Inetpub', + 'SELECT%20', + 'alert(', + 'apache', + 'autoload', + 'backdoor', + 'backup', + 'bash_history', + 'c:', + 'cat%20', + 'cgi-bin', + 'cjfuns', + 'cmd=', + 'cmdshell', + 'composer', + 'composer.php', + 'core-plugin', + 'curl', + 'diskVolume', + 'document.cookie', + 'eval(', + 'eval-stdin.php', + 'ftp', + 'gaocc', + 'get.asp', + 'get.cgi', + 'get.php', + 'httpd', + 'id_rsa', + 'include.php', + 'indexes', + 'inetpub', + 'install.php', + 'javascript:', + 'localhost', + 'modext', + 'netcat', + 'nginx', + 'onerror=', + 'onload=', + 'passwd', + 'password.txt', + 'passwords.txt', + 'phpmyadmin', + 'plugins', + 'select%20', + 'sftp.json', + 'sh_history', + 'shadow', + 'ssh', + 'system32', + 'uname', + 'url:file', + 'url:http', + 'vsftpd', + 'web.config', + 'wget', + 'win.ini', + 'wp', + 'wp-config', + 'wwwroot', +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/assets/lists/user-agent.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/assets/lists/user-agent.php new file mode 100644 index 0000000..553fb29 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/assets/lists/user-agent.php @@ -0,0 +1,36 @@ +=8.0", + "ext-mbstring": "*", + "bcosca/fatfree-core": "3.9.1", + "phpmailer/phpmailer": "6.9.3", + "ruler/ruler": "0.4.0", + "matomo/device-detector": "6.4.7" + }, + "extra": { + "paas": { + "document-root": "/", + "index-document": "index.php", + "nginx-includes": ["nginx.conf"] + } + }, + "require-dev": { + "squizlabs/php_codesniffer": "3.10.3", + "phpstan/phpstan": "2.1.22" + }, + "scripts": { + "compile": [ + "chmod 755 tmp", + "chmod 740 config", + "chmod 755 assets", + "chmod 755 assets/rules", + "chmod 755 assets/logs", + "chmod 755 assets/lists", + "chmod 640 composer.json", + "chmod 640 composer.lock", + "chmod -R 750 config" + ] + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/composer.lock b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/composer.lock new file mode 100644 index 0000000..4728919 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/composer.lock @@ -0,0 +1,463 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "f7d566341a12b85af399060b56600688", + "packages": [ + { + "name": "bcosca/fatfree-core", + "version": "3.9.1", + "source": { + "type": "git", + "url": "https://github.com/f3-factory/fatfree-core.git", + "reference": "15167bf8377c262a5354e2c419c383aaa4d41280" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/f3-factory/fatfree-core/zipball/15167bf8377c262a5354e2c419c383aaa4d41280", + "reference": "15167bf8377c262a5354e2c419c383aaa4d41280", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "autoload": { + "classmap": [ + "." + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "description": "A powerful yet easy-to-use PHP micro-framework designed to help you build dynamic and robust Web applications - fast!", + "homepage": "http://fatfreeframework.com/", + "support": { + "issues": "https://github.com/f3-factory/fatfree-core/issues", + "source": "https://github.com/f3-factory/fatfree-core/tree/3.9.1" + }, + "funding": [ + { + "url": "https://www.buymeacoffee.com/ikkez", + "type": "buy_me_a_coffee" + }, + { + "url": "https://github.com/ikkez", + "type": "github" + } + ], + "time": "2025-08-09T10:27:39+00:00" + }, + { + "name": "matomo/device-detector", + "version": "6.4.7", + "source": { + "type": "git", + "url": "https://github.com/matomo-org/device-detector.git", + "reference": "e53eed31bb1530851feebe52bd64c3451da19e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/e53eed31bb1530851feebe52bd64c3451da19e77", + "reference": "e53eed31bb1530851feebe52bd64c3451da19e77", + "shasum": "" + }, + "require": { + "mustangostang/spyc": "*", + "php": "^7.2|^8.0" + }, + "replace": { + "piwik/device-detector": "self.version" + }, + "require-dev": { + "matthiasmullie/scrapbook": "^1.4.7", + "mayflower/mo4-coding-standard": "^v9.0.0", + "phpstan/phpstan": "^1.10.44", + "phpunit/phpunit": "^8.5.8", + "psr/cache": "^1.0.1", + "psr/simple-cache": "^1.0.1", + "slevomat/coding-standard": "<8.16.0", + "symfony/yaml": "^5.1.7" + }, + "suggest": { + "doctrine/cache": "Can directly be used for caching purpose", + "ext-yaml": "Necessary for using the Pecl YAML parser" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeviceDetector\\": "" + }, + "exclude-from-classmap": [ + "Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "The Matomo Team", + "email": "hello@matomo.org", + "homepage": "https://matomo.org/team/" + } + ], + "description": "The Universal Device Detection library, that parses User Agents and detects devices (desktop, tablet, mobile, tv, cars, console, etc.), clients (browsers, media players, mobile apps, feed readers, libraries, etc), operating systems, devices, brands and models.", + "homepage": "https://matomo.org", + "keywords": [ + "devicedetection", + "parser", + "useragent" + ], + "support": { + "forum": "https://forum.matomo.org/", + "issues": "https://github.com/matomo-org/device-detector/issues", + "source": "https://github.com/matomo-org/matomo", + "wiki": "https://dev.matomo.org/" + }, + "time": "2025-08-20T17:20:16+00:00" + }, + { + "name": "mustangostang/spyc", + "version": "0.6.3", + "source": { + "type": "git", + "url": "https://github.com/mustangostang/spyc.git", + "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mustangostang/spyc/zipball/4627c838b16550b666d15aeae1e5289dd5b77da0", + "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "4.3.*@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.5.x-dev" + } + }, + "autoload": { + "files": [ + "Spyc.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "mustangostang", + "email": "vlad.andersen@gmail.com" + } + ], + "description": "A simple YAML loader/dumper class for PHP", + "homepage": "https://github.com/mustangostang/spyc/", + "keywords": [ + "spyc", + "yaml", + "yml" + ], + "support": { + "issues": "https://github.com/mustangostang/spyc/issues", + "source": "https://github.com/mustangostang/spyc/tree/0.6.3" + }, + "time": "2019-09-10T13:16:29+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.9.3", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/2f5c94fe7493efc213f643c23b1b1c249d40f47e", + "reference": "2f5c94fe7493efc213f643c23b1b1c249d40f47e", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.7.2", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.3" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2024-11-24T18:04:13+00:00" + }, + { + "name": "ruler/ruler", + "version": "v0.4.0", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/Ruler.git", + "reference": "fd3f0d010fc4baa034cd3358f10d8f7eca938a32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/Ruler/zipball/fd3f0d010fc4baa034cd3358f10d8f7eca938a32", + "reference": "fd3f0d010fc4baa034cd3358f10d8f7eca938a32", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.12 | ^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ruler\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com/" + } + ], + "description": "A simple stateless production rules engine for modern PHP.", + "homepage": "https://github.com/bobthecow/Ruler", + "keywords": [ + "engine", + "rules" + ], + "support": { + "issues": "https://github.com/bobthecow/Ruler/issues", + "source": "https://github.com/bobthecow/Ruler/tree/v0.4.0" + }, + "time": "2022-06-27T14:01:32+00:00" + } + ], + "packages-dev": [ + { + "name": "phpstan/phpstan", + "version": "2.1.22", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/41600c8379eb5aee63e9413fe9e97273e25d57e4", + "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-08-04T19:17:37+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.10.3", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "62d32998e820bddc40f99f8251958aed187a5c9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/62d32998e820bddc40f99f8251958aed187a5c9c", + "reference": "62d32998e820bddc40f99f8251958aed187a5c9c", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-09-18T10:38:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.0", + "ext-mbstring": "*" + }, + "platform-dev": {}, + "platform-overrides": { + "php": "8" + }, + "plugin-api-version": "2.6.0" +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/config/apiEndpoints.ini b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/config/apiEndpoints.ini new file mode 100644 index 0000000..678a423 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/config/apiEndpoints.ini @@ -0,0 +1,6 @@ +[routes] + +; POST request to check if userName is blacklisted +; params {"value": "userNameToMatch"} +POST /api/v1/blacklist/search = \Controllers\Api\Blacklist->search + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/config/config.ini b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/config/config.ini new file mode 100644 index 0000000..4e67aec --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/config/config.ini @@ -0,0 +1,46 @@ +[globals] + +;Common constants +DEBUG = 0 +UI = ui/ +AUTOLOAD = app/ +SITE = http://localhost +PACKAGE = system +START_DATE = "1969-01-01" +USER_AGENT = tirreno-console + +;DB +SALT = "^*(!)$)" +KEEP_SESSION_IN_DB=1 +MIN_PASSWORD_LENGTH=8 +DEFAULT_API_KEY_NAME=system +DEFAULT_API_KEY_QUOTE=100 +NUMBER_OF_TOP_USERS=10 +RENEW_PASSWORD_LINK_TIME=86400 +REVIEWED_QUEUE_CNT_CACHE_TIME=600 +LAST_EVENT_CACHE_TIME=600 +NO_EVENTS_TIME = 86400 + +;Passwords +PEPPER = BBlcw0shYHXXCBIIXxaiBySgn3GW0Uzj + +;Logs +LOGS = assets/logs/ +LOG_FILE = error.log +LOG_SQL_FILE = sql.log +PRINT_ERROR_TRACE_TO_LOG = 1 +PRINT_SQL_LOG_AFTER_EACH_SCRIPT_CALL = 0 +LOG_DELIMITER = "========================" + +;Emails +SEND_EMAIL=1 +SMTP_DEBUG=0 + +;Enrichment +ENRICHMENT_API = https://api.tirreno.com + +;Custom tuning +FORCE_HTTPS = false +ALLOW_FORGOT_PASSWORD = false +ALLOW_EMAIL_PHONE = false +LOGBOOK_LIMIT = 1000 diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/config/crons.ini b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/config/crons.ini new file mode 100644 index 0000000..b732a80 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/config/crons.ini @@ -0,0 +1,11 @@ +[crons] +queuesClearer = Crons\QueuesClearer->clearQueues, 0-10 0 * * 2 +notificationsHandler = Crons\NotificationsHandler->prepareNotifications, * * * * * +retentionPolicyViolations = Crons\RetentionPolicyViolations->gatherViolations, 0-10 0 * * * +deletionQueueHandler = Crons\DeletionQueueHandler->processQueue, * * * * * +batchedNewEvents = Crons\BatchedNewEvents->gatherNewEventsBatch, * * * * * +totals = Crons\Totals->calculateTotals, * * * * * +riskScoreQueueHandler = Crons\RiskScoreQueueHandler->processQueue, * * * * * +blacklistQueueHandler = Crons\BlacklistQueueHandler->processQueue, * * * * * +logbookRotation = Crons\LogbookRotation->rotateRequests, "0-10 * * * *" +enrichmentQueueHandler = Crons\EnrichmentQueueHandler->processQueue, * * * * * diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/config/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/config/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/config/index.php @@ -0,0 +1,3 @@ +saveRule +POST /admin/removeBlacklisted [ajax] = \Controllers\Admin\Data\Navigation->removeFromBlacklist +POST /admin/removeWatchlisted [ajax] = \Controllers\Admin\Data\Navigation->removeFromWatchlist +POST /admin/reenrichment [ajax] = \Controllers\Admin\Data\Navigation->enrichEntity +POST /admin/manageUser [ajax] = \Controllers\Admin\Data\Navigation->manageUser + +GET /admin/search [ajax] = \Controllers\Admin\Data\Navigation->getSearchResults +GET /admin/checkRule [ajax] = \Controllers\Admin\Data\Navigation->checkRule +GET /admin/scoreDetails [ajax] = \Controllers\Admin\Data\Navigation->getUserScoreDetails +GET /admin/loadChart [ajax] = \Controllers\Admin\Data\Navigation->getChart +GET /admin/loadTopTen [ajax] = \Controllers\Admin\Data\Navigation->getTopTen +GET /admin/loadDashboardStat [ajax] = \Controllers\Admin\Data\Navigation->getDashboardStat +GET /admin/loadCountries [ajax] = \Controllers\Admin\Data\Navigation->getCountries +GET /admin/loadMap [ajax] = \Controllers\Admin\Data\Navigation->getMap +GET /admin/loadReviewQueueCount [ajax] = \Controllers\Admin\Data\Navigation->getReviewUsersQueueCount +GET /admin/timeFrameTotal [ajax] = \Controllers\Admin\Data\Navigation->getTimeFrameTotal + +;panel +GET /admin/emailDetails [ajax] = \Controllers\Admin\Data\Navigation->getEmailDetails +GET /admin/eventDetails [ajax] = \Controllers\Admin\Data\Navigation->getEventDetails +GET /admin/phoneDetails [ajax] = \Controllers\Admin\Data\Navigation->getPhoneDetails +GET /admin/deviceDetails [ajax] = \Controllers\Admin\Data\Navigation->getDeviceDetails +GET /admin/logbookDetails [ajax] = \Controllers\Admin\Data\Navigation->getLogbookDetails + +GET /admin/enrichmentDetails [ajax] = \Controllers\Admin\Data\Navigation->getNotCheckedEntitiesCount + +;tile details +GET /admin/loadDomainDetails [ajax] = \Controllers\Admin\Data\Navigation->getDomainDetails +GET /admin/loadBotDetails [ajax] = \Controllers\Admin\Data\Navigation->getBotDetails +GET /admin/loadIspDetails [ajax] = \Controllers\Admin\Data\Navigation->getIspDetails +GET /admin/loadUserDetails [ajax] = \Controllers\Admin\Data\Navigation->getUserDetails +GET /admin/loadIpDetails [ajax] = \Controllers\Admin\Data\Navigation->getIpDetails + +;tables +GET /admin/loadEvents [ajax] = \Controllers\Admin\Data\Navigation->getEvents +GET /admin/loadUsers [ajax] = \Controllers\Admin\Data\Navigation->getUsers +GET /admin/loadReviewQueue [ajax] = \Controllers\Admin\Data\Navigation->getReviewUsersQueue +GET /admin/loadBlacklist [ajax] = \Controllers\Admin\Data\Navigation->getBlacklist +GET /admin/loadDevices [ajax] = \Controllers\Admin\Data\Navigation->getDevices +GET /admin/loadBots [ajax] = \Controllers\Admin\Data\Navigation->getBots +GET /admin/loadResources [ajax] = \Controllers\Admin\Data\Navigation->getResources +GET /admin/loadPhones [ajax] = \Controllers\Admin\Data\Navigation->getPhones +GET /admin/loadFieldAuditTrail [ajax] = \Controllers\Admin\Data\Navigation->getFieldAuditTrail +GET /admin/loadIsps [ajax] = \Controllers\Admin\Data\Navigation->getIsps +GET /admin/loadIps [ajax] = \Controllers\Admin\Data\Navigation->getIps +GET /admin/loadEmails [ajax] = \Controllers\Admin\Data\Navigation->getEmails +GET /admin/loadDomains [ajax] = \Controllers\Admin\Data\Navigation->getDomains +GET /admin/loadLogbook [ajax] = \Controllers\Admin\Data\Navigation->getLogbook +GET /admin/loadUsageStats [ajax] = \Controllers\Admin\Data\Navigation->getUsageStats +;ajax section end + +;pages section start +POST|GET /signup = \Controllers\Navigation->visitSignupPage +POST|GET /login = \Controllers\Navigation->visitLoginPage +POST|GET @logout: /logout = \Controllers\Navigation->visitLogoutPage +POST|GET /forgot-password = \Controllers\Navigation->visitForgotPasswordPage +POST|GET /password-recovering/@renewKey = \Controllers\Navigation->visitPasswordRecoveringPage + +GET /change-email/@renewKey = \Controllers\Navigation->visitChangeEmailPage + +GET / = \Controllers\Admin\Home\Navigation->showIndexPage +GET /event = \Controllers\Admin\Events\Navigation->showIndexPage +GET /review-queue = \Controllers\Admin\ReviewQueue\Navigation->showIndexPage +GET /blacklist = \Controllers\Admin\Blacklist\Navigation->showIndexPage +GET /logbook = \Controllers\Admin\Logbook\Navigation->showIndexPage +GET /watchlist = \Controllers\Admin\Watchlist\Navigation->showIndexPage + +POST|GET @api: /api = \Controllers\Admin\Api\Navigation->showIndexPage +POST|GET @rules: /rules = \Controllers\Admin\Rules\Navigation->showIndexPage +POST|GET @settings: /settings = \Controllers\Admin\Settings\Navigation->showIndexPage +POST|GET @manualCheck: /manual-check = \Controllers\Admin\ManualCheck\Navigation->showIndexPage + +;pages with reenrichment +POST|GET /id/@userId = \Controllers\Admin\User\Navigation->showIndexPage +POST|GET /bot/@botId = \Controllers\Admin\Bot\Navigation->showIndexPage +POST|GET /ip/@ipId = \Controllers\Admin\IP\Navigation->showIndexPage +POST|GET /domain/@domainId = \Controllers\Admin\Domain\Navigation->showIndexPage + +GET /id = \Controllers\Admin\Users\Navigation->showIndexPage +GET /bot = \Controllers\Admin\Bots\Navigation->showIndexPage +GET /ip = \Controllers\Admin\IPs\Navigation->showIndexPage +GET /isp = \Controllers\Admin\ISPs\Navigation->showIndexPage +GET /isp/@ispId = \Controllers\Admin\ISP\Navigation->showIndexPage +GET /country = \Controllers\Admin\Countries\Navigation->showIndexPage +GET /country/@countryId = \Controllers\Admin\Country\Navigation->showIndexPage +GET /domain = \Controllers\Admin\Domains\Navigation->showIndexPage +GET /resource = \Controllers\Admin\Resources\Navigation->showIndexPage +GET /resource/@resourceId = \Controllers\Admin\Resource\Navigation->showIndexPage +;pages section end diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/cron.json b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/cron.json new file mode 100644 index 0000000..80c8c60 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/cron.json @@ -0,0 +1,7 @@ +{ + "jobs": [ + { + "command": "*/10 * * * * php index.php /cron" + } + ] +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/eslint.config.js b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/eslint.config.js new file mode 100644 index 0000000..1da6afc --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/eslint.config.js @@ -0,0 +1,64 @@ +module.exports = [ + { + ignores: [ + "vendor/**/**", + "ui/js/vendor/**/*" + ], + languageOptions: { + ecmaVersion: 2016, + sourceType: "module", + globals: { + browser: "readonly", + jQuery: "readonly", + uPlot: "readonly", + Choices: "readonly", + parse: "readonly", + "$": "readonly", + document: "readonly", + window: "readonly" + }, + } + }, + { + files: [ + "ui/js/parts/*.js", + "ui/js/parts/*/*.js", + "ui/js/pages/*.js", + "ui/js/endpoints/*.js" + ], + rules: { + indent: ["error", 4, { SwitchCase: 1 }], + "linebreak-style": ["error", "unix"], + quotes: ["error", "single"], + semi: ["error", "always"], + "no-trailing-spaces": "error", + "space-before-blocks": ["error", "always"], + "space-in-parens": ["error", "never"], + "brace-style": ["error", "1tbs", { allowSingleLine: true }], + "keyword-spacing": ["error", { before: true, after: true }], + "no-unused-vars": "off", + "no-var": "error", + "space-before-function-paren": ["error", { + anonymous: "never", + named: "never", + asyncArrow: "always" + }], + "no-unused-expressions": ["error", { + allowShortCircuit: true, + allowTernary: true + }], + "no-undef": "off", + "no-shadow": "error", + "no-mixed-spaces-and-tabs": "error", + "no-multi-spaces": "off", + "no-useless-escape": "error", + "no-return-await": "error", + "prefer-arrow-callback": "warn", + "object-curly-spacing": ["error", "never"], + "array-bracket-spacing": ["error", "never"], + "max-len": ["error", { code: 120, ignoreComments: true, ignoreUrls: true }], + "comma-dangle": "off", + "security/detect-object-injection": "off" + } + } +]; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/index.php new file mode 100644 index 0000000..98a5cb4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/index.php @@ -0,0 +1,75 @@ + '/libs/ruler/ruler/src/', + 'PHPMailer\\PHPMailer\\' => '/libs/phpmailer/phpmailer/src/', + ]; + + foreach ($libs as $namespace => $path) { + if (str_starts_with($className, $namespace)) { + require __DIR__ . $path . str_replace([$namespace, '\\'], ['', '/'], $className) . '.php'; + break; + } + } + }); +} + +$f3 = \Base::instance(); + +//Load configuration file with all project variables +$f3->config('config/config.ini'); + +//Load specific configuration only for local development +$localConfigFile = \Utils\Variables::getConfigFile(); +$localConfigFile = sprintf('config/%s', $localConfigFile); + +//Load local configuration file +if (file_exists($localConfigFile)) { + $f3->config($localConfigFile); +} + +//Override server host +$f3->set('HOST', \Utils\Variables::getSite()); + +//Load routes configuration +$f3->config('config/routes.ini'); +$f3->config('config/apiEndpoints.ini'); + +//Use custom onError function +$f3->set('ONERROR', \Utils\ErrorHandler::getOnErrorHandler()); + +//Load dictionary file +$f3->set('LOCALES', 'app/Dictionary/'); +$f3->set('LANGUAGE', 'en'); + +if (\Utils\Variables::getDB()) { + \Utils\Updates::syncUpdates(); + + // Load cron job runner + $cron = \Utils\Cron::instance(); +} + +$f3->run(); diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/install/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/install/index.php new file mode 100644 index 0000000..6c31413 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/install/index.php @@ -0,0 +1,397 @@ + + body { + background-color: #2b2a3d; + color: #d7e6e1; + font-family: monospace, monospace; + padding: 50px; + font-size: 13px; + } + input[type="text"],input[type="email"]{ + width: 550px; + background-color: #151220; + color: #d7e6e1; + font-size: 100%; + } + label { + text-align: right; + display: block; + } + a { + color: #6e9fff; + } +'); + +$installerHead = 'Installer' . $style . ''; +$okTile = '[ OK ]'; +$failTile = '[ FAIL ]'; +$nullTile = '[ -- ]'; +$backButton = ''; + +$formBody = ( +' +

Database connection details

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
 
+
+'); + +function resultHtmlStart() { + global $installerHead, $logo; + return $installerHead . '
' . $logo;
+}
+
+function resultHtmlEnd() {
+    return '
'; +} + +function formHtml() { + global $installerHead, $formBody; + return $installerHead . $formBody . ''; +} + +function finishOk() { + $out = "\n\n======================== Setup completed! ========================"; + $out .= "\n* Please delete the ./install directory and all its included files."; + $out .= "\n* Visit /signup to create your account."; + + return $out; +} + +function finishError() { + global $backButton; + + $out = "\n====================== Something went wrong ======================"; + $out .= "\n$backButton"; + + return $out; +} + +$steps = [ + [ + 'description' => 'Compatibility checks', + 'tasks' => [ + ['description' => 'PHP version', 'status' => null], + ['description' => 'PDO PostgreSQL driver', 'status' => null], + ['description' => 'Configuration folder (/config) read/write permission', 'status' => null], + ['description' => '.htaccess available', 'status' => null], + ['description' => 'cURL', 'status' => null], + ['description' => 'Memory limit (Min. 128MB)', 'status' => null], + ], + ], + [ + 'description' => 'Database params', + 'tasks' => [ + ['description' => 'Schema accessible', 'status' => null], + ['description' => 'Database name', 'status' => null], + ['description' => 'Database user', 'status' => null], + ['description' => 'Database password', 'status' => null], + ['description' => 'Database host', 'status' => null], + ['description' => 'Database port', 'status' => null], + ], + ], + [ + 'description' => 'Database setup', + 'tasks' => [ + ['description' => 'Database connection', 'status' => null], + /*['description' => 'Database version', 'status' => null],*/ + ['description' => 'Apply database schema', 'status' => null], + ], + ], + [ + 'description' => 'Config build', + 'tasks' => [ + ['description' => 'Write config file', 'status' => null], + ], + ], +]; + +function proceed() { + $out = ''; + if (configAlreadyExists()) { + $out .= resultHtmlStart(); + $out .= "\nThe app is already configured."; + $out .= resultHtmlEnd(); + + echo $out; + return; + } + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $out .= resultHtmlStart(); + [$status, $result, $config] = execute($_POST); + $out .= $result; + $out .= $status ? finishOk() : finishError(); + $out .= resultHtmlEnd(); + } else { + substituteFormWithEnv(); + $out .= formHtml(); + } + + echo $out; +} + +function execute(array $values) { + global $steps; + + $out = ''; + + compatibilityCheck(0, $steps); + $out .= printTasks($steps[0]); + if (!tasksCompleted($steps[0])) { + return [false, $out, null]; + } + + dbConfig(1, $values, $steps); + $out .= printTasks($steps[1]); + if (!tasksCompleted($steps[1])) { + return [false, $out, null]; + } + + dbSaveConfig(2, $values, $steps); + $out .= printTasks($steps[2]); + if (!tasksCompleted($steps[2])) { + return [false, $out, null]; + } + + if (strval($values['mode'] ?? '') !== 'schema') { + $config = saveConfig(3, $values, $steps); + $out .= printTasks($steps[3]); + if (!tasksCompleted($steps[3])) { + return [false, $out, null]; + } + } + + return [true, $out, $config]; +} + +function configAlreadyExists(): bool { + return (getenv('SITE') && getenv('DATABASE_URL')) || file_exists('../config/local/config.local.ini'); +} + +function substituteFormWithEnv(): void { + global $formBody; + + if (strval($_GET['mode'] ?? '') === 'schema') { + $formBody = preg_replace( + '/(]*>)/i', + '$1', + $formBody + ); + } + + $dbUrl = getenv('DATABASE_URL'); + if ($dbUrl) { + $parts = parse_url($dbUrl); + if ($parts) { + $values = [ + 'db_user' => $parts['user'] ?? '', + 'db_pass' => $parts['pass'] ?? '', + 'db_host' => $parts['host'] ?? '', + 'db_port' => $parts['port'] ?? '', + 'db_name' => trim(($parts['path'] ?? ''), '/'), + ]; + + foreach ($values as $key => $value) { + $safe = htmlspecialchars($value, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8'); + $formBody = preg_replace( + '/(]*\bname="' . preg_quote($key, '/') . '"(?![^>]*\bvalue=)[^>]*)(>)/i', + '$1 value="' . $safe . '"$2', + $formBody + ); + } + } + } +} + +function compatibilityCheck(int $step, array &$steps) { + $steps[$step]['tasks'][0]['status'] = version_compare(PHP_VERSION, MIN_PHP_VERSION) >= 0; + $steps[$step]['tasks'][1]['status'] = extension_loaded('pdo_pgsql') && extension_loaded('pgsql'); + + try { + if (is_writable('../config')) { + $f = fopen('../config/local/config.local.ini', 'w'); + fclose($f); + unlink('../config/local/config.local.ini'); + $steps[$step]['tasks'][2]['status'] = true; + } else { + $steps[$step]['tasks'][2]['status'] = false; + } + } catch (\Exception $e) { + $steps[$step]['tasks'][2]['status'] = false; + } + + $steps[$step]['tasks'][3]['status'] = is_file('../.htaccess') && is_readable('../.htaccess'); + + $steps[$step]['tasks'][4]['status'] = extension_loaded('curl'); + + $memoryLimit = @ini_get('memory_limit'); + $memLim = $memoryLimit; + preg_match('#^(\d+)(\w+)$#', strtolower($memLim), $match); + $memLim = match ($match[2]) { + 'g' => intval($memLim) * 1024 * 1024 * 1024, + 'm' => intval($memLim) * 1024 * 1024, + 'k' => intval($memLim) * 1024, + default => intval($memLim), + }; + $steps[$step]['tasks'][5]['status'] = $memLim >= MIN_MEMORY_LIM; +} + +function dbConfig(int $step, array &$values, array &$steps) { + $steps[$step]['tasks'][0]['status'] = is_file('./install.sql'); + + $steps[$step]['tasks'][1]['status'] = $values['db_name'] !== ''; + $steps[$step]['tasks'][2]['status'] = $values['db_user'] !== ''; + $steps[$step]['tasks'][3]['status'] = $values['db_pass'] !== ''; + $steps[$step]['tasks'][4]['status'] = $values['db_host'] !== ''; + $steps[$step]['tasks'][5]['status'] = $values['db_port'] !== ''; +} + +function saveConfig(int $step, array $values, array &$steps): ?array { + $configData = null; + try { + $httpHost = strtolower(filter_var($_SERVER['HTTP_HOST'], FILTER_SANITIZE_URL)); + if (strpos($httpHost, 'www.') === 0) { + $httpHost = substr($httpHost, 4); + } elseif (substr_count($httpHost, '.') == 1) { + $httpHost = 'www.' . $httpHost; + } + $httpHost = explode(':', $httpHost)[0]; + + $configData = [ + 'SITE' => $httpHost, + 'DATABASE_URL' => "postgres://$values[db_user]:$values[db_pass]@$values[db_host]:$values[db_port]/$values[db_name]", + 'PEPPER' => strval(bin2hex(random_bytes(32))), + ]; + if ($values['admin_email'] !== '') { + $configData['ADMIN_EMAIL'] = $values['admin_email']; + } + + $config = "\n[globals]"; + foreach ($configData as $key => $value) { + $config .= "\n$key=$value"; + } + + $configPath = '../config/local/config.local.ini'; + $configFile = fopen($configPath, 'w'); + fwrite($configFile, $config); + fclose($configFile); + $steps[$step]['tasks'][0]['status'] = true; + } catch (\Exception $e) { + $steps[$step]['tasks'][0]['status'] = false; + $steps[$step]['tasks'][0]['error'] = $e->getMessage(); + } + + return $configData; +} + +function dbSaveConfig(int $step, array $values, array &$steps) { + $database = null; + + $dsn = "pgsql:dbname=$values[db_name];host=$values[db_host];port=$values[db_port]"; + $driverOptions = array( + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION + ); + + try { + $database = new \PDO($dsn, $values['db_user'], $values['db_pass'], $driverOptions); + } catch (\Exception $e) { + $steps[$step]['tasks'][0]['status'] = false; + $steps[$step]['tasks'][0]['error'] = $e->getMessage(); + return; + } + + $steps[$step]['tasks'][0]['status'] = true; + + /*$query = $database->query('SELECT VERSION()'); + [$dbVersion] = $query->fetch(\PDO::FETCH_NUM); + if (!preg_match('/PostgreSQL (\d+\.\d+)/', $dbVersion, $matches) || version_compare($matches[1], '12.0', '<')) { + $steps[$step]['tasks'][1]['status'] = false; + return; + } + $steps[$step]['tasks'][1]['status'] = true;*/ + + try { + $sql = file_get_contents('./install.sql'); + $database->exec($sql); + } catch (\Exception $e) { + $steps[$step]['tasks'][1]['status'] = false; + $steps[$step]['tasks'][1]['error'] = $e->getMessage(); + return; + } + + $steps[$step]['tasks'][1]['status'] = true; +} + +function printTasks(array $tasks) { + global $okTile, $failTile, $nullTile; + + $out = ''; + $side = intdiv(64 - strlen($tasks['description']), 2); + $header = str_repeat('=', $side) . ' ' . $tasks['description'] . ' ' . str_repeat('=', $side); + $out .= "\n\n" . $header; + foreach ($tasks['tasks'] as $task) { + $st = ($task['status'] === true) ? $okTile : (($task['status'] === false) ? $failTile : $nullTile); + $err = array_key_exists('error', $task) ? ' (' . $task['error'] . ')' : ''; + $out .= "\n" . $st . ' ' . $task['description'] . $err; + } + + return $out; +} + +function tasksCompleted(array $tasks) { + $results = array_column($tasks['tasks'], 'status'); + + return count(array_filter($results)) === count($results); +} + + +proceed(); diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/install/install.sql b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/install/install.sql new file mode 100644 index 0000000..3f49850 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/install/install.sql @@ -0,0 +1,1221 @@ +SET statement_timeout = '10800s'; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; +SET default_table_access_method = heap; + + +-- Extensions -- +CREATE EXTENSION IF NOT EXISTS citext; +CREATE EXTENSION IF NOT EXISTS pg_stat_statements; +CREATE EXTENSION IF NOT EXISTS pgcrypto; + + +-- Types -- +CREATE TYPE blacklist_type AS ENUM ( + 'email', + 'phone', + 'ip' +); + + +CREATE TYPE dshb_operators_change_email_status AS ENUM ( + 'unused', + 'used', + 'invalidated' +); + + +CREATE TYPE dshb_operators_forgot_password_status AS ENUM ( + 'unused', + 'used', + 'invalidated' +); + + +CREATE DOMAIN email AS citext + CONSTRAINT email_check CHECK ((VALUE OPERATOR(~) '^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$'::citext)); + + +CREATE TYPE queue_account_operation_action AS ENUM ( + 'blacklist', + 'delete', + 'calculate_risk_score', + 'enrichment' +); + + +CREATE TYPE queue_account_operation_status AS ENUM ( + 'waiting', + 'executing', + 'completed', + 'failed' +); + + +CREATE TYPE unreviewed_items_reminder_frequency AS ENUM ( + 'daily', + 'weekly', + 'off' +); + + +-- Functions -- +CREATE FUNCTION dshb_api_co_owners_creator_check() +RETURNS TRIGGER AS $$ +BEGIN + IF EXISTS (SELECT 1 FROM dshb_api WHERE creator = NEW.operator) + THEN + RAISE EXCEPTION 'A row with operator % already exists in the dshb_api table''s creator column', NEW.operator; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + + +CREATE FUNCTION event_lastseen() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.lastseen < OLD.lastseen THEN + NEW.lastseen := OLD.lastseen; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + + +CREATE FUNCTION restrict_update() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.key <> OLD.key THEN + RAISE EXCEPTION 'not allowed'; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + + +CREATE FUNCTION search_whole_db(_like_pattern text) +RETURNS TABLE(_tbl regclass, _ctid tid) AS $$ +BEGIN + FOR _tbl IN + SELECT c.oid::regclass + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind = 'r' -- only tables + AND n.nspname !~ '^(pg_|information_schema)' -- exclude system schemas + ORDER BY n.nspname, c.relname + LOOP + RETURN QUERY EXECUTE format( + 'SELECT $1, ctid FROM %s t WHERE t::text ~~ %L', + _tbl, '%' || _like_pattern || '%' + ); + END LOOP; +END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION queue_new_events_cursor_check() +RETURNS TRIGGER +AS $$ +BEGIN + IF (SELECT COUNT(*) FROM queue_new_events_cursor) > 0 THEN + RAISE EXCEPTION 'A row in this table already exists'; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE plpgsql; + +-- Tables -- +CREATE SEQUENCE event_logbook_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_logbook ( + id bigint NOT NULL DEFAULT nextval('event_logbook_id_seq'::regclass), + ended timestamp(3) without time zone DEFAULT now() NOT NULL, + key smallint NOT NULL, + ip INET, + event bigint, + error_type smallint NOT NULL, + error_text text, + raw text, + raw_time text, + started timestamp(3) without time zone +); + +ALTER SEQUENCE event_logbook_id_seq OWNED BY event_logbook.id; + +CREATE UNIQUE INDEX event_logbook_id_uidx ON event_logbook USING btree (id); +CREATE INDEX event_logbook_ended_idx ON event_logbook USING brin (ended); +CREATE INDEX event_logbook_key_idx ON event_logbook USING btree (key); +CREATE INDEX event_logbook_raw_time_idx ON event_logbook USING btree (raw_time); + + +-- sequence for user sessions decisions -- +CREATE SEQUENCE session_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_session ( + id bigint NOT NULL, + key smallint NOT NULL, + account_id bigint NOT NULL, + total_visit integer DEFAULT 0, + total_device integer DEFAULT 0, + total_ip integer DEFAULT 0, + total_country integer DEFAULT 0, + lastseen timestamp(3) without time zone NOT NULL, + created timestamp(3) without time zone NOT NULL, + updated timestamp without time zone NOT NULL +); + +CREATE UNIQUE INDEX event_session_id_uidx ON event_session USING btree (id); +CREATE INDEX event_session_account_id_idx ON event_session USING btree (account_id); +CREATE INDEX event_session_lastseen_idx ON event_session USING btree (lastseen); +CREATE INDEX event_session_lastseen_updated_idx ON event_session(lastseen, updated) WHERE lastseen >= updated; + + +CREATE TABLE countries ( + id character varying(64) NOT NULL, + value character varying(64) NOT NULL, + serial integer NOT NULL +); + +CREATE UNIQUE INDEX countries_serial_uidx ON countries USING btree (serial); +CREATE UNIQUE INDEX countries_value_uidx ON countries USING btree (value); + +ALTER TABLE ONLY countries ADD CONSTRAINT countries_id_pkey PRIMARY KEY (id); + + +CREATE TABLE event_type ( + id smallint NOT NULL, + value text NOT NULL, + name text NOT NULL +); + +ALTER TABLE ONLY event_type ADD CONSTRAINT event_type_id_pkey PRIMARY KEY (id); + + +CREATE TABLE event_error_type ( + id smallint NOT NULL, + value text NOT NULL, + name text NOT NULL +); + +ALTER TABLE ONLY event_error_type ADD CONSTRAINT event_error_type_id_pkey PRIMARY KEY (id); + + +CREATE TABLE event_http_method ( + id smallint NOT NULL, + value TEXT NOT NULL, + name TEXT NOT NULL +); + +ALTER TABLE ONLY event_http_method ADD CONSTRAINT event_http_method_id_pkey PRIMARY KEY (id); + + +CREATE TABLE dshb_rules ( + id integer NOT NULL +); + +ALTER TABLE ONLY dshb_rules ADD CONSTRAINT dshb_rules_id_pkey PRIMARY KEY (id); + + +CREATE TABLE dshb_sessions ( + session_id character varying(255) NOT NULL, + data text, + ip character varying(45), + agent character varying(300), + stamp integer +); + +ALTER TABLE ONLY dshb_sessions ADD CONSTRAINT dshb_sessions_session_id_pkey PRIMARY KEY (session_id); + + +CREATE TABLE dshb_api_co_owners ( + operator BIGINT NOT NULL, + api BIGINT NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +ALTER TABLE ONLY dshb_api_co_owners ADD CONSTRAINT dshb_api_co_owners_operator_key UNIQUE (operator); +ALTER TABLE ONLY dshb_api_co_owners ADD CONSTRAINT dshb_api_co_owners_operator_api_pkey PRIMARY KEY (operator, api); + + +CREATE SEQUENCE event_account_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_account ( + id BIGINT NOT NULL DEFAULT nextval('event_account_id_seq'::regclass), + userid character varying(100) NOT NULL, + created timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + key int, + lastip inet, + lastseen timestamp without time zone NOT NULL, + fullname text, + is_important boolean DEFAULT false NOT NULL, + firstname character varying(100), + middlename character varying(100), + lastname character varying(100), + total_visit integer DEFAULT 0, + total_country integer DEFAULT 0, + total_ip integer DEFAULT 0, + total_device integer DEFAULT 0, + score_updated_at timestamp without time zone, + score integer, + score_details text, + lastemail integer, + lastphone integer, + total_shared_ip integer DEFAULT 0, + total_shared_phone integer DEFAULT 0, + reviewed boolean DEFAULT false, + fraud boolean, + latest_decision timestamp without time zone, + score_recalculate boolean DEFAULT true, + session_id bigint, + updated timestamp without time zone NOT NULL +); + +ALTER SEQUENCE event_account_id_seq OWNED BY event_account.id; + +CREATE UNIQUE INDEX event_account_id_uidx ON event_account USING btree (id); +CREATE UNIQUE INDEX event_account_id_key_uidx ON event_account USING btree (id, key); +CREATE INDEX event_account_lastseen_idx ON event_account USING btree (lastseen); +CREATE INDEX event_account_userid_idx ON event_account USING btree (userid); +CREATE INDEX event_account_key_idx ON event_account USING btree (key); +CREATE INDEX event_account_lastseen_updated_idx ON event_account(lastseen, updated) WHERE lastseen >= updated; + +ALTER TABLE ONLY event_account ADD CONSTRAINT event_account_id_pkey PRIMARY KEY (id); +ALTER TABLE ONLY event_account ADD CONSTRAINT event_account_userid_key_key UNIQUE (userid, key); + + +CREATE SEQUENCE dshb_api_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- use int for `key` everywhere +CREATE TABLE dshb_api ( + id int NOT NULL DEFAULT nextval('dshb_api_id_seq'::regclass), + key text NOT NULL, + quote integer NOT NULL, + creator BIGINT NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + skip_enriching_attributes jsonb DEFAULT '[]'::jsonb NOT NULL, + retention_policy smallint DEFAULT 0 NOT NULL, + skip_blacklist_sync boolean DEFAULT true NOT NULL, + token character varying, + last_call_reached boolean +); + +ALTER SEQUENCE dshb_api_id_seq OWNED BY dshb_api.id; + +CREATE UNIQUE INDEX dshb_api_key_uidx ON dshb_api USING btree (key); + +ALTER TABLE ONLY dshb_api ADD CONSTRAINT dshb_api_key_key UNIQUE (key); +ALTER TABLE ONLY dshb_api ADD CONSTRAINT dshb_api_id_pkey PRIMARY KEY (id); + + +CREATE SEQUENCE event_device_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_device ( + id BIGINT NOT NULL DEFAULT nextval('event_device_id_seq'::regclass), + account_id bigint NOT NULL, + key smallint NOT NULL, + created timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + lastseen timestamp without time zone DEFAULT '1970-01-01 00:00:00'::timestamp without time zone NOT NULL, + updated timestamp without time zone NOT NULL, + user_agent bigint NOT NULL, + lang text, + total_visit integer DEFAULT 0 +); + +ALTER SEQUENCE event_device_id_seq OWNED BY event_device.id; + +CREATE INDEX event_device_key_idx ON event_device USING btree (key); +CREATE INDEX event_device_account_id_idx ON event_device USING btree (account_id); +CREATE INDEX event_device_lastseen_idx ON event_device USING btree (lastseen); +CREATE INDEX event_device_user_agent_idx ON event_device USING btree (user_agent); +CREATE INDEX event_device_lastseen_updated_idx ON event_device(lastseen, updated) WHERE lastseen >= updated; + +ALTER TABLE ONLY event_device ADD CONSTRAINT event_device_id_pkey PRIMARY KEY (id); +ALTER TABLE ONLY event_device ADD CONSTRAINT event_device_account_id_key_user_agent_key UNIQUE (account_id, key, user_agent); + + +CREATE SEQUENCE dshb_logs_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE dshb_logs ( + id BIGINT NOT NULL DEFAULT nextval('dshb_logs_id_seq'::regclass), + text text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + +ALTER SEQUENCE dshb_logs_id_seq OWNED BY dshb_logs.id; + +ALTER TABLE ONLY dshb_logs ADD CONSTRAINT dshb_logs_id_pkey PRIMARY KEY (id); + + +CREATE SEQUENCE dshb_manual_check_history_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE dshb_manual_check_history ( + id BIGINT NOT NULL DEFAULT nextval('dshb_manual_check_history_id_seq'::regclass), + operator BIGINT NOT NULL, + type text NOT NULL, + search_query text NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +ALTER SEQUENCE dshb_manual_check_history_id_seq OWNED BY dshb_manual_check_history.id; + +ALTER TABLE ONLY dshb_manual_check_history ADD CONSTRAINT dshb_manual_check_history_id_pkey PRIMARY KEY (id); + + +CREATE SEQUENCE dshb_message_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE dshb_message ( + id BIGINT NOT NULL DEFAULT nextval('dshb_message_id_seq'::regclass), + text text, + title text, + created_at timestamp without time zone DEFAULT now() +); + +ALTER SEQUENCE dshb_message_id_seq OWNED BY dshb_message.id; + + +CREATE SEQUENCE dshb_operators_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE dshb_operators ( + id BIGINT NOT NULL DEFAULT nextval('dshb_operators_id_seq'::regclass), + email citext NOT NULL, + password text, + firstname text, + lastname text, + city text, + state text, + zip text, + country text, + company text, + vat text, + is_active smallint DEFAULT 0, + activation_key text, + created_at timestamp with time zone DEFAULT now(), + street text, + is_closed smallint DEFAULT 0, + timezone text DEFAULT 'UTC'::text NOT NULL, + review_queue_cnt smallint, + review_queue_updated_at timestamp without time zone, + last_event_time timestamp without time zone, + unreviewed_items_reminder_freq unreviewed_items_reminder_frequency DEFAULT 'weekly'::unreviewed_items_reminder_frequency NOT NULL, + last_unreviewed_items_reminder timestamp without time zone +); + +ALTER SEQUENCE dshb_operators_id_seq OWNED BY dshb_operators.id; + +CREATE UNIQUE INDEX dshb_operators_activation_key_uidx ON dshb_operators USING btree (activation_key); +CREATE UNIQUE INDEX dshb_operators_email_uidx ON dshb_operators USING btree (email); + +ALTER TABLE ONLY dshb_operators ADD CONSTRAINT dshb_operators_id_pkey PRIMARY KEY (id); + + +CREATE SEQUENCE dshb_operators_change_email_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE dshb_operators_change_email ( + id BIGINT NOT NULL DEFAULT nextval('dshb_operators_change_email_id_seq'::regclass), + operator_id BIGINT NOT NULL, + renew_key text NOT NULL, + status dshb_operators_change_email_status DEFAULT 'unused'::dshb_operators_change_email_status NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + email text NOT NULL +); + +ALTER SEQUENCE dshb_operators_change_email_id_seq OWNED BY dshb_operators_change_email.id; + +CREATE INDEX dshb_operators_change_email_operator_id_status_idx ON dshb_operators_change_email USING btree (operator_id, status); + +ALTER TABLE ONLY dshb_operators_change_email ADD CONSTRAINT dshb_operators_change_email_id_pkey PRIMARY KEY (id); +ALTER TABLE ONLY dshb_operators_change_email ADD CONSTRAINT dshb_operators_change_email_renew_key_key UNIQUE (renew_key); + + +CREATE SEQUENCE dshb_operators_forgot_password_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE dshb_operators_forgot_password ( + id BIGINT NOT NULL DEFAULT nextval('dshb_operators_forgot_password_id_seq'::regclass), + operator_id BIGINT NOT NULL, + renew_key text NOT NULL, + status dshb_operators_forgot_password_status DEFAULT 'unused'::dshb_operators_forgot_password_status NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +ALTER SEQUENCE dshb_operators_forgot_password_id_seq OWNED BY dshb_operators_forgot_password.id; + +CREATE INDEX dshb_operators_forgot_password_operator_id_status_idx ON dshb_operators_forgot_password USING btree (operator_id, status); + +ALTER TABLE ONLY dshb_operators_forgot_password ADD CONSTRAINT dshb_operators_forgot_password_pkey PRIMARY KEY (id); +ALTER TABLE ONLY dshb_operators_forgot_password ADD CONSTRAINT dshb_operators_forgot_password_renew_key_key UNIQUE (renew_key); + + +CREATE SEQUENCE dshb_operators_rules_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE dshb_operators_rules ( + id BIGINT NOT NULL DEFAULT nextval('dshb_operators_rules_id_seq'::regclass), + rule_id integer, + value integer DEFAULT 0, + created_at timestamp with time zone DEFAULT now(), + key integer, + proportion real, + proportion_updated_at timestamp without time zone +); + +ALTER SEQUENCE dshb_operators_rules_id_seq OWNED BY dshb_operators_rules.id; + +ALTER TABLE ONLY dshb_operators_rules ADD CONSTRAINT dshb_operators_rules_id_pkey PRIMARY KEY (id); + + +CREATE SEQUENCE event_email_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_email ( + id BIGINT NOT NULL DEFAULT nextval('event_email_id_seq'::regclass), + account_id BIGINT NOT NULL, + email email, + lastseen timestamp without time zone NOT NULL, + created timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + key integer DEFAULT 0 NOT NULL, + checked boolean, + data_breach boolean, + profiles integer, + blockemails boolean, + domain_contact_email boolean, + domain BIGINT, + fraud_detected boolean DEFAULT false NOT NULL, + hash text, + alert_list boolean, + data_breaches integer, + earliest_breach text +); + +ALTER SEQUENCE event_email_id_seq OWNED BY event_email.id; + +CREATE INDEX event_email_email_idx ON event_email USING btree (email); +CREATE INDEX event_email_key_idx ON event_email USING btree (key); +CREATE INDEX event_email_lastseen_idx ON event_email USING btree (lastseen); +CREATE INDEX event_email_domain_idx ON event_email USING btree (domain); +CREATE INDEX event_email_account_id_idx ON event_email USING btree (account_id); + +ALTER TABLE ONLY event_email ADD CONSTRAINT event_email_id_pkey PRIMARY KEY (id); +ALTER TABLE ONLY event_email ADD CONSTRAINT event_email_account_id_email_key UNIQUE (account_id, email); + + +CREATE SEQUENCE event_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event ( + id bigint NOT NULL DEFAULT nextval('event_id_seq'::regclass), + key smallint NOT NULL, + account bigint NOT NULL, + ip bigint NOT NULL, + url bigint NOT NULL, + device bigint NOT NULL, + "time" timestamp(3) without time zone NOT NULL, + query bigint, + payload json, + traceid character varying(36) DEFAULT NULL::character varying, + referer bigint, + type smallint DEFAULT 1 NOT NULL, + email integer, + phone integer, + http_code smallint, + http_method smallint, + session_id bigint NOT NULL +); + +ALTER SEQUENCE event_id_seq OWNED BY event.id; + +CREATE UNIQUE INDEX event_id_uidx ON event USING btree (id); +CREATE INDEX event_device_idx ON event USING btree (device); +CREATE INDEX event_account_idx ON event USING btree (account); +CREATE INDEX event_referer_idx ON event USING btree (referer); +CREATE INDEX event_time_idx ON event USING brin ("time"); +CREATE INDEX event_url_idx ON event USING btree (url); +CREATE INDEX event_key_idx ON event USING btree (key); +CREATE INDEX event_query_idx ON event USING btree (query); +CREATE INDEX event_type_idx ON event USING btree (type); +CREATE INDEX event_ip_idx ON event USING btree (ip); +CREATE INDEX event_email_idx ON event USING btree (email); +CREATE INDEX event_session_id_idx ON event USING btree (session_id); +CREATE INDEX event_key_ip_idx ON event (ip, key); +CREATE INDEX event_ip_account_key_idx ON event (ip, account, key); + + +CREATE SEQUENCE event_country_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_country ( + id BIGINT NOT NULL DEFAULT nextval('event_country_id_seq'::regclass), + key integer NOT NULL, + country integer NOT NULL, + total_visit integer DEFAULT 0, + total_ip integer DEFAULT 0, + total_account integer DEFAULT 0, + lastseen timestamp without time zone, + created timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated timestamp without time zone NOT NULL +); + +ALTER SEQUENCE event_country_id_seq OWNED BY event_country.id; + +ALTER TABLE ONLY event_country ADD CONSTRAINT event_country_id_pkey PRIMARY KEY (id); +ALTER TABLE ONLY event_country ADD CONSTRAINT event_country_country_key_key UNIQUE (country, key); +CREATE INDEX event_country_lastseen_updated_idx ON event_country(lastseen, updated) WHERE lastseen >= updated; + + +CREATE SEQUENCE event_domain_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_domain ( + id BIGINT NOT NULL DEFAULT nextval('event_domain_id_seq'::regclass), + key smallint NOT NULL, + domain text, + ip inet, + geo_ip character varying(9) DEFAULT NULL::character varying, + geo_html character varying(9) DEFAULT NULL::character varying, + web_server character varying(36) DEFAULT NULL::character varying, + hostname text DEFAULT NULL::character varying, + emails text, + phone character varying(19) DEFAULT NULL::character varying, + discovery_date timestamp without time zone, + blockdomains boolean, + disposable_domains boolean, + total_visit integer DEFAULT 0, + total_account integer DEFAULT 0, + lastseen timestamp without time zone NOT NULL, + created timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated timestamp without time zone NOT NULL, + free_email_provider boolean, + tranco_rank integer, + creation_date timestamp without time zone, + expiration_date timestamp without time zone, + return_code integer, + closest_snapshot text, + checked boolean, + mx_record boolean, + disabled boolean DEFAULT NULL +); + +ALTER SEQUENCE event_domain_id_seq OWNED BY event_domain.id; + +ALTER TABLE ONLY event_domain ADD CONSTRAINT event_domain_key_domain_key UNIQUE (key, domain); +ALTER TABLE ONLY event_domain ADD CONSTRAINT event_domain_id_pkey PRIMARY KEY (id); +CREATE INDEX event_domain_lastseen_updated_idx ON event_domain(lastseen, updated) WHERE lastseen >= updated; + + +CREATE SEQUENCE event_incorrect_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_incorrect ( + id bigint NOT NULL DEFAULT nextval('event_incorrect_id_seq'::regclass), + payload json NOT NULL, + created timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + errors json, + traceid character varying(36) DEFAULT NULL::character varying, + key integer +); + +ALTER SEQUENCE event_incorrect_id_seq OWNED BY event_incorrect.id; + +ALTER TABLE ONLY event_incorrect ADD CONSTRAINT event_incorrect_id_pkey PRIMARY KEY (id); + + +CREATE SEQUENCE event_ip_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_ip ( + id bigint NOT NULL DEFAULT nextval('event_ip_id_seq'::regclass), + ip inet NOT NULL, + key smallint NOT NULL, + country smallint NOT NULL, + cidr text, + data_center boolean, + tor boolean, + vpn boolean, + checked boolean DEFAULT false, + relay boolean, + lastseen timestamp without time zone DEFAULT '1970-01-01 00:00:00'::timestamp without time zone NOT NULL, + created timestamp without time zone DEFAULT now() NOT NULL, + updated timestamp without time zone NOT NULL, + lastcheck timestamp without time zone, + total_visit integer DEFAULT 0, + blocklist boolean, + isp bigint, + shared smallint DEFAULT 0, + domains_count json, + fraud_detected boolean DEFAULT false NOT NULL, + hash text, + alert_list boolean, + starlink boolean +); + +ALTER SEQUENCE event_ip_id_seq OWNED BY event_ip.id; + +CREATE INDEX event_ip_checked_idx ON event_ip USING btree (checked); +CREATE INDEX event_ip_country_idx ON event_ip USING btree (country); +CREATE INDEX event_ip_ip_idx ON event_ip USING gist (ip inet_ops); +CREATE INDEX event_ip_key_idx ON event_ip USING btree (key); +CREATE INDEX event_ip_lastseen_idx ON event_ip USING btree (lastseen); +CREATE INDEX event_ip_isp_idx ON event_ip USING btree (isp); +CREATE INDEX event_ip_country_key_idx ON event_ip (country, key); +CREATE INDEX event_ip_key_isp_idx ON event_ip (key, isp); +CREATE INDEX event_ip_lastseen_updated_idx ON event_ip(lastseen, updated) WHERE lastseen >= updated; + +ALTER TABLE ONLY event_ip ADD CONSTRAINT event_ip_key_ip UNIQUE (key, ip); +ALTER TABLE ONLY event_ip ADD CONSTRAINT event_ip_id_pkey PRIMARY KEY (id); + + +CREATE SEQUENCE event_isp_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_isp ( + id BIGINT NOT NULL DEFAULT nextval('event_isp_id_seq'::regclass), + key smallint NOT NULL, + asn integer NOT NULL, + name text, + description text, + total_visit integer DEFAULT 0, + total_account integer DEFAULT 0, + lastseen timestamp without time zone, + created timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated timestamp without time zone NOT NULL +); + +ALTER SEQUENCE event_isp_id_seq OWNED BY event_isp.id; + +CREATE INDEX event_isp_key_id ON event_isp USING btree (key); + +ALTER TABLE ONLY event_isp ADD CONSTRAINT event_isp_key_asn_key UNIQUE (key, asn); +ALTER TABLE ONLY event_isp ADD CONSTRAINT event_isp_id_pkey PRIMARY KEY (id); +CREATE INDEX event_isp_asn_ids ON event_isp(asn); +CREATE INDEX event_isp_lastseen_updated_idx ON event_isp(lastseen, updated) WHERE lastseen >= updated; + + +CREATE SEQUENCE event_phone_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_phone ( + id bigint NOT NULL DEFAULT nextval('event_phone_id_seq'::regclass), + account_id BIGINT NOT NULL, + key integer NOT NULL, + phone_number character varying(19) NOT NULL, + calling_country_code integer, + national_format character varying DEFAULT NULL::character varying, + country_code smallint, + validation_errors json, + mobile_country_code smallint, + mobile_network_code smallint, + carrier_name character varying(128) DEFAULT NULL::character varying, + type character varying(32) DEFAULT NULL::character varying, + lastseen timestamp without time zone NOT NULL, + created timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated timestamp without time zone NOT NULL, + checked boolean, + shared smallint DEFAULT 0, + fraud_detected boolean DEFAULT false NOT NULL, + hash text, + alert_list boolean, + profiles integer, + iso_country_code character varying(8), + invalid boolean +); + +ALTER SEQUENCE event_phone_id_seq OWNED BY event_phone.id; + +CREATE INDEX event_phone_account_id_idx ON event_phone USING btree (account_id); +CREATE INDEX event_phone_lastseen_updated_idx ON event_phone(lastseen, updated) WHERE lastseen >= updated; + +ALTER TABLE ONLY event_phone ADD CONSTRAINT event_phone_key_account_id_phone_number_key UNIQUE (key, account_id, phone_number); +ALTER TABLE ONLY event_phone ADD CONSTRAINT event_phone_id_pkey PRIMARY KEY (id); + + +CREATE SEQUENCE event_referer_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_referer ( + id bigint NOT NULL DEFAULT nextval('event_referer_id_seq'::regclass), + key smallint NOT NULL, + referer text, + lastseen timestamp without time zone NOT NULL, + created timestamp without time zone DEFAULT now() NOT NULL +); + +ALTER SEQUENCE event_referer_id_seq OWNED BY event_referer.id; + +CREATE INDEX event_referer_key_idx ON event_referer USING btree (key); + +ALTER TABLE ONLY event_referer ADD CONSTRAINT event_referer_id_pkey PRIMARY KEY (id); +ALTER TABLE ONLY event_referer ADD CONSTRAINT event_referer_referer_key_key UNIQUE (referer, key); + + + +CREATE SEQUENCE event_ua_parsed_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_ua_parsed ( + id BIGINT NOT NULL DEFAULT nextval('event_ua_parsed_id_seq'::regclass), + device text, + browser_name text, + browser_version text, + os_name text, + os_version text, + ua text, + uuid uuid, + modified boolean, + checked boolean DEFAULT false NOT NULL, + key smallint NOT NULL, + created timestamp without time zone DEFAULT now() NOT NULL +); + +ALTER SEQUENCE event_ua_parsed_id_seq OWNED BY event_ua_parsed.id; + +ALTER TABLE ONLY event_ua_parsed ADD CONSTRAINT event_ua_parsed_ua_key_key UNIQUE (ua, key); +ALTER TABLE ONLY event_ua_parsed ADD CONSTRAINT event_ua_parsed_id_pkey PRIMARY KEY (id); + + +CREATE SEQUENCE event_url_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_url ( + id BIGINT NOT NULL DEFAULT nextval('event_url_id_seq'::regclass), + key smallint NOT NULL, + url text, + lastseen timestamp without time zone DEFAULT '1970-01-01 00:00:00'::timestamp without time zone NOT NULL, + created timestamp without time zone DEFAULT now() NOT NULL, + updated timestamp without time zone NOT NULL, + title text, + total_visit integer DEFAULT 0, + total_ip integer DEFAULT 0, + total_device integer DEFAULT 0, + total_account integer DEFAULT 0, + total_country integer DEFAULT 0, + http_code smallint +); + +ALTER SEQUENCE event_url_id_seq OWNED BY event_url.id; + +CREATE INDEX event_url_key_idx ON event_url USING btree (key); +CREATE INDEX event_url_url_idx ON event_url USING btree (url); +CREATE INDEX event_url_lastseen_idx ON event_url USING btree (lastseen); +CREATE INDEX event_url_lastseen_updated_idx ON event_url(lastseen, updated) WHERE lastseen >= updated; + + +ALTER TABLE ONLY event_url ADD CONSTRAINT event_url_id_pkey PRIMARY KEY (id); +ALTER TABLE ONLY event_url ADD CONSTRAINT event_url_url_key_key UNIQUE (url, key); + + +CREATE SEQUENCE event_url_query_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE event_url_query ( + id BIGINT NOT NULL DEFAULT nextval('event_url_id_seq'::regclass), + key smallint NOT NULL, + url integer NOT NULL, + query text, + lastseen timestamp without time zone NOT NULL, + created timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + +ALTER SEQUENCE event_url_query_id_seq OWNED BY event_url_query.id; + +CREATE INDEX event_url_query_url_idx ON event_url_query USING btree (url); +CREATE INDEX event_url_query_key_idx ON event_url_query USING btree (key); + +ALTER TABLE ONLY event_url_query ADD CONSTRAINT event_url_query_id_pkey PRIMARY KEY (id); +ALTER TABLE ONLY event_url_query ADD CONSTRAINT event_url_query_key_url_query_key UNIQUE (key, url, query); + + +CREATE SEQUENCE migrations_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE migrations ( + id BIGINT NOT NULL DEFAULT nextval('migrations_id_seq'::regclass), + name character varying(255) NOT NULL, + created_at timestamp without time zone NOT NULL +); + +ALTER SEQUENCE migrations_id_seq OWNED BY migrations.id; + +ALTER TABLE ONLY migrations ADD CONSTRAINT migrations_id_pkey PRIMARY KEY (id); + + +CREATE SEQUENCE queue_account_operation_id_seq + AS BIGINT + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE queue_account_operation ( + id BIGINT NOT NULL DEFAULT nextval('queue_account_operation_id_seq'::regclass), + created timestamp without time zone DEFAULT now() NOT NULL, + updated timestamp without time zone DEFAULT now() NOT NULL, + event_account bigint, + key smallint NOT NULL, + action queue_account_operation_action NOT NULL, + status queue_account_operation_status DEFAULT 'waiting'::queue_account_operation_status NOT NULL +); + +ALTER SEQUENCE queue_account_operation_id_seq OWNED BY queue_account_operation.id; + +CREATE INDEX queue_account_operation_event_account_key_action_idx ON queue_account_operation USING btree (event_account, key, action); +CREATE INDEX queue_account_operation_status_updated_idx ON queue_account_operation USING btree (status, updated); + +ALTER TABLE ONLY queue_account_operation ADD CONSTRAINT queue_account_operation_id_pkey PRIMARY KEY (id); + + +CREATE TABLE queue_new_events_cursor ( + last_event_id bigint NOT NULL, + locked boolean NOT NULL, + updated timestamp without time zone DEFAULT now() +); + + +-- Triggers -- +CREATE TRIGGER dshb_api_co_owners_creator_check BEFORE INSERT OR UPDATE ON dshb_api_co_owners FOR EACH ROW EXECUTE FUNCTION dshb_api_co_owners_creator_check(); + +CREATE TRIGGER emp_stamp BEFORE UPDATE ON event_account FOR EACH ROW EXECUTE FUNCTION event_lastseen(); +CREATE TRIGGER emp_stamp BEFORE UPDATE ON event_country FOR EACH ROW EXECUTE FUNCTION event_lastseen(); +CREATE TRIGGER emp_stamp BEFORE UPDATE ON event_device FOR EACH ROW EXECUTE FUNCTION event_lastseen(); +CREATE TRIGGER emp_stamp BEFORE UPDATE ON event_ip FOR EACH ROW EXECUTE FUNCTION event_lastseen(); +CREATE TRIGGER emp_stamp BEFORE UPDATE ON event_referer FOR EACH ROW EXECUTE FUNCTION event_lastseen(); +CREATE TRIGGER emp_stamp BEFORE UPDATE ON event_url FOR EACH ROW EXECUTE FUNCTION event_lastseen(); +CREATE TRIGGER emp_stamp BEFORE UPDATE ON event_url_query FOR EACH ROW EXECUTE FUNCTION event_lastseen(); +CREATE TRIGGER emp_stamp BEFORE UPDATE ON event_session FOR EACH ROW EXECUTE FUNCTION event_lastseen(); + +CREATE TRIGGER queue_new_events_cursor_trigger BEFORE INSERT ON queue_new_events_cursor FOR EACH ROW EXECUTE FUNCTION queue_new_events_cursor_check(); + +CREATE TRIGGER restrict_update BEFORE UPDATE ON dshb_operators_rules FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_account FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_country FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_device FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_domain FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_email FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_ip FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_isp FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_phone FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_referer FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_ua_parsed FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_url FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_url_query FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_session FOR EACH ROW EXECUTE FUNCTION restrict_update(); +CREATE TRIGGER restrict_update BEFORE UPDATE ON event_logbook FOR EACH ROW EXECUTE FUNCTION restrict_update(); + +-- Constraints -- +ALTER TABLE ONLY dshb_api ADD CONSTRAINT dshb_api_creator_fkey FOREIGN KEY (creator) REFERENCES dshb_operators(id) ON UPDATE CASCADE ON DELETE CASCADE; + +ALTER TABLE ONLY dshb_manual_check_history ADD CONSTRAINT dshb_manual_check_history_operator_fkey FOREIGN KEY (operator) REFERENCES dshb_operators(id) ON UPDATE CASCADE ON DELETE CASCADE; + +ALTER TABLE ONLY dshb_operators_change_email ADD CONSTRAINT dshb_operators_change_email_operator_id_fkey FOREIGN KEY (operator_id) REFERENCES dshb_operators(id) ON DELETE CASCADE; + +ALTER TABLE ONLY dshb_operators_forgot_password ADD CONSTRAINT dshb_operators_forgot_password_operator_id_fkey FOREIGN KEY (operator_id) REFERENCES dshb_operators(id) ON DELETE CASCADE; + +ALTER TABLE ONLY dshb_operators_rules ADD CONSTRAINT dshb_operators_rules_rule_id_fkey FOREIGN KEY (rule_id) REFERENCES dshb_rules(id) ON DELETE CASCADE; + +ALTER TABLE ONLY event_domain ADD CONSTRAINT event_domain_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON DELETE CASCADE; + +ALTER TABLE ONLY event_country ADD CONSTRAINT event_country_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON DELETE CASCADE; + +ALTER TABLE ONLY event_phone ADD CONSTRAINT event_phone_account_id_key_fkey FOREIGN KEY (account_id, key) REFERENCES event_account(id, key) ON UPDATE CASCADE ON DELETE CASCADE; + +ALTER TABLE ONLY event_isp ADD CONSTRAINT event_isp_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON DELETE CASCADE; + +ALTER TABLE ONLY event_referer ADD CONSTRAINT event_referer_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON DELETE CASCADE; + +ALTER TABLE ONLY event_ua_parsed ADD CONSTRAINT event_ua_parsed_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON DELETE CASCADE; + +ALTER TABLE ONLY event_url ADD CONSTRAINT event_url_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON DELETE CASCADE; + +ALTER TABLE ONLY event_account ADD CONSTRAINT event_account_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON DELETE CASCADE; + +ALTER TABLE ONLY event_session ADD CONSTRAINT event_session_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON DELETE CASCADE; +ALTER TABLE ONLY event_session ADD CONSTRAINT event_session_id_key_fkey FOREIGN KEY (account_id, key) REFERENCES event_account(id, key) ON UPDATE CASCADE ON DELETE CASCADE; + +ALTER TABLE ONLY event_device ADD CONSTRAINT event_device_account_id_key_fkey FOREIGN KEY (account_id, key) REFERENCES event_account(id, key) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER TABLE ONLY event_device ADD CONSTRAINT event_device_user_agent_fkey FOREIGN KEY (user_agent) REFERENCES event_ua_parsed(id); + +ALTER TABLE ONLY queue_account_operation ADD CONSTRAINT queue_account_operation_event_account_fkey FOREIGN KEY (event_account) REFERENCES event_account(id) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER TABLE ONLY queue_account_operation ADD CONSTRAINT queue_account_operation_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON UPDATE CASCADE ON DELETE CASCADE; + +ALTER TABLE ONLY dshb_api_co_owners ADD CONSTRAINT dshb_api_co_owners_api_fkey FOREIGN KEY (api) REFERENCES dshb_api(id) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER TABLE ONLY dshb_api_co_owners ADD CONSTRAINT dshb_api_co_owners_operator_fkey FOREIGN KEY (operator) REFERENCES dshb_operators(id) ON UPDATE CASCADE ON DELETE CASCADE; + +ALTER TABLE ONLY event_email ADD CONSTRAINT event_email_domain_fkey FOREIGN KEY (domain) REFERENCES event_domain(id); +ALTER TABLE ONLY event_email ADD CONSTRAINT event_email_account_id_key_fkey FOREIGN KEY (account_id, key) REFERENCES event_account(id, key) ON UPDATE CASCADE ON DELETE CASCADE; + +ALTER TABLE ONLY event_ip ADD CONSTRAINT event_ip_isp_fkey FOREIGN KEY (isp) REFERENCES event_isp(id); +ALTER TABLE ONLY event_ip ADD CONSTRAINT event_ip_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON DELETE CASCADE; + +ALTER TABLE ONLY event_url_query ADD CONSTRAINT event_url_query_url_fkey FOREIGN KEY (url) REFERENCES event_url(id); +ALTER TABLE ONLY event_url_query ADD CONSTRAINT event_url_query_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON DELETE CASCADE; + +ALTER TABLE ONLY event_logbook ADD CONSTRAINT event_logbook_error_type_fkey FOREIGN KEY (error_type) REFERENCES event_error_type(id); +ALTER TABLE ONLY event_logbook ADD CONSTRAINT event_logbook_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON DELETE CASCADE; + +ALTER TABLE ONLY event ADD CONSTRAINT event_referer_fkey FOREIGN KEY (referer) REFERENCES event_referer(id); +ALTER TABLE ONLY event ADD CONSTRAINT event_type_fkey FOREIGN KEY (type) REFERENCES event_type(id); +ALTER TABLE ONLY event ADD CONSTRAINT event_http_method_fkey FOREIGN KEY (http_method) REFERENCES event_http_method(id); +ALTER TABLE ONLY event ADD CONSTRAINT event_account_fkey FOREIGN KEY (account) REFERENCES event_account(id); +ALTER TABLE ONLY event ADD CONSTRAINT event_ip_fkey FOREIGN KEY (ip) REFERENCES event_ip(id); +ALTER TABLE ONLY event ADD CONSTRAINT event_device_fkey FOREIGN KEY (device) REFERENCES event_device(id); +ALTER TABLE ONLY event ADD CONSTRAINT event_url_fkey FOREIGN KEY (url) REFERENCES event_url(id); +ALTER TABLE ONLY event ADD CONSTRAINT event_query_fkey FOREIGN KEY (query) REFERENCES event_url_query(id); +ALTER TABLE ONLY event ADD CONSTRAINT event_session_id_fkey FOREIGN KEY (session_id) REFERENCES event_session(id); +ALTER TABLE ONLY event ADD CONSTRAINT event_email_fkey FOREIGN KEY (email) REFERENCES event_email(id); +ALTER TABLE ONLY event ADD CONSTRAINT event_phone_fkey FOREIGN KEY (phone) REFERENCES event_phone(id); +ALTER TABLE ONLY event ADD CONSTRAINT event_key_fkey FOREIGN KEY (key) REFERENCES dshb_api(id) ON DELETE CASCADE; + +-- INSERTS -- +INSERT INTO queue_new_events_cursor (last_event_id, locked) VALUES (0, false); + +INSERT INTO event_type (id, value, name) VALUES + (1, 'page_view', 'Page View'), + (2, 'page_edit', 'Page Edit'), + (3, 'page_delete', 'Page Delete'), + (4, 'page_search', 'Page Search'), + (5, 'account_login', 'Login'), + (6, 'account_logout', 'Logout'), + (7, 'account_login_fail', 'Login Fail'), + (8, 'account_registration', 'Registration'), + (9, 'account_email_change', 'Email Change'), + (10, 'account_password_change', 'Password Change'), + (11, 'account_edit', 'Account Edit'); + +INSERT INTO event_http_method (id, value, name) VALUES + (1, 'get', 'GET'), + (2, 'post', 'POST'), + (3, 'head', 'HEAD'), + (4, 'put', 'PUT'), + (5, 'delete', 'DELETE'), + (6, 'patch', 'PATCH'), + (7, 'trace', 'TRACE'), + (8, 'connect', 'CONNECT'), + (9, 'options', 'OPTIONS'), + (10, 'link', 'LINK'), + (11, 'unlink', 'UNLINK'); + +INSERT INTO event_error_type (id, value, name) VALUES + (0, 'success', 'Successful'), + (1, 'validation_error', 'Successful with warnings'), + (2, 'critical_validation_error', 'Request failed'), + (3, 'critical_error', 'Request failed'); + +INSERT INTO dshb_rules (id) VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10), + (11), (12), (13), (14), (15), (16), (17), (18), (19), (20), (21), (22), (23), (24), (25), (26), + (27), (28), (29), (30), (31), (32), (33), (34), (35), (36), (37), (38), (39), (40), (41), (42), + (43), (44), (45), (46), (47), (48), (49), (50), (51), (52), (53), (54), (55), (56), (57), (58), + (59), (60), (61), (62), (63), (64), (65), (66), (67), (68), (69), (70), (71), (72), (73), (74), + (75), (76), (77), (78), (79), (80), (81), (82), (83), (84), (85), (86), (87), (88), (89), (90), + (91), (92), (93), (94), (95), (96), (97), (98), (99), (100), (101), (102), (103), (104), (105), + (106), (107), (108); + +INSERT INTO countries (id, value, serial) VALUES ('AD', 'Andorra', 6), ('AE', 'United Arab Emirates', 236), + ('AF', 'Afghanistan', 1), ('AG', 'Antigua & Barbuda', 10), ('AI', 'Anguilla', 8), ('AL', 'Albania', 3), + ('AM', 'Armenia', 12), ('AN', 'Netherlands Antilles', 252), ('AO', 'Angola', 7), ('AP', 'Asia/Pacific Region', 250), + ('AQ', 'Antarctica', 9), ('AR', 'Argentina', 11), ('AS', 'American Samoa', 5), ('AT', 'Austria', 15), + ('AU', 'Australia', 14), ('AW', 'Aruba', 13), ('AX', 'Åland Islands', 2), ('AZ', 'Azerbaijan', 16), + ('BA', 'Bosnia & Herzegovina', 28), ('BB', 'Barbados', 20), ('BD', 'Bangladesh', 19), ('BE', 'Belgium', 22), + ('BF', 'Burkina Faso', 36), ('BG', 'Bulgaria', 35), ('BH', 'Bahrain', 18), ('BI', 'Burundi', 37), + ('BJ', 'Benin', 24), ('BL', 'St. Barthélemy', 205), ('BM', 'Bermuda', 25), ('BN', 'Brunei', 34), + ('BO', 'Bolivia', 27), ('BQ', 'Caribbean Netherlands', 42), ('BR', 'Brazil', 31), ('BS', 'Bahamas', 17), + ('BT', 'Bhutan', 26), ('BV', 'Bouvet Island', 30), ('BW', 'Botswana', 29), ('BY', 'Belarus', 21), + ('BZ', 'Belize', 23), ('CA', 'Canada', 40), ('CC', 'Cocos (Keeling) Islands', 49), ('CD', 'Congo - Kinshasa', 53), + ('CF', 'Central African Republic', 44), ('CG', 'Congo - Brazzaville', 52), ('CH', 'Switzerland', 216), ('CI', 'Côte d’Ivoire', 56), + ('CK', 'Cook Islands', 54), ('CL', 'Chile', 46), ('CM', 'Cameroon', 39), ('CN', 'China', 47), + ('CO', 'Colombia', 50), ('CR', 'Costa Rica', 55), ('CS', 'Czechoslovakia', 253), ('CU', 'Cuba', 58), + ('CV', 'Cape Verde', 41), ('CW', 'Curaçao', 59), ('CX', 'Christmas Island', 48), ('CY', 'Cyprus', 60), + ('CZ', 'Czechia', 61), ('DE', 'Germany', 85), ('DJ', 'Djibouti', 63), ('DK', 'Denmark', 62), + ('DM', 'Dominica', 64), ('DO', 'Dominican Republic', 65), ('DZ', 'Algeria', 4), ('EC', 'Ecuador', 66), + ('EE', 'Estonia', 71), ('EG', 'Egypt', 67), ('EH', 'Western Sahara', 246), ('ER', 'Eritrea', 70), + ('ES', 'Spain', 203), ('ET', 'Ethiopia', 73), ('EU', 'European Union', 251), ('FI', 'Finland', 77), + ('FJ', 'Fiji', 76), ('FK', 'Falkland Islands', 74), ('FM', 'Micronesia', 143), ('FO', 'Faroe Islands', 75), + ('FR', 'France', 78), ('GA', 'Gabon', 82), ('GB', 'Great Britain', 237), ('GD', 'Grenada', 90), + ('GE', 'Georgia', 84), ('GF', 'French Guiana', 79), ('GG', 'Guernsey', 94), ('GH', 'Ghana', 86), + ('GI', 'Gibraltar', 87), ('GL', 'Greenland', 89), ('GM', 'Gambia', 83), ('GN', 'Guinea', 95), + ('GP', 'Guadeloupe', 91), ('GQ', 'Equatorial Guinea', 69), ('GR', 'Greece', 88), ('GS', 'South Georgia & South Sandwich Islands', 200), + ('GT', 'Guatemala', 93), ('GU', 'Guam', 92), ('GW', 'Guinea-Bissau', 96), ('GY', 'Guyana', 97), + ('HK', 'Hong Kong SAR China', 101), ('HM', 'Heard & McDonald Islands', 99), ('HN', 'Honduras', 100), ('HR', 'Croatia', 57), + ('HT', 'Haiti', 98), ('HU', 'Hungary', 102), ('ID', 'Indonesia', 105), ('IE', 'Ireland', 108), + ('IL', 'Israel', 110), ('IM', 'Isle of Man', 109), ('IN', 'India', 104), ('IO', 'British Indian Ocean Territory', 32), + ('IQ', 'Iraq', 107), ('IR', 'Iran', 106), ('IS', 'Iceland', 103), ('IT', 'Italy', 111), + ('JE', 'Jersey', 114), ('JM', 'Jamaica', 112), ('JO', 'Jordan', 115), ('JP', 'Japan', 113), + ('KE', 'Kenya', 117), ('KG', 'Kyrgyzstan', 120), ('KH', 'Cambodia', 38), ('KI', 'Kiribati', 118), + ('KM', 'Comoros', 51), ('KN', 'St. Kitts & Nevis', 207), ('KP', 'North Korea', 163), ('KR', 'South Korea', 201), + ('KW', 'Kuwait', 119), ('KY', 'Cayman Islands', 43), ('KZ', 'Kazakhstan', 116), ('LA', 'Laos', 121), + ('LB', 'Lebanon', 123), ('LC', 'St. Lucia', 208), ('LI', 'Liechtenstein', 127), ('LK', 'Sri Lanka', 204), + ('LR', 'Liberia', 125), ('LS', 'Lesotho', 124), ('LT', 'Lithuania', 128), ('LU', 'Luxembourg', 129), + ('LV', 'Latvia', 122), ('LY', 'Libya', 126), ('MA', 'Morocco', 149), ('MC', 'Monaco', 145), + ('MD', 'Moldova', 144), ('ME', 'Montenegro', 147), ('MF', 'St. Martin', 209), ('MG', 'Madagascar', 131), + ('MH', 'Marshall Islands', 137), ('MK', 'North Macedonia', 164), ('ML', 'Mali', 135), ('MM', 'Myanmar (Burma)', 151), + ('MN', 'Mongolia', 146), ('MO', 'Macao SAR China', 130), ('MP', 'Northern Mariana Islands', 165), ('MQ', 'Martinique', 138), + ('MR', 'Mauritania', 139), ('MS', 'Montserrat', 148), ('MT', 'Malta', 136), ('MU', 'Mauritius', 140), + ('MV', 'Maldives', 134), ('MW', 'Malawi', 132), ('MX', 'Mexico', 142), ('MY', 'Malaysia', 133), + ('MZ', 'Mozambique', 150), ('N/A', 'Not Available', 0), ('NA', 'Namibia', 152), ('NC', 'New Caledonia', 156), + ('NE', 'Niger', 159), ('NF', 'Norfolk Island', 162), ('NG', 'Nigeria', 160), ('NI', 'Nicaragua', 158), + ('NL', 'Netherlands', 155), ('NO', 'Norway', 166), ('NP', 'Nepal', 154), ('NR', 'Nauru', 153), + ('NU', 'Niue', 161), ('NZ', 'New Zealand', 157), ('OM', 'Oman', 167), ('PA', 'Panama', 171), + ('PE', 'Peru', 174), ('PF', 'French Polynesia', 80), ('PG', 'Papua New Guinea', 172), ('PH', 'Philippines', 175), + ('PK', 'Pakistan', 168), ('PL', 'Poland', 177), ('PM', 'St. Pierre & Miquelon', 210), ('PN', 'Pitcairn Islands', 176), + ('PR', 'Puerto Rico', 179), ('PS', 'Palestinian Territories', 170), ('PT', 'Portugal', 178), ('PW', 'Palau', 169), + ('PY', 'Paraguay', 173), ('QA', 'Qatar', 180), ('RE', 'Réunion', 181), ('RO', 'Romania', 182), + ('RS', 'Serbia', 190), ('RU', 'Russia', 183), ('RW', 'Rwanda', 184), ('SA', 'Saudi Arabia', 188), + ('SB', 'Solomon Islands', 197), ('SC', 'Seychelles', 191), ('SD', 'Sudan', 212), ('SE', 'Sweden', 215), + ('SG', 'Singapore', 193), ('SH', 'St. Helena', 206), ('SI', 'Slovenia', 196), ('SJ', 'Svalbard & Jan Mayen', 214), + ('SK', 'Slovakia', 195), ('SL', 'Sierra Leone', 192), ('SM', 'San Marino', 186), ('SN', 'Senegal', 189), + ('SO', 'Somalia', 198), ('SR', 'Suriname', 213), ('SS', 'South Sudan', 202), ('ST', 'São Tomé & Príncipe', 187), + ('SV', 'El Salvador', 68), ('SX', 'Sint Maarten', 194), ('SY', 'Syria', 217), ('SZ', 'Eswatini', 72), + ('TC', 'Turks & Caicos Islands', 230), ('TD', 'Chad', 45), ('TF', 'French Southern Territories', 81), ('TG', 'Togo', 223), + ('TH', 'Thailand', 221), ('TJ', 'Tajikistan', 219), ('TK', 'Tokelau', 224), ('TL', 'Timor-Leste', 222), + ('TM', 'Turkmenistan', 229), ('TN', 'Tunisia', 227), ('TO', 'Tonga', 225), ('TR', 'Turkey', 228), + ('TT', 'Trinidad & Tobago', 226), ('TV', 'Tuvalu', 231), ('TW', 'Taiwan', 218), ('TZ', 'Tanzania', 220), + ('UA', 'Ukraine', 235), ('UG', 'Uganda', 234), ('UK', 'United Kingdom', 255), ('UM', 'U.S. Outlying Islands', 232), + ('US', 'United States', 238), ('UY', 'Uruguay', 239), ('UZ', 'Uzbekistan', 240), ('VA', 'Vatican City', 242), + ('VC', 'St. Vincent & Grenadines', 211), ('VE', 'Venezuela', 243), ('VG', 'British Virgin Islands', 33), ('VI', 'U.S. Virgin Islands', 233), + ('VN', 'Vietnam', 244), ('VU', 'Vanuatu', 241), ('WF', 'Wallis & Futuna', 245), ('WS', 'Samoa', 185), + ('YE', 'Yemen', 247), ('YT', 'Mayotte', 141), ('YU', 'Yugoslavia', 254), ('ZA', 'South Africa', 199), + ('ZM', 'Zambia', 248), ('ZW', 'Zimbabwe', 249); diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/.github/FUNDING.yml b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/.github/FUNDING.yml new file mode 100644 index 0000000..2491c6a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: ikkez +buy_me_a_coffee: ikkez diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/CHANGELOG.md b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/CHANGELOG.md new file mode 100644 index 0000000..9657e94 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/CHANGELOG.md @@ -0,0 +1,1081 @@ +CHANGELOG + +3.9.1 (09.08.2025) +--- +* fix: return type hint for unserialize, closes [#391](https://github.com/f3-factory/fatfree-core/issues/391) +* feat: Web->slug, specify the separator, [#397](https://github.com/f3-factory/fatfree-core/issues/397) +* feat: added hive pull method, [#395](https://github.com/f3-factory/fatfree-core/issues/395) +* fix: throw exceptions instead of deprecated USER_ERROR [#393](https://github.com/f3-factory/fatfree-core/issues/393) +* fix: error thrown by `read()` for cache, [#392](https://github.com/f3-factory/fatfree-core/issues/392) +* fix: failsafe when passing null to explode parameter + +3.9.0 (29.12.2024) +--- +* feat [#377], DB\SQL: check TABLE_SCHEMA in fields introspection query +* feat [#379]: Added `REROUTE_TRAILING_SLASH` to optionally disable rerouting +* feat [#380], Audit: Adding new mac address validation +* fix [#372], Audit: `isprivate()`, `FILTER_FLAG_NO_PRIV_RANGE` usage +* fix [#378]: websocket handlers call +* fix: php 8.4 session handler compatibility +* fix: various php 8.4 warnings +* fix [#382]: PHP 8.4 `E_STRICT` deprecation fix +* fix [#371]: trim string based routes handler +* fix [bcosca/fatfree#1285]: multi-line template tags +* fix [#375]: PHP 8.4 Fixes for implicit nullability deprecation +* fix [#374]: http_build_query usage deprecation fix +* fix [#369]: unify key string replacement for cache getter/setter + +3.8.2 (24.07.2023) +--- +* feat, Base->format: optimize international date formatting for php8.1+ +* fix, Base->format: keep php7 compatibility in date formatter [#360](https://github.com/f3-factory/fatfree-core/issues/360) +* fix, Markdown: unicode chars not captured correctly for headline slugs [#363](https://github.com/f3-factory/fatfree-core/issues/363) +* fix, Preview->resolve: PHP8+ error, undefined variable $hash [#359](https://github.com/f3-factory/fatfree-core/issues/359) +* fix, Base->clean: PHP8+ error, strip_tags with null value usage + +3.8.1 (05.11.2022) +--- +* fix: CORS preflight request fails to find route for ajax-only definitions [bcosca/fatfree#1242](https://github.com/bcosca/fatfree/issues/1242) +* fix: add realpath to captcha font filepath, [#314](https://github.com/bcosca/fatfree-core/issues/314) +* fix: case-insensitive custom tag matching [#353](https://github.com/bcosca/fatfree-core/issues/353) +* fix: php8 error suppression on invalid locale constant access [bcosca/fatfree#1259](https://github.com/bcosca/fatfree/issues/1259) +* fix: iteration over inaccessible object properties, fixes [#350](https://github.com/bcosca/fatfree-core/issues/350) +* feat: let jig handle utf8 issues more gracefully [#352](https://github.com/bcosca/fatfree-core/issues/352) +* fix: BC issue for pre php74 +* fix: ensure template->parse does render zero text-node, [#354](https://github.com/bcosca/fatfree-core/issues/354) +* fix: DB\SQL\Mapper: allow to pass an empty array as $filter, fixes [bcosca/fatfree#1257](https://github.com/bcosca/fatfree/issues/1257) +* fix: adhoc null fields in Twig are executed as callable, [#310](https://github.com/bcosca/fatfree-core/issues/310) +* fix: ensure merged default PARAMS are properly encoded when building alias, [#345](https://github.com/bcosca/fatfree-core/issues/345) +* fix: Added CORS defaults that are not initialized +* fix: SQL cache schema for $fields +* fix: adhoc field with null value php81 issue, [#339](https://github.com/bcosca/fatfree-core/issues/339) +* fix: check against correct identity flags when using IDENTITY_INSERT for sql server +* added missing file location in error handler [bcosca/fatfree#1255](https://github.com/bcosca/fatfree/issues/1255) +* Web->request, add option to adjust accept-encoding in curl engine [#355](https://github.com/bcosca/fatfree-core/issues/355) + +3.8.0 (15 Feb 2022) +--- +* Feat: allow access to previous session data in cache-based session handler +* Feat: pass session information to onSuspect Session handler +* Fix: PHP 8.1 compatibility fixes [#332](https://github.com/bcosca/fatfree-core/issues/332) [#333](https://github.com/bcosca/fatfree-core/issues/333) +* Fix: check for critical schemes in url validation +* Fix: plural format syntax with empty param, [#325](https://github.com/bcosca/fatfree-core/issues/325) +* Fix: DB mapper not able to fetch field scheme in sqlite views +* Fix: capitalization of array key X-Http-Method-Override in headers [#327](https://github.com/bcosca/fatfree-core/issues/327) +* Fix SMTP: allow RFC2047 encoded words in From/To/Cc/Bcc headers +* Fix: use correct ternary value, [#323](https://github.com/bcosca/fatfree-core/issues/323) +* Fix: trace not present in error handler when in CLI mode and !DEBUG, [#323](https://github.com/bcosca/fatfree-core/issues/323) + +3.7.3 (13 Dec 2020) +--- +* NEW: added auto_increment detection, [bcosca/fatfree#1192](https://github.com/bcosca/fatfree/issues/1192), [bcosca/fatfree#1093](https://github.com/bcosca/fatfree/issues/1093), [bcosca/fatfree#1175](https://github.com/bcosca/fatfree/issues/1175), [#290](https://github.com/bcosca/fatfree-core/issues/290) +* added SMTP dialog error handling, [#317](https://github.com/bcosca/fatfree-core/issues/317) +* Fix: Check active transaction before rollback/commit (PHP8 issue) +* refactored increment/decrement operator to preceed variables +* added error output in CLI mode, [bcosca/fatfree#1185](https://github.com/bcosca/fatfree/issues/1185) +* Set PORT to 80 when SERVER_PORT is an empty string +* Fix: unescape dbname when extracting from dsn, [#316](https://github.com/bcosca/fatfree-core/issues/316) +* Fix: handling of PDO prepare() errors +* Fix: edge case in DB\SQL->schema(): PK not detected in PgSQL when the column is also a FK [bcosca/fatfree#1207](https://github.com/bcosca/fatfree/issues/1207) +* Fix: Escape literal hyphens in regex character classes, [bcosca/fatfree#1206](https://github.com/bcosca/fatfree/issues/1206) +* Fix: error highlighting +* Fix: pagination with order by on virtual fields +* Fixed a couple PHPDOC issues + +3.7.2 (28 May 2020) +--- +* CHANGED, View->sandbox: disable escaping when rendering as text/plain, [bcosca/fatfree#654](https://github.com/bcosca/fatfree/issues/654) +* update HTTP protocol checks, [bcosca/fatfree#1190](https://github.com/bcosca/fatfree/issues/1190) +* Base->clear: close vulnerability on variable compilation, [bcosca/fatfree#1191](https://github.com/bcosca/fatfree/issues/1191) +* DB\SQL\Mapper: fix empty ID after insert, [bcosca/fatfree#1175](https://github.com/bcosca/fatfree/issues/1175) +* DB\SQL\Mapper: fix using correct key variable for grouped sql pagination sets +* Fix return type of 'count' in Cursor->paginate(), [bcosca/fatfree#1187](https://github.com/bcosca/fatfree/issues/1187) +* Bug fix, Web->minify: fix minification of ES6 template literals, [bcosca/fatfree#1178](https://github.com/bcosca/fatfree/issues/1178) +* Bug fix, config: refactoring custom section parser regex, [bcosca/fatfree#1149](https://github.com/bcosca/fatfree/issues/1149) +* Bug fix: token resolve on non-alias reroute paths, [ref. 221f0c9](https://github.com/bcosca/fatfree-core/commit/221f0c930f8664565c9825faeb9ed9af0f7a01c8) +* Websocket: Improved event handler usage +* optimized internal get calls +* only use cached lexicon when a $ttl was given +* only use money_format up until php7.4, [bcosca/fatfree#1174](https://github.com/bcosca/fatfree/issues/1174) + +3.7.1 (30. December 2019) +--- +* Base->build: Add support for brace-enclosed route tokens +* Base->reroute, fix duplicate fragment issue on non-alias routes +* DB\SQL\Mapper: fix empty check for pkey when reloading after insert +* Web->minify: fix minification with multiple files, [bcosca/fatfree#1152](https://github.com/bcosca/fatfree/issues/1152), [#bcosca/fatfree#1169](https://github.com/bcosca/fatfree/issues/1169) + +3.7.0 (26. November 2019) +--- +* NEW: Matrix, added select and walk methods for array processing and validation tools +* NEW: Added configurable file locking via LOCK var +* NEW: json support for dictionary files +* NEW: $die parameter on ONREROUTE hook +* NEW: Added SameSite cookie support for php7.3+ (JAR.samesite), [bcosca/fatfree#1165](https://github.com/bcosca/fatfree/issues/1165) +* NEW, DB\SQL\Mapper: added updateAll method to batch-update multiple records at once +* CHANGED, DB\SQL\Mapper: Throw error on update/erase if the table has no primary key, [#285](https://github.com/bcosca/fatfree-core/issues/285) +* Cache, Redis: Added ability to set a Redis password, [#287](https://github.com/bcosca/fatfree-core/issues/287) +* DB\SQL\Session: make datatype of data column configurable, [bcosca/fatfree#1130](https://github.com/bcosca/fatfree/issues/1130) +* DB\SQL\Mapper: only add adhoc fields in count queries that are used for grouping +* DB\SQL\Mapper: fixed inserting an already loaded record again (duplicating), [bcosca/fatfree#1093](https://github.com/bcosca/fatfree/issues/1093) +* Magic (Mappers): fix isset check on existing properties +* SMTP: added support for Bounce mail recipient ("Sender" header) +* OAuth2: make query string encode type configurable, [#268](https://github.com/bcosca/fatfree-core/issues/268) [#269](https://github.com/bcosca/fatfree-core/issues/269) +* Web: Added more cyrillic letters to diacritics, [bcosca/fatfree#1158](https://github.com/bcosca/fatfree/issues/1158) +* Web: Fixed url string falsely detected as comment section [9ac8e615](https://github.com/bcosca/fatfree-core/commit/9ac8e615ccaf750b49497a3c86161331b24e637f) +* Web: added file inspection for mime-type detection, [#270](https://github.com/bcosca/fatfree-core/issues/270), [bcosca/fatfree#1138](https://github.com/bcosca/fatfree/issues/1138) +* WS: Fixed processing all queued data frames inside the buffer, [#277](https://github.com/bcosca/fatfree-core/issues/277) +* WS: Allow packet size override +* Markdown: Support mixed `strong` and `italic` elements, [#276](https://github.com/bcosca/fatfree-core/issues/276) +* Markdown: Keep spaces around `=` sign in ini code blocks +* Added route alias key name validation, [#243](https://github.com/bcosca/fatfree-core/issues/243) +* Added fragment argument to alias method, [#282](https://github.com/bcosca/fatfree-core/issues/282) +* Allow adding fragment to reroute, [#1156](https://github.com/bcosca/fatfree/issues/1156) +* Added additional HTTP status codes, [#283](https://github.com/bcosca/fatfree-core/issues/283) +* Added X-Forwarded-For IP to log entries, [bcosca/fatfree#1042](https://github.com/bcosca/fatfree/issues/1042) +* Bug fix: broken custom date/time formatting, [bcosca/fatfree#1147](https://github.com/bcosca/fatfree/issues/1147) +* Bug fix: duplicate UI path rendering edge-case in Views and minify, [bcosca/fatfree#1152](https://github.com/bcosca/fatfree/issues/1152) +* Bug fix: unicode chars in custom config section keys, [bcosca/fatfree#1149](https://github.com/bcosca/fatfree/issues/1149) +* Bug fix: ensure valid reroute path in location header, [bcosca/fatfree#1140](https://github.com/bcosca/fatfree/issues/1140) +* Bug fix: use dictionary path for lexicon caching-hash +* Bug fix, php7.3: number format ternary, [bcosca/fatfree#1142](https://github.com/bcosca/fatfree/issues/1142) +* fix PHPdoc and variable inspection, [bcosca/fatfree#865](https://github.com/bcosca/fatfree/issues/865), [bcosca/fatfree#1128](https://github.com/bcosca/fatfree/issues/1128) + +3.6.5 (24 December 2018) +--- +* NEW: Log, added timestamp to each line +* NEW: Auth, added support for custom compare method, [#116](https://github.com/bcosca/fatfree-core/issues/116) +* NEW: cache tag support for mongo & jig mapper, ref [#166](https://github.com/bcosca/fatfree-core/issues/116) +* NEW: Allow PHP functions as template token filters +* Web: Fix double redirect bug when running cURL with open_basedir disabled +* Web: Cope with responses from HTTP/2 servers +* Web->filler: remove very first space, when $std is false +* Web\OAuth2: Cope with HTTP/2 responses +* Web\OAuth2: take Content-Type header into account for json decoding, [#250](https://github.com/bcosca/fatfree-core/issues/250) [#251](https://github.com/bcosca/fatfree-core/issues/251) +* Web\OAuth2: fixed empty results on some endpoints [#250](https://github.com/bcosca/fatfree-core/issues/250) +* DB\SQL\Mapper: optimize mapper->count memory usage +* DB\SQL\Mapper: New table alias operator +* DB\SQL\Mapper: fix count() performance on non-grouped result sets, [bcosca/fatfree#1114](https://github.com/bcosca/fatfree/issues/1114) +* DB\SQL: Support for CTE in postgreSQL, [bcosca/fatfree#1107](https://github.com/bcosca/fatfree/issues/1107), [bcosca/fatfree#1116](https://github.com/bcosca/fatfree/issues/1116), [bcosca/fatfree#1021](https://github.com/bcosca/fatfree/issues/1021) +* DB\SQL->log: Remove extraneous whitespace +* DB\SQL: Added ability to add inline comments per SQL query +* CLI\WS, Refactoring: Streamline socket server +* CLI\WS: Add option for dropping query in OAuth2 URI +* CLI\WS: Add URL-safe base64 encoding +* CLI\WS: Detect errors in returned JSON values +* CLI\WS: Added support for Sec-WebSocket-Protocol header +* Matrix->calendar: Allow unix timestamp as date argument +* Basket: Access basket item by _id [#260](https://github.com/bcosca/fatfree-core/issues/260) +* SMTP: Added TLS 1.2 support [bcosca/fatfree#1115](https://github.com/bcosca/fatfree/issues/1115) +* SMTP->send: Respect $log argument +* Base->cast: recognize binary and octal numbers in config +* Base->cast: add awareness of hexadecimal literals +* Base->abort: Remove unnecessary Content-Encoding header +* Base->abort: Ensure headers have not been flushed +* Base->format: Differentiate between long- and full-date (with localized weekday) formats +* Base->format: Conform with intl extension's number output +* Enable route handler to override Access-Control headers in response to OPTIONS request, [#257](https://github.com/bcosca/fatfree-core/issues/257) +* Augment filters with a var_export function +* Bug fix php7.3: Fix template parse regex to be compatible with strict PCRE2 rules for hyphen placement in a character class +* Bug fix, Cache->set: update creation time when updating existing cache entries +* Bug fix: incorrect ICU date/time formatting +* Bug fix, Jig: lazy write on empty data +* Bug fix: Method uppercase to avoid route failure [#252](https://github.com/bcosca/fatfree-core/issues/252) +* Fixed error description when (PSR-11) `CONTAINER` fails to resolve a class [#253](https://github.com/bcosca/fatfree-core/issues/253) +* Mitigate CSRF predictability/vulnerability +* Expose Mapper->factory() method + +3.6.4 (19 April 2018) +--- +* NEW: Added Dependency Injection support with CONTAINER variable [#221](https://github.com/bcosca/fatfree-core/issues/221) +* NEW: configurable LOGGABLE error codes [#1091](https://github.com/bcosca/fatfree/issues/1091#issuecomment-364674701) +* NEW: JAR.lifetime option, [#178](https://github.com/bcosca/fatfree-core/issues/178) +* Template: reduced Prefab calls +* Template: optimized reflection for better derivative support, [bcosca/fatfree#1088](https://github.com/bcosca/fatfree/issues/1088) +* Template: optimized parsing for template attributes and tokens +* DB\Mongo: fixed logging with mongodb extention +* DB\Jig: added lazy-loading [#7e1cd9b9b89](https://github.com/bcosca/fatfree-core/commit/7e1cd9b9b89c4175d0f6b86ced9d9bd49c04ac39) +* DB\Jig\Mapper: Added group feature, bcosca/fatfree#616 +* DB\SQL\Mapper: fix PostgreSQL RETURNING ID when no pkey is available, [bcosca/fatfree#1069](https://github.com/bcosca/fatfree/issues/1069), [#230](https://github.com/bcosca/fatfree-core/issues/230) +* DB\SQL\Mapper: disable order clause auto-quoting when it's already been quoted +* Web->location: add failsafe for geoip_region_name_by_code() [#GB:Bxyn9xn9AgAJ](https://groups.google.com/d/msg/f3-framework/APau4wnwNzE/Bxyn9xn9AgAJ) +* Web->request: Added proxy support [#e936361b](https://github.com/bcosca/fatfree-core/commit/e936361bc03010c4c7c38a396562e5e96a8a100d) +* Web->mime: Added JFIF format +* Markdown: handle line breaks in paragraph blocks, [bcosca/fatfree#1100](https://github.com/bcosca/fatfree/issues/1100) +* config: reduced cast calls on parsing config sections +* Patch empty SERVER_NAME [bcosca/fatfree#1084](https://github.com/bcosca/fatfree/issues/1084) +* Bugfix: unreliable request headers in Web->request() response [bcosca/fatfree#1092](https://github.com/bcosca/fatfree/issues/1092) +* Fixed, View->render: utilizing multiple UI paths, [bcosca/fatfree#1083](https://github.com/bcosca/fatfree/issues/1083) +* Fixed URL parsing with PHP 5.4 [#247](https://github.com/bcosca/fatfree-core/issues/247) +* Fixed PHP 7.2 warnings when session is active prematurely, [#238](https://github.com/bcosca/fatfree-core/issues/238) +* Fixed setcookie $expire variable type [#240](https://github.com/bcosca/fatfree-core/issues/240) +* Fixed expiration time when updating an existing cookie + +3.6.3 (31 December 2017) +--- +* PHP7 fix: remove deprecated (unset) cast +* Web->request: restricted follow_location to 3XX responses only +* CLI mode: refactored arguments parsing +* CLI mode: fixed query string encoding +* SMTP: Refactor parsing of attachments +* SMTP: clean-up mail headers for multipart messages, [#1065](https://github.com/bcosca/fatfree/issues/1065) +* config: fixed performance issues on parsing config files +* config: cast command parameters in config entries to php type & constant, [#1030](https://github.com/bcosca/fatfree/issues/1030) +* config: reduced registry calls +* config: skip hive escaping when resolving dynamic config vars, [#1030](https://github.com/bcosca/fatfree/issues/1030) +* Bug fix: Incorrect cookie lifetime computation, [#1070](https://github.com/bcosca/fatfree/issues/1070), [#1016](https://github.com/bcosca/fatfree/issues/1016) +* DB\SQL\Mapper: use RETURNING option instead of a sequence query to get lastInsertId in PostgreSQL, [#1069](https://github.com/bcosca/fatfree/issues/1069), [#230](https://github.com/bcosca/fatfree-core/issues/230) +* DB\SQL\Session: check if _agent is too long for SQL based sessions [#236](https://github.com/bcosca/fatfree-core/issues/236) +* DB\SQL\Session: fix Session handler table creation issue on SQL Server, [#899](https://github.com/bcosca/fatfree/issues/899) +* DB\SQL: fix oracle db issue with empty error variable, [#1072](https://github.com/bcosca/fatfree/issues/1072) +* DB\SQL\Mapper: fix sorting issues on SQL Server, [#1052](https://github.com/bcosca/fatfree/issues/1052) [#225](https://github.com/bcosca/fatfree-core/issues/225) +* Prevent directory traversal attacks on filesystem based cache [#1073](https://github.com/bcosca/fatfree/issues/1073) +* Bug fix, Template: PHP constants used in include with attribute, [#983](https://github.com/bcosca/fatfree/issues/983) +* Bug fix, Template: Numeric value in expression alters PHP_EOL context +* Template: use existing linefeed instead of PHP_EOL, [#1048](https://github.com/bcosca/fatfree/issues/1048) +* Template: make newline interpolation handling configurable [#223](https://github.com/bcosca/fatfree-core/issues/223) +* Template: add beforerender to Preview +* fix custom FORMATS without modifiers +* Cache: Refactor Cache->reset for XCache +* Cache: loosen reset cache key pattern, [#1041](https://github.com/bcosca/fatfree/issues/1041) +* XCache: suffix reset only works if xcache.admin.enable_auth is disabled +* Added HTTP 103 as recently approved by the IETF +* LDAP changes to for AD flexibility [#227](https://github.com/bcosca/fatfree-core/issues/227) +* Hide debug trace from ajax errors when DEBUG=0 [#1071](https://github.com/bcosca/fatfree/issues/1071) +* fix View->render using potentially wrong cache entry + +3.6.2 (26 June 2017) +--- +* Return a status code > 0 when dying on error [#220](https://github.com/bcosca/fatfree-core/issues/220) +* fix SMTP line width [#215](https://github.com/bcosca/fatfree-core/issues/215) +* Allow using a custom field for ldap user id checking [#217](https://github.com/bcosca/fatfree-core/issues/217) +* NEW: DB\SQL->exists: generic method to check if SQL table exists +* Pass handler to route handler and hooks [#1035](https://github.com/bcosca/fatfree/issues/1035) +* pass carriage return of multiline dictionary keys +* Better Web->slug customization +* fix incorrect header issue [#211](https://github.com/bcosca/fatfree-core/issues/211) +* fix schema issue on databases with case-sensitive collation, fixes [#209](https://github.com/bcosca/fatfree-core/issues/209) +* Add filter for deriving C-locale equivalent of a number +* Bug fix: @LANGUAGE remains unchanged after override +* abort: added Header pre-check +* Assemble URL after ONREROUTE +* Add reroute argument to skip script termination +* Invoke ONREROUTE after headers are sent +* SQLite switch to backtick as quote +* Bug fix: Incorrect timing in SQL query logs +* DB\SQL\Mapper: Cast return value of count to integer +* Patched $_SERVER['REQUEST_URI'] to ensure it contains a relative URI +* Tweak debug verbosity +* fix php carriage return issue in preview->build [#205](https://github.com/bcosca/fatfree-core/pull/205) +* fixed template string resolution [#205](https://github.com/bcosca/fatfree-core/pull/205) +* Fixed unexpected default seed on CACHE set [#1028](https://github.com/bcosca/fatfree/issues/1028) +* DB\SQL\Mapper: Optimized field escaping on options +* Optimize template conversion to PHP file + +3.6.1 (2 April 2017) +--- +* NEW: Recaptcha plugin [#194](https://github.com/bcosca/fatfree-core/pull/194) +* NEW: MB variable for detecting multibyte support +* NEW: DB\SQL: Cache parsed schema for the TTL duration +* NEW: quick erase flag on Jig/Mongo/SQL mappers [#193](https://github.com/bcosca/fatfree-core/pull/193) +* NEW: Allow OPTIONS method to return a response body [#171](https://github.com/bcosca/fatfree-core/pull/171) +* NEW: Add support for Memcached (bcosca/fatfree#997) +* NEW: Rudimentary preload resource (HTTP2 server) support via template push() +* NEW: Add support for new MongoDB driver [#177](https://github.com/bcosca/fatfree-core/pull/177) +* Changed: template filter are all lowercase now +* Changed: Fix template lookup inconsistency: removed base dir from UI on render +* Changed: count() method now has an options argument [#192](https://github.com/bcosca/fatfree-core/pull/192) +* Changed: SMTP, Spit out error message if any +* \DB\SQL\Mapper: refactored row count strategy +* DB\SQL\Mapper: Allow non-scalar values to be assigned as mapper property +* DB\SQL::PARAM_FLOAT: remove cast to float (#106 and bcosca/fatfree#984) (#191) +* DB\SQL\mapper->erase: allow empty string +* DB\SQL\mapper->insert: fields reset after successful INSERT +* Add option to debounce Cursor->paginate subset [#195](https://github.com/bcosca/fatfree-core/pull/195) +* View: Don't delete sandboxed variables (#198) +* Preview: Optimize compilation of template expressions +* Preview: Use shorthand tag for direct rendering +* Preview->resolve(): new tweak to allow template persistence as option +* Web: Expose diacritics translation table +* SMTP: Enable logging of message body only when $log argument is 'verbose' +* SMTP: Convert headers to camelcase for consistency +* make cache seed more flexible, #164 +* Improve trace details for DEBUG>2 +* Enable config() to read from an array of input files +* Improved alias and reroute regex +* Make camelCase and snakeCase Unicode-aware +* format: Provision for optional whitespaces +* Break APCu-BC dependence +* Old PHP 5.3 cleanup +* Debug log must include HTTP query +* Recognize X-Forwarded-Port header (bcosca/fatfree#1002) +* Avoid use of deprecated mcrypt module +* Return only the client's IP when using the `X-Forwarded-For` header to deduce an IP address +* Remove orphan mutex locks on termination (#157) +* Use 80 as default port number to avoid issues when `$_SERVER['SERVER_PORT']` is not existing +* fread replaced with readfile() for simple send() usecase +* Bug fix: request URI with multiple leading slashes, #203 +* Bug fix: Query generates wrong adhoc field value +* Bug fix: SMTP stream context issue #200 +* Bug fix: child pseudo class selector in minify, bcosca/fatfree#1008 +* Bug fix: "Undefined index: CLI" error (#197) +* Bug fix: cast Cache-Control expire time to int, bcosca/fatfree#1004 +* Bug fix: Avoid issuance of multiple Content-Type headers for nested templates +* Bug fix: wildcard token issue with digits (bcosca/fatfree#996) +* Bug fix: afterupdate ignored when row does not change +* Bug fix: session handler read() method for PHP7 (need strict string) #184 #185 +* Bug fix: reroute mocking in CLI mode (#183) +* Bug fix: Reroute authoritative relative references (#181) +* Bug fix: locales order and charset hyphen +* Bug fix: base stripped twice in router (#176) + +3.6.0 (19 November 2016) +--- +* NEW: [cli] request type +* NEW: console-friendly CLI mode +* NEW: lexicon caching +* NEW: Silent operator skips startup error check (#125) +* NEW: DB\SQL->trans() +* NEW: custom config section parser, i.e. [conf > Foo::bar] +* NEW: support for cache tags in SQL +* NEW: custom FORMATS +* NEW: Mongo mapper fields whitelist +* NEW: WebSocket server +* NEW: Base->extend method (#158) +* NEW: Implement framework variable caching via config, i.e. FOO = "bar" | 3600 +* NEW: Lightweight OAuth2 client +* NEW: SEED variable, configurable app-specific hashing prefix (#149, bcosca/fatfree#951, bcosca/fatfree#884, bcosca/fatfree#629) +* NEW: CLI variable +* NEW: Web->send, specify custom filename (#124) +* NEW: Web->send, added flushing flag (#131) +* NEW: Indexed route wildcards, now exposed in PARAMS['*'] +* Changed: PHP 5.4 is now the minimum version requirement +* Changed: Prevent database wrappers from being cloned +* Changed: Router works on PATH instead of URI (#126) NB: PARAMS.0 no longer contains the query string +* Changed: Removed ALIASES autobuilding (#118) +* Changed: Route wildcards match empty strings (#119) +* Changed: Disable default debug highlighting, HIGHLIGHT is false now +* General PHP 5.4 optimizations +* Optimized config parsing +* Optimized Base->recursive +* Optimized header extraction +* Optimized cache/expire headers +* Optimized session_start behaviour (bcosca/fatfree#673) +* Optimized reroute regex +* Tweaked cookie removal +* Better route precedence order +* Performance tweak: reduced cache calls +* Refactored lexicon (LOCALES) build-up, much faster now +* Added turkish locale bug workaround +* Geo->tzinfo Update to UTC +* Added Xcache reset (bcosca/fatfree#928) +* Redis cache: allow db name in dsn +* SMTP: Improve server emulation responses +* SMTP: Optimize transmission envelope +* SMTP: Implement mock transmission +* SMTP: Various bug fixes and feature improvements +* SMTP: quit on failed authentication +* Geo->weather: force metric units +* Base->until: Implement CLI interoperability +* Base->format: looser plural syntax +* Base->format: Force decimal as default number format +* Base->merge: Added $keep flag to save result to the hive key +* Base->reroute: Allow array as URL argument for aliasing +* Base->alias: Allow query string (or array) to be appended to alias +* Permit reroute to named routes with URL query segment +* Sync COOKIE global on set() +* Permit non-hive variables to use JS dot notation +* RFC2616: Use absolute URIs for Location header +* Matrix->calendar: Check if calendar extension is loaded +* Markdown: require start of line/whitespace for text processing (#136) +* DB\[SQL|Jig|Mongo]->log(FALSE) disables logging +* DB\SQL->exec: Added timestamp toggle to db log +* DB\SQL->schema: Remove unnecessary line terminators +* DB\SQL\Mapper: allow array filter with empty string +* DB\SQL\Mapper: optimized handling for key-less tables +* DB\SQL\Mapper: added float support (#106) +* DB\SQL\Session: increased default column sizes (#148, bcosca/fatfree#931, bcosca/fatfree#950) +* Web: Catch cURL errors +* Optimize Web->receive (bcosca/fatfree#930) +* Web->minify: fix arbitrary file download vulnerability +* Web->request: fix cache control max-age detection (bcosca/fatfree#908) +* Web->request: Add request headers & error message to return value (bcosca/fatfree#737) +* Web->request: Refactored response to HTTP request +* Web->send flush while sending big files +* Image->rgb: allow hex strings +* Image->captcha: Check if GD module supports TrueType +* Image->load: Return FALSE on load failure +* Image->resize: keep aspect ratio when only width or height was given +* Updated OpenID lib (bcosca/fatfree#965) +* Audit->card: add new mastercard "2" BIN range (bcosca/fatfree#954) +* Deprecated: Bcrypt class +* Preview->render: optimized detection to remove short open PHP tags and allow xml tags (#133) +* Display file and line number in exception handler (bcosca/fatfree#967) +* Added error reporting level to Base->error and ERROR.level (bcosca/fatfree#957) +* Added optional custom cache instance to Session (#141) +* CLI-aware mock() +* XFRAME and PACKAGE can be switched off now (#128) +* Bug fix: wrong time calculation on memcache reset (#170) +* Bug fix: encode CLI parameters +* Bug fix: Close connection on abort explicitly (#162) +* Bug fix: Image->identicon, Avoid double-size sprite rotation (and possible segfault) +* Bug fix: Image->render and Image->dump, removed unnecessary 2nd argument (#146) +* Bug fix: Magic->offsetset, access property as array element (#147) +* Bug fix: multi-line custom template tag parsing (bcosca/fatfree#935) +* Bug fix: cache headers on errors (bcosca/fatfree#885) +* Bug fix: Web, deprecated CURLOPT_SSL_VERIFYHOST in curl +* Bug fix: Web, Invalid user error constant (bcosca/fatfree#962) +* Bug fix: Web->request, redirections for domain-less location (#135) +* Bug fix: DB\SQL\Mapper, reset changed flag after update (#142, #152) +* Bug fix: DB\SQL\Mapper, fix changed flag when using assignment operator #143 #150 #151 +* Bug fix: DB\SQL\Mapper, revival of the HAVING clause +* Bug fix: DB\SQL\Mapper, pgsql with non-integer primary keys (bcosca/fatfree#916) +* Bug fix: DB\SQL\Session, quote table name (bcosca/fatfree#977) +* Bug fix: snakeCase returns word starting with underscore (bcosca/fatfree#927) +* Bug fix: mock does not populate PATH variable +* Bug fix: Geo->weather API key (#129) +* Bug fix: Incorrect compilation of array element with zero index +* Bug fix: Compilation of array construct is incorrect +* Bug fix: Trailing slash redirection on UTF-8 paths (#121) + +3.5.1 (31 December 2015) +--- +* NEW: ttl attribute in template tag +* NEW: allow anonymous function for template filter +* NEW: format modifier for international and custom currency symbol +* NEW: Image->data() returns image resource +* NEW: extract() get prefixed array keys from an assoc array +* NEW: Optimized and faster Template parser with full support for HTML5 empty tags +* NEW: Added support for {@token} encapsulation syntax in routes definition +* NEW: DB\SQL->exec(), automatically shift to 1-based query arguments +* NEW: abort() flush output +* Added referenced value to devoid() +* Template token filters are now resolved within Preview->token() +* Web->_curl: restrict redirections to HTTP +* Web->minify(), skip importing of external files +* Improved session and error handling in until() +* Get the error trace array with the new $format parameter +* Better support for unicode URLs +* Optimized TZ detection with date_default_timezone_get() +* format() Provide default decimal places +* Optimize code: remove redundant TTL checks +* Optimized timeout handling in Web->request() +* Improved PHPDoc hints +* Added missing russian DIACRITICS letters +* DB\Cursor: allow child implementation of reset() +* DB\Cursor: Copyfrom now does an internal call to set() +* DB\SQL: Provide the ability to disable SQL logging +* DB\SQL: improved query analysis to trigger fetchAll +* DB\SQL\Mapper: added support for binary table columns +* SQL,JIG,MONGO,CACHE Session handlers refactored and optimized +* SMTP Refactoring and optimization +* Bug fix: SMTP, Align quoted_printable_encode() with SMTP specs (dot-stuffing) +* Bug fix: SMTP, Send buffered optional headers to output +* Bug fix: SMTP, Content-Transfer-Encoding for non-TLS connections +* Bug fix: SMTP, Single attachment error +* Bug fix: Cursor->load not always mapping to first record +* Bug fix: dry SQL mapper should not trigger 'load' +* Bug fix: Code highlighting on empty text +* Bug fix: Image->resize, round dimensions instead of cast +* Bug fix: whitespace handling in $f3->compile() +* Bug fix: TTL of `View` and `Preview` (`Template`) +* Bug fix: token filter regex +* Bug fix: Template, empty attributes +* Bug fix: Preview->build() greedy regex +* Bug fix: Web->minify() single-line comment on last line +* Bug fix: Web->request(), follow_location with cURL and open_basedir +* Bug fix: Web->send() Single quotes around filename not interpreted correctly by some browsers + +3.5.0 (2 June 2015) +--- +* NEW: until() method for long polling +* NEW: abort() to disconnect HTTP client (and continue execution) +* NEW: SQL Mapper->required() returns TRUE if field is not nullable +* NEW: PREMAP variable for allowing prefixes to handlers named after HTTP verbs +* NEW: [configs] section to allow config includes +* NEW: Test->passed() returns TRUE if no test failed +* NEW: SQL mapper changed() function +* NEW: fatfree-core composer support +* NEW: constants() method to expose constants +* NEW: Preview->filter() for configurable token filters +* NEW: CORS variable for Cross-Origin Resource Sharing support, #731 +* Change in behavior: Switch to htmlspecialchars for escaping +* Change in behavior: No movement in cursor position after erase(), #797 +* Change in behavior: ERROR.trace is a multiline string now +* Change in behavior: Strict token recognition in href attribute +* Router fix: loose method search +* Better route precedence order, #12 +* Preserve contents of ROUTES, #723 +* Alias: allow array of parameters +* Improvements on reroute method +* Fix for custom Jig session files +* Audit: better mobile detection +* Audit: add argument to test string as browser agent +* DB mappers: abort insert/update/erase from hooks, #684 +* DB mappers: Allow array inputs in copyfrom() +* Cache,SQL,Jig,Mongo Session: custom callback for suspect sessions +* Fix for unexpected HIVE values when defining an empty HIVE array +* SQL mapper: check for results from CALL and EXEC queries, #771 +* SQL mapper: consider SQL schema prefix, #820 +* SQL mapper: write to log before execution to + enable tracking of PDOStatement error +* Add SQL Mapper->table() to return table name +* Allow override of the schema in SQL Mapper->schema() +* Improvement: Keep JIG table as reference, #758 +* Expand regex to include whitespaces in SQL DB dsn, #817 +* View: Removed reserved variables $fw and $implicit +* Add missing newlines after template expansion +* Web->receive: fix for complex field names, #806 +* Web: Improvements in socket engine +* Web: customizable user_agent for all engines, #822 +* SMTP: Provision for Content-ID in attachments +* Image + minify: allow absolute paths +* Promote framework error to E_USER_ERROR +* Geo->weather switch to OpenWeather +* Expose mask() and grab() methods for routing +* Expose trace() method to expose the debug backtrace +* Implement recursion strategy using IteratorAggregate, #714 +* Exempt whitespace between % and succeeding operator from being minified, #773 +* Optimized error detection and ONERROR handler, fatfree-core#18 +* Tweak error log output +* Optimized If-Modified-Since cache header usage +* Improved APCu compatibility, #724 +* Bug fix: Web::send fails on filename with spaces, #810 +* Bug fix: overwrite limit in findone() +* Bug fix: locale-specific edge cases affecting SQL schema, #772 +* Bug fix: Newline stripping in config() +* Bug fix: bracket delimited identifier for sybase and dblib driver +* Bug fix: Mongo mapper collection->count driver compatibility +* Bug fix: SQL Mapper->set() forces adhoc value if already defined +* Bug fix: Mapper ignores HAVING clause +* Bug fix: Constructor invocation in call() +* Bug fix: Wrong element returned by ajax/sync request +* Bug fix: handling of non-consecutive compound key members +* Bug fix: Virtual fields not retrieved when group option is present, #757 +* Bug fix: group option generates incorrect SQL query, #757 +* Bug fix: ONERROR does not receive PARAMS on fatal error + +3.4.0 (1 January 2015) +--- +* NEW: [redirects] section +* NEW: Custom config sections +* NEW: User-defined AUTOLOAD function +* NEW: ONREROUTE variable +* NEW: Provision for in-memory Jig database (#727) +* Return run() result (#687) +* Pass result of run() to mock() (#687) +* Add port suffix to REALM variable +* New attribute in tag to extend hive +* Adjust unit tests and clean up templates +* Expose header-related methods +* Web->request: allow content array +* Preserve contents of ROUTES (#723) +* Smart detection of PHP functions in template expressions +* Add afterrender() hook to View class +* Implement ArrayAccess and magic properties on hive +* Improvement on mocking of superglobals and request body +* Fix table creation for pgsql handled sessions +* Add QUERY to hive +* Exempt E_NOTICE from default error_reporting() +* Add method to build alias routes from template, fixes #693 +* Fix dangerous caching of cookie values +* Fix multiple encoding in nested templates +* Fix node attribute parsing for empty/zero values +* Apply URL encoding on BASE to emulate v2 behavior (#123) +* Improve Base->map performance (#595) +* Add simple backtrace for fatal errors +* Count Cursor->load() results (#581) +* Add form field name to Web->receive() callback arguments +* Fix missing newlines after template expansion +* Fix overwrite of ENCODING variable +* limit & offset workaround for SQL Server, fixes #671 +* SQL Mapper->find: GROUP BY SQL compliant statement +* Bug fix: Missing abstract method fields() +* Bug fix: Auto escaping does not work with mapper objects (#710) +* Bug fix: 'with' attribute in tag raise error when no token + inside +* View rendering: optional Content-Type header +* Bug fix: Undefined variable: cache (#705) +* Bug fix: Routing does not work if project base path includes valid + special URI character (#704) +* Bug fix: Template hash collision (#702) +* Bug fix: Property visibility is incorrect (#697) +* Bug fix: Missing Allow header on HTTP 405 response +* Bug fix: Double quotes in lexicon files (#681) +* Bug fix: Space should not be mandatory in ICU pluralization format string +* Bug fix: Incorrect log entry when SQL query contains a question mark +* Bug fix: Error stack trace +* Bug fix: Cookie expiration (#665) +* Bug fix: OR operator (||) parsed incorrectly +* Bug fix: Routing treatment of * wildcard character +* Bug fix: Mapper copyfrom() method doesn't allow class/object callbacks + (#590) +* Bug fix: exists() creates elements/properties (#591) +* Bug fix: Wildcard in routing pattern consumes entire query string (#592) +* Bug fix: Workaround bug in latest MongoDB driver +* Bug fix: Default error handler silently fails for AJAX request with + DEBUG>0 (#599) +* Bug fix: Mocked BODY overwritten (#601) +* Bug fix: Undefined pkey (#607) + +3.3.0 (8 August 2014) +--- +* NEW: Attribute in tag to extend hive +* NEW: Image overlay with transparency and alignment control +* NEW: Allow redirection of specified route patterns to a URL +* Bug fix: Missing AND operator in SQL Server schema query (Issue #576) +* Count Cursor->load() results (Feature request #581) +* Mapper copyfrom() method doesn't allow class/object callbacks (Issue #590) +* Bug fix: exists() creates elements/properties (Issue #591) +* Bug fix: Wildcard in routing pattern consumes entire query string + (Issue #592) +* Tweak Base->map performance (Issue #595) +* Bug fix: Default error handler silently fails for AJAX request with + DEBUG>0 (Issue #599) +* Bug fix: Mocked BODY overwritten (Issue #601) +* Bug fix: Undefined pkey (Issue #607) +* Bug fix: beforeupdate() position (Issue #633) +* Bug fix: exists() return value for cached keys +* Bug fix: Missing error code in UNLOAD handler +* Bug fix: OR operator (||) parsed incorrectly +* Add input name parameter to custom slug function +* Apply URL encoding on BASE to emulate v2 behavior (Issue #123) +* Reduce mapper update() iterations +* Bug fix: Routing treatment of * wildcard character +* SQL Mapper->find: GROUP BY SQL compliant statement +* Work around bug in latest MongoDB driver +* Work around probable race condition and optimize cache access +* View rendering: Optional Content-Type header +* Fix missing newlines after template expansion +* Add form field name to Web->receive() callback arguments +* Quick reference: add RAW variable + +3.2.2 (19 March 2014) +--- +* NEW: Locales set automatically (Feature request #522) +* NEW: Mapper dbtype() +* NEW: before- and after- triggers for all mappers +* NEW: Decode HTML5 entities if PHP>5.3 detected (Feature request #552) +* NEW: Send credentials only if AUTH is present in the SMTP extension + response (Feature request #545) +* NEW: BITMASK variable to allow ENT_COMPAT override +* NEW: Redis support for caching +* Enable SMTP feature detection +* Enable extended ICU custom date format (Feature request #555) +* Enable custom time ICU format +* Add option to turn off session table creation (Feature request #557) +* Enhanced template token rendering and custom filters (Feature request + #550) +* Avert multiple loads in DB-managed sessions (Feature request #558) +* Add EXEC to associative fetch +* Bug fix: Building template tokens breaks on inline OR condition (Issue + #573) +* Bug fix: SMTP->send does not use the $log parameter (Issue #571) +* Bug fix: Allow setting sqlsrv primary keys on insert (Issue #570) +* Bug fix: Generated query for obtaining table schema in sqlsrv incorrect + (Bug #565) +* Bug fix: SQL mapper flag set even when value has not changed (Bug #562) +* Bug fix: Add XFRAME config option (Feature request #546) +* Bug fix: Incorrect parsing of comments (Issue #541) +* Bug fix: Multiple Set-Cookie headers (Issue #533) +* Bug fix: Mapper is dry after save() +* Bug fix: Prevent infinite loop when error handler is triggered + (Issue #361) +* Bug fix: Mapper tweaks not passing primary keys as arguments +* Bug fix: Zero indexes in dot-notated arrays fail to compile +* Bug fix: Prevent GROUP clause double-escaping +* Bug fix: Regression of zlib compression bug +* Bug fix: Method copyto() does not include ad hoc fields +* Check existence of OpenID mode (Issue #529) +* Generate a 404 when a tokenized class doesn't exist +* Fix SQLite quotes (Issue #521) +* Bug fix: BASE is incorrect on Windows + +3.2.1 (7 January 2014) +--- +* NEW: EMOJI variable, UTF->translate(), UTF->emojify(), and UTF->strrev() +* Allow empty strings in config() +* Add support for turning off php://input buffering via RAW + (FALSE by default) +* Add Cursor->load() and Cursor->find() TTL support +* Support Web->receive() large file downloads via PUT +* ONERROR safety check +* Fix session CSRF cookie detection +* Framework object now passed to route handler contructors +* Allow override of DIACRITICS +* Various code optimizations +* Support log disabling (Issue #483) +* Implicit mapper load() on authentication +* Declare abstract methods for Cursor derivatives +* Support single-quoted HTML/XML attributes (Feature request #503) +* Relax property visibility of mappers and derivatives +* Deprecated: {{~ ~}} instructions and {{* *}} comments; Use {~ ~} and + {* *} instead +* Minor fix: Audit->ipv4() return value +* Bug fix: Backslashes in BASE not converted on Windows +* Bug fix: UTF->substr() with negative offset and specified length +* Bug fix: Replace named URL tokens on render() +* Bug fix: BASE is not empty when run from document root +* Bug fix: stringify() recursion + +3.2.0 (18 December 2013) +--- +* NEW: Automatic CSRF protection (with IP and User-Agent checks) for + sessions mapped to SQL-, Jig-, Mongo- and Cache-based backends +* NEW: Named routes +* NEW: PATH variable; returns the URL relative to BASE +* NEW: Image->captcha() color parameters +* NEW: Ability to access MongoCuror thru the cursor() method +* NEW: Mapper->fields() method returns array of field names +* NEW: Mapper onload(), oninsert(), onupdate(), and onerase() event + listeners/triggers +* NEW: Preview class (a lightweight template engine) +* NEW: rel() method derives path from URL relative to BASE; useful for + rerouting +* NEW: PREFIX variable for prepending a string to a dictionary term; + Enable support for prefixed dictionary arrays and .ini files (Feature + request #440) +* NEW: Google static map plugin +* NEW: devoid() method +* Introduce clean(); similar to scrub(), except that arg is passed by + value +* Use $ttl for cookie expiration (Issue #457) +* Fix needs_rehash() cost comparison +* Add pass-by-reference argument to exists() so if method returns TRUE, + a subsequent get() is unnecessary +* Improve MySQL support +* Move esc(), raw(), and dupe() to View class where they more + appropriately belong +* Allow user-defined fields in SQL mapper constructor (Feature request + #450) +* Re-implement the pre-3.0 template resolve() feature +* Remove redundant instances of session_commit() +* Add support for input filtering in Mapper->copyfrom() +* Prevent intrusive behavior of Mapper->copyfrom() +* Support multiple SQL primary keys +* Support custom tag attributes/inline tokens defined at runtime + (Feature request #438) +* Broader support for HTTP basic auth +* Prohibit Jig _id clear() +* Add support for detailed stringify() output +* Add base directory to UI path as fallback +* Support Test->expect() chaining +* Support __tostring() in stringify() +* Trigger error on invalid CAPTCHA length (Issue #458) +* Bug fix: exists() pass-by-reference argument returns incorrect value +* Bug fix: DB Exec does not return affected row if query contains a + sub-SELECT (Issue #437) +* Improve seed generator and add code for detecting of acceptable + limits in Image->captcha() (Feature request #460) +* Add decimal format ICU extension +* Bug fix: 404-reported URI contains HTTP query +* Bug fix: Data type detection in DB->schema() +* Bug fix: TZ initialization +* Bug fix: paginate() passes incorrect argument to count() +* Bug fix: Incorrect query when reloading after insert() +* Bug fix: SQL preg_match error in pdo_type matching (Issue #447) +* Bug fix: Missing merge() function (Issue #444) +* Bug fix: BASE misdefined in command line mode +* Bug fix: Stringifying hive may run infinite (Issue #436) +* Bug fix: Incomplete stringify() when DEBUG<3 (Issue #432) +* Bug fix: Redirection of basic auth (Issue #430) +* Bug fix: Filter only PHP code (including short tags) in templates +* Bug fix: Markdown paragraph parser does not convert PHP code blocks + properly +* Bug fix: identicon() colors on same keys are randomized +* Bug fix: quotekey() fails on aliased keys +* Bug fix: Missing _id in Jig->find() return value +* Bug fix: LANGUAGE/LOCALES handling +* Bug fix: Loose comparison in stringify() + +3.1.2 (5 November 2013) +--- +* Abandon .chm help format; Package API documentation in plain HTML; + (Launch lib/api/index.html in your browser) +* Deprecate BAIL in favor of HALT (default: TRUE) +* Revert to 3.1.0 autoload behavior; Add support for lowercase folder + names +* Allow Spring-style HTTP method overrides +* Add support for SQL Server-based sessions +* Capture full X-Forwarded-For header +* Add protection against malicious scripts; Extra check if file was really + uploaded +* Pass-thru page limit in return value of Cursor->paginate() +* Optimize code: Implement single-pass escaping +* Short circuit Jig->find() if source file is empty +* Bug fix: PHP globals passed by reference in hive() result (Issue #424) +* Bug fix: ZIP mime type incorrect behavior +* Bug fix: Jig->erase() filter malfunction +* Bug fix: Mongo->select() group +* Bug fix: Unknown bcrypt constant + +3.1.1 (13 October 2013) +--- +* NEW: Support OpenID attribute exchange +* NEW: BAIL variable enables/disables continuance of execution on non-fatal + errors +* Deprecate BAIL in favor of HALT (default: FALSE) +* Add support for Oracle +* Mark cached queries in log (Feature Request #405) +* Implement Bcrypt->needs_reshash() +* Add entropy to SQL cache hash; Add uuid() method to DB backends +* Find real document root; Simplify debug paths +* Permit OpenID required fields to be declared as comma-separated string or + array +* Pass modified filename as argument to user-defined function in + Web->receive() +* Quote keys in optional SQL clauses (Issue #408) +* Allow UNLOAD to override fatal error detection (Issue #404) +* Mutex operator precedence error (Issue #406) +* Bug fix: exists() malfunction (Issue #401) +* Bug fix: Jig mapper triggers error when loading from CACHE (Issue #403) +* Bug fix: Array index check +* Bug fix: OpenID verified() return value +* Bug fix: Basket->find() should return a set of results (Issue #407); + Also implemented findone() for consistency with mappers +* Bug fix: PostgreSQL last insert ID (Issue #410) +* Bug fix: $port component URL overwritten by _socket() +* Bug fix: Calculation of elapsed time + +3.1.0 (20 August 2013) +--- +* NEW: Web->filler() returns a chunk of text from the standard + Lorem Ipsum passage +* Change in behavior: Drop support for JSON serialization +* SQL->exec() now returns value of RETURNING clause +* Add support for $ttl argument in count() (Issue #393) +* Allow UI to be overridden by custom $path +* Return result of PDO primitives: begintransaction(), rollback(), and + commit() +* Full support for PHP 5.5 +* Flush buffers only when DEBUG=0 +* Support class->method, class::method, and lambda functions as + Web->basic() arguments +* Commit session on Basket->save() +* Optional enlargement in Image->resize() +* Support authentication on hosts running PHP-CGI +* Change visibility level of Cache properties +* Prevent ONERROR recursion +* Work around Apache pre-2.4 VirtualDocumentRoot bug +* Prioritize cURL in HTTP engine detection +* Bug fix: Minify tricky JS +* Bug fix: desktop() detection +* Bug fix: Double-slash on TEMP-relative path +* Bug fix: Cursor mapping of first() and last() records +* Bug fix: Premature end of Web->receive() on multiple files +* Bug fix: German umlaute to its corresponding grammatically-correct + equivalent + +3.0.9 (12 June 2013) +--- +* NEW: Web->whois() +* NEW: Template tags +* Improve CACHE consistency +* Case-insensitive MIME type detection +* Support pre-PHP 5.3.4 in Prefab->instance() +* Refactor isdesktop() and ismobile(); Add isbot() +* Add support for Markdown strike-through +* Work around ODBC's lack of quote() support +* Remove useless Prefab destructor +* Support multiple cache instances +* Bug fix: Underscores in OpenId keys mangled +* Refactor format() +* Numerous tweaks +* Bug fix: MongoId object not preserved +* Bug fix: Double-quotes included in lexicon() string (Issue #341) +* Bug fix: UTF-8 formatting mangled on Windows (Issue #342) +* Bug fix: Cache->load() error when CACHE is FALSE (Issue #344) +* Bug fix: send() ternary expression +* Bug fix: Country code constants + +3.0.8 (17 May 2013) +--- +* NEW: Bcrypt lightweight hashing library\ +* Return total number of records in superset in Cursor->paginate() +* ONERROR short-circuit (Enhancement #334) +* Apply quotes/backticks on DB identifiers +* Allow enabling/disabling of SQL log +* Normalize glob() behavior (Issue #330) +* Bug fix: mbstring 2-byte text truncation (Issue #325) +* Bug fix: Unsupported operand types (Issue #324) + +3.0.7 (2 May 2013) +--- +* NEW: route() now allows an array of routing patterns as first argument; + support array as first argument of map() +* NEW: entropy() for calculating password strength (NIST 800-63) +* NEW: AGENT variable containing auto-detected HTTP user agent string +* NEW: ismobile() and isdesktop() methods +* NEW: Prefab class and descendants now accept constructor arguments +* Change in behavior: Cache->exists() now returns timestamp and TTL of + cache entry or FALSE if not found (Feature request #315) +* Preserve timestamp and TTL when updating cache entry (Feature request + #316) +* Improved currency formatting with C99 compliance +* Suppress unnecessary program halt at startup caused by misconfigured + server +* Add support for dashes in custom attribute names in templates +* Bug fix: Routing precedene (Issue #313) +* Bug fix: Remove Jig _id element from document property +* Bug fix: Web->rss() error when not enough items in the feed (Issue #299) +* Bug fix: Web engine fallback (Issue #300) +* Bug fix: and formatting +* Bug fix: Text rendering of text with trailing punctuation (Issue #303) +* Bug fix: Incorrect regex in SMTP + +3.0.6 (31 Mar 2013) +--- +* NEW: Image->crop() +* Modify documentation blocks for PHPDoc interoperability +* Allow user to control whether Base->rerouet() uses a permanent or + temporary redirect +* Allow JAR elements to be set individually +* Refactor DB\SQL\Mapper->insert() to cope with autoincrement fields +* Trigger error when captcha() font is missing +* Remove unnecessary markdown regex recursion +* Check for scalars instead of DB\SQL strings +* Implement more comprehensive diacritics table +* Add option for disabling 401 errors when basic auth() fails +* Add markdown syntax highlighting for Apache configuration +* Markdown->render() deprecated to remove dependency on UI variable; + Feature replaced by Markdown->convert() to enable translation from + markdown string to HTML +* Optimize factory() code of all data mappers +* Apply backticks on MySQL table names +* Bug fix: Routing failure when directory path contains a tilde (Issue #291) +* Bug fix: Incorrect markdown parsing of strong/em sequences and inline HTML +* Bug fix: Cached page not echoed (Issue #278) +* Bug fix: Object properties not escaped when rendering +* Bug fix: OpenID error response ignored +* Bug fix: memcache_get_extended_stats() timeout +* Bug fix: Base->set() doesn't pass TTL to Cache->set() +* Bug fix: Base->scrub() ignores pass-thru * argument (Issue #274) + +3.0.5 (16 Feb 2013) +--- +* NEW: Markdown class with PHP, HTML, and .ini syntax highlighting support +* NEW: Options for caching of select() and find() results +* NEW: Web->acceptable() +* Add send() argument for forcing downloads +* Provide read() option for applying Unix LF as standard line ending +* Bypass lexicon() call if LANGUAGE is undefined +* Load fallback language dictionary if LANGUAGE is undefined +* map() now checks existence of class/methods for non-tokenized URLs +* Improve error reporting of non-existent Template methods +* Address output buffer issues on some servers +* Bug fix: Setting DEBUG to 0 won't suppress the stack trace when the + content type is application/json (Issue #257) +* Bug fix: Image dump/render additional arguments shifted +* Bug fix: ob_clean() causes buffer issues with zlib compression +* Bug fix: minify() fails when commenting CSS @ rules (Issue #251) +* Bug fix: Handling of commas inside quoted strings +* Bug fix: Glitch in stringify() handling of closures +* Bug fix: dry() in mappers returns TRUE despite being hydrated by + factory() (Issue #265) +* Bug fix: expect() not handling flags correctly +* Bug fix: weather() fails when server is unreachable + +3.0.4 (29 Jan 2013) +--- +* NEW: Support for ICU/CLDR pluralization +* NEW: User-defined FALLBACK language +* NEW: minify() now recognizes CSS @import directives +* NEW: UTF->bom() returns byte order mark for UTF-8 encoding +* Expose SQL\Mapper->schema() +* Change in behavior: Send error response as JSON string if AJAX request is + detected +* Deprecated: afind*() methods +* Discard output buffer in favor of debug output +* Make _id available to Jig queries +* Magic class now implements ArrayAccess +* Abort execution on startup errors +* Suppress stack trace on DEBUG level 0 +* Allow single = as equality operator in Jig query expressions +* Abort OpenID discovery if Web->request() fails +* Mimic PHP *RECURSION* in stringify() +* Modify Jig parser to allow wildcard-search using preg_match() +* Abort execution after error() execution +* Concatenate cached/uncached minify() iterations; Prevent spillover + caching of previous minify() result +* Work around obscure PHP session id regeneration bug +* Revise algorithm for Jig filter involving undefined fields (Issue #230) +* Use checkdnsrr() instead of gethostbyname() in DNSBL check +* Auto-adjust pagination to cursor boundaries +* Add Romanian diacritics +* Bug fix: Root namespace reference and sorting with undefined Jig fields +* Bug fix: Greedy receive() regex +* Bug fix: Default LANGUAGE always 'en' +* Bug fix: minify() hammers cache backend +* Bug fix: Previous values of primary keys not saved during factory() + instantiation +* Bug fix: Jig find() fails when search key is not present in all records +* Bug fix: Jig SORT_DESC (Issue #233) +* Bug fix: Error reporting (Issue #225) +* Bug fix: language() return value + +3.0.3 (29 Dec 2013) +--- +* NEW: [ajax] and [sync] routing pattern modifiers +* NEW: Basket class (session-based pseudo-mapper, shopping cart, etc.) +* NEW: Test->message() method +* NEW: DB profiling via DB->log() +* NEW: Matrix->calendar() +* NEW: Audit->card() and Audit->mod10() for credit card verification +* NEW: Geo->weather() +* NEW: Base->relay() accepts comma-separated callbacks; but unlike + Base->chain(), result of previous callback becomes argument of the next +* Numerous performance tweaks +* Interoperability with new MongoClient class +* Web->request() now recognizes gzip and deflate encoding +* Differences in behavior of Web->request() engines rectified +* mutex() now uses an ID as argument (instead of filename to make it clear + that specified file is not the target being locked, but a primitive + cross-platform semaphore) +* DB\SQL\Mapper field _id now returned even in the absence of any + auto-increment field +* Magic class spinned off as a separate file +* ISO 3166-1 alpha-2 table updated +* Apache redirect emulation for PHP 5.4 CLI server mode +* Framework instance now passed as argument to any user-defined shutdown + function +* Cache engine now used as storage for Web->minify() output +* Flag added for enabling/disabling Image class filter history +* Bug fix: Trailing routing token consumes HTTP query +* Bug fix: LANGUAGE spills over to LOCALES setting +* Bug fix: Inconsistent dry() return value +* Bug fix: URL-decoding + +3.0.2 (23 Dec 2013) +--- +* NEW: Syntax-highlighted stack traces via Base->highlight(); boolean + HIGHLIGHT global variable can be used to enable/disable this feature +* NEW: Template engine tag +* NEW: Image->captcha() +* NEW: DNSBL-based spammer detection (ported from 2.x) +* NEW: paginate(), first(), and last() methods for data mappers +* NEW: X-HTTP-Method-Override header now recognized +* NEW: Base->chain() method for executing callbacks in succession +* NEW: HOST global variable; derived from either $_SERVER['SERVER_NAME'] or + gethostname() +* NEW: REALM global variable representing full canonical URI +* NEW: Auth plug-in +* NEW: Pingback plug-in (implements both Pingback 1.0 protocol client and + server) +* NEW: DEBUG verbosity can now reach up to level 3; Base->stringify() drills + down to object properties at this setting +* NEW: HTTP PATCH method added to recognized HTTP ReST methods +* Web->slug() now trims trailing dashes +* Web->request() now allows relative local URLs as argument +* Use of PARAMS in route handlers now unnecessary; framework now passes two + arguments to route handlers: the framework object instance and an array + containing the captured values of tokens in route patterns +* Standardized timeout settings among Web->request() backends +* Session IDs regenerated for additional security +* Automatic HTTP 404 responses by Base->call() now restricted to route + handlers +* Empty comments in ini-style files now parsed properly +* Use file_get_contents() in methods that don't involve high concurrency + +3.0.1 (14 Dec 2013) +--- +* Major rewrite of much of the framework's core features diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/COPYING b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/COPYING new file mode 100644 index 0000000..3c7236c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/COPYING @@ -0,0 +1,621 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +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 diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/README.md b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/README.md new file mode 100644 index 0000000..c55722c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/README.md @@ -0,0 +1,29 @@ +# fatfree-core +Fat-Free Framework core library + +### Usage: + +First make sure to add a proper url rewrite configuration to your server, see https://fatfreeframework.com/3.6/routing-engine#DynamicWebSites + +**without composer:** + +```php +$f3 = require('lib/base.php'); +``` + +**with composer:** + +``` +composer require bcosca/fatfree-core +``` + +```php +require("vendor/autoload.php"); +$f3 = \Base::instance(); +``` + +--- +For the main repository (demo package), see https://github.com/bcosca/fatfree +For the test bench and unit tests, see https://github.com/f3-factory/fatfree-dev +For the user guide, see https://fatfreeframework.com/user-guide +For the documentation, see https://fatfreeframework.com/api-reference diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/audit.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/audit.php new file mode 100644 index 0000000..bf5bd26 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/audit.php @@ -0,0 +1,202 @@ +. + +*/ + +//! Data validator +class Audit extends Prefab { + + //@{ User agents + const + UA_Mobile='android|blackberry|phone|ipod|palm|windows\s+ce', + UA_Desktop='bsd|linux|os\s+[x9]|solaris|windows', + UA_Bot='bot|crawl|slurp|spider'; + //@} + + /** + * Return TRUE if string is a valid URL + * @return bool + * @param $str string + **/ + function url($str) { + return is_string(filter_var($str,FILTER_VALIDATE_URL)) + && !preg_match('/^(javascript|php):\/\/.*$/i', $str); + } + + /** + * Return TRUE if string is a valid e-mail address; + * Check DNS MX records if specified + * @return bool + * @param $str string + * @param $mx boolean + **/ + function email($str,$mx=TRUE) { + $hosts=[]; + return is_string(filter_var($str,FILTER_VALIDATE_EMAIL)) && + (!$mx || getmxrr(substr($str,strrpos($str,'@')+1),$hosts)); + } + + /** + * Return TRUE if string is a valid IPV4 address + * @return bool + * @param $addr string + **/ + function ipv4($addr) { + return (bool)filter_var($addr,FILTER_VALIDATE_IP,FILTER_FLAG_IPV4); + } + + /** + * Return TRUE if string is a valid IPV6 address + * @return bool + * @param $addr string + **/ + function ipv6($addr) { + return (bool)filter_var($addr,FILTER_VALIDATE_IP,FILTER_FLAG_IPV6); + } + + /** + * Return TRUE if IP address is within private range + * @return bool + * @param $addr string + **/ + function isprivate($addr) { + return (bool)filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) + && !(bool)filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE); + } + + /** + * Return TRUE if IP address is within reserved range + * @return bool + * @param $addr string + **/ + function isreserved($addr) { + return !(bool)filter_var($addr,FILTER_VALIDATE_IP, + FILTER_FLAG_IPV4|FILTER_FLAG_IPV6|FILTER_FLAG_NO_RES_RANGE); + } + + /** + * Return TRUE if IP address is neither private nor reserved + * @return bool + * @param $addr string + **/ + function ispublic($addr) { + return (bool)filter_var($addr,FILTER_VALIDATE_IP, + FILTER_FLAG_IPV4|FILTER_FLAG_IPV6| + FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE); + } + + /** + * Return TRUE if user agent is a desktop browser + * @return bool + * @param $agent string + **/ + function isdesktop($agent=NULL) { + if (!isset($agent)) + $agent=Base::instance()->AGENT; + return (bool)preg_match('/('.self::UA_Desktop.')/i',$agent) && + !$this->ismobile($agent); + } + + /** + * Return TRUE if user agent is a mobile device + * @return bool + * @param $agent string + **/ + function ismobile($agent=NULL) { + if (!isset($agent)) + $agent=Base::instance()->AGENT; + return (bool)preg_match('/('.self::UA_Mobile.')/i',$agent); + } + + /** + * Return TRUE if user agent is a Web bot + * @return bool + * @param $agent string + **/ + function isbot($agent=NULL) { + if (!isset($agent)) + $agent=Base::instance()->AGENT; + return (bool)preg_match('/('.self::UA_Bot.')/i',$agent); + } + + /** + * Return TRUE if specified ID has a valid (Luhn) Mod-10 check digit + * @return bool + * @param $id string + **/ + function mod10($id) { + if (!ctype_digit($id)) + return FALSE; + $id=strrev($id); + $sum=0; + for ($i=0,$l=strlen($id);$i<$l;++$i) + $sum+=$id[$i]+$i%2*(($id[$i]>4)*-4+$id[$i]%5); + return !($sum%10); + } + + /** + * Return credit card type if number is valid + * @return string|FALSE + * @param $id string + **/ + function card($id) { + $id=preg_replace('/[^\d]/','',$id); + if ($this->mod10($id)) { + if (preg_match('/^3[47][0-9]{13}$/',$id)) + return 'American Express'; + if (preg_match('/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/',$id)) + return 'Diners Club'; + if (preg_match('/^6(?:011|5[0-9][0-9])[0-9]{12}$/',$id)) + return 'Discover'; + if (preg_match('/^(?:2131|1800|35\d{3})\d{11}$/',$id)) + return 'JCB'; + if (preg_match('/^5[1-5][0-9]{14}$|'. + '^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)\d{12}$/',$id)) + return 'MasterCard'; + if (preg_match('/^4[0-9]{12}(?:[0-9]{3})?$/',$id)) + return 'Visa'; + } + return FALSE; + } + + /** + * Return entropy estimate of a password (NIST 800-63) + * @return int|float + * @param $str string + **/ + function entropy($str) { + $len=strlen($str); + return 4*min($len,1)+($len>1?(2*(min($len,8)-1)):0)+ + ($len>8?(1.5*(min($len,20)-8)):0)+($len>20?($len-20):0)+ + 6*(bool)(preg_match( + '/[A-Z].*?[0-9[:punct:]]|[0-9[:punct:]].*?[A-Z]/',$str)); + } + + /** + * Return TRUE if string is a valid MAC address including EUI-64 format + * @return bool + * @param $addr string + **/ + function mac($addr) { + return (bool)filter_var($addr,FILTER_VALIDATE_MAC) + || preg_match('/^([0-9a-f]{2}:){3}ff:fe(:[0-9a-f]{2}){3}$/i', $addr); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/auth.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/auth.php new file mode 100644 index 0000000..f201cce --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/auth.php @@ -0,0 +1,262 @@ +. + +*/ + +//! Authorization/authentication plug-in +class Auth { + + //@{ Error messages + const + E_LDAP='LDAP connection failure', + E_SMTP='SMTP connection failure'; + //@} + + protected + //! Auth storage + $storage, + //! Mapper object + $mapper, + //! Storage options + $args, + //! Custom compare function + $func; + + /** + * Jig storage handler + * @return bool + * @param $id string + * @param $pw string + * @param $realm string + **/ + protected function _jig($id,$pw,$realm) { + $success = (bool) + call_user_func_array( + [$this->mapper,'load'], + [ + array_merge( + [ + '@'.$this->args['id'].'==?'. + ($this->func?'':' AND @'.$this->args['pw'].'==?'). + (isset($this->args['realm'])? + (' AND @'.$this->args['realm'].'==?'):''), + $id + ], + ($this->func?[]:[$pw]), + (isset($this->args['realm'])?[$realm]:[]) + ) + ] + ); + if ($success && $this->func) + $success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw'])); + return $success; + } + + /** + * MongoDB storage handler + * @return bool + * @param $id string + * @param $pw string + * @param $realm string + **/ + protected function _mongo($id,$pw,$realm) { + $success = (bool) + $this->mapper->load( + [$this->args['id']=>$id]+ + ($this->func?[]:[$this->args['pw']=>$pw])+ + (isset($this->args['realm'])? + [$this->args['realm']=>$realm]:[]) + ); + if ($success && $this->func) + $success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw'])); + return $success; + } + + /** + * SQL storage handler + * @return bool + * @param $id string + * @param $pw string + * @param $realm string + **/ + protected function _sql($id,$pw,$realm) { + $success = (bool) + call_user_func_array( + [$this->mapper,'load'], + [ + array_merge( + [ + $this->args['id'].'=?'. + ($this->func?'':' AND '.$this->args['pw'].'=?'). + (isset($this->args['realm'])? + (' AND '.$this->args['realm'].'=?'):''), + $id + ], + ($this->func?[]:[$pw]), + (isset($this->args['realm'])?[$realm]:[]) + ) + ] + ); + if ($success && $this->func) + $success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw'])); + return $success; + } + + /** + * LDAP storage handler + * @return bool + * @param $id string + * @param $pw string + **/ + protected function _ldap($id,$pw) { + $port=(int)($this->args['port']?:389); + $filter=$this->args['filter']=$this->args['filter']?:"uid=".$id; + $this->args['attr']=$this->args['attr']?:["uid"]; + array_walk($this->args['attr'], + function($attr)use(&$filter,$id) { + $filter=str_ireplace($attr."=*",$attr."=".$id,$filter);}); + $dc=@ldap_connect($this->args['dc'],$port); + if ($dc && + ldap_set_option($dc,LDAP_OPT_PROTOCOL_VERSION,3) && + ldap_set_option($dc,LDAP_OPT_REFERRALS,0) && + ldap_bind($dc,$this->args['rdn'],$this->args['pw']) && + ($result=ldap_search($dc,$this->args['base_dn'], + $filter,$this->args['attr'])) && + ldap_count_entries($dc,$result) && + ($info=ldap_get_entries($dc,$result)) && + $info['count']==1 && + @ldap_bind($dc,$info[0]['dn'],$pw) && + @ldap_close($dc)) { + return in_array($id,(array_map(function($value){return $value[0];}, + array_intersect_key($info[0], + array_flip($this->args['attr'])))),TRUE); + } + throw new \Exception(self::E_LDAP); + } + + /** + * SMTP storage handler + * @return bool + * @param $id string + * @param $pw string + **/ + protected function _smtp($id,$pw) { + $socket=@fsockopen( + (strtolower($this->args['scheme'])=='ssl'? + 'ssl://':'').$this->args['host'], + $this->args['port']); + $dialog=function($cmd=NULL) use($socket) { + if (!is_null($cmd)) + fputs($socket,$cmd."\r\n"); + $reply=''; + while (!feof($socket) && + ($info=stream_get_meta_data($socket)) && + !$info['timed_out'] && $str=fgets($socket,4096)) { + $reply.=$str; + if (preg_match('/(?:^|\n)\d{3} .+\r\n/s', + $reply)) + break; + } + return $reply; + }; + if ($socket) { + stream_set_blocking($socket,TRUE); + $dialog(); + $fw=Base::instance(); + $dialog('EHLO '.$fw->HOST); + if (strtolower($this->args['scheme'])=='tls') { + $dialog('STARTTLS'); + stream_socket_enable_crypto( + $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); + $dialog('EHLO '.$fw->HOST); + } + // Authenticate + $dialog('AUTH LOGIN'); + $dialog(base64_encode($id)); + $reply=$dialog(base64_encode($pw)); + $dialog('QUIT'); + fclose($socket); + return (bool)preg_match('/^235 /',$reply); + } + throw new \Exception(self::E_SMTP); + } + + /** + * Login auth mechanism + * @return bool + * @param $id string + * @param $pw string + * @param $realm string + **/ + function login($id,$pw,$realm=NULL) { + return $this->{'_'.$this->storage}($id,$pw,$realm); + } + + /** + * HTTP basic auth mechanism + * @return bool + * @param $func callback + **/ + function basic($func=NULL) { + $fw=Base::instance(); + $realm=$fw->REALM; + $hdr=NULL; + if (isset($_SERVER['HTTP_AUTHORIZATION'])) + $hdr=$_SERVER['HTTP_AUTHORIZATION']; + elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) + $hdr=$_SERVER['REDIRECT_HTTP_AUTHORIZATION']; + if (!empty($hdr)) + list($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW'])= + explode(':',base64_decode(substr($hdr,6))); + if (isset($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']) && + $this->login( + $_SERVER['PHP_AUTH_USER'], + $func? + $fw->call($func,$_SERVER['PHP_AUTH_PW']): + $_SERVER['PHP_AUTH_PW'], + $realm + )) + return TRUE; + if (PHP_SAPI!='cli') + header('WWW-Authenticate: Basic realm="'.$realm.'"'); + $fw->status(401); + return FALSE; + } + + /** + * Instantiate class + * @return object + * @param $storage string|object + * @param $args array + * @param $func callback + **/ + function __construct($storage,?array $args=NULL,$func=NULL) { + if (is_object($storage) && is_a($storage,'DB\Cursor')) { + $this->storage=$storage->dbtype(); + $this->mapper=$storage; + unset($ref); + } + else + $this->storage=$storage; + $this->args=$args; + $this->func=$func; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/base.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/base.php new file mode 100644 index 0000000..01267c6 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/base.php @@ -0,0 +1,3733 @@ +. + +*/ + +//! Factory class for single-instance objects +abstract class Prefab { + + /** + * Return class instance + * @return static + **/ + static function instance() { + if (!Registry::exists($class=get_called_class())) { + $ref=new ReflectionClass($class); + $args=func_get_args(); + Registry::set($class, + $args?$ref->newinstanceargs($args):new $class); + } + return Registry::get($class); + } + +} +/** + * @property string ALIAS + * @property array ALIASES + * @property string|array AUTOLOAD + * @property bool|string CACHE + * @property bool CASELESS + * @property callable|\Prefab|\Psr\Container\ContainerInterface CONTAINER + * @property array COOKIE + * @property array GET + * @property array POST + * @property array REQUEST + * @property array SESSION + * @property array FILES + * @property array SERVER + * @property array ENV + * @property array CORS + * @property int DEBUG + * @property array DIACRITICS + * @property string DNSBL + * @property array EMOJI + * @property string ENCODING + * @property bool ESCAPE + * @property string EXEMPT + * @property object EXCEPTION + * @property string FALLBACK + * @property array FORMATS + * @property string FRAGMENT + * @property bool HALT + * @property bool HIGHLIGHT + * @property array JAR + * @property string LANGUAGE + * @property string LOCALES + * @property string|array LOGGABLE + * @property string LOGS + * @property mixed ONERROR + * @property mixed ONREROUTE + * @property string|null PACKAGE + * @property array PARAMS + * @property string PLUGINS + * @property string PREFIX + * @property string PREMAP + * @property bool QUIET + * @property bool RAW + * @property array ROUTES + * @property string SEED + * @property string SERIALIZER + * @property string TEMP + * @property float TIME + * @property bool REROUTE_TRAILING_SLASH + * @property string TZ + * @property string UI + * @property callback UNLOAD + * @property string UPLOADS + * @property string URI + * @property string VERB + * @property string VERSION + * @property string|null XFRAME + * @property-read string AGENT + * @property-read bool AJAX + * @property-read string BASE + * @property-read string BODY + * @property-read bool CLI + * @property-read array ERROR + * @property-read array HEADERS + * @property-read string HOST + * @property-read string IP + * @property-read string PATH + * @property-read string PATTERN + * @property-read int PORT + * @property-read string QUERY + * @property-read string REALM + * @property-read string RESPONSE + * @property-read string ROOT + * @property-read string SCHEME + */ +//! Base structure +final class Base extends Prefab implements ArrayAccess { + + //@{ Framework details + const + PACKAGE='Fat-Free Framework', + VERSION='3.9.1-Release'; + //@} + + //@{ HTTP status codes (RFC 2616) + const + HTTP_100='Continue', + HTTP_101='Switching Protocols', + HTTP_103='Early Hints', + HTTP_200='OK', + HTTP_201='Created', + HTTP_202='Accepted', + HTTP_203='Non-Authorative Information', + HTTP_204='No Content', + HTTP_205='Reset Content', + HTTP_206='Partial Content', + HTTP_300='Multiple Choices', + HTTP_301='Moved Permanently', + HTTP_302='Found', + HTTP_303='See Other', + HTTP_304='Not Modified', + HTTP_305='Use Proxy', + HTTP_307='Temporary Redirect', + HTTP_308='Permanent Redirect', + HTTP_400='Bad Request', + HTTP_401='Unauthorized', + HTTP_402='Payment Required', + HTTP_403='Forbidden', + HTTP_404='Not Found', + HTTP_405='Method Not Allowed', + HTTP_406='Not Acceptable', + HTTP_407='Proxy Authentication Required', + HTTP_408='Request Timeout', + HTTP_409='Conflict', + HTTP_410='Gone', + HTTP_411='Length Required', + HTTP_412='Precondition Failed', + HTTP_413='Request Entity Too Large', + HTTP_414='Request-URI Too Long', + HTTP_415='Unsupported Media Type', + HTTP_416='Requested Range Not Satisfiable', + HTTP_417='Expectation Failed', + HTTP_421='Misdirected Request', + HTTP_422='Unprocessable Entity', + HTTP_423='Locked', + HTTP_429='Too Many Requests', + HTTP_451='Unavailable For Legal Reasons', + HTTP_500='Internal Server Error', + HTTP_501='Not Implemented', + HTTP_502='Bad Gateway', + HTTP_503='Service Unavailable', + HTTP_504='Gateway Timeout', + HTTP_505='HTTP Version Not Supported', + HTTP_507='Insufficient Storage', + HTTP_511='Network Authentication Required'; + //@} + + const + //! Mapped PHP globals + GLOBALS='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV', + //! HTTP verbs + VERBS='GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS', + //! Default directory permissions + MODE=0755, + //! Syntax highlighting stylesheet + CSS='code.css'; + + //@{ Request types + const + REQ_SYNC=1, + REQ_AJAX=2, + REQ_CLI=4; + //@} + + //@{ Error messages + const + E_Pattern='Invalid routing pattern: %s', + E_Named='Named route does not exist: %s', + E_Alias='Invalid named route alias: %s', + E_Fatal='Fatal error: %s', + E_Open='Unable to open %s', + E_Routes='No routes specified', + E_Class='Invalid class %s', + E_Method='Invalid method %s', + E_Hive='Invalid hive key %s'; + //@} + + private + //! Globals + $hive, + //! Initial settings + $init, + //! Language lookup sequence + $languages, + //! Mutex locks + $locks=[], + //! Default fallback language + $fallback='en'; + + /** + * Sync PHP global with corresponding hive key + * @return array + * @param $key string + **/ + function sync($key) { + return $this->hive[$key]=&$GLOBALS['_'.$key]; + } + + /** + * Return the parts of specified hive key + * @return array + * @param $key string + **/ + private function cut($key) { + return preg_split('/\[\h*[\'"]?(.+?)[\'"]?\h*\]|(->)|\./', + $key,-1,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE); + } + + /** + * Replace tokenized URL with available token values + * @return string + * @param $url array|string + * @param $addParams boolean merge default PARAMS from hive into args + * @param $args array + **/ + function build($url, $args=[], $addParams=TRUE) { + if ($addParams) + $args+=$this->recursive($this->hive['PARAMS'], function($val) { + return implode('/', array_map('urlencode', explode('/', $val ?? ''))); + }); + if (is_array($url)) + foreach ($url as &$var) { + $var=$this->build($var,$args, false); + unset($var); + } + else { + $i=0; + $url=preg_replace_callback('/(\{)?@(\w+)(?(1)\})|(\*)/', + function($match) use(&$i,$args) { + if (isset($match[2]) && + array_key_exists($match[2],$args)) + return $args[$match[2]]; + if (isset($match[3]) && + array_key_exists($match[3],$args)) { + if (!is_array($args[$match[3]])) + return $args[$match[3]]; + ++$i; + return $args[$match[3]][$i-1]; + } + return $match[0]; + },$url); + } + return $url; + } + + /** + * Parse string containing key-value pairs + * @return array + * @param $str string + **/ + function parse($str) { + preg_match_all('/(\w+|\*)\h*=\h*(?:\[(.+?)\]|(.+?))(?=,|$)/', + $str,$pairs,PREG_SET_ORDER); + $out=[]; + foreach ($pairs as $pair) + if ($pair[2]) { + $out[$pair[1]]=[]; + foreach (explode(',',$pair[2]) as $val) + array_push($out[$pair[1]],$val); + } + else + $out[$pair[1]]=trim($pair[3]); + return $out; + } + + /** + * Cast string variable to PHP type or constant + * @param $val + * @return mixed + */ + function cast($val) { + if ($val && preg_match('/^(?:0x[0-9a-f]+|0[0-7]+|0b[01]+)$/i',$val)) + return intval($val,0); + if (is_numeric($val)) + return $val+0; + $val=trim($val?:''); + if (preg_match('/^\w+$/i',$val) && defined($val)) + return constant($val); + return $val; + } + + /** + * Convert JS-style token to PHP expression + * @return string + * @param $str string + * @param $evaluate bool compile expressions as well or only convert variable access + **/ + function compile($str, $evaluate=TRUE) { + return (!$evaluate) + ? preg_replace_callback( + '/^@(\w+)((?:\..+|\[(?:(?:[^\[\]]*|(?R))*)\])*)/', + function($expr) { + $str='$'.$expr[1]; + if (isset($expr[2])) + $str.=preg_replace_callback( + '/\.([^.\[\]]+)|\[((?:[^\[\]\'"]*|(?R))*)\]/', + function($sub) { + $val=isset($sub[2]) ? $sub[2] : $sub[1]; + if (ctype_digit($val)) + $val=(int)$val; + $out='['.$this->export($val).']'; + return $out; + }, + $expr[2] + ); + return $str; + }, + $str + ) + : preg_replace_callback( + '/(?|::)\w+)?)'. + '((?:\.\w+|\[(?:(?:[^\[\]]*|(?R))*)\]|(?:\->|::)\w+|\()*)/', + function($expr) { + $str='$'.$expr[1]; + if (isset($expr[2])) + $str.=preg_replace_callback( + '/\.(\w+)(\()?|\[((?:[^\[\]]*|(?R))*)\]/', + function($sub) { + if (empty($sub[2])) { + if (ctype_digit($sub[1])) + $sub[1]=(int)$sub[1]; + $out='['. + (isset($sub[3])? + $this->compile($sub[3]): + $this->export($sub[1])). + ']'; + } + else + $out=function_exists($sub[1])? + $sub[0]: + ('['.$this->export($sub[1]).']'.$sub[2]); + return $out; + }, + $expr[2] + ); + return $str; + }, + $str + ); + } + + /** + * Get hive key reference/contents; Add non-existent hive keys, + * array elements, and object properties by default + * @return mixed + * @param $key string + * @param $add bool + * @param $var mixed + **/ + function &ref($key,$add=TRUE,&$var=NULL) { + $null=NULL; + $parts=$this->cut($key); + if ($parts[0]=='SESSION') { + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); + $this->sync('SESSION'); + } + elseif (!preg_match('/^\w+$/',$parts[0])) + throw new \Exception(\sprintf(self::E_Hive,$this->stringify($key))); + if (is_null($var)) { + if ($add) + $var=&$this->hive; + else + $var=$this->hive; + } + $obj=FALSE; + foreach ($parts as $part) + if ($part=='->') + $obj=TRUE; + elseif ($obj) { + $obj=FALSE; + if (!is_object($var)) + $var=new stdClass; + if ($add || property_exists($var,$part)) + $var=&$var->$part; + else { + $var=&$null; + break; + } + } + else { + if (!is_array($var)) + $var=[]; + if ($add || array_key_exists($part,$var)) + $var=&$var[$part]; + else { + $var=&$null; + break; + } + } + return $var; + } + + /** + * Return TRUE if hive key is set + * (or return timestamp and TTL if cached) + * @return bool + * @param $key string + * @param $val mixed + **/ + function exists($key,&$val=NULL) { + $val=$this->ref($key,FALSE); + return isset($val)? + TRUE: + (Cache::instance()->exists($this->hash($key).'.var',$val)?:FALSE); + } + + /** + * Return TRUE if hive key is empty and not cached + * @param $key string + * @param $val mixed + * @return bool + **/ + function devoid($key,&$val=NULL) { + $val=$this->ref($key,FALSE); + return empty($val) && + (!Cache::instance()->exists($this->hash($key).'.var',$val) || + !$val); + } + + /** + * Bind value to hive key + * @return mixed + * @param $key string + * @param $val mixed + * @param $ttl int + **/ + function set($key,$val,$ttl=0) { + $time=(int)$this->hive['TIME']; + if (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) { + $this->set('REQUEST'.$expr[2],$val); + if ($expr[1]=='COOKIE') { + $parts=$this->cut($key); + $jar=$this->unserialize($this->serialize($this->hive['JAR'])); + unset($jar['lifetime']); + if (version_compare(PHP_VERSION, '7.3.0') >= 0) { + unset($jar['expire']); + if (isset($_COOKIE[$parts[1]])) + setcookie($parts[1],'',['expires'=>0]+$jar); + if ($ttl) + $jar['expires']=$time+$ttl; + setcookie($parts[1],$val?:'',$jar); + } else { + unset($jar['samesite']); + if (isset($_COOKIE[$parts[1]])) + call_user_func_array('setcookie', + array_merge([$parts[1],''],['expire'=>0]+$jar)); + if ($ttl) + $jar['expire']=$time+$ttl; + call_user_func_array('setcookie',[$parts[1],$val?:'']+$jar); + } + $_COOKIE[$parts[1]]=$val; + return $val; + } + } + else switch ($key) { + case 'CACHE': + $val=Cache::instance()->load($val); + break; + case 'ENCODING': + ini_set('default_charset',$val); + if (extension_loaded('mbstring')) + mb_internal_encoding($val); + break; + case 'FALLBACK': + $this->fallback=$val; + $lang=$this->language($this->hive['LANGUAGE']); + case 'LANGUAGE': + if (!isset($lang)) + $val=$this->language($val); + $lex=$this->lexicon($this->hive['LOCALES'],$ttl); + case 'LOCALES': + if (isset($lex) || $lex=$this->lexicon($val,$ttl)) + foreach ($lex as $dt=>$dd) { + $ref=&$this->ref($this->hive['PREFIX'].$dt); + $ref=$dd; + unset($ref); + } + break; + case 'TZ': + date_default_timezone_set($val); + break; + } + $ref=&$this->ref($key); + $ref=$val; + if (preg_match('/^JAR\b/',$key)) { + if ($key=='JAR.lifetime') + $this->set('JAR.expire',$val==0?0: + (is_int($val)?$time+$val:strtotime($val))); + else { + if ($key=='JAR.expire') + $this->hive['JAR']['lifetime']=max(0,$val-$time); + $jar=$this->unserialize($this->serialize($this->hive['JAR'])); + unset($jar['expire']); + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + if (version_compare(PHP_VERSION, '7.3.0') >= 0) + session_set_cookie_params($jar); + else { + unset($jar['samesite']); + call_user_func_array('session_set_cookie_params',$jar); + } + } + } + if ($ttl) + // Persist the key-value pair + Cache::instance()->set($this->hash($key).'.var',$val,$ttl); + return $ref; + } + + /** + * Retrieve contents of hive key + * @return mixed + * @param $key string + * @param $args string|array + **/ + function get($key,$args=NULL) { + if (is_string($val=$this->ref($key,FALSE)) && !is_null($args)) + return call_user_func_array( + [$this,'format'], + array_merge([$val],is_array($args)?$args:[$args]) + ); + if (is_null($val)) { + // Attempt to retrieve from cache + if (Cache::instance()->exists($this->hash($key).'.var',$data)) + return $data; + } + return $val; + } + + /** + * Unset hive key + * @param $key string + **/ + function clear($key) { + // Normalize array literal + $cache=Cache::instance(); + $parts=$this->cut($key); + if ($key=='CACHE') + // Clear cache contents + $cache->reset(); + elseif (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) { + $this->clear('REQUEST'.$expr[2]); + if ($expr[1]=='COOKIE') { + $parts=$this->cut($key); + $jar=$this->hive['JAR']; + unset($jar['lifetime']); + $jar['expire']=0; + if (version_compare(PHP_VERSION, '7.3.0') >= 0) { + $jar['expires']=$jar['expire']; + unset($jar['expire']); + setcookie($parts[1],'',$jar); + } else { + unset($jar['samesite']); + call_user_func_array('setcookie', + array_merge([$parts[1],''],$jar)); + } + unset($_COOKIE[$parts[1]]); + } + } + elseif ($parts[0]=='SESSION') { + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); + if (empty($parts[1])) { + // End session + session_unset(); + session_destroy(); + $this->clear('COOKIE.'.session_name()); + } + $this->sync('SESSION'); + } + if (!isset($parts[1]) && array_key_exists($parts[0],$this->init)) + // Reset global to default value + $this->hive[$parts[0]]=$this->init[$parts[0]]; + else { + $val=preg_replace('/^(\$hive)/','$this->hive', + $this->compile('@hive.'.$key, FALSE)); + eval('unset('.$val.');'); + if ($parts[0]=='SESSION') { + session_commit(); + session_start(); + } + if ($cache->exists($hash=$this->hash($key).'.var')) + // Remove from cache + $cache->clear($hash); + } + } + + /** + * Return TRUE if hive variable is 'on' + * @return bool + * @param $key string + **/ + function checked($key) { + $ref=&$this->ref($key); + return $ref=='on'; + } + + /** + * Return TRUE if property has public visibility + * @return bool + * @param $obj object + * @param $key string + **/ + function visible($obj,$key) { + if (property_exists($obj,$key)) { + $ref=new ReflectionProperty(get_class($obj),$key); + $out=$ref->ispublic(); + unset($ref); + return $out; + } + return FALSE; + } + + /** + * Multi-variable assignment using associative array + * @param $vars array + * @param $prefix string + * @param $ttl int + **/ + function mset(array $vars,$prefix='',$ttl=0) { + foreach ($vars as $key=>$val) + $this->set($prefix.$key,$val,$ttl); + } + + /** + * Publish hive contents + * @return array + **/ + function hive() { + return $this->hive; + } + + /** + * Copy contents of hive variable to another + * @return mixed + * @param $src string + * @param $dst string + **/ + function copy($src,$dst) { + $ref=&$this->ref($dst); + return $ref=$this->ref($src,FALSE); + } + + /** + * Concatenate string to hive string variable + * @return string + * @param $key string + * @param $val string + **/ + function concat($key,$val) { + $ref=&$this->ref($key); + $ref.=$val; + return $ref; + } + + /** + * Swap keys and values of hive array variable + * @return array + * @param $key string + * @public + **/ + function flip($key) { + $ref=&$this->ref($key); + return $ref=array_combine(array_values($ref),array_keys($ref)); + } + + /** + * Add element to the end of hive array variable + * @return mixed + * @param $key string + * @param $val mixed + **/ + function push($key,$val) { + $ref=&$this->ref($key); + $ref[]=$val; + return $val; + } + + /** + * Remove last element of hive array variable + * @return mixed + * @param $key string + **/ + function pop($key) { + $ref=&$this->ref($key); + return array_pop($ref); + } + + /** + * Add element to the beginning of hive array variable + * @return mixed + * @param $key string + * @param $val mixed + **/ + function unshift($key,$val) { + $ref=&$this->ref($key); + array_unshift($ref,$val); + return $val; + } + + /** + * Remove first element of hive array variable + * @return mixed + * @param $key string + **/ + function shift($key) { + $ref=&$this->ref($key); + return array_shift($ref); + } + + /** + * Merge array with hive array variable + * @return array + * @param $key string + * @param $src string|array + * @param $keep bool + **/ + function merge($key,$src,$keep=FALSE) { + $ref=&$this->ref($key); + if (!$ref) + $ref=[]; + $out=array_merge($ref,is_string($src)?$this->hive[$src]:$src); + if ($keep) + $ref=$out; + return $out; + } + + /** + * Extend hive array variable with default values from $src + * @return array + * @param $key string + * @param $src string|array + * @param $keep bool + **/ + function extend($key,$src,$keep=FALSE) { + $ref=&$this->ref($key); + if (!$ref) + $ref=[]; + $out=array_replace_recursive( + is_string($src)?$this->hive[$src]:$src,$ref); + if ($keep) + $ref=$out; + return $out; + } + + /** + * fetch a hive key result, then clear the key + * @param string $key + * @return mixed + */ + function pull($key): mixed + { + $value = $this->get($key); + $this->clear($key); + return $value; + } + + /** + * Convert backslashes to slashes + * @return string + * @param $str string + **/ + function fixslashes($str) { + return $str?strtr($str,'\\','/'):$str; + } + + /** + * Split comma-, semi-colon, or pipe-separated string + * @return array + * @param $str string + * @param $noempty bool + **/ + function split($str,$noempty=TRUE) { + return array_map('trim', + preg_split('/[,;|]/',$str?:'',0,$noempty?PREG_SPLIT_NO_EMPTY:0)); + } + + /** + * Convert PHP expression/value to compressed exportable string + * @return string + * @param $arg mixed + * @param $stack array + **/ + function stringify($arg,?array $stack=NULL) { + if ($stack) { + foreach ($stack as $node) + if ($arg===$node) + return '*RECURSION*'; + } + else + $stack=[]; + switch (gettype($arg)) { + case 'object': + $str=''; + foreach (get_object_vars($arg) as $key=>$val) + $str.=($str?',':''). + $this->export($key).'=>'. + $this->stringify($val, + array_merge($stack,[$arg])); + return get_class($arg).'::__set_state(['.$str.'])'; + case 'array': + $str=''; + $num=isset($arg[0]) && + ctype_digit(implode('',array_keys($arg))); + foreach ($arg as $key=>$val) + $str.=($str?',':''). + ($num?'':($this->export($key).'=>')). + $this->stringify($val,array_merge($stack,[$arg])); + return '['.$str.']'; + default: + return $this->export($arg); + } + } + + /** + * Flatten array values and return as CSV string + * @return string + * @param $args array + **/ + function csv(array $args) { + return implode(',',array_map('stripcslashes', + array_map([$this,'stringify'],$args))); + } + + /** + * Convert snakecase string to camelcase + * @return string + * @param $str string + **/ + function camelcase($str) { + return preg_replace_callback( + '/_(\pL)/u', + function($match) { + return strtoupper($match[1]); + }, + $str + ); + } + + /** + * Convert camelcase string to snakecase + * @return string + * @param $str string + **/ + function snakecase($str) { + return strtolower(preg_replace('/(?!^)\p{Lu}/u','_\0',$str)); + } + + /** + * Return -1 if specified number is negative, 0 if zero, + * or 1 if the number is positive + * @return int + * @param $num mixed + **/ + function sign($num) { + return $num?($num/abs($num)):0; + } + + /** + * Extract values of array whose keys start with the given prefix + * @return array + * @param $arr array + * @param $prefix string + **/ + function extract($arr,$prefix) { + $out=[]; + foreach (preg_grep('/^'.preg_quote($prefix,'/').'/',array_keys($arr)) + as $key) + $out[substr($key,strlen($prefix))]=$arr[$key]; + return $out; + } + + /** + * Convert class constants to array + * @return array + * @param $class object|string + * @param $prefix string + **/ + function constants($class,$prefix='') { + $ref=new ReflectionClass($class); + return $this->extract($ref->getconstants(),$prefix); + } + + /** + * Generate 64bit/base36 hash + * @return string + * @param $str + **/ + function hash($str) { + return str_pad(base_convert( + substr(sha1($str?:''),-16),16,36),11,'0',STR_PAD_LEFT); + } + + /** + * Return Base64-encoded equivalent + * @return string + * @param $data string + * @param $mime string + **/ + function base64($data,$mime) { + return 'data:'.$mime.';base64,'.base64_encode($data); + } + + /** + * Convert special characters to HTML entities + * @return string + * @param $str string + **/ + function encode($str) { + return @htmlspecialchars($str,$this->hive['BITMASK'], + $this->hive['ENCODING'])?:$this->scrub($str); + } + + /** + * Convert HTML entities back to characters + * @return string + * @param $str string + **/ + function decode($str) { + return htmlspecialchars_decode($str,$this->hive['BITMASK']); + } + + /** + * Invoke callback recursively for all data types + * @return mixed + * @param $arg mixed + * @param $func callback + * @param $stack array + **/ + function recursive($arg,$func,$stack=[]) { + if ($stack) { + foreach ($stack as $node) + if ($arg===$node) + return $arg; + } + switch (gettype($arg)) { + case 'object': + $ref=new ReflectionClass($arg); + if ($ref->iscloneable()) { + $arg=clone($arg); + $cast=($it=is_a($arg,'IteratorAggregate'))? + iterator_to_array($arg):get_object_vars($arg); + foreach ($cast as $key=>$val) { + // skip inaccessible properties #350 + if (!$it && !isset($arg->$key)) + continue; + $arg->$key=$this->recursive( + $val,$func,array_merge($stack,[$arg])); + } + } + return $arg; + case 'array': + $copy=[]; + foreach ($arg as $key=>$val) + $copy[$key]=$this->recursive($val,$func, + array_merge($stack,[$arg])); + return $copy; + } + return $func($arg); + } + + /** + * Remove HTML tags (except those enumerated) and non-printable characters + * NB: This method doesn't mitigate XSS/code injection attacks. + * @return mixed + * @param $arg mixed + * @param $tags string + **/ + function clean($arg,$tags=NULL) { + return $this->recursive($arg, + function($val) use($tags) { + if ($tags!='*') + $val=trim(strip_tags($val??'', + '<'.implode('><',$this->split($tags)).'>')); + return trim(preg_replace( + '/[\x00-\x08\x0B\x0C\x0E-\x1F]/','',$val)); + } + ); + } + + /** + * Similar to clean(), except that variable is passed by reference + * @return mixed + * @param $var mixed + * @param $tags string + **/ + function scrub(&$var,$tags=NULL) { + return $var=$this->clean($var,$tags); + } + + /** + * Return locale-aware formatted string + * @return string + **/ + function format() { + $args=func_get_args(); + $val=array_shift($args); + // Get formatting rules + $conv=localeconv(); + return preg_replace_callback( + '/\{\s*(?P\d+)\s*(?:,\s*(?P\w+)\s*'. + '(?:,\s*(?P(?:\w+(?:\s*\{.+?\}\s*,?\s*)?)*)'. + '(?:,\s*(?P.+?))?)?)?\s*\}/', + function($expr) use($args,$conv) { + /** + * @var string $pos + * @var string $mod + * @var string $type + * @var string $prop + */ + extract($expr); + /** + * @var string $thousands_sep + * @var string $negative_sign + * @var string $positive_sign + * @var string $frac_digits + * @var string $decimal_point + * @var string $int_curr_symbol + * @var string $currency_symbol + */ + extract($conv); + if (!array_key_exists($pos,$args)) + return $expr[0]; + if (isset($type)) { + if (isset($this->hive['FORMATS'][$type])) + return $this->call( + $this->hive['FORMATS'][$type], + [ + $args[$pos], + isset($mod)?$mod:null, + isset($prop)?$prop:null + ] + ); + $php81=version_compare(PHP_VERSION, '8.1.0')>=0; + switch ($type) { + case 'plural': + preg_match_all('/(?\w+)'. + '(?:\s*\{\s*(?.*?)\s*\})/', + $mod,$matches,PREG_SET_ORDER); + $ord=['zero','one','two']; + foreach ($matches as $match) { + /** @var string $tag */ + /** @var string $data */ + extract($match); + if (isset($ord[$args[$pos]]) && + $tag==$ord[$args[$pos]] || $tag=='other') + return str_replace('#',$args[$pos],$data); + } + case 'number': + if (isset($mod)) + switch ($mod) { + case 'integer': + return number_format( + $args[$pos],0,'',$thousands_sep); + case 'currency': + $int=$cstm=FALSE; + if (isset($prop) && + $cstm=!$int=($prop=='int')) + $currency_symbol=$prop; + if (!$cstm && + function_exists('money_format') && + version_compare(PHP_VERSION,'7.4.0')<0) + return money_format( + '%'.($int?'i':'n'),$args[$pos]); + $fmt=[ + 0=>'(nc)',1=>'(n c)', + 2=>'(nc)',10=>'+nc', + 11=>'+n c',12=>'+ nc', + 20=>'nc+',21=>'n c+', + 22=>'nc +',30=>'n+c', + 31=>'n +c',32=>'n+ c', + 40=>'nc+',41=>'n c+', + 42=>'nc +',100=>'(cn)', + 101=>'(c n)',102=>'(cn)', + 110=>'+cn',111=>'+c n', + 112=>'+ cn',120=>'cn+', + 121=>'c n+',122=>'cn +', + 130=>'+cn',131=>'+c n', + 132=>'+ cn',140=>'c+n', + 141=>'c+ n',142=>'c +n' + ]; + if ($args[$pos]<0) { + $sgn=$negative_sign; + $pre='n'; + } + else { + $sgn=$positive_sign; + $pre='p'; + } + return str_replace( + ['+','n','c'], + [$sgn,number_format( + abs($args[$pos]), + $frac_digits, + $decimal_point, + $thousands_sep), + $int?$int_curr_symbol + :$currency_symbol], + $fmt[(int)( + (${$pre.'_cs_precedes'}%2). + (${$pre.'_sign_posn'}%5). + (${$pre.'_sep_by_space'}%3) + )] + ); + case 'percent': + return number_format( + $args[$pos]*100,0,$decimal_point, + $thousands_sep).'%'; + } + $frac=$args[$pos]-(int)$args[$pos]; + return number_format( + $args[$pos], + isset($prop)? + $prop: + ($frac?strlen($frac)-2:0), + $decimal_point,$thousands_sep); + case 'date': + if ($php81) { + $lang = $this->split($this->LANGUAGE); + // requires intl extension + $dateType=(empty($mod) || $mod=='short') ? IntlDateFormatter::SHORT : + ($mod=='full' ? IntlDateFormatter::FULL : IntlDateFormatter::LONG); + $pattern = $dateType === IntlDateFormatter::SHORT + ? (($ptn=IntlDatePatternGenerator::create($lang[0])) + ? $ptn->getBestPattern('yyyyMMdd') : null) : null; + $formatter = new IntlDateFormatter($lang[0],$dateType, + IntlDateFormatter::NONE, null,null, $pattern); + return $formatter->format($args[$pos]); + } else { + if (empty($mod) || $mod=='short') + $prop='%x'; + elseif ($mod=='full') + $prop='%A, %d %B %Y'; + elseif ($mod!='custom') + $prop='%d %B %Y'; + return strftime($prop,$args[$pos]); + } + case 'time': + if ($php81) { + $lang = $this->split($this->LANGUAGE); + // requires intl extension + $formatter = new IntlDateFormatter($lang[0], + IntlDateFormatter::NONE, + (empty($mod) || $mod=='short') + ? IntlDateFormatter::SHORT : + ($mod=='full' ? IntlDateFormatter::LONG : IntlDateFormatter::MEDIUM), + IntlTimeZone::createTimeZone($this->hive['TZ'])); + return $formatter->format($args[$pos]); + } else { + if (empty($mod) || $mod=='short') + $prop='%X'; + elseif ($mod!='custom') + $prop='%r'; + return strftime($prop,$args[$pos]); + } + default: + return $expr[0]; + } + } + return $args[$pos]; + }, + $val + ); + } + + /** + * Return string representation of expression + * @return string + * @param $expr mixed + **/ + function export($expr) { + return var_export($expr,TRUE); + } + + /** + * Assign/auto-detect language + * @return string + * @param $code string + **/ + function language($code) { + $code=preg_replace('/\h+|;q=[0-9.]+/','',$code?:''); + $code.=($code?',':'').$this->fallback; + $this->languages=[]; + foreach (array_reverse(explode(',',$code)) as $lang) + if (preg_match('/^(\w{2})(?:-(\w{2}))?\b/i',$lang,$parts)) { + // Generic language + array_unshift($this->languages,$parts[1]); + if (isset($parts[2])) { + // Specific language + $parts[0]=$parts[1].'-'.($parts[2]=strtoupper($parts[2])); + array_unshift($this->languages,$parts[0]); + } + } + $this->languages=array_unique($this->languages); + $locales=[]; + $windows=preg_match('/^win/i',PHP_OS); + // Work around PHP's Turkish locale bug + foreach (preg_grep('/^(?!tr)/i',$this->languages) as $locale) { + if ($windows) { + $parts=explode('-',$locale); + if (!defined('ISO::LC_'.$parts[0])) + continue; + $locale=constant('ISO::LC_'.$parts[0]); + if (isset($parts[1]) && + defined($cc='ISO::CC_'.strtolower($parts[1]))) + $locale.='-'.constant($cc); + } + $locale=str_replace('-','_',$locale); + $locales[]=$locale.'.'.ini_get('default_charset'); + $locales[]=$locale; + } + setlocale(LC_ALL,$locales); + return $this->hive['LANGUAGE']=implode(',',$this->languages); + } + + /** + * Return lexicon entries + * @return array + * @param $path string + * @param $ttl int + **/ + function lexicon($path,$ttl=0) { + $languages=$this->languages?:explode(',',$this->fallback); + $cache=Cache::instance(); + if ($ttl && $cache->exists( + $hash=$this->hash(implode(',',$languages).$path).'.dic',$lex)) + return $lex; + $lex=[]; + foreach ($languages as $lang) + foreach ($this->split($path) as $dir) + if ((is_file($file=($base=$dir.$lang).'.php') || + is_file($file=$base.'.php')) && + is_array($dict=require($file))) + $lex+=$dict; + elseif (is_file($file=$base.'.json') && + is_array($dict=json_decode(file_get_contents($file), true))) + $lex+=$dict; + elseif (is_file($file=$base.'.ini')) { + preg_match_all( + '/(?<=^|\n)(?:'. + '\[(?.+?)\]|'. + '(?[^\h\r\n;].*?)\h*=\h*'. + '(?(?:\\\\\h*\r?\n|.+?)*)'. + ')(?=\r?\n|$)/', + $this->read($file),$matches,PREG_SET_ORDER); + if ($matches) { + $prefix=''; + foreach ($matches as $match) + if ($match['prefix']) + $prefix=$match['prefix'].'.'; + elseif (!array_key_exists( + $key=$prefix.$match['lval'],$lex)) + $lex[$key]=trim(preg_replace( + '/\\\\\h*\r?\n/',"\n",$match['rval'])); + } + } + if ($ttl) + $cache->set($hash,$lex,$ttl); + return $lex; + } + + /** + * Return string representation of PHP value + * @return string + * @param $arg mixed + **/ + function serialize($arg) { + switch (strtolower($this->hive['SERIALIZER'])) { + case 'igbinary': + return igbinary_serialize($arg); + default: + return serialize($arg); + } + } + + /** + * Return PHP value derived from string + * @return mixed + * @param $arg mixed + **/ + function unserialize($arg) { + switch (strtolower($this->hive['SERIALIZER'])) { + case 'igbinary': + return igbinary_unserialize($arg); + default: + return unserialize($arg); + } + } + + /** + * Send HTTP status header; Return text equivalent of status code + * @return string + * @param $code int + **/ + function status($code) { + $reason=@constant('self::HTTP_'.$code); + if (!$this->hive['CLI'] && !headers_sent()) + header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason); + return $reason; + } + + /** + * Send cache metadata to HTTP client + * @param $secs int + **/ + function expire($secs=0) { + if (!$this->hive['CLI'] && !headers_sent()) { + $secs=(int)$secs; + if ($this->hive['PACKAGE']) + header('X-Powered-By: '.$this->hive['PACKAGE']); + if ($this->hive['XFRAME']) + header('X-Frame-Options: '.$this->hive['XFRAME']); + header('X-XSS-Protection: 1; mode=block'); + header('X-Content-Type-Options: nosniff'); + if ($this->hive['VERB']=='GET' && $secs) { + $time=microtime(TRUE); + header_remove('Pragma'); + header('Cache-Control: max-age='.$secs); + header('Expires: '.gmdate('r',round($time+$secs))); + header('Last-Modified: '.gmdate('r')); + } + else { + header('Pragma: no-cache'); + header('Cache-Control: no-cache, no-store, must-revalidate'); + header('Expires: '.gmdate('r',0)); + } + } + } + + /** + * Return HTTP user agent + * @return string + **/ + function agent() { + $headers=$this->hive['HEADERS']; + return isset($headers['X-Operamini-Phone-UA'])? + $headers['X-Operamini-Phone-UA']: + (isset($headers['X-Skyfire-Phone'])? + $headers['X-Skyfire-Phone']: + (isset($headers['User-Agent'])? + $headers['User-Agent']:'')); + } + + /** + * Return TRUE if XMLHttpRequest detected + * @return bool + **/ + function ajax() { + $headers=$this->hive['HEADERS']; + return isset($headers['X-Requested-With']) && + $headers['X-Requested-With']=='XMLHttpRequest'; + } + + /** + * Sniff IP address + * @return string + **/ + function ip() { + $headers=$this->hive['HEADERS']; + return isset($headers['Client-IP'])? + $headers['Client-IP']: + (isset($headers['X-Forwarded-For'])? + explode(',',$headers['X-Forwarded-For'])[0]: + (isset($_SERVER['REMOTE_ADDR'])? + $_SERVER['REMOTE_ADDR']:'')); + } + + /** + * Return filtered stack trace as a formatted string (or array) + * @return string|array + * @param $trace array|NULL + * @param $format bool + **/ + function trace(?array $trace=NULL,$format=TRUE) { + if (!$trace) { + $trace=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $frame=$trace[0]; + if (isset($frame['file']) && $frame['file']==__FILE__) + array_shift($trace); + } + $debug=$this->hive['DEBUG']; + $trace=array_filter( + $trace, + function($frame) use($debug) { + return isset($frame['file']) && + ($debug>1 || + (($frame['file']!=__FILE__ || $debug) && + (empty($frame['function']) || + !preg_match('/^(?:(?:trigger|user)_error|'. + '__call|call_user_func)/',$frame['function'])))); + } + ); + if (!$format) + return $trace; + $out=''; + $eol="\n"; + // Analyze stack trace + foreach ($trace as $frame) { + $line=''; + if (isset($frame['class'])) + $line.=$frame['class'].$frame['type']; + if (isset($frame['function'])) + $line.=$frame['function'].'('. + ($debug>2 && isset($frame['args'])? + $this->csv($frame['args']):'').')'; + $src=$this->fixslashes(str_replace($_SERVER['DOCUMENT_ROOT']. + '/','',$frame['file'])).':'.$frame['line']; + $out.='['.$src.'] '.$line.$eol; + } + return $out; + } + + /** + * Log error; Execute ONERROR handler if defined, else display + * default error page (HTML for synchronous requests, JSON string + * for AJAX requests) + * @param $code int + * @param $text string + * @param $trace array + * @param $level int + **/ + function error($code,$text='',?array $trace=NULL,$level=0) { + $prior=$this->hive['ERROR']; + $header=$this->status($code); + $req=$this->hive['VERB'].' '.$this->hive['PATH']; + if ($this->hive['QUERY']) + $req.='?'.$this->hive['QUERY']; + if (!$text) + $text='HTTP '.$code.' ('.$req.')'; + $trace=$this->trace($trace); + $loggable=$this->hive['LOGGABLE']; + if (!is_array($loggable)) + $loggable=$this->split($loggable); + foreach ($loggable as $status) + if ($status=='*' || + preg_match('/^'.preg_replace('/\D/','\d',$status).'$/',(string) $code)) { + error_log($text); + foreach (explode("\n",$trace) as $nexus) + if ($nexus) + error_log($nexus); + break; + } + if ($highlight=(!$this->hive['CLI'] && !$this->hive['AJAX'] && + $this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS))) + $trace=$this->highlight($trace); + $this->hive['ERROR']=[ + 'status'=>$header, + 'code'=>$code, + 'text'=>$text, + 'trace'=>$trace, + 'level'=>$level + ]; + $this->expire(-1); + $handler=$this->hive['ONERROR']; + $this->hive['ONERROR']=NULL; + $eol="\n"; + if ((!$handler || + $this->call($handler,[$this,$this->hive['PARAMS']], + 'beforeroute,afterroute')===FALSE) && + !$prior && !$this->hive['QUIET']) { + $error=array_diff_key( + $this->hive['ERROR'], + $this->hive['DEBUG']? + []: + ['trace'=>1] + ); + if ($this->hive['CLI']) + echo PHP_EOL.'==================================='.PHP_EOL. + 'ERROR '.$error['code'].' - '.$error['status'].PHP_EOL. + $error['text'].PHP_EOL.PHP_EOL.(isset($error['trace']) ? $error['trace'] : ''); + else + echo $this->hive['AJAX']? + json_encode($error): + (''.$eol. + ''.$eol. + ''. + ''.$code.' '.$header.''. + ($highlight? + (''):''). + ''.$eol. + ''.$eol. + '

'.$header.'

'.$eol. + '

'.$this->encode($text?:$req).'

'.$eol. + ($this->hive['DEBUG']?('
'.$trace.'
'.$eol):''). + ''.$eol. + ''); + } + if ($this->hive['HALT']) + die(1); + } + + /** + * Mock HTTP request + * @return mixed + * @param $pattern string + * @param $args array + * @param $headers array + * @param $body string + **/ + function mock($pattern, + ?array $args=NULL,?array $headers=NULL,$body=NULL) { + if (!$args) + $args=[]; + $types=['sync','ajax','cli']; + preg_match('/([\|\w]+)\h+(?:@(\w+)(?:(\(.+?)\))*|([^\h]+))'. + '(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts); + $verb=strtoupper($parts[1]); + if ($parts[2]) { + if (empty($this->hive['ALIASES'][$parts[2]])) + throw new \Exception(sprintf(self::E_Named,$parts[2])); + $parts[4]=$this->hive['ALIASES'][$parts[2]]; + $parts[4]=$this->build($parts[4], + isset($parts[3])?$this->parse($parts[3]):[]); + } + if (empty($parts[4])) + throw new \Exception(sprintf(self::E_Pattern,$pattern)); + $url=parse_url($parts[4]); + parse_str(isset($url['query'])?$url['query']:'',$GLOBALS['_GET']); + if (preg_match('/GET|HEAD/',$verb)) + $GLOBALS['_GET']=array_merge($GLOBALS['_GET'],$args); + $GLOBALS['_POST']=$verb=='POST'?$args:[]; + $GLOBALS['_REQUEST']=array_merge($GLOBALS['_GET'],$GLOBALS['_POST']); + foreach ($headers?:[] as $key=>$val) + $_SERVER['HTTP_'.strtr(strtoupper($key),'-','_')]=$val; + $this->hive['VERB']=$verb; + $this->hive['PATH']=$url['path']; + $this->hive['URI']=$this->hive['BASE'].$url['path']; + if ($GLOBALS['_GET']) + $this->hive['URI'].='?'.http_build_query($GLOBALS['_GET']); + $this->hive['BODY']=''; + if (!preg_match('/GET|HEAD/',$verb)) + $this->hive['BODY']=$body?:http_build_query($args); + $this->hive['AJAX']=isset($parts[5]) && + preg_match('/ajax/i',$parts[5]); + $this->hive['CLI']=isset($parts[5]) && + preg_match('/cli/i',$parts[5]); + return $this->run(); + } + + /** + * Assemble url from alias name + * @return string + * @param $name string + * @param $params array|string + * @param $query string|array + * @param $fragment string + **/ + function alias($name,$params=[],$query=NULL,$fragment=NULL) { + if (!is_array($params)) + $params=$this->parse($params); + if (empty($this->hive['ALIASES'][$name])) + throw new \Exception(sprintf(self::E_Named,$name)); + $url=$this->build($this->hive['ALIASES'][$name],$params); + if (is_array($query)) + $query=http_build_query($query); + return $url.($query?('?'.$query):'').($fragment?'#'.$fragment:''); + } + + /** + * Bind handler to route pattern + * @return NULL + * @param $pattern string|array + * @param $handler callback + * @param $ttl int + * @param $kbps int + **/ + function route($pattern,$handler,$ttl=0,$kbps=0) { + $types=['sync','ajax','cli']; + $alias=null; + if (is_array($pattern)) { + foreach ($pattern as $item) + $this->route($item,$handler,$ttl,$kbps); + return; + } + preg_match('/([\|\w]+)\h+(?:(?:@?(.+?)\h*:\h*)?(@(\w+)|[^\h]+))'. + '(?:\h+\[('.implode('|',$types).')\])?/u',$pattern,$parts); + if (isset($parts[2]) && $parts[2]) { + if (!preg_match('/^\w+$/',$parts[2])) + throw new \Exception(sprintf(self::E_Alias,$parts[2])); + $this->hive['ALIASES'][$alias=$parts[2]]=$parts[3]; + } + elseif (!empty($parts[4])) { + if (empty($this->hive['ALIASES'][$parts[4]])) + throw new \Exception(sprintf(self::E_Named,$parts[4])); + $parts[3]=$this->hive['ALIASES'][$alias=$parts[4]]; + } + if (empty($parts[3])) + throw new \Exception(sprintf(self::E_Pattern,$pattern)); + $type=empty($parts[5])?0:constant('self::REQ_'.strtoupper($parts[5])); + foreach ($this->split($parts[1]) as $verb) { + if (!preg_match('/'.self::VERBS.'/',$verb)) + $this->error(501,$verb.' '.$this->hive['URI']); + $this->hive['ROUTES'][$parts[3]][$type][strtoupper($verb)]= + [is_string($handler) ? trim($handler) : $handler,$ttl,$kbps,$alias]; + } + } + + /** + * Reroute to specified URI + * @return NULL + * @param $url array|string + * @param $permanent bool + * @param $die bool + **/ + function reroute($url=NULL,$permanent=FALSE,$die=TRUE) { + if (!$url) + $url=$this->hive['REALM']; + if (is_array($url)) + $url=call_user_func_array([$this,'alias'],$url); + elseif (preg_match('/^(?:@([^\/()?#]+)(?:\((.+?)\))*(\?[^#]+)*(#.+)*)/', + $url,$parts) && isset($this->hive['ALIASES'][$parts[1]])) + $url=$this->build($this->hive['ALIASES'][$parts[1]], + isset($parts[2])?$this->parse($parts[2]):[]). + (isset($parts[3])?$parts[3]:'').(isset($parts[4])?$parts[4]:''); + else + $url=$this->build($url); + if (($handler=$this->hive['ONREROUTE']) && + $this->call($handler,[$url,$permanent,$die])!==FALSE) + return; + if ($url[0]!='/' && !preg_match('/^\w+:\/\//i',$url)) + $url='/'.$url; + if ($url[0]=='/' && (empty($url[1]) || $url[1]!='/')) { + $port=$this->hive['PORT']; + $port=in_array($port,[80,443])?'':(':'.$port); + $url=$this->hive['SCHEME'].'://'. + $this->hive['HOST'].$port.$this->hive['BASE'].$url; + } + if ($this->hive['CLI']) + $this->mock('GET '.$url.' [cli]'); + else { + header('Location: '.$url); + $this->status($permanent?301:302); + if ($die) + die; + } + } + + /** + * Provide ReST interface by mapping HTTP verb to class method + * @return NULL + * @param $url string + * @param $class string|object + * @param $ttl int + * @param $kbps int + **/ + function map($url,$class,$ttl=0,$kbps=0) { + if (is_array($url)) { + foreach ($url as $item) + $this->map($item,$class,$ttl,$kbps); + return; + } + foreach (explode('|',self::VERBS) as $method) + $this->route($method.' '.$url,is_string($class)? + $class.'->'.$this->hive['PREMAP'].strtolower($method): + [$class,$this->hive['PREMAP'].strtolower($method)], + $ttl,$kbps); + } + + /** + * Redirect a route to another URL + * @return NULL + * @param $pattern string|array + * @param $url string + * @param $permanent bool + */ + function redirect($pattern,$url,$permanent=TRUE) { + if (is_array($pattern)) { + foreach ($pattern as $item) + $this->redirect($item,$url,$permanent); + return; + } + $this->route($pattern,function($fw) use($url,$permanent) { + $fw->reroute($url,$permanent); + }); + } + + /** + * Return TRUE if IPv4 address exists in DNSBL + * @return bool + * @param $ip string + **/ + function blacklisted($ip) { + if ($this->hive['DNSBL'] && + !in_array($ip, + is_array($this->hive['EXEMPT'])? + $this->hive['EXEMPT']: + $this->split($this->hive['EXEMPT']))) { + // Reverse IPv4 dotted quad + $rev=implode('.',array_reverse(explode('.',$ip))); + foreach (is_array($this->hive['DNSBL'])? + $this->hive['DNSBL']: + $this->split($this->hive['DNSBL']) as $server) + // DNSBL lookup + if (checkdnsrr($rev.'.'.$server,'A')) + return TRUE; + } + return FALSE; + } + + /** + * Applies the specified URL mask and returns parameterized matches + * @return $args array + * @param $pattern string + * @param $url string|NULL + **/ + function mask($pattern,$url=NULL) { + if (!$url) + $url=$this->rel($this->hive['URI']); + $case=$this->hive['CASELESS']?'i':''; + $wild=preg_quote($pattern,'/'); + $i=0; + while (is_int($pos=strpos($wild,'\*'))) { + $wild=substr_replace($wild,'(?P<_'.$i.'>[^\?]*)',$pos,2); + ++$i; + } + preg_match('/^'. + preg_replace( + '/((\\\{)?@(\w+\b)(?(2)\\\}))/', + '(?P<\3>[^\/\?]+)', + $wild).'\/?$/'.$case.'um',$url,$args); + foreach (array_keys($args) as $key) { + if (preg_match('/^_\d+$/',$key)) { + if (empty($args['*'])) + $args['*']=$args[$key]; + else { + if (is_string($args['*'])) + $args['*']=[$args['*']]; + array_push($args['*'],$args[$key]); + } + unset($args[$key]); + } + elseif (is_numeric($key) && $key) + unset($args[$key]); + } + return $args; + } + + /** + * Match routes against incoming URI + * @return mixed + **/ + function run() { + if ($this->blacklisted($this->hive['IP'])) + // Spammer detected + $this->error(403); + if (!$this->hive['ROUTES']) + // No routes defined + throw new \Exception(self::E_Routes); + // Match specific routes first + $paths=[]; + foreach ($keys=array_keys($this->hive['ROUTES']) as $key) { + $path=preg_replace('/@\w+/','*@',$key); + if (substr($path,-1)!='*') + $path.='+'; + $paths[]=$path; + } + $vals=array_values($this->hive['ROUTES']); + array_multisort($paths,SORT_DESC,$keys,$vals); + $this->hive['ROUTES']=array_combine($keys,$vals); + // Convert to BASE-relative URL + $req=urldecode($this->hive['PATH']); + $preflight=FALSE; + if ($cors=(isset($this->hive['HEADERS']['Origin']) && + $this->hive['CORS']['origin'])) { + $cors=$this->hive['CORS']; + header('Access-Control-Allow-Origin: '.$cors['origin']); + header('Access-Control-Allow-Credentials: '. + $this->export($cors['credentials'])); + $preflight= + isset($this->hive['HEADERS']['Access-Control-Request-Method']); + } + $allowed=[]; + foreach ($this->hive['ROUTES'] as $pattern=>$routes) { + if (!$args=$this->mask($pattern,$req)) + continue; + ksort($args); + $route=NULL; + $ptr=$this->hive['CLI']?self::REQ_CLI:$this->hive['AJAX']+1; + if (isset($routes[$ptr][$this->hive['VERB']]) || + ($preflight && isset($routes[$ptr])) || + isset($routes[$ptr=0])) + $route=$routes[$ptr]; + if (!$route) + continue; + if (isset($route[$this->hive['VERB']]) && !$preflight) { + if ($this->hive['REROUTE_TRAILING_SLASH']===TRUE && + $this->hive['VERB']=='GET' && + preg_match('/.+\/$/',$this->hive['PATH'])) + $this->reroute(substr($this->hive['PATH'],0,-1). + ($this->hive['QUERY']?('?'.$this->hive['QUERY']):'')); + list($handler,$ttl,$kbps,$alias)=$route[$this->hive['VERB']]; + // Capture values of route pattern tokens + $this->hive['PARAMS']=$args; + // Save matching route + $this->hive['ALIAS']=$alias; + $this->hive['PATTERN']=$pattern; + if ($cors && $cors['expose']) + header('Access-Control-Expose-Headers: '. + (is_array($cors['expose'])? + implode(',',$cors['expose']):$cors['expose'])); + if (is_string($handler)) { + // Replace route pattern tokens in handler if any + $handler=preg_replace_callback('/({)?@(\w+\b)(?(1)})/', + function($id) use($args) { + $pid=count($id)>2?2:1; + return isset($args[$id[$pid]])? + $args[$id[$pid]]: + $id[0]; + }, + $handler + ); + if (preg_match('/(.+)\h*(?:->|::)/',$handler,$match) && + !class_exists($match[1])) + $this->error(404); + } + // Process request + $result=NULL; + $body=''; + $now=microtime(TRUE); + if (preg_match('/GET|HEAD/',$this->hive['VERB']) && $ttl) { + // Only GET and HEAD requests are cacheable + $headers=$this->hive['HEADERS']; + $cache=Cache::instance(); + $cached=$cache->exists( + $hash=$this->hash($this->hive['VERB'].' '. + $this->hive['URI']).'.url',$data); + if ($cached) { + if (isset($headers['If-Modified-Since']) && + strtotime($headers['If-Modified-Since'])+ + $ttl>$now) { + $this->status(304); + die; + } + // Retrieve from cache backend + list($headers,$body,$result)=$data; + if (!$this->hive['CLI']) + array_walk($headers,'header'); + $this->expire($cached[0]+$ttl-$now); + } + else + // Expire HTTP client-cached page + $this->expire($ttl); + } + else + $this->expire(0); + if (!strlen($body)) { + if (!$this->hive['RAW'] && !$this->hive['BODY']) + $this->hive['BODY']=file_get_contents('php://input'); + ob_start(); + // Call route handler + $result=$this->call($handler,[$this,$args,$handler], + 'beforeroute,afterroute'); + $body=ob_get_clean(); + if (isset($cache) && !error_get_last()) { + // Save to cache backend + $cache->set($hash,[ + // Remove cookies + preg_grep('/Set-Cookie\:/',headers_list(), + PREG_GREP_INVERT),$body,$result],$ttl); + } + } + $this->hive['RESPONSE']=$body; + if (!$this->hive['QUIET']) { + if ($kbps) { + $ctr=0; + foreach (str_split($body,1024) as $part) { + // Throttle output + ++$ctr; + if ($ctr/$kbps>($elapsed=microtime(TRUE)-$now) && + !connection_aborted()) + usleep(round(1e6*($ctr/$kbps-$elapsed))); + echo $part; + } + } + else + echo $body; + } + if ($result || $this->hive['VERB']!='OPTIONS') + return $result; + } + $allowed=array_merge($allowed,array_keys($route)); + } + if (!$allowed) + // URL doesn't match any route + $this->error(404); + elseif (!$this->hive['CLI']) { + if (!preg_grep('/Allow:/',$headers_send=headers_list())) + // Unhandled HTTP method + header('Allow: '.implode(',',array_unique($allowed))); + if ($cors) { + if (!preg_grep('/Access-Control-Allow-Methods:/',$headers_send)) + header('Access-Control-Allow-Methods: OPTIONS,'. + implode(',',$allowed)); + if ($cors['headers'] && + !preg_grep('/Access-Control-Allow-Headers:/',$headers_send)) + header('Access-Control-Allow-Headers: '. + (is_array($cors['headers'])? + implode(',',$cors['headers']): + $cors['headers'])); + if ($cors['ttl']>0) + header('Access-Control-Max-Age: '.$cors['ttl']); + } + if ($this->hive['VERB']!='OPTIONS') + $this->error(405); + } + return FALSE; + } + + /** + * Loop until callback returns TRUE (for long polling) + * @return mixed + * @param $func callback + * @param $args array + * @param $timeout int + **/ + function until($func,$args=NULL,$timeout=60) { + if (!$args) + $args=[]; + $time=time(); + $max=ini_get('max_execution_time'); + $limit=max(0,($max?min($timeout,$max):$timeout)-1); + $out=''; + // Turn output buffering on + ob_start(); + // Not for the weak of heart + while ( + // No error occurred + !$this->hive['ERROR'] && + // Got time left? + time()-$time+1<$limit && + // Still alive? + !connection_aborted() && + // Restart session + !headers_sent() && + (session_status()==PHP_SESSION_ACTIVE || session_start()) && + // CAUTION: Callback will kill host if it never becomes truthy! + !$out=$this->call($func,$args)) { + if (!$this->hive['CLI']) + session_commit(); + // Hush down + sleep(1); + } + ob_flush(); + flush(); + return $out; + } + + /** + * Disconnect HTTP client; + * Set FcgidOutputBufferSize to zero if server uses mod_fcgid; + * Disable mod_deflate when rendering text/html output + **/ + function abort() { + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); + $out=''; + while (ob_get_level()) + $out=ob_get_clean().$out; + if (!headers_sent()) { + header('Content-Length: '.strlen($out)); + header('Connection: close'); + } + session_commit(); + echo $out; + flush(); + if (function_exists('fastcgi_finish_request')) + fastcgi_finish_request(); + } + + /** + * Grab the real route handler behind the string expression + * @return string|array + * @param $func string + * @param $args array + **/ + function grab($func,$args=NULL) { + if (preg_match('/(.+)\h*(->|::)\h*(.+)/s',$func,$parts)) { + // Convert string to executable PHP callback + if (!class_exists($parts[1])) + throw new \Exception(sprintf(self::E_Class,$parts[1])); + if ($parts[2]=='->') { + if (is_subclass_of($parts[1],'Prefab')) + $parts[1]=call_user_func($parts[1].'::instance'); + elseif (isset($this->hive['CONTAINER'])) { + $container=$this->hive['CONTAINER']; + if (is_object($container) && is_callable([$container,'has']) + && $container->has($parts[1])) // PSR11 + $parts[1]=call_user_func([$container,'get'],$parts[1]); + elseif (is_callable($container)) + $parts[1]=call_user_func($container,$parts[1],$args); + elseif (is_string($container) && + is_subclass_of($container,'Prefab')) + $parts[1]=call_user_func($container.'::instance')-> + get($parts[1]); + else + throw new \Exception(sprintf(self::E_Class, + $this->stringify($parts[1]))); + } + else { + $ref=new ReflectionClass($parts[1]); + $parts[1]=method_exists($parts[1],'__construct') && $args? + $ref->newinstanceargs($args): + $ref->newinstance(); + } + } + $func=[$parts[1],$parts[3]]; + } + return $func; + } + + /** + * Execute callback/hooks (supports 'class->method' format) + * @return mixed|FALSE + * @param $func callback + * @param $args mixed + * @param $hooks string + **/ + function call($func,$args=NULL,$hooks='') { + if (!is_array($args)) + $args=[$args]; + // Grab the real handler behind the string representation + if (is_string($func)) + $func=$this->grab($func,$args); + // Execute function; abort if callback/hook returns FALSE + if (!is_callable($func)) + // No route handler + if ($hooks=='beforeroute,afterroute') { + $allowed=[]; + if (is_array($func)) + $allowed=array_intersect( + array_map('strtoupper',get_class_methods($func[0])), + explode('|',self::VERBS) + ); + header('Allow: '.implode(',',$allowed)); + $this->error(405); + } + else + throw new \Exception(sprintf(self::E_Method, + is_string($func)?$func:$this->stringify($func))); + $obj=FALSE; + if (is_array($func)) { + $hooks=$this->split($hooks); + $obj=TRUE; + } + // Execute pre-route hook if any + if ($obj && $hooks && in_array($hook='beforeroute',$hooks) && + method_exists($func[0],$hook) && + call_user_func_array([$func[0],$hook],$args)===FALSE) + return FALSE; + // Execute callback + $out=call_user_func_array($func,$args?:[]); + if ($out===FALSE) + return FALSE; + // Execute post-route hook if any + if ($obj && $hooks && in_array($hook='afterroute',$hooks) && + method_exists($func[0],$hook) && + call_user_func_array([$func[0],$hook],$args)===FALSE) + return FALSE; + return $out; + } + + /** + * Execute specified callbacks in succession; Apply same arguments + * to all callbacks + * @return array + * @param $funcs array|string + * @param $args mixed + **/ + function chain($funcs,$args=NULL) { + $out=[]; + foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func) + $out[]=$this->call($func,$args); + return $out; + } + + /** + * Execute specified callbacks in succession; Relay result of + * previous callback as argument to the next callback + * @return array + * @param $funcs array|string + * @param $args mixed + **/ + function relay($funcs,$args=NULL) { + foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func) + $args=[$this->call($func,$args)]; + return array_shift($args); + } + + /** + * Configure framework according to .ini-style file settings; + * If optional 2nd arg is provided, template strings are interpreted + * @return object + * @param $source string|array + * @param $allow bool + **/ + function config($source,$allow=FALSE) { + if (is_string($source)) + $source=$this->split($source); + if ($allow) + $preview=Preview::instance(); + foreach ($source as $file) { + preg_match_all( + '/(?<=^|\n)(?:'. + '\[(?
+ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/forgotPassword.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/forgotPassword.html new file mode 100644 index 0000000..ec98e9e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/forgotPassword.html @@ -0,0 +1,34 @@ + +
+
+ +
+
+ diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/index.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/index.html new file mode 100644 index 0000000..4675934 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/index.html @@ -0,0 +1,2 @@ +

Hello, World!

+

It works

\ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/index.php @@ -0,0 +1,3 @@ + +
+
+ +
+
+ diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/logout.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/logout.html new file mode 100644 index 0000000..dea81d8 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/logout.html @@ -0,0 +1,15 @@ + +
+
+ +
+
+ diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/passwordRecovering.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/passwordRecovering.html new file mode 100644 index 0000000..58fd8f1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/passwordRecovering.html @@ -0,0 +1,28 @@ + +
+
+ +
+
+ diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/signup.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/signup.html new file mode 100644 index 0000000..a56ccf1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/pages/signup.html @@ -0,0 +1,29 @@ + +
+
+ +
+
+ diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/breadcrumbs.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/breadcrumbs.html new file mode 100644 index 0000000..aa85b1d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/breadcrumbs.html @@ -0,0 +1,31 @@ +{** + +**} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/deviceType.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/deviceType.html new file mode 100644 index 0000000..3a398e2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/deviceType.html @@ -0,0 +1,14 @@ +
+
+
+
+ +
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/entityType.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/entityType.html new file mode 100644 index 0000000..fc895a5 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/entityType.html @@ -0,0 +1,14 @@ +
+
+
+
+ +
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/eventType.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/eventType.html new file mode 100644 index 0000000..8242bd6 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/eventType.html @@ -0,0 +1,14 @@ +
+
+
+
+ +
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/index.php @@ -0,0 +1,3 @@ + +
+
+
+ +
+
+
+ diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/rules.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/rules.html new file mode 100644 index 0000000..ff8af0d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/rules.html @@ -0,0 +1,15 @@ +
+
+
+
+ +
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/scoresRange.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/scoresRange.html new file mode 100644 index 0000000..f2a2831 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/choices/scoresRange.html @@ -0,0 +1,17 @@ +{~ + $scores = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]; +~} +
+
+
+
+ +
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/footer.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/footer.html new file mode 100644 index 0000000..f28c562 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/footer.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/footerAdmin.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/footerAdmin.html new file mode 100644 index 0000000..f28c562 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/footerAdmin.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/addCoOwnerForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/addCoOwnerForm.html new file mode 100644 index 0000000..a18b265 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/addCoOwnerForm.html @@ -0,0 +1,10 @@ +
+ + + + + + + + +
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/changeEmailForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/changeEmailForm.html new file mode 100644 index 0000000..56971fc --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/changeEmailForm.html @@ -0,0 +1,31 @@ +
+
+ + + +
+
{{ @AdminSettings_changeEmail_form_title }}

+
+
+ +
+
+ +
+ {{ \sprintf(@AdminSettings_changeEmail_approval_pending, @waitingForConfirmation) }} +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/changePasswordForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/changePasswordForm.html new file mode 100644 index 0000000..1e3d6fe --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/changePasswordForm.html @@ -0,0 +1,41 @@ +
+
+ + + +
+
{{ @AdminSettings_changePassword_form_title }}

+
+
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/checkUpdatesForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/checkUpdatesForm.html new file mode 100644 index 0000000..772854e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/checkUpdatesForm.html @@ -0,0 +1,23 @@ +
+
+ + + +
+
{{ @AdminSettings_checkUpdates_form_title }}
+
+ +
+
+
+

{{ @AdminSettings_form_checkUpdates_currentVerision | raw }}{{ @CURRENT_VERSION }}

+

{{ @AdminSettings_form_checkUpdates_confirmationMessage | raw }}

+
+
+
+ +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/closeAccountForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/closeAccountForm.html new file mode 100644 index 0000000..b9f8816 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/closeAccountForm.html @@ -0,0 +1,22 @@ +
+
+ + + +
+

{{ @AdminSettings_form_closeAccount_title }}

+
+ +
+
+
+

{{ @AdminSettings_form_closeAccount_confirmationMessage | raw }}

+
+
+
+ +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/deleteUserForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/deleteUserForm.html new file mode 100644 index 0000000..fa0910a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/deleteUserForm.html @@ -0,0 +1,7 @@ +
+ + + + + {{ @AdminUser_scheduled_for_removal }} +
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/enrichAllForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/enrichAllForm.html new file mode 100644 index 0000000..2309bcc --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/enrichAllForm.html @@ -0,0 +1,32 @@ +
+
+ + +
+

{{ @AdminApi_manual_enrichment_form_title }}

+
+ +
+
+
+

{{ @AdminApi_manual_enrichment_form_confirmationMessage | raw }}

+
+
+
+ + {~ + $disableButton = true; + foreach ($API_KEYS as $key) { + if ($key['apiToken'] !== null) { + $disableButton = false; + } + } + + $disableButton = ($disableButton || !$NOT_CHECKED)? 'disabled' : ''; + ~} + +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/enrichmentButtonForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/enrichmentButtonForm.html new file mode 100644 index 0000000..5ce597d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/enrichmentButtonForm.html @@ -0,0 +1,9 @@ +
+ + + + + +
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/filtersForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/filtersForm.html new file mode 100644 index 0000000..bf81fd7 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/filtersForm.html @@ -0,0 +1,66 @@ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/forgotPasswordForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/forgotPasswordForm.html new file mode 100644 index 0000000..9a417f2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/forgotPasswordForm.html @@ -0,0 +1,22 @@ +
+ + +
+
+ +
+
+ +
+ +
+ +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/globalSearchForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/globalSearchForm.html new file mode 100644 index 0000000..79fa048 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/globalSearchForm.html @@ -0,0 +1,20 @@ +
+
+ +
+ +
+ +
+ + + +
+ + + +
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/index.php @@ -0,0 +1,3 @@ + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+
+ diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/logoutForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/logoutForm.html new file mode 100644 index 0000000..de46674 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/logoutForm.html @@ -0,0 +1,16 @@ +
+ + +

{{ @Logout_text }}

+ +
+ +
+ +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/manualCheckForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/manualCheckForm.html new file mode 100644 index 0000000..4a8c904 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/manualCheckForm.html @@ -0,0 +1,75 @@ +
+
+ + +
+

{{ @AdminManualCheck_form_title }}

+
+ +
+
+ + + + + + +
+
+ +
+ +
+ +
+
+
+ + + +
+
+
+ +
+
+
+
+ +{** +
+
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+
+ +
+ +
+**} +
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/manualCheckShortForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/manualCheckShortForm.html new file mode 100644 index 0000000..745633a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/manualCheckShortForm.html @@ -0,0 +1,7 @@ +
+ + + + + {{ @value->search_query }} +
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/notificationPreferencesForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/notificationPreferencesForm.html new file mode 100644 index 0000000..72bcd4b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/notificationPreferencesForm.html @@ -0,0 +1,39 @@ +
+
+ + + +
+
{{ @AdminSettings_notificationPreferences_title }}

+
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/recoveryForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/recoveryForm.html new file mode 100644 index 0000000..7bf78f3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/recoveryForm.html @@ -0,0 +1,28 @@ + +
+ + +
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/reloadRulesForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/reloadRulesForm.html new file mode 100644 index 0000000..f20d239 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/reloadRulesForm.html @@ -0,0 +1,7 @@ +
+ + +
+ +
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/removeCoOwnerForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/removeCoOwnerForm.html new file mode 100644 index 0000000..3ba1fa8 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/removeCoOwnerForm.html @@ -0,0 +1,6 @@ +
+ + + + +
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/resetKeyForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/resetKeyForm.html new file mode 100644 index 0000000..3dcf816 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/resetKeyForm.html @@ -0,0 +1,6 @@ +
+ + + + +
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/retentionPolicyForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/retentionPolicyForm.html new file mode 100644 index 0000000..8c8b7a3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/retentionPolicyForm.html @@ -0,0 +1,36 @@ +
+
+ + + + +
+
{{ @AdminRetentionPolicy_form_title }}

+
+
+ +
+
+ {{ @AdminRetentionPolicy_form_field_policy_warning }} +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/riskScoreButtonForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/riskScoreButtonForm.html new file mode 100644 index 0000000..d37999d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/riskScoreButtonForm.html @@ -0,0 +1,8 @@ +
+ + + + +
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/searchForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/searchForm.html new file mode 100644 index 0000000..9088fad --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/searchForm.html @@ -0,0 +1,8 @@ +
+
+ + + + +
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/signupForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/signupForm.html new file mode 100644 index 0000000..5f723c8 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/signupForm.html @@ -0,0 +1,34 @@ +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+ +
+ + +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/thresholdValuesForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/thresholdValuesForm.html new file mode 100644 index 0000000..5ad7259 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/thresholdValuesForm.html @@ -0,0 +1,60 @@ +
+
+ + + + +
+
{{ @AdminThresholdValues_form_title }}

+
+
+ +
+
+ {{ @AdminThresholdValues_form_field_warning }} +
+ +
+ = 0 ? 'disabled' : '') }}> +
+ + = 6 ? 'disabled' : '') }}> +
+ + = 18 ? 'disabled' : '') }}> +
+ + = 24 ? 'disabled' : '') }}> +
+ + = 33 ? 'disabled' : '') }}> + +
+
+
+ +
+ +
+ + +
+ + +
+ + +
+ + + +
+
+
+
+ +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/timeZoneForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/timeZoneForm.html new file mode 100644 index 0000000..e5f9116 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/timeZoneForm.html @@ -0,0 +1,28 @@ +
+
+ + + +
+
{{ @AdminTimeZone_form_title }}

+
+
+ +
+
+
+ +
+ + + +
+
+
+
+ +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/updateApiUsageForm.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/updateApiUsageForm.html new file mode 100644 index 0000000..a858872 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/forms/updateApiUsageForm.html @@ -0,0 +1,34 @@ +
+ + + + +
+
+

{{ @AdminApi_form_confirmationMessage | raw }}

+ + + + +
+ +
+ +
+
+
+
+{*** +
+ +
+ +
+
+***} +
+
+
+ +
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/header.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/header.html new file mode 100644 index 0000000..f5337b2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/header.html @@ -0,0 +1,16 @@ + + + + + + + + + {{ @PAGE_TITLE }} + + + + + + + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/headerAdmin.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/headerAdmin.html new file mode 100644 index 0000000..b942610 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/headerAdmin.html @@ -0,0 +1,55 @@ + + + + + + + + + + {{ @PAGE_TITLE }} + + + + + + + + + + + + + + + + + {** https://stackoverflow.com/a/50474341 **} + + {** https://stackoverflow.com/a/58270601 **} + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/home/counters.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/home/counters.html new file mode 100644 index 0000000..a5d3c9c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/home/counters.html @@ -0,0 +1,67 @@ + + +{*** + +***} + + + + + + + + + + + + +
+
+ {{ @AdminHome_total_events }}

+

+
+

+ {{ @AdminHome_view_all }} +
+
+ {{ @AdminHome_total_users }}

+

+
+

+ {{ @AdminHome_view_all }} +
+
+ {{ @AdminHome_total_ips }}

+

+
+

+ {{ @AdminHome_view_all }} +
+
+ {{ @AdminHome_total_countries }}

+

+
+

+ {{ @AdminHome_view_all }} +
+
+ {{ @AdminHome_total_urls }}

+

+
+

+ {{ @AdminHome_view_all }} +
+
+ {{ @AdminHome_total_users_for_review }}

+

+
+

+ {{ @AdminHome_view_all }} +
+
+ {{ @AdminHome_total_blocked_users }}

+

+
+

+ {{ @AdminHome_view_all }} +
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/home/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/home/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/home/index.php @@ -0,0 +1,3 @@ + + + +
+
+ {{ @AdminHome_top10_most_active_users }}

+
+ +
+ + + +
+
+ {{ @AdminHome_top10_active_countries }}

+
+ +
+ + + +
+
+ {{ @AdminHome_top10_active_urls }}

+
+ +
+ + + + + +
+
+ {{ @AdminHome_top10_ips_with_the_most_users }}

+
+ +
+ + + +
+
+ {{ @AdminHome_top10_users_with_most_login_fail }}

+
+ +
+ + + +
+
+ {{ @AdminHome_top10_users_with_the_most_ips }}

+
+ +
+ + + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/home/topTenTable.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/home/topTenTable.html new file mode 100644 index 0000000..7b6a839 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/home/topTenTable.html @@ -0,0 +1,8 @@ + + + + + + + +
{{@c1}}{{@c2}}
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/index.php @@ -0,0 +1,3 @@ + + + + +
+ + + + + + + + {~ + $ICON_PATH = in_array($country['iso'], [null, 'N/A', 'AN', 'CS', 'YU']) ? 'icons/null.svg' : 'flags/'.strtolower($country['iso']).'.svg'; + ~} + + +

{{@country.iso}}{{ @title }}

+
+ +

{{@country.iso}}{{ @title }}

+
+
+
+ + + +

{{ @title }}

+
+ +

{{ @title }}

+
+
+
+
+
+
+ + +
{{ @subtitle }}
+
+
+ diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/leftMenu.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/leftMenu.html new file mode 100644 index 0000000..6949b21 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/leftMenu.html @@ -0,0 +1,147 @@ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/logoAdmin.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/logoAdmin.html new file mode 100644 index 0000000..cc5f883 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/logoAdmin.html @@ -0,0 +1,3 @@ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/mapWithCountries.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/mapWithCountries.html new file mode 100644 index 0000000..71a932d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/mapWithCountries.html @@ -0,0 +1,36 @@ +
+ + + + + +
+
+
+
+
+ {{ @AdminCountries_map_title }} +

+

+
+
+ +
+
+ + + + + + + + + + + + +
{{ @Base_table_column_country }}{{ @Base_table_column_country_code }}{{ @Base_table_column_total_users }}{{ @Base_table_column_total_actions }}{{ @Base_table_column_total_ips }}
+
+
+ +
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/mapWithIps.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/mapWithIps.html new file mode 100644 index 0000000..e48c586 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/mapWithIps.html @@ -0,0 +1,34 @@ +
+
+
+ {{ @AdminIps_map_title }} +

+

+
+
+ +
+
+
+ +
+
+ + + + + + + + + + + {*** + ***} + + +
{{ @Base_table_column_ip }}{{ @Base_table_column_country }}{{ @Base_table_column_asn }}{{ @Base_table_column_netname }}{{ @Base_table_column_ip_type }}{{ @Base_table_column_total_actions }}{{ @Base_table_column_total_users }}
+
+
+ +
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/notification.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/notification.html new file mode 100644 index 0000000..3012448 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/notification.html @@ -0,0 +1,17 @@ + + + + +
[{{ \Utils\ElapsedDate::short(@TS) }}]  {{ @MESSAGE | raw }}
+
+ + + + + +
[{{ \Utils\ElapsedDate::short(@TS) }}]  {{ @MESSAGE | raw }}
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/devicePanel.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/devicePanel.html new file mode 100644 index 0000000..603e720 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/devicePanel.html @@ -0,0 +1,30 @@ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/emailPanel.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/emailPanel.html new file mode 100644 index 0000000..16b1c28 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/emailPanel.html @@ -0,0 +1,80 @@ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/eventPanel.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/eventPanel.html new file mode 100644 index 0000000..1e50c8f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/eventPanel.html @@ -0,0 +1,208 @@ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/index.php @@ -0,0 +1,3 @@ + +
+

{{@Details_placeholder}}

+
+ + + + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/phonePanel.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/phonePanel.html new file mode 100644 index 0000000..3b1b3e9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/panel/phonePanel.html @@ -0,0 +1,50 @@ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/popups/closeAccountPopup.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/popups/closeAccountPopup.html new file mode 100644 index 0000000..9e27611 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/popups/closeAccountPopup.html @@ -0,0 +1,19 @@ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/popups/enrichAllPopup.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/popups/enrichAllPopup.html new file mode 100644 index 0000000..3856d9f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/popups/enrichAllPopup.html @@ -0,0 +1,17 @@ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/popups/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/popups/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/popups/index.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/scoreDetails.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/scoreDetails.html new file mode 100644 index 0000000..8157212 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/scoreDetails.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/systemNotification.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/systemNotification.html new file mode 100644 index 0000000..f64919c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/systemNotification.html @@ -0,0 +1,9 @@ + + {~ foreach (@SYSTEM_MESSAGES as @id=>@message): ~} +
+ [{{ \Utils\ElapsedDate::short(@message.created_at) }}]  {{ @message.text | raw }} +
+ {~ endforeach ~} +
+ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/blacklist.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/blacklist.html new file mode 100644 index 0000000..91e01e4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/blacklist.html @@ -0,0 +1,37 @@ + +
+ +
+
+
+ +
+
+
+ {{ @AdminBlacklst_table_title }} +

+
+
+ +
+
+ + + + + + + + + + + + + + + + +
{{ @Base_table_column_user_risk_score_and_email }}{{ @Base_table_column_blacklist_added }}{{ @Base_table_column_blacklist_type }}{{ @Base_table_column_blacklist_value }}{{ @Base_table_column_blacklist_actions }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/bots.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/bots.html new file mode 100644 index 0000000..36c651e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/bots.html @@ -0,0 +1,40 @@ + +
+ + + + + +
+
+
+ +
+
+
+ {{ @AdminBots_table_title }} +

+

+
+
+ +
+
+ + + + + + + + + + + + + + +
{{ @Base_table_column_device_id }}{{ @Base_table_column_device_name }}{{ @Base_table_column_os }}{{ @Base_table_column_modified }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/devices.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/devices.html new file mode 100644 index 0000000..527ea69 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/devices.html @@ -0,0 +1,26 @@ +
+
+
+ {{ @AdminDevices_table_title }} +

+

+
+
+ +
+
+ + + + + + + + + + + +
{{ @Base_table_column_device_created }}{{ @Base_table_column_device_name }}{{ @Base_table_column_os }}{{ @Base_table_column_browser }}{{ @Base_table_column_device_lang }}{{ @Base_table_column_modified }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/domains.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/domains.html new file mode 100644 index 0000000..37a8f13 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/domains.html @@ -0,0 +1,51 @@ + +
+ + + + + +
+
+
+ +
+
+
+ + + {{ @AdminDomains_table_same_ip }} +

+
+ + {{ @AdminDomains_table_title }} +

+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + +
{{ @Base_table_column_domain }}{{ @Base_table_column_free_email_provider }}{{ @Base_table_column_tranco_rank }}{{ @Base_table_column_unavailable }}{{ @Base_table_column_disposable }}{{ @Base_table_column_domain_registered }}{{ @Base_table_column_total_users }}{{ @Base_table_column_total_fraud_users }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/emails.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/emails.html new file mode 100644 index 0000000..33bc20d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/emails.html @@ -0,0 +1,46 @@ +
+
+
+ {{ @AdminEmails_table_title }} +

+
+
+ + +
+ + + + + +
+
+
+ +
+
+ + + + + + + + + + + + {******} + + + + + + {*** + ***} + + +
{{ @Base_table_column_email_noprofiles }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/events.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/events.html new file mode 100644 index 0000000..7740878 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/events.html @@ -0,0 +1,85 @@ + + + + + +
+ + + + + +
+
+
+
+ +
+
+
+ + {{ @title }} + {{ @AdminEvents_table_title }} + +

+

+
+
+ +
+
+ + + +
+ + + +
+
+
+ + + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ @Base_table_column_session }}{{ @Base_table_column_user_risk_score_and_email }}{{ @Base_table_column_last_action_timestamp }}{{ @Base_table_column_event_type }}{{ @Base_table_column_ip }}{{ @Base_table_column_ip_type }}{{ @Base_table_column_device }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/index.php @@ -0,0 +1,3 @@ + +
+ + + + + +
+
+ + +
+
+
+ + + {{@title}} + + + {{ @AdminIps_table_title }} + + + +

+

+
+
+ +
+
+ + + + + + + + + + + + + + + + + {*** + ***} + + +
{{ @Base_table_column_ip }}{{ @Base_table_column_country }}{{ @Base_table_column_asn }}{{ @Base_table_column_netname }}{{ @Base_table_column_ip_type }}{{ @Base_table_column_total_actions }}{{ @Base_table_column_total_users }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/isps.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/isps.html new file mode 100644 index 0000000..3041b5b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/isps.html @@ -0,0 +1,41 @@ + +
+ + + + + +
+
+
+ +
+
+
+ {{ @AdminIsps_table_title }} +

+
+
+ +
+
+ + + + + + + + + + + + + + + + +
{{ @Base_table_column_asn }}{{ @Base_table_column_netname }}{{ @Base_table_column_total_actions }}{{ @Base_table_column_total_ips }}{{ @Base_table_column_total_users }}{{ @Base_table_column_total_fraud_users }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/logbook.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/logbook.html new file mode 100644 index 0000000..7253921 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/logbook.html @@ -0,0 +1,40 @@ + +
+ + + + + +
+
+
+ +
+
+
+ {{ @AdminLogbook_table_title }} +

+

+
+
+ +
+
+ + + + + + + + + + + + + + +
{{ @AdminLogbook_column_ip }}{{ @Base_table_column_local_timestamp }}{{ @Base_table_column_error_type }}{{ @Base_table_column_error_text }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/manualCheckHistory.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/manualCheckHistory.html new file mode 100644 index 0000000..100b5e4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/manualCheckHistory.html @@ -0,0 +1,22 @@ +
+
+

{{ @AdminManualCheck_history_title }}

+
+ +
+
+ + + + + + + + + +
{{ @AdminManualCheck_form_types[@value->type] }} + +
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/manualCheckResult.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/manualCheckResult.html new file mode 100644 index 0000000..e424881 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/manualCheckResult.html @@ -0,0 +1,24 @@ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/payloads/fieldAuditTrail.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/payloads/fieldAuditTrail.html new file mode 100644 index 0000000..889be82 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/payloads/fieldAuditTrail.html @@ -0,0 +1,24 @@ +
+
+
+ {{ @AdminFieldAuditTrail_table_title }} +

+
+
+ +
+
+ + + + + + + + + + +
{{ @Base_table_column_audit_trail_created }}{{ @Base_table_column_audit_trail_field }}{{ @Base_table_column_audit_trail_old_value }}{{ @Base_table_column_audit_trail_new_value }}{{ @Base_table_column_audit_trail_parent }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/phones.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/phones.html new file mode 100644 index 0000000..603545e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/phones.html @@ -0,0 +1,44 @@ +
+
+
+ {{ @AdminPhones_table_title }} +

+
+
+ + +
+ + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + {*** + ***} + + +
{{ @Base_table_column_user_phone }}{{ @Base_table_column_phone_invalid }}{{ @Base_table_column_country }}{{ @Base_table_column_phone_carrier_name }}{{ @Base_table_column_phone_type }}{{ @Base_table_column_total_users }}{{ @Base_table_column_phone_blacklist }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/resources.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/resources.html new file mode 100644 index 0000000..a6ee0f5 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/resources.html @@ -0,0 +1,43 @@ + +
+ + + + + +
+
+
+ +
+ +
+
+ {{ @AdminResources_table_title }} +

+
+
+ +
+
+ + + + + + + + + + + + + + + + + +
{{ @Base_table_column_url }}{{ @Base_table_column_code }}{{ @Base_table_column_total_users }}{{ @Base_table_column_total_countries }}{{ @Base_table_column_total_ips }}{{ @Base_table_column_total_actions }}{{ @Base_table_column_suspicious_url }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/reviewQueue.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/reviewQueue.html new file mode 100644 index 0000000..a269084 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/reviewQueue.html @@ -0,0 +1,43 @@ + +
+ + + + + +
+
+
+ +
+
+
+ {{ @AdminReviewQueue_table_title }} +

+

+
+
+ +
+
+ + + + + + + + + + + + + + + + + +
{{ @Base_table_column_user_risk_score_and_email }}{{ @Base_table_column_added_to_review }}{{ @Base_table_column_user_firstname }}{{ @Base_table_column_user_lastname }}{{ @Base_table_column_user_registered }}{{ @Base_table_column_action_buttons }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/rules.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/rules.html new file mode 100644 index 0000000..998ca4e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/rules.html @@ -0,0 +1,93 @@ +
+
+
+ {{ @AdminRules_table_title }} + {{ count(@RULES) }}

+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ @AdminRules_table_header_code }}{{ @AdminRules_table_header_group }}{{ @AdminRules_table_header_description }}{{ @AdminRules_table_header_proportion }}{{ @AdminRules_table_header_weight }}{{ @AdminRules_table_header_users }}
+

{{@RULES[@I].uid}}

+
+ + + {{@RULES[@I].type}} + + +

{{@RULES[@I].type}}

+
+
+
+

{{@RULES[@I].name}}

+

{{@RULES[@I].descr}}

+
+ + + {{ @PROPORTION }} + + + + + +
+ +
+
+
+
+ + + + + + +
+ +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/sharedOperators.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/sharedOperators.html new file mode 100644 index 0000000..dd329e8 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/sharedOperators.html @@ -0,0 +1,37 @@ +
+
+
{{ @AdminApi_shared_keys_title }}

+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
{{ @operator.email }}{{ @operator.is_active ? 'Active' : 'Inactive' }} + +
{{ @AdminApi_shared_keys_empty }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/tokens.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/tokens.html new file mode 100644 index 0000000..524334b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/tokens.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + +
{{ @AdminApi_table_column_sensor_key }}{{ @AdminApi_table_column_created_at }}{{ @AdminApi_table_column_action }}
{{ @API_KEY.key }}{{ \Utils\ElapsedDate::short(@API_KEY.created_at) }} + +
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/usageStats.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/usageStats.html new file mode 100644 index 0000000..f7e0eda --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/usageStats.html @@ -0,0 +1,22 @@ +
+
+
{{ @AdminApi_token_management_title }}

+
+
+ +
+
+ + + + + + + + + + +
{{ @AdminApi_token_management_plan_col }}{{ @AdminApi_token_management_subscription_status_col }}{{ @AdminApi_token_management_last_period_usage_col }}{{ @AdminApi_token_management_next_billed_col }}{{ @AdminApi_token_management_update_payment_action }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/users.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/users.html new file mode 100644 index 0000000..e4c7f1f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/tables/users.html @@ -0,0 +1,44 @@ + +
+ + + + + +
+
+
+ +
+
+
+ {{ @AdminUsers_table_title }} +

+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + +
{{ @Base_table_column_user_risk_score_and_email }}{{ @Base_table_column_userid }}{{ @Base_table_column_user_firstname }}{{ @Base_table_column_user_lastname }}{{ @Base_table_column_user_registered }}{{ @Base_table_column_lastseen }}{{ @Base_table_column_reviewed_status_short }}
+
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/timeZoneSelector.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/timeZoneSelector.html new file mode 100644 index 0000000..e810ad5 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/timeZoneSelector.html @@ -0,0 +1,8 @@ +
+ +
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/userBlock.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/userBlock.html new file mode 100644 index 0000000..ade76f4 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/userBlock.html @@ -0,0 +1,43 @@ + + + {~ + $fraud = 'null'; + if(true === $USER['fraud']) $fraud = 'true'; + if(false === $USER['fraud']) $fraud = 'false'; + ~} + +
+
+
+ +
+ +
+
+
+ +{~ + $scoreUpd = '−'; + + if ($USER['score_updated_at']) { + try { + $date = new DateTime($USER['score_updated_at']); + $scoreUpd = 'Last updated: '.$date->format('d/m/Y'); + } catch (Exception $e) { + $scoreUpd = '−'; + } + } + + if ($USER['fraud'] !== null) { + [$score, $cls] = $USER['fraud'] ? ['X', 'low'] : ['OK', 'high']; + } else { + [$score, $cls] = \Utils\RulesClasses::getUserScoreClass($USER['score']); + } +~} + +

+ {{ @score | raw }}{{ @title }} +

diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/watchListBlock.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/watchListBlock.html new file mode 100644 index 0000000..9667021 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/watchListBlock.html @@ -0,0 +1,48 @@ + + + {~ + $fraud = 'null'; + if(true === $USER['fraud']) $fraud = 'true'; + if(false === $USER['fraud']) $fraud = 'false'; + ~} + +
+
+
+ +
+ +
+
+
+ + + + {~ + $title = "$title"; + ~} + + + + + + + +{~ + if ($USER['fraud'] !== null) { + [$score, $cls] = $USER['fraud'] ? ['X', 'low'] : ['OK', 'high']; + } else { + [$score, $cls] = \Utils\RulesClasses::getUserScoreClass($USER['score']); + } +~} + +

+ {{ @score | raw }}{{ @title }} +

diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/welcomeMessage.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/welcomeMessage.html new file mode 100644 index 0000000..1e5cf79 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/welcomeMessage.html @@ -0,0 +1 @@ + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/bot.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/bot.html new file mode 100644 index 0000000..eeb148f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/bot.html @@ -0,0 +1,35 @@ + + + + + + + + + + +
+
+ {{ @Base_table_column_device_name }}

+

+
+

+
+
+ {{ @Base_table_column_os }}

+

+
+

+
+
+ {{ @Base_table_column_browser }}

+

+
+

+
+
+ {{ @Base_table_column_modified }}

+

+
+

+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/country.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/country.html new file mode 100644 index 0000000..ab1230c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/country.html @@ -0,0 +1,24 @@ + + + + + + + + +
+
+ {{ @AdminCountry_counters_total_users }}

+
+

+
+
+ {{ @AdminCountry_counters_total_ips }}

+
+

+
+
+ {{ @AdminCountry_counters_total_events }}

+
+

+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/domain.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/domain.html new file mode 100644 index 0000000..31497a0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/domain.html @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + +
+
+ {{ @AdminDomain_counters_free_email_provider }}

+
+

+
+
+ {{ @AdminDomain_counters_tranco_rank }}

+
+

+
+
+ {{ @AdminDomain_counters_unavailable }}

+
+

+
+
+ {{ @AdminDomain_counters_disposable }}

+
+

+
+
+ {{ @AdminDomain_registration_date }}

+
+

+
+
+ {{ @AdminDomain_expiration_date }}

+
+

+
+
+ {{ @AdminDomain_counters_accounts }}

+
+

+
+
+ {{ @AdminDomain_counters_fraud }}

+
+

+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/index.php @@ -0,0 +1,3 @@ + + + +
+ {{ @AdminIp_counters_country }}

+
+

+ + + +
+ {{ @AdminIp_counters_asn }}

+
+

+ + + +
+ {{ @AdminIp_counters_blocklist }}

+
+

+ + + +
+ {{ @AdminIp_counters_blacklist }}

+
+

+ + + + + +
+ {{ @AdminIp_counters_datacenter }}

+
+

+ + + +
+ {{ @AdminIp_counters_vpn }}

+
+

+ + + +
+ {{ @AdminIp_counters_tor }}

+
+

+ + + +
+ {{ @AdminIp_counters_apple_relay }}

+
+

+ + + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/isp.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/isp.html new file mode 100644 index 0000000..694fcf5 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/isp.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + +
+
+ {{ @AdminIsp_asn }}

+
+

+
+
+ {{ @AdminIsp_counters_accounts }}

+
+

+
+
+ {{ @AdminIsp_counters_total_ips }}

+
+

+
+
+ {{ @AdminIsp_counters_visits }}

+
+

+
+
+ {{ @AdminIsp_counters_fraud }}

+
+

+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/resource.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/resource.html new file mode 100644 index 0000000..69adc41 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/resource.html @@ -0,0 +1,31 @@ + + + + + + + + + + +
+
+ {{ @AdminResource_counters_total_users }}

+
+

+
+
+ {{ @AdminResource_counters_total_countries }}

+
+

+
+
+ {{ @AdminResource_counters_total_ips }}

+
+

+
+
+ {{ @AdminResource_counters_total_events }}

+
+

+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/user.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/user.html new file mode 100644 index 0000000..2c2d449 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/user.html @@ -0,0 +1,29 @@ + + + + + + + + +
+
{{ @AdminUser_counters_total_new_devices }}

+

+
+

+
+
{{ @AdminUser_counters_total_new_ips }}

+

+
+

+
+
{{ @AdminUser_counters_total_sessions }}

+

+
+

+
+
{{ @AdminUser_counters_total_events_max }}

+

+
+

+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/userDetails.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/userDetails.html new file mode 100644 index 0000000..e5a41d8 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/parts/widgets/userDetails.html @@ -0,0 +1,160 @@ + + + + + + + + + + + +
+
+
+ {{ @AdminUser_widgets_id }}

+
+
+ +
+
+ {{ @Base_table_column_user_registered }} + +
+
+ {{ @Base_table_column_lastseen }} + +
+
+ {{ @Base_table_column_latest_decision }} + +
+
+ {{ @Base_table_column_reviewed_status }} + +
+
+ {{ @Base_table_column_user_firstname }} + +
+
+ {{ @Base_table_column_user_lastname }} + +
+
+ {{ @Base_table_column_userid }} + +
+
+
+
+
+ {{ @AdminUser_widgets_ips_warning }}

+
+
+ +
+
+ {{ @Base_table_column_ip_datacenter }} + +
+
+ {{ @Base_table_column_ip_vpn }} + +
+
+ {{ @Base_table_column_ip_tor }} + +
+
+ {{ @Base_table_column_ip_apple_relay }} + +
+
+ {{ @Base_table_column_ip_shared }} + +
+
+ {{ @Base_table_column_ip_spamlist }} + +
+
+ {{ @Base_table_column_ip_blacklist }} + +
+
+
+
+
+ {{ @UserDetails_day_card_title }}

+
+
+ +
+
+ {{ @UserDetails_failed_login_count }} + +
+
+ {{ @UserDetails_password_reset_count }} + +
+
+ {{ @UserDetails_auth_error_count }} + +
+
+ {{ @UserDetails_off_hours_login_count }} + +
+
+ {{ @UserDetails_device_count }} + +
+
+ {{ @UserDetails_ip_count }} + +
+
+ {{ @UserDetails_session_count }} + +
+
+
+
+
+ {{ @UserDetails_week_card_title }}

+
+
+ +
+
+ {{ @UserDetails_failed_login_count }} + +
+
+ {{ @UserDetails_password_reset_count }} + +
+
+ {{ @UserDetails_auth_error_count }} + +
+
+ {{ @UserDetails_off_hours_login_count }} + +
+
+ {{ @UserDetails_device_count }} + +
+
+ {{ @UserDetails_ip_count }} + +
+
+ {{ @UserDetails_session_count }} + +
+
+
diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/snippets/curl.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/snippets/curl.html new file mode 100644 index 0000000..1f0b2c2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/snippets/curl.html @@ -0,0 +1,22 @@ +curl -X POST \ + -d 'userName=' \ + -d 'emailAddress=' \ + -d 'ipAddress=' \ + -d 'url=' \ + -d 'userAgent=' \ + -d 'eventTime=' \ + -d 'firstName=' \ + -d 'lastName=' \ + -d 'fullName=' \ + -d 'pageTitle=' \ + -d 'phoneNumber=' \ + -d 'httpReferer=' \ + -d 'httpCode=' \ + -d 'browserLanguage=' \ + -d 'eventType=' \ + -d 'httpMethod=' \ + -d 'userCreated=' \ + -d 'payload=' \ + -H 'Api-Key: {{ @apiKey }}' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + {{ @apiUrl }} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/snippets/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/snippets/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/snippets/index.php @@ -0,0 +1,3 @@ + '', + + // User email (string) + 'emailAddress' => '', + + // User IP address (string) + 'ipAddress' => '', + + // URL path of visited page (string) + 'url' => '', + + // User-agent of user request (string) + 'userAgent' => '', + + // Event UTC timestamp ('Y-m-d H:i:s.v' string) + 'eventTime' => '', + + /////////// Optional fields /////////// + + // User first name (string) + 'firstName' => '', + + // User last name (string) + 'lastName' => '', + + // User full name (string) + 'fullName' => '', + + // Title of visited page (string) + 'pageTitle' => '', + + // User phone number (string) + 'phoneNumber' => '', + + // Referer of visited page (string) + 'httpReferer' => '', + + // Status code for page visit (string) + 'httpCode' => '', + + // User browser language (string) + 'browserLanguage' => '', + + // Type of user action from event types list (string) + 'eventType' => '', + + // Type of HTTP request from list (string) + 'httpMethod' => '', + + // User created UTC timestamp ('Y-m-d H:i:s' string) + 'userCreated' => '' + + // Payload (array) + 'payload' => [], +); + +// Convert the event data to a query string +$dataString = http_build_query($data); + +// Set up apiKey +$headers = [ + 'Api-Key: ' . $apiKey, + 'Content-Type: application/x-www-form-urlencoded', +]; + +// Create a curl request to send the event data to the API + +$ch = curl_init(); +// CURLOPT_URL value is sensor URL +curl_setopt_array($ch, array( + CURLOPT_URL => '{{ @apiUrl }}', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $dataString, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_TIMEOUT_MS => 1000, + CURLOPT_HTTPHEADER => $headers +)); + +// Send the curl request and capture the response +$response = curl_exec($ch); + +// Close the curl request +curl_close($ch); diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/snippets/python.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/snippets/python.html new file mode 100644 index 0000000..d2ff91e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/snippets/python.html @@ -0,0 +1,74 @@ +import requests +from urllib.parse import urlencode + + +url = '{{ @apiUrl }}' # sensor URL +headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Api-Key': '{{ @apiKey }}' # Tracking ID +} + +# Replace each key value with actual info +data = { + + ########### Required fields ########### + + # Unique value that allows identification of a user. Ex: alice54 (string) + 'userName': '', + + # User email (string) + 'emailAddress': '', + + # User IP address (string) + 'ipAddress': '', + + # URL path of visited page (string) + 'url': '', + + # User-agent of user request (string) + 'userAgent': '', + + # Event UTC timestamp ('Y-m-d H:i:s.v' string) + 'eventTime': '', + + ########### Optional fields ########### + + # User first name (string) + 'firstName': '', + + # User last name (string) + 'lastName': '', + + # User full name (string) + 'fullName': '', + + # Title of visited page (string) + 'pageTitle': '', + + # User phone number (string) + 'phoneNumber': '', + + # Referer of visited page (string) + 'httpReferer': '', + + # Status code for page visit (string) + 'httpCode': '', + + # User browser language (string) + 'browserLanguage': '', + + # Type of user action from event types list (string) + 'eventType': '', + + # Type of HTTP request from list (string) + 'httpMethod': '', + + # User created UTC timestamp ('Y-m-d H:i:s' string) + 'userCreated': '' + + # Payload (array) + 'payload': [], +} + +data = {k: v for k, v in data.items() if v is not None} +response = requests.post(url, data=data, headers=headers) diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/snippets/ruby.html b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/snippets/ruby.html new file mode 100644 index 0000000..f69d881 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/ui/templates/snippets/ruby.html @@ -0,0 +1,76 @@ +require 'net/http' +require 'uri' +require 'cgi' + +url = URI.parse('{{ @apiUrl }}') # sensor URL +headers = { + 'Content-Type' => 'application/x-www-form-urlencoded', + 'Api-Key' => '{{ @apiKey }}' # Tracking ID +} +# Replace each key value with actual info +data = { + + ########### Required fields ########### + + # Unique value that allows identification of a user. Ex: alice54 (string) + 'userName': '', + + # User email (string) + 'emailAddress': '', + + # User IP address (string) + 'ipAddress': '', + + # URL path of visited page (string) + 'url': '', + + # User-agent of user request (string) + 'userAgent': '', + + # Event UTC timestamp ('Y-m-d H:i:s.v' string) + 'eventTime': '', + + ########### Optional fields ########### + + # User first name (string) + 'firstName': '', + + # User last name (string) + 'lastName': '', + + # User full name (string) + 'fullName': '', + + # Title of visited page (string) + 'pageTitle': '', + + # User phone number (string) + 'phoneNumber': '', + + # Referer of visited page (string) + 'httpReferer': '', + + # Status code for page visit (string) + 'httpCode': '', + + # User browser language (string) + 'browserLanguage': '', + + # Type of user action from event types list (string) + 'eventType': '', + + # Type of HTTP request from list (string) + 'httpMethod': '', + + # User created UTC timestamp ('Y-m-d H:i:s' string) + 'userCreated': '', + + # Payload + 'payload': [], +} + +http = Net::HTTP.new(url.host, url.port) +http.use_ssl = true if url.scheme == 'https' +request = Net::HTTP::Post.new(url, headers) +request.body = URI.encode_www_form(data) +response = http.request(request)
.+?)\]|'. + '(?[^\h\r\n;].*?)\h*=\h*'. + '(?(?:\\\\\h*\r?\n|.+?)*)'. + ')(?=\r?\n|$)/', + $this->read($file), + $matches,PREG_SET_ORDER); + if ($matches) { + $sec='globals'; + $cmd=[]; + foreach ($matches as $match) { + if ($match['section']) { + $sec=$match['section']; + if (preg_match( + '/^(?!(?:global|config|route|map|redirect)s\b)'. + '(.*?)(?:\s*[:>])/i',$sec,$msec) && + !$this->exists($msec[1])) + $this->set($msec[1],NULL); + preg_match('/^(config|route|map|redirect)s\b|'. + '^(.+?)\s*\>\s*(.*)/i',$sec,$cmd); + continue; + } + if ($allow) + foreach (['lval','rval'] as $ndx) + $match[$ndx]=$preview-> + resolve($match[$ndx],NULL,0,FALSE,FALSE); + if (!empty($cmd)) { + isset($cmd[3])? + $this->call($cmd[3], + [$match['lval'],$match['rval'],$cmd[2]]): + call_user_func_array( + [$this,$cmd[1]], + array_merge([$match['lval']], + str_getcsv($cmd[1]=='config'? + $this->cast($match['rval']): + $match['rval'],",",'"', "\\")) + ); + } + else { + $rval=preg_replace( + '/\\\\\h*(\r?\n)/','\1',$match['rval']); + $ttl=NULL; + if (preg_match('/^(.+)\|\h*(\d+)$/',$rval,$tmp)) { + array_shift($tmp); + list($rval,$ttl)=$tmp; + } + $args=array_map( + function($val) { + $val=$this->cast($val); + if (is_string($val)) + $val=strlen($val)? + preg_replace('/\\\\"/','"',$val): + NULL; + return $val; + }, + // Mark quoted strings with 0x00 whitespace + str_getcsv(preg_replace( + '/(?[^:]+)(?:\:(?.+))?/', + $sec,$parts); + $func=isset($parts['func'])?$parts['func']:NULL; + $custom=(strtolower($parts['section'])!='globals'); + if ($func) + $args=[$this->call($func,$args)]; + if (count($args)>1) + $args=[$args]; + if (isset($ttl)) + $args=array_merge($args,[$ttl]); + call_user_func_array( + [$this,'set'], + array_merge( + [ + ($custom?($parts['section'].'.'):''). + $match['lval'] + ], + $args + ) + ); + } + } + } + } + return $this; + } + + /** + * Create mutex, invoke callback then drop ownership when done + * @return mixed + * @param $id string + * @param $func callback + * @param $args mixed + **/ + function mutex($id,$func,$args=NULL) { + if (!is_dir($tmp=$this->hive['TEMP'])) + mkdir($tmp,self::MODE,TRUE); + // Use filesystem lock + if (is_file($lock=$tmp. + $this->hive['SEED'].'.'.$this->hash($id).'.lock') && + filemtime($lock)+ini_get('max_execution_time')locks[$id]=$lock; + $out=$this->call($func,$args); + fclose($handle); + @unlink($lock); + unset($this->locks[$id]); + return $out; + } + + /** + * Read file (with option to apply Unix LF as standard line ending) + * @return string + * @param $file string + * @param $lf bool + **/ + function read($file, $lf = FALSE) + { + if (!file_exists($file)) { + return false; + } + + $out = file_get_contents($file); + + return $lf ? preg_replace('/\r\n|\r/', "\n", $out) : $out; + } + + /** + * Exclusive file write + * @return int|FALSE + * @param $file string + * @param $data mixed + * @param $append bool + **/ + function write($file,$data,$append=FALSE) { + return file_put_contents($file,$data,$this->hive['LOCK']|($append?FILE_APPEND:0)); + } + + /** + * Apply syntax highlighting + * @return string + * @param $text string + **/ + function highlight($text) { + $out=''; + $pre=FALSE; + $text=trim($text); + if ($text && !preg_match('/^<\?php/',$text)) { + $text=''. + $this->encode($token[1]).''): + ('>'.$this->encode($token))). + ''; + return $out?(''.$out.''):$text; + } + + /** + * Dump expression with syntax highlighting + * @param $expr mixed + **/ + function dump($expr) { + echo $this->highlight($this->stringify($expr)); + } + + /** + * Return path (and query parameters) relative to the base directory + * @return string + * @param $url string + **/ + function rel($url) { + return preg_replace('/^(?:https?:\/\/)?'. + preg_quote($this->hive['BASE'],'/').'(\/.*|$)/','\1',$url); + } + + /** + * Namespace-aware class autoloader + * @return mixed + * @param $class string + **/ + protected function autoload($class) { + $class=$this->fixslashes(ltrim($class,'\\')); + /** @var callable $func */ + $func=NULL; + if (is_array($path=$this->hive['AUTOLOAD']) && + isset($path[1]) && is_callable($path[1])) + list($path,$func)=$path; + foreach ($this->split($this->hive['PLUGINS'].';'.$path) as $auto) + if (($func && is_file($file=$func($auto.$class).'.php')) || + is_file($file=$auto.$class.'.php') || + is_file($file=$auto.strtolower($class).'.php') || + is_file($file=strtolower($auto.$class).'.php')) + return require($file); + } + + /** + * Execute framework/application shutdown sequence + * @param $cwd string + **/ + function unload($cwd) { + chdir($cwd); + if (!($error=error_get_last()) && + session_status()==PHP_SESSION_ACTIVE) + session_commit(); + foreach ($this->locks as $lock) + @unlink($lock); + $handler=$this->hive['UNLOAD']; + if ((!$handler || $this->call($handler,$this)===FALSE) && + $error && in_array($error['type'], + [E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR])) + // Fatal error detected + $this->error(500, + sprintf(self::E_Fatal,$error['message']),[$error]); + } + + /** + * Convenience method for checking hive key + * @return mixed + * @param $key string + **/ + #[\ReturnTypeWillChange] + function offsetexists($key) { + return $this->exists($key); + } + + /** + * Convenience method for assigning hive value + * @return mixed + * @param $key string + * @param $val mixed + **/ + #[\ReturnTypeWillChange] + function offsetset($key,$val) { + return $this->set($key,$val); + } + + /** + * Convenience method for retrieving hive value + * @return mixed + * @param $key string + **/ + #[\ReturnTypeWillChange] + function &offsetget($key) { + $val=&$this->ref($key); + return $val; + } + + /** + * Convenience method for removing hive key + * @param $key string + **/ + #[\ReturnTypeWillChange] + function offsetunset($key) { + $this->clear($key); + } + + /** + * Alias for offsetexists() + * @return mixed + * @param $key string + **/ + function __isset($key) { + return $this->offsetexists($key); + } + + /** + * Alias for offsetset() + * @return mixed + * @param $key string + * @param $val mixed + **/ + function __set($key,$val) { + return $this->offsetset($key,$val); + } + + /** + * Alias for offsetget() + * @return mixed + * @param $key string + **/ + function &__get($key) { + $val=&$this->offsetget($key); + return $val; + } + + /** + * Alias for offsetunset() + * @param $key string + **/ + function __unset($key) { + $this->offsetunset($key); + } + + /** + * Call function identified by hive key + * @return mixed + * @param $key string + * @param $args array + **/ + function __call($key,array $args) { + if ($this->exists($key,$val)) + return call_user_func_array($val,$args); + throw new \Exception(sprintf(self::E_Method,$key)); + } + + //! Prohibit cloning + private function __clone() { + } + + //! Bootstrap + function __construct() { + // Managed directives + ini_set('default_charset',$charset='UTF-8'); + if (extension_loaded('mbstring')) + mb_internal_encoding($charset); + ini_set('display_errors',0); + // Deprecated directives + @ini_set('magic_quotes_gpc',0); + @ini_set('register_globals',0); + // Intercept errors/exceptions; PHP5.3-compatible + if (PHP_VERSION_ID >= 80400) { + $check = error_reporting((E_ALL) & ~(E_NOTICE | E_USER_NOTICE)); + } else { + $check = error_reporting((E_ALL | E_STRICT) & ~(E_NOTICE | E_USER_NOTICE)); + } + set_exception_handler( + function($obj) { + /** @var Exception $obj */ + $this->hive['EXCEPTION']=$obj; + $this->error(500, + $obj->getmessage().' '. + '['.$obj->getFile().':'.$obj->getLine().']', + $obj->gettrace()); + } + ); + set_error_handler( + function($level,$text,$file,$line) { + if ($level & error_reporting()) { + $trace=$this->trace(null, false); + array_unshift($trace,['file'=>$file,'line'=>$line]); + $this->error(500,$text,$trace,$level); + } + } + ); + if (!isset($_SERVER['SERVER_NAME']) || $_SERVER['SERVER_NAME']==='') + $_SERVER['SERVER_NAME']=gethostname(); + $headers=[]; + if ($cli=(PHP_SAPI=='cli')) { + // Emulate HTTP request + $_SERVER['REQUEST_METHOD']='GET'; + if (!isset($_SERVER['argv'][1])) { + ++$_SERVER['argc']; + $_SERVER['argv'][1]='/'; + } + $req=$query=''; + if (substr($_SERVER['argv'][1],0,1)=='/') { + $req=$_SERVER['argv'][1]; + $query=parse_url($req,PHP_URL_QUERY); + } else { + foreach($_SERVER['argv'] as $i=>$arg) { + if (!$i) continue; + if (preg_match('/^\-(\-)?(\w+)(?:\=(.*))?$/',$arg,$m)) { + foreach($m[1]?[$m[2]]:str_split($m[2]) as $k) + $query.=($query?'&':'').urlencode($k).'='; + if (isset($m[3])) + $query.=urlencode($m[3]); + } else + $req.='/'.$arg; + } + if (!$req) + $req='/'; + if ($query) + $req.='?'.$query; + } + $_SERVER['REQUEST_URI']=$req; + parse_str($query?:'',$GLOBALS['_GET']); + } + elseif (function_exists('getallheaders')) { + foreach (getallheaders() as $key=>$val) { + $tmp=strtoupper(strtr($key,'-','_')); + // TODO: use ucwords delimiters for php 5.4.32+ & 5.5.16+ + $key=strtr(ucwords(strtolower(strtr($key,'-',' '))),' ','-'); + $headers[$key]=$val; + if (isset($_SERVER['HTTP_'.$tmp])) + $headers[$key]=&$_SERVER['HTTP_'.$tmp]; + } + } + else { + if (isset($_SERVER['CONTENT_LENGTH'])) + $headers['Content-Length']=&$_SERVER['CONTENT_LENGTH']; + if (isset($_SERVER['CONTENT_TYPE'])) + $headers['Content-Type']=&$_SERVER['CONTENT_TYPE']; + foreach (array_keys($_SERVER) as $key) + if (substr($key,0,5)=='HTTP_') + $headers[strtr(ucwords(strtolower(strtr( + substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key]; + } + if (isset($headers['X-Http-Method-Override'])) + $_SERVER['REQUEST_METHOD']=$headers['X-Http-Method-Override']; + elseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method'])) + $_SERVER['REQUEST_METHOD']=strtoupper($_POST['_method']); + $scheme=isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' || + isset($headers['X-Forwarded-Proto']) && + $headers['X-Forwarded-Proto']=='https'?'https':'http'; + // Create hive early on to expose header methods + $this->hive=['HEADERS'=>&$headers]; + if (function_exists('apache_setenv')) { + // Work around Apache pre-2.4 VirtualDocumentRoot bug + $_SERVER['DOCUMENT_ROOT']=str_replace($_SERVER['SCRIPT_NAME'],'', + $_SERVER['SCRIPT_FILENAME']); + apache_setenv("DOCUMENT_ROOT",$_SERVER['DOCUMENT_ROOT']); + } + $_SERVER['DOCUMENT_ROOT']=realpath($_SERVER['DOCUMENT_ROOT']); + $base=''; + if (!$cli) + $base=rtrim($this->fixslashes( + dirname($_SERVER['SCRIPT_NAME'])),'/'); + $uri=parse_url((preg_match('/^\w+:\/\//',$_SERVER['REQUEST_URI'])?'': + $scheme.'://'.$_SERVER['SERVER_NAME']).$_SERVER['REQUEST_URI']); + $_SERVER['REQUEST_URI']=$uri['path']. + (isset($uri['query'])?'?'.$uri['query']:''). + (isset($uri['fragment'])?'#'.$uri['fragment']:''); + $path=preg_replace('/^'.preg_quote($base,'/').'/','',$uri['path']); + $jar=[ + 'expire'=>0, + 'lifetime'=>0, + 'path'=>$base?:'/', + 'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) && + !filter_var($_SERVER['SERVER_NAME'],FILTER_VALIDATE_IP)? + $_SERVER['SERVER_NAME']:'', + 'secure'=>($scheme=='https'), + 'httponly'=>TRUE, + 'samesite'=>'Lax', + ]; + $port=80; + if (!empty($headers['X-Forwarded-Port'])) + $port=$headers['X-Forwarded-Port']; + elseif (!empty($_SERVER['SERVER_PORT'])) + $port=$_SERVER['SERVER_PORT']; + // Default configuration + $this->hive+=[ + 'AGENT'=>$this->agent(), + 'AJAX'=>$this->ajax(), + 'ALIAS'=>NULL, + 'ALIASES'=>[], + 'AUTOLOAD'=>'./', + 'BASE'=>$base, + 'BITMASK'=>ENT_COMPAT, + 'BODY'=>NULL, + 'CACHE'=>FALSE, + 'CASELESS'=>TRUE, + 'CLI'=>$cli, + 'CORS'=>[], + 'DEBUG'=>0, + 'DIACRITICS'=>[], + 'DNSBL'=>'', + 'EMOJI'=>[], + 'ENCODING'=>$charset, + 'ERROR'=>NULL, + 'ESCAPE'=>TRUE, + 'EXCEPTION'=>NULL, + 'EXEMPT'=>NULL, + 'FALLBACK'=>$this->fallback, + 'FORMATS'=>[], + 'FRAGMENT'=>isset($uri['fragment'])?$uri['fragment']:'', + 'HALT'=>TRUE, + 'HIGHLIGHT'=>FALSE, + 'HOST'=>$_SERVER['SERVER_NAME'], + 'IP'=>$this->ip(), + 'JAR'=>$jar, + 'LANGUAGE'=>isset($headers['Accept-Language'])? + $this->language($headers['Accept-Language']): + $this->fallback, + 'LOCALES'=>'./', + 'LOCK'=>LOCK_EX, + 'LOGGABLE'=>'*', + 'LOGS'=>'./', + 'MB'=>extension_loaded('mbstring'), + 'ONERROR'=>NULL, + 'ONREROUTE'=>NULL, + 'PACKAGE'=>self::PACKAGE, + 'PARAMS'=>[], + 'PATH'=>$path, + 'REROUTE_TRAILING_SLASH'=>TRUE, + 'PATTERN'=>NULL, + 'PLUGINS'=>$this->fixslashes(__DIR__).'/', + 'PORT'=>$port, + 'PREFIX'=>NULL, + 'PREMAP'=>'', + 'QUERY'=>isset($uri['query'])?$uri['query']:'', + 'QUIET'=>FALSE, + 'RAW'=>FALSE, + 'REALM'=>$scheme.'://'.$_SERVER['SERVER_NAME']. + (!in_array($port,[80,443])?(':'.$port):''). + $_SERVER['REQUEST_URI'], + 'RESPONSE'=>'', + 'ROOT'=>$_SERVER['DOCUMENT_ROOT'], + 'ROUTES'=>[], + 'SCHEME'=>$scheme, + 'SEED'=>$this->hash($_SERVER['SERVER_NAME'].$base), + 'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php', + 'TEMP'=>'tmp/', + 'TIME'=>&$_SERVER['REQUEST_TIME_FLOAT'], + 'TZ'=>@date_default_timezone_get(), + 'UI'=>'./', + 'UNLOAD'=>NULL, + 'UPLOADS'=>'./', + 'URI'=>&$_SERVER['REQUEST_URI'], + 'VERB'=>&$_SERVER['REQUEST_METHOD'], + 'VERSION'=>self::VERSION, + 'XFRAME'=>'SAMEORIGIN' + ]; + $this->hive['CORS']+=[ + 'headers'=>'', + 'origin'=>FALSE, + 'credentials'=>FALSE, + 'expose'=>FALSE, + 'ttl'=>0 + ]; + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) { + unset($jar['expire']); + session_cache_limiter(''); + if (version_compare(PHP_VERSION, '7.3.0') >= 0) + session_set_cookie_params($jar); + else { + unset($jar['samesite']); + call_user_func_array('session_set_cookie_params',$jar); + } + } + if (PHP_SAPI=='cli-server' && + preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI'])) + $this->reroute('/'); + if (ini_get('auto_globals_jit')) { + // Override setting + $GLOBALS['_ENV']=$_ENV; + $GLOBALS['_REQUEST']=$_REQUEST; + } + // Sync PHP globals with corresponding hive keys + $this->init=$this->hive; + foreach (explode('|',self::GLOBALS) as $global) { + $sync=$this->sync($global); + $this->init+=[ + $global=>preg_match('/SERVER|ENV/',$global)?$sync:[] + ]; + } + if ($check && $error=error_get_last()) + // Error detected + $this->error(500, + sprintf(self::E_Fatal,$error['message']),[$error]); + date_default_timezone_set($this->hive['TZ']); + // Register framework autoloader + spl_autoload_register([$this,'autoload']); + // Register shutdown handler + register_shutdown_function([$this,'unload'],getcwd()); + } + +} + +//! Cache engine +class Cache extends Prefab { + + protected + //! Cache DSN + $dsn, + //! Prefix for cache entries + $prefix, + //! MemCache or Redis object + $ref; + + /** + * Return timestamp and TTL of cache entry or FALSE if not found + * @return array|FALSE + * @param $key string + * @param $val mixed + **/ + function exists($key,&$val=NULL) { + $fw=Base::instance(); + if (!$this->dsn) + return FALSE; + $ndx=$this->prefix.'.'.$key; + $parts=explode('=',$this->dsn,2); + switch ($parts[0]) { + case 'apc': + case 'apcu': + $raw=call_user_func($parts[0].'_fetch',$ndx); + break; + case 'redis': + $raw=$this->ref->get($ndx); + break; + case 'memcache': + $raw=memcache_get($this->ref,$ndx); + break; + case 'memcached': + $raw=$this->ref->get($ndx); + break; + case 'wincache': + $raw=wincache_ucache_get($ndx); + break; + case 'xcache': + $raw=xcache_get($ndx); + break; + case 'folder': + $raw=$fw->read($parts[1] + .str_replace(['/','\\'],'',$ndx)); + break; + } + if (!empty($raw)) { + list($val,$time,$ttl)=(array)$fw->unserialize($raw); + if ($ttl===0 || $time+$ttl>microtime(TRUE)) + return [$time,$ttl]; + $val=null; + $this->clear($key); + } + return FALSE; + } + + /** + * Store value in cache + * @return mixed|FALSE + * @param $key string + * @param $val mixed + * @param $ttl int + **/ + function set($key,$val,$ttl=0) { + $fw=Base::instance(); + if (!$this->dsn) + return TRUE; + $ndx=$this->prefix.'.'.$key; + if ($cached=$this->exists($key)) + $ttl=$cached[1]; + $data=$fw->serialize([$val,microtime(TRUE),$ttl]); + $parts=explode('=',$this->dsn,2); + switch ($parts[0]) { + case 'apc': + case 'apcu': + return call_user_func($parts[0].'_store',$ndx,$data,$ttl); + case 'redis': + return $this->ref->set($ndx,$data,$ttl?['ex'=>$ttl]:[]); + case 'memcache': + return memcache_set($this->ref,$ndx,$data,0,$ttl); + case 'memcached': + return $this->ref->set($ndx,$data,$ttl); + case 'wincache': + return wincache_ucache_set($ndx,$data,$ttl); + case 'xcache': + return xcache_set($ndx,$data,$ttl); + case 'folder': + return $fw->write($parts[1]. + str_replace(['/','\\'],'',$ndx),$data); + } + return FALSE; + } + + /** + * Retrieve value of cache entry + * @return mixed|FALSE + * @param $key string + **/ + function get($key) { + return $this->dsn && $this->exists($key,$data)?$data:FALSE; + } + + /** + * Delete cache entry + * @return bool + * @param $key string + **/ + function clear($key) { + if (!$this->dsn) + return; + $ndx=$this->prefix.'.'.$key; + $parts=explode('=',$this->dsn,2); + switch ($parts[0]) { + case 'apc': + case 'apcu': + return call_user_func($parts[0].'_delete',$ndx); + case 'redis': + return $this->ref->del($ndx); + case 'memcache': + return memcache_delete($this->ref,$ndx); + case 'memcached': + return $this->ref->delete($ndx); + case 'wincache': + return wincache_ucache_delete($ndx); + case 'xcache': + return xcache_unset($ndx); + case 'folder': + return @unlink($parts[1].$ndx); + } + return FALSE; + } + + /** + * Clear contents of cache backend + * @return bool + * @param $suffix string + **/ + function reset($suffix=NULL) { + if (!$this->dsn) + return TRUE; + $regex='/'.preg_quote($this->prefix.'.','/').'.*'. + preg_quote($suffix?:'','/').'/'; + $parts=explode('=',$this->dsn,2); + switch ($parts[0]) { + case 'apc': + case 'apcu': + $info=call_user_func($parts[0].'_cache_info', + $parts[0]=='apcu'?FALSE:'user'); + if (!empty($info['cache_list'])) { + $key=array_key_exists('info', + $info['cache_list'][0])?'info':'key'; + foreach ($info['cache_list'] as $item) + if (preg_match($regex,$item[$key])) + call_user_func($parts[0].'_delete',$item[$key]); + } + return TRUE; + case 'redis': + $keys=$this->ref->keys($this->prefix.'.*'.$suffix); + foreach($keys as $key) + $this->ref->del($key); + return TRUE; + case 'memcache': + foreach (memcache_get_extended_stats( + $this->ref,'slabs') as $slabs) + foreach (array_filter(array_keys($slabs),'is_numeric') + as $id) + foreach (memcache_get_extended_stats( + $this->ref,'cachedump',$id) as $data) + if (is_array($data)) + foreach (array_keys($data) as $key) + if (preg_match($regex,$key)) + memcache_delete($this->ref,$key); + return TRUE; + case 'memcached': + foreach ($this->ref->getallkeys()?:[] as $key) + if (preg_match($regex,$key)) + $this->ref->delete($key); + return TRUE; + case 'wincache': + $info=wincache_ucache_info(); + foreach ($info['ucache_entries'] as $item) + if (preg_match($regex,$item['key_name'])) + wincache_ucache_delete($item['key_name']); + return TRUE; + case 'xcache': + if ($suffix && !ini_get('xcache.admin.enable_auth')) { + $cnt=xcache_count(XC_TYPE_VAR); + for ($i=0;$i<$cnt;++$i) { + $list=xcache_list(XC_TYPE_VAR,$i); + foreach ($list['cache_list'] as $item) + if (preg_match($regex,$item['name'])) + xcache_unset($item['name']); + } + } else + xcache_unset_by_prefix($this->prefix.'.'); + return TRUE; + case 'folder': + if ($glob=@glob($parts[1].'*')) + foreach ($glob as $file) + if (preg_match($regex,basename($file))) + @unlink($file); + return TRUE; + } + return FALSE; + } + + /** + * Load/auto-detect cache backend + * @return string + * @param $dsn bool|string + * @param $seed bool|string + **/ + function load($dsn,$seed=NULL) { + $fw=Base::instance(); + if ($dsn=trim($dsn)) { + if (preg_match('/^redis=(.+)/',$dsn,$parts) && + extension_loaded('redis')) { + list($host,$port,$db,$password)=explode(':',$parts[1])+[1=>6379,2=>NULL,3=>NULL]; + $this->ref=new Redis; + if(!$this->ref->connect($host,$port,2)) + $this->ref=NULL; + if(!empty($password)) + $this->ref->auth($password); + if(isset($db)) + $this->ref->select($db); + } + elseif (preg_match('/^memcache=(.+)/',$dsn,$parts) && + extension_loaded('memcache')) + foreach ($fw->split($parts[1]) as $server) { + list($host,$port)=explode(':',$server)+[1=>11211]; + if (empty($this->ref)) + $this->ref=@memcache_connect($host,$port)?:NULL; + else + memcache_add_server($this->ref,$host,$port); + } + elseif (preg_match('/^memcached=(.+)/',$dsn,$parts) && + extension_loaded('memcached')) + foreach ($fw->split($parts[1]) as $server) { + list($host,$port)=explode(':',$server)+[1=>11211]; + if (empty($this->ref)) + $this->ref=new Memcached(); + $this->ref->addServer($host,$port); + } + if (empty($this->ref) && !preg_match('/^folder\h*=/',$dsn)) + $dsn=($grep=preg_grep('/^(apc|wincache|xcache)/', + array_map('strtolower',get_loaded_extensions())))? + // Auto-detect + current($grep): + // Use filesystem as fallback + ('folder='.$fw->TEMP.'cache/'); + if (preg_match('/^folder\h*=\h*(.+)/',$dsn,$parts) && + !is_dir($parts[1])) + mkdir($parts[1],Base::MODE,TRUE); + } + $this->prefix=$seed?:$fw->SEED; + return $this->dsn=$dsn; + } + + /** + * Class constructor + * @param $dsn bool|string + **/ + function __construct($dsn=FALSE) { + if ($dsn) + $this->load($dsn); + } + +} + +//! View handler +class View extends Prefab { + + private + //! Temporary hive + $temp; + + protected + //! Template file + $file, + //! Post-rendering handler + $trigger, + //! Nesting level + $level=0; + + /** @var \Base Framework instance */ + protected $fw; + + function __construct() { + $this->fw=\Base::instance(); + } + + /** + * Encode characters to equivalent HTML entities + * @return string + * @param $arg mixed + **/ + function esc($arg) { + return $this->fw->recursive($arg, + function($val) { + return is_string($val)?$this->fw->encode($val):$val; + } + ); + } + + /** + * Decode HTML entities to equivalent characters + * @return string + * @param $arg mixed + **/ + function raw($arg) { + return $this->fw->recursive($arg, + function($val) { + return is_string($val)?$this->fw->decode($val):$val; + } + ); + } + + /** + * Create sandbox for template execution + * @return string + * @param $hive array + * @param $mime string + **/ + protected function sandbox(?array $hive=NULL,$mime=NULL) { + $fw=$this->fw; + $implicit=FALSE; + if (is_null($hive)) { + $implicit=TRUE; + $hive=$fw->hive(); + } + if ($this->level<1 || $implicit) { + if (!$fw->CLI && $mime && !headers_sent() && + !preg_grep ('/^Content-Type:/',headers_list())) + header('Content-Type: '.$mime.'; '. + 'charset='.$fw->ENCODING); + if ($fw->ESCAPE && (!$mime || + preg_match('/^(text\/html|(application|text)\/(.+\+)?xml)$/i',$mime))) + $hive=$this->esc($hive); + if (isset($hive['ALIASES'])) + $hive['ALIASES']=$fw->build($hive['ALIASES']); + } + $this->temp=$hive; + unset($fw,$hive,$implicit,$mime); + extract($this->temp); + $this->temp=NULL; + ++$this->level; + ob_start(); + require($this->file); + --$this->level; + return ob_get_clean(); + } + + /** + * Render template + * @return string + * @param $file string + * @param $mime string + * @param $hive array + * @param $ttl int + **/ + function render($file,$mime='text/html',?array $hive=NULL,$ttl=0) { + $fw=$this->fw; + $cache=Cache::instance(); + foreach ($fw->split($fw->UI) as $dir) { + if ($cache->exists($hash=$fw->hash($dir.$file),$data)) + return $data; + if (is_file($this->file=$fw->fixslashes($dir.$file))) { + if (isset($_COOKIE[session_name()]) && + !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); + $fw->sync('SESSION'); + $data=$this->sandbox($hive,$mime); + if (isset($this->trigger['afterrender'])) + foreach($this->trigger['afterrender'] as $func) + $data=$fw->call($func,[$data, $dir.$file]); + if ($ttl) + $cache->set($hash,$data,$ttl); + return $data; + } + } + throw new \Exception(sprintf(Base::E_Open,$file)); + } + + /** + * post rendering handler + * @param $func callback + */ + function afterrender($func) { + $this->trigger['afterrender'][]=$func; + } + +} + +//! Lightweight template engine +class Preview extends View { + + protected + //! token filter + $filter=[ + 'c'=>'$this->c', + 'esc'=>'$this->esc', + 'raw'=>'$this->raw', + 'export'=>'Base::instance()->export', + 'alias'=>'Base::instance()->alias', + 'format'=>'Base::instance()->format' + ]; + + protected + //! newline interpolation + $interpolation=true; + + /** + * Enable/disable markup parsing interpolation + * mainly used for adding appropriate newlines + * @param $bool bool + */ + function interpolation($bool) { + $this->interpolation=$bool; + } + + /** + * Return C-locale equivalent of number + * @return string + * @param $val int|float + **/ + function c($val) { + $locale=setlocale(LC_NUMERIC,0); + setlocale(LC_NUMERIC,'C'); + $out=(string)(float)$val; + $locale=setlocale(LC_NUMERIC,$locale); + return $out; + } + + /** + * Convert token to variable + * @return string + * @param $str string + **/ + function token($str) { + $str=trim(preg_replace('/\{\{(.+?)\}\}/s','\1',$this->fw->compile($str))); + if (preg_match('/^(.+)(?fw->split(trim($parts[2],"\xC2\xA0")) as $func) + $str=((empty($this->filter[$cmd=$func]) && + function_exists($cmd)) || + is_string($cmd=$this->filter($func)))? + $cmd.'('.$str.')': + 'Base::instance()->'. + 'call($this->filter(\''.$func.'\'),['.$str.'])'; + } + return $str; + } + + /** + * Register or get (one specific or all) token filters + * @param string $key + * @param string|closure $func + * @return array|closure|string + */ + function filter($key=NULL,$func=NULL) { + if (!$key) + return array_keys($this->filter); + $key=strtolower($key); + if (!$func) + return $this->filter[$key]; + $this->filter[$key]=$func; + } + + /** + * Assemble markup + * @return string + * @param $node string + **/ + protected function build($node) { + return preg_replace_callback( + '/\{~(.+?)~\}|\{\*(.+?)\*\}|\{\-(.+?)\-\}|'. + '\{\{(.+?)\}\}((\r?\n)*)/s', + function($expr) { + if ($expr[1]) + $str='token($expr[1]).' ?>'; + elseif ($expr[2]) + return ''; + elseif ($expr[3]) + $str=$expr[3]; + else { + $str='token($expr[4])).')'. + ($this->interpolation? + (!empty($expr[6])?'."'.$expr[6].'"':''):'').' ?>'; + if (isset($expr[5])) + $str.=$expr[5]; + } + return $str; + }, + $node + ); + } + + /** + * Render template string + * @return string + * @param $node string|array + * @param $hive array + * @param $ttl int + * @param $persist bool + * @param $escape bool + **/ + function resolve($node,?array $hive=NULL,$ttl=0,$persist=FALSE,$escape=NULL) { + $hash=null; + $fw=$this->fw; + $cache=Cache::instance(); + if ($escape!==NULL) { + $esc=$fw->ESCAPE; + $fw->ESCAPE=$escape; + } + if ($ttl || $persist) + $hash=$fw->hash($fw->serialize($node)); + if ($ttl && $cache->exists($hash,$data)) + return $data; + if ($persist) { + if (!is_dir($tmp=$fw->TEMP)) + mkdir($tmp,Base::MODE,TRUE); + if (!is_file($this->file=($tmp. + $fw->SEED.'.'.$hash.'.php'))) + $fw->write($this->file,$this->build($node)); + if (isset($_COOKIE[session_name()]) && + !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); + $fw->sync('SESSION'); + $data=$this->sandbox($hive); + } + else { + if (!$hive) + $hive=$fw->hive(); + if ($fw->ESCAPE) + $hive=$this->esc($hive); + extract($hive); + unset($hive); + ob_start(); + eval(' ?>'.$this->build($node).'set($hash,$data,$ttl); + if ($escape!==NULL) + $fw->ESCAPE=$esc; + return $data; + } + + /** + * Parse template string + * @return string + * @param $text string + **/ + function parse($text) { + // Remove PHP code and comments + return preg_replace( + '/\h*<\?(?!xml)(?:php|\s*=)?.+?\?>\h*|'. + '\{\*.+?\*\}/is','', $text); + } + + /** + * Render template + * @return string + * @param $file string + * @param $mime string + * @param $hive array + * @param $ttl int + **/ + function render($file,$mime='text/html',?array $hive=NULL,$ttl=0) { + $fw=$this->fw; + $cache=Cache::instance(); + if (!is_dir($tmp=$fw->TEMP)) + mkdir($tmp,Base::MODE,TRUE); + foreach ($fw->split($fw->UI) as $dir) { + if ($cache->exists($hash=$fw->hash($dir.$file),$data)) + return $data; + if (is_file($view=$fw->fixslashes($dir.$file))) { + if (!is_file($this->file=($tmp. + $fw->SEED.'.'.$fw->hash($view).'.php')) || + filemtime($this->file)read($view); + if (isset($this->trigger['beforerender'])) + foreach ($this->trigger['beforerender'] as $func) + $contents=$fw->call($func, [$contents, $view]); + $text=$this->parse($contents); + $fw->write($this->file,$this->build($text)); + } + if (isset($_COOKIE[session_name()]) && + !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); + $fw->sync('SESSION'); + $data=$this->sandbox($hive,$mime); + if(isset($this->trigger['afterrender'])) + foreach ($this->trigger['afterrender'] as $func) + $data=$fw->call($func, [$data, $view]); + if ($ttl) + $cache->set($hash,$data,$ttl); + return $data; + } + } + throw new \Exception(sprintf(Base::E_Open,$file)); + } + + /** + * post rendering handler + * @param $func callback + */ + function beforerender($func) { + $this->trigger['beforerender'][]=$func; + } + +} + +//! ISO language/country codes +class ISO extends Prefab { + + //@{ ISO 3166-1 country codes + const + CC_af='Afghanistan', + CC_ax='Åland Islands', + CC_al='Albania', + CC_dz='Algeria', + CC_as='American Samoa', + CC_ad='Andorra', + CC_ao='Angola', + CC_ai='Anguilla', + CC_aq='Antarctica', + CC_ag='Antigua and Barbuda', + CC_ar='Argentina', + CC_am='Armenia', + CC_aw='Aruba', + CC_au='Australia', + CC_at='Austria', + CC_az='Azerbaijan', + CC_bs='Bahamas', + CC_bh='Bahrain', + CC_bd='Bangladesh', + CC_bb='Barbados', + CC_by='Belarus', + CC_be='Belgium', + CC_bz='Belize', + CC_bj='Benin', + CC_bm='Bermuda', + CC_bt='Bhutan', + CC_bo='Bolivia', + CC_bq='Bonaire, Sint Eustatius and Saba', + CC_ba='Bosnia and Herzegovina', + CC_bw='Botswana', + CC_bv='Bouvet Island', + CC_br='Brazil', + CC_io='British Indian Ocean Territory', + CC_bn='Brunei Darussalam', + CC_bg='Bulgaria', + CC_bf='Burkina Faso', + CC_bi='Burundi', + CC_kh='Cambodia', + CC_cm='Cameroon', + CC_ca='Canada', + CC_cv='Cape Verde', + CC_ky='Cayman Islands', + CC_cf='Central African Republic', + CC_td='Chad', + CC_cl='Chile', + CC_cn='China', + CC_cx='Christmas Island', + CC_cc='Cocos (Keeling) Islands', + CC_co='Colombia', + CC_km='Comoros', + CC_cg='Congo', + CC_cd='Congo, The Democratic Republic of', + CC_ck='Cook Islands', + CC_cr='Costa Rica', + CC_ci='Côte d\'ivoire', + CC_hr='Croatia', + CC_cu='Cuba', + CC_cw='Curaçao', + CC_cy='Cyprus', + CC_cz='Czech Republic', + CC_dk='Denmark', + CC_dj='Djibouti', + CC_dm='Dominica', + CC_do='Dominican Republic', + CC_ec='Ecuador', + CC_eg='Egypt', + CC_sv='El Salvador', + CC_gq='Equatorial Guinea', + CC_er='Eritrea', + CC_ee='Estonia', + CC_et='Ethiopia', + CC_fk='Falkland Islands (Malvinas)', + CC_fo='Faroe Islands', + CC_fj='Fiji', + CC_fi='Finland', + CC_fr='France', + CC_gf='French Guiana', + CC_pf='French Polynesia', + CC_tf='French Southern Territories', + CC_ga='Gabon', + CC_gm='Gambia', + CC_ge='Georgia', + CC_de='Germany', + CC_gh='Ghana', + CC_gi='Gibraltar', + CC_gr='Greece', + CC_gl='Greenland', + CC_gd='Grenada', + CC_gp='Guadeloupe', + CC_gu='Guam', + CC_gt='Guatemala', + CC_gg='Guernsey', + CC_gn='Guinea', + CC_gw='Guinea-Bissau', + CC_gy='Guyana', + CC_ht='Haiti', + CC_hm='Heard Island and McDonald Islands', + CC_va='Holy See (Vatican City State)', + CC_hn='Honduras', + CC_hk='Hong Kong', + CC_hu='Hungary', + CC_is='Iceland', + CC_in='India', + CC_id='Indonesia', + CC_ir='Iran, Islamic Republic of', + CC_iq='Iraq', + CC_ie='Ireland', + CC_im='Isle of Man', + CC_il='Israel', + CC_it='Italy', + CC_jm='Jamaica', + CC_jp='Japan', + CC_je='Jersey', + CC_jo='Jordan', + CC_kz='Kazakhstan', + CC_ke='Kenya', + CC_ki='Kiribati', + CC_kp='Korea, Democratic People\'s Republic of', + CC_kr='Korea, Republic of', + CC_kw='Kuwait', + CC_kg='Kyrgyzstan', + CC_la='Lao People\'s Democratic Republic', + CC_lv='Latvia', + CC_lb='Lebanon', + CC_ls='Lesotho', + CC_lr='Liberia', + CC_ly='Libya', + CC_li='Liechtenstein', + CC_lt='Lithuania', + CC_lu='Luxembourg', + CC_mo='Macao', + CC_mk='Macedonia, The Former Yugoslav Republic of', + CC_mg='Madagascar', + CC_mw='Malawi', + CC_my='Malaysia', + CC_mv='Maldives', + CC_ml='Mali', + CC_mt='Malta', + CC_mh='Marshall Islands', + CC_mq='Martinique', + CC_mr='Mauritania', + CC_mu='Mauritius', + CC_yt='Mayotte', + CC_mx='Mexico', + CC_fm='Micronesia, Federated States of', + CC_md='Moldova, Republic of', + CC_mc='Monaco', + CC_mn='Mongolia', + CC_me='Montenegro', + CC_ms='Montserrat', + CC_ma='Morocco', + CC_mz='Mozambique', + CC_mm='Myanmar', + CC_na='Namibia', + CC_nr='Nauru', + CC_np='Nepal', + CC_nl='Netherlands', + CC_nc='New Caledonia', + CC_nz='New Zealand', + CC_ni='Nicaragua', + CC_ne='Niger', + CC_ng='Nigeria', + CC_nu='Niue', + CC_nf='Norfolk Island', + CC_mp='Northern Mariana Islands', + CC_no='Norway', + CC_om='Oman', + CC_pk='Pakistan', + CC_pw='Palau', + CC_ps='Palestinian Territory, Occupied', + CC_pa='Panama', + CC_pg='Papua New Guinea', + CC_py='Paraguay', + CC_pe='Peru', + CC_ph='Philippines', + CC_pn='Pitcairn', + CC_pl='Poland', + CC_pt='Portugal', + CC_pr='Puerto Rico', + CC_qa='Qatar', + CC_re='Réunion', + CC_ro='Romania', + CC_ru='Russian Federation', + CC_rw='Rwanda', + CC_bl='Saint Barthélemy', + CC_sh='Saint Helena, Ascension and Tristan da Cunha', + CC_kn='Saint Kitts and Nevis', + CC_lc='Saint Lucia', + CC_mf='Saint Martin (French Part)', + CC_pm='Saint Pierre and Miquelon', + CC_vc='Saint Vincent and The Grenadines', + CC_ws='Samoa', + CC_sm='San Marino', + CC_st='Sao Tome and Principe', + CC_sa='Saudi Arabia', + CC_sn='Senegal', + CC_rs='Serbia', + CC_sc='Seychelles', + CC_sl='Sierra Leone', + CC_sg='Singapore', + CC_sk='Slovakia', + CC_sx='Sint Maarten (Dutch Part)', + CC_si='Slovenia', + CC_sb='Solomon Islands', + CC_so='Somalia', + CC_za='South Africa', + CC_gs='South Georgia and The South Sandwich Islands', + CC_ss='South Sudan', + CC_es='Spain', + CC_lk='Sri Lanka', + CC_sd='Sudan', + CC_sr='Suriname', + CC_sj='Svalbard and Jan Mayen', + CC_sz='Swaziland', + CC_se='Sweden', + CC_ch='Switzerland', + CC_sy='Syrian Arab Republic', + CC_tw='Taiwan, Province of China', + CC_tj='Tajikistan', + CC_tz='Tanzania, United Republic of', + CC_th='Thailand', + CC_tl='Timor-Leste', + CC_tg='Togo', + CC_tk='Tokelau', + CC_to='Tonga', + CC_tt='Trinidad and Tobago', + CC_tn='Tunisia', + CC_tr='Turkey', + CC_tm='Turkmenistan', + CC_tc='Turks and Caicos Islands', + CC_tv='Tuvalu', + CC_ug='Uganda', + CC_ua='Ukraine', + CC_ae='United Arab Emirates', + CC_gb='United Kingdom', + CC_us='United States', + CC_um='United States Minor Outlying Islands', + CC_uy='Uruguay', + CC_uz='Uzbekistan', + CC_vu='Vanuatu', + CC_ve='Venezuela', + CC_vn='Viet Nam', + CC_vg='Virgin Islands, British', + CC_vi='Virgin Islands, U.S.', + CC_wf='Wallis and Futuna', + CC_eh='Western Sahara', + CC_ye='Yemen', + CC_zm='Zambia', + CC_zw='Zimbabwe'; + //@} + + //@{ ISO 639-1 language codes (Windows-compatibility subset) + const + LC_af='Afrikaans', + LC_am='Amharic', + LC_ar='Arabic', + LC_as='Assamese', + LC_ba='Bashkir', + LC_be='Belarusian', + LC_bg='Bulgarian', + LC_bn='Bengali', + LC_bo='Tibetan', + LC_br='Breton', + LC_ca='Catalan', + LC_co='Corsican', + LC_cs='Czech', + LC_cy='Welsh', + LC_da='Danish', + LC_de='German', + LC_dv='Divehi', + LC_el='Greek', + LC_en='English', + LC_es='Spanish', + LC_et='Estonian', + LC_eu='Basque', + LC_fa='Persian', + LC_fi='Finnish', + LC_fo='Faroese', + LC_fr='French', + LC_gd='Scottish Gaelic', + LC_gl='Galician', + LC_gu='Gujarati', + LC_he='Hebrew', + LC_hi='Hindi', + LC_hr='Croatian', + LC_hu='Hungarian', + LC_hy='Armenian', + LC_id='Indonesian', + LC_ig='Igbo', + LC_is='Icelandic', + LC_it='Italian', + LC_ja='Japanese', + LC_ka='Georgian', + LC_kk='Kazakh', + LC_km='Khmer', + LC_kn='Kannada', + LC_ko='Korean', + LC_lb='Luxembourgish', + LC_lo='Lao', + LC_lt='Lithuanian', + LC_lv='Latvian', + LC_mi='Maori', + LC_ml='Malayalam', + LC_mr='Marathi', + LC_ms='Malay', + LC_mt='Maltese', + LC_ne='Nepali', + LC_nl='Dutch', + LC_no='Norwegian', + LC_oc='Occitan', + LC_or='Oriya', + LC_pl='Polish', + LC_ps='Pashto', + LC_pt='Portuguese', + LC_qu='Quechua', + LC_ro='Romanian', + LC_ru='Russian', + LC_rw='Kinyarwanda', + LC_sa='Sanskrit', + LC_si='Sinhala', + LC_sk='Slovak', + LC_sl='Slovenian', + LC_sq='Albanian', + LC_sv='Swedish', + LC_ta='Tamil', + LC_te='Telugu', + LC_th='Thai', + LC_tk='Turkmen', + LC_tr='Turkish', + LC_tt='Tatar', + LC_uk='Ukrainian', + LC_ur='Urdu', + LC_vi='Vietnamese', + LC_wo='Wolof', + LC_yo='Yoruba', + LC_zh='Chinese'; + //@} + + /** + * Return list of languages indexed by ISO 639-1 language code + * @return array + **/ + function languages() { + return \Base::instance()->constants($this,'LC_'); + } + + /** + * Return list of countries indexed by ISO 3166-1 country code + * @return array + **/ + function countries() { + return \Base::instance()->constants($this,'CC_'); + } + +} + +//! Container for singular object instances +final class Registry { + + private static + //! Object catalog + $table; + + /** + * Return TRUE if object exists in catalog + * @return bool + * @param $key string + **/ + static function exists($key) { + return isset(self::$table[$key]); + } + + /** + * Add object to catalog + * @return object + * @param $key string + * @param $obj object + **/ + static function set($key,$obj) { + return self::$table[$key]=$obj; + } + + /** + * Retrieve object from catalog + * @return object + * @param $key string + **/ + static function get($key) { + return self::$table[$key]; + } + + /** + * Delete object from catalog + * @param $key string + **/ + static function clear($key) { + self::$table[$key]=NULL; + unset(self::$table[$key]); + } + + //! Prohibit cloning + private function __clone() { + } + + //! Prohibit instantiation + private function __construct() { + } + +} + +return Base::instance(); diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/basket.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/basket.php new file mode 100644 index 0000000..95c1c71 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/basket.php @@ -0,0 +1,238 @@ +. + +*/ + +//! Session-based pseudo-mapper +class Basket extends Magic { + + //@{ Error messages + const + E_Field='Undefined field %s'; + //@} + + protected + //! Session key + $key, + //! Current item identifier + $id, + //! Current item contents + $item=[]; + + /** + * Return TRUE if field is defined + * @return bool + * @param $key string + **/ + function exists($key) { + return array_key_exists($key,$this->item); + } + + /** + * Assign value to field + * @return scalar|FALSE + * @param $key string + * @param $val scalar + **/ + function set($key,$val) { + return ($key=='_id')?FALSE:($this->item[$key]=$val); + } + + /** + * Retrieve value of field + * @return scalar|FALSE + * @param $key string + **/ + function &get($key) { + if ($key=='_id') + return $this->id; + if (array_key_exists($key,$this->item)) + return $this->item[$key]; + throw new \Exception(sprintf(self::E_Field,$key)); + } + + /** + * Delete field + * @return NULL + * @param $key string + **/ + function clear($key) { + unset($this->item[$key]); + } + + /** + * Return items that match key/value pair; + * If no key/value pair specified, return all items + * @return array + * @param $key string + * @param $val mixed + **/ + function find($key=NULL,$val=NULL) { + $out=[]; + if (isset($_SESSION[$this->key])) { + foreach ($_SESSION[$this->key] as $id=>$item) + if (!isset($key) || + array_key_exists($key,$item) && $item[$key]==$val || + $key=='_id' && $id==$val) { + $obj=clone($this); + $obj->id=$id; + $obj->item=$item; + $out[]=$obj; + } + } + return $out; + } + + /** + * Return first item that matches key/value pair + * @return object|FALSE + * @param $key string + * @param $val mixed + **/ + function findone($key,$val) { + return ($data=$this->find($key,$val))?$data[0]:FALSE; + } + + /** + * Map current item to matching key/value pair + * @return array + * @param $key string + * @param $val mixed + **/ + function load($key,$val) { + if ($found=$this->find($key,$val)) { + $this->id=$found[0]->id; + return $this->item=$found[0]->item; + } + $this->reset(); + return []; + } + + /** + * Return TRUE if current item is empty/undefined + * @return bool + **/ + function dry() { + return !$this->item; + } + + /** + * Return number of items in basket + * @return int + **/ + function count() { + return isset($_SESSION[$this->key])?count($_SESSION[$this->key]):0; + } + + /** + * Save current item + * @return array + **/ + function save() { + if (!$this->id) + $this->id=uniqid('',TRUE); + $_SESSION[$this->key][$this->id]=$this->item; + return $this->item; + } + + /** + * Erase item matching key/value pair + * @return bool + * @param $key string + * @param $val mixed + **/ + function erase($key,$val) { + $found=$this->find($key,$val); + if ($found && $id=$found[0]->id) { + unset($_SESSION[$this->key][$id]); + if ($id==$this->id) + $this->reset(); + return TRUE; + } + return FALSE; + } + + /** + * Reset cursor + * @return NULL + **/ + function reset() { + $this->id=NULL; + $this->item=[]; + } + + /** + * Empty basket + * @return NULL + **/ + function drop() { + unset($_SESSION[$this->key]); + } + + /** + * Hydrate item using hive array variable + * @return NULL + * @param $var array|string + **/ + function copyfrom($var) { + if (is_string($var)) + $var=\Base::instance()->$var; + foreach ($var as $key=>$val) + $this->set($key,$val); + } + + /** + * Populate hive array variable with item contents + * @return NULL + * @param $key string + **/ + function copyto($key) { + $var=&\Base::instance()->ref($key); + foreach ($this->item as $key=>$field) + $var[$key]=$field; + } + + /** + * Check out basket contents + * @return array + **/ + function checkout() { + if (isset($_SESSION[$this->key])) { + $out=$_SESSION[$this->key]; + unset($_SESSION[$this->key]); + return $out; + } + return []; + } + + /** + * Instantiate class + * @return void + * @param $key string + **/ + function __construct($key='basket') { + $this->key=$key; + if (session_status()!=PHP_SESSION_ACTIVE) + session_start(); + Base::instance()->sync('SESSION'); + $this->reset(); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/bcrypt.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/bcrypt.php new file mode 100644 index 0000000..88979b9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/bcrypt.php @@ -0,0 +1,96 @@ +. +* +**/ + +/** +* Lightweight password hashing library (PHP 5.5+ only) +* @deprecated Use http://php.net/manual/en/ref.password.php instead +**/ +class Bcrypt extends Prefab { + + //@{ Error messages + const + E_CostArg='Invalid cost parameter', + E_SaltArg='Salt must be at least 22 alphanumeric characters'; + //@} + + //! Default cost + const + COST=10; + + /** + * Generate bcrypt hash of string + * @return string|FALSE + * @param $pw string + * @param $salt string + * @param $cost int + **/ + function hash($pw,$salt=NULL,$cost=self::COST) { + if ($cost<4 || $cost>31) + throw new \Exception(self::E_CostArg); + $len=22; + if ($salt) { + if (!preg_match('/^[[:alnum:]\.\/]{'.$len.',}$/',$salt)) + throw new \Exception(self::E_SaltArg); + } + else { + $raw=16; + $iv=''; + if (!$iv && extension_loaded('openssl')) + $iv=openssl_random_pseudo_bytes($raw); + if (!$iv) + for ($i=0;$i<$raw;++$i) + $iv.=chr(mt_rand(0,255)); + $salt=str_replace('+','.',base64_encode($iv)); + } + $salt=substr($salt,0,$len); + $hash=crypt($pw,sprintf('$2y$%02d$',$cost).$salt); + return strlen($hash)>13?$hash:FALSE; + } + + /** + * Check if password is still strong enough + * @return bool + * @param $hash string + * @param $cost int + **/ + function needs_rehash($hash,$cost=self::COST) { + list($pwcost)=sscanf($hash,"$2y$%d$"); + return $pwcost<$cost; + } + + /** + * Verify password against hash using timing attack resistant approach + * @return bool + * @param $pw string + * @param $hash string + **/ + function verify($pw,$hash) { + $val=crypt($pw,$hash); + $len=strlen($val); + if ($len!=strlen($hash) || $len<14) + return FALSE; + $out=0; + for ($i=0;$i<$len;++$i) + $out|=(ord($val[$i])^ord($hash[$i])); + return $out===0; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/cli/ws.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/cli/ws.php new file mode 100644 index 0000000..623b03a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/cli/ws.php @@ -0,0 +1,487 @@ +. + +*/ + +namespace CLI; + +//! RFC6455 WebSocket server +class WS { + + const + //! UUID magic string + Magic='258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + //! Max packet size + Packet=65536; + + //@{ Mask bits for first byte of header + const + Text=0x01, + Binary=0x02, + Close=0x08, + Ping=0x09, + Pong=0x0a, + OpCode=0x0f, + Finale=0x80; + //@} + + //@{ Mask bits for second byte of header + const + Length=0x7f; + //@} + + protected + $addr, + $ctx, + $wait, + $sockets, + $protocol, + $agents=[], + $events=[]; + + /** + * Allocate stream socket + * @return NULL + * @param $socket resource + **/ + function alloc($socket) { + if (is_bool($buf=$this->read($socket))) + return; + // Get WebSocket headers + $hdrs=[]; + $EOL="\r\n"; + $verb=NULL; + $uri=NULL; + foreach (explode($EOL,trim($buf)) as $line) + if (preg_match('/^(\w+)\s(.+)\sHTTP\/[\d.]{1,3}$/', + trim($line),$match)) { + $verb=$match[1]; + $uri=$match[2]; + } + else + if (preg_match('/^(.+): (.+)/',trim($line),$match)) + // Standardize header + $hdrs[ + strtr( + ucwords( + strtolower( + strtr($match[1],'-',' ') + ) + ),' ','-' + ) + ]=$match[2]; + else { + $this->close($socket); + return; + } + if (empty($hdrs['Upgrade']) && + empty($hdrs['Sec-Websocket-Key'])) { + // Not a WebSocket request + if ($verb && $uri) + $this->write( + $socket, + 'HTTP/1.1 400 Bad Request'.$EOL. + 'Connection: close'.$EOL.$EOL + ); + $this->close($socket); + return; + } + // Handshake + $buf='HTTP/1.1 101 Switching Protocols'.$EOL. + 'Upgrade: websocket'.$EOL. + 'Connection: Upgrade'.$EOL; + if (isset($hdrs['Sec-Websocket-Protocol'])) + $buf.='Sec-WebSocket-Protocol: '. + $hdrs['Sec-Websocket-Protocol'].$EOL; + $buf.='Sec-WebSocket-Accept: '. + base64_encode( + sha1($hdrs['Sec-Websocket-Key'].WS::Magic,TRUE) + ).$EOL.$EOL; + if ($this->write($socket,$buf)) { + // Connect agent to server + $this->sockets[(int)$socket]=$socket; + $this->agents[(int)$socket]= + new Agent($this,$socket,$verb,$uri,$hdrs); + } + } + + /** + * Close stream socket + * @return NULL + * @param $socket resource + **/ + function close($socket) { + if (isset($this->agents[(int)$socket])) + unset($this->sockets[(int)$socket],$this->agents[(int)$socket]); + stream_socket_shutdown($socket,STREAM_SHUT_WR); + @fclose($socket); + } + + /** + * Read from stream socket + * @return string|FALSE + * @param $socket resource + * @param $len int + **/ + function read($socket,$len=0) { + if (!$len) + $len=WS::Packet; + if (is_string($buf=@fread($socket,$len)) && + strlen($buf) && strlen($buf)<$len) + return $buf; + if (isset($this->events['error']) && + is_callable($func=$this->events['error'])) + $func($this); + $this->close($socket); + return FALSE; + } + + /** + * Write to stream socket + * @return int|FALSE + * @param $socket resource + * @param $buf string + **/ + function write($socket,$buf) { + for ($i=0,$bytes=0;$ievents['error']) && + is_callable($func=$this->events['error'])) + $func($this); + $this->close($socket); + return FALSE; + } + return $bytes; + } + + /** + * Return socket agents + * @return array + * @param $uri string + ***/ + function agents($uri=NULL) { + return array_filter( + $this->agents, + /** + * @var $val Agent + * @return bool + */ + function($val) use($uri) { + return $uri?($val->uri()==$uri):TRUE; + } + ); + } + + /** + * Return event handlers + * @return array + **/ + function events() { + return $this->events; + } + + /** + * Bind function to event handler + * @return object + * @param $event string + * @param $func callable + **/ + function on($event,$func) { + $this->events[$event]=$func; + return $this; + } + + /** + * Terminate server + **/ + function kill() { + die; + } + + /** + * Execute the server process + **/ + function run() { + // Assign signal handlers + declare(ticks=1); + pcntl_signal(SIGINT,[$this,'kill']); + pcntl_signal(SIGTERM,[$this,'kill']); + gc_enable(); + // Activate WebSocket listener + $listen=stream_socket_server( + $this->addr,$errno,$errstr, + STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, + $this->ctx + ); + $socket=socket_import_stream($listen); + register_shutdown_function(function() use($listen) { + foreach ($this->sockets as $socket) + if ($socket!=$listen) + $this->close($socket); + $this->close($listen); + if (isset($this->events['stop']) && + is_callable($func=$this->events['stop'])) + $func($this); + }); + if ($errstr) + throw new \Exception($errstr); + if (isset($this->events['start']) && + is_callable($func=$this->events['start'])) + $func($this); + $this->sockets=[(int)$listen=>$listen]; + $empty=[]; + $wait=$this->wait; + while (TRUE) { + $active=$this->sockets; + $mark=microtime(TRUE); + $count=@stream_select( + $active,$empty,$empty,(int)$wait,round(1e6*($wait-(int)$wait)) + ); + if (is_bool($count) && $wait) { + if (isset($this->events['error']) && + is_callable($func=$this->events['error'])) + $func($this); + die; + } + if ($count) { + // Process active connections + foreach ($active as $socket) { + if (!is_resource($socket)) + continue; + if ($socket==$listen) { + if ($socket=@stream_socket_accept($listen,0)) + $this->alloc($socket); + else + if (isset($this->events['error']) && + is_callable($func=$this->events['error'])) + $func($this); + } + else { + $id=(int)$socket; + if (isset($this->agents[$id])) + $this->agents[$id]->fetch(); + } + } + $wait-=microtime(TRUE)-$mark; + while ($wait<1e-6) { + $wait+=$this->wait; + $count=0; + } + } + if (!$count) { + $mark=microtime(TRUE); + foreach ($this->sockets as $id=>$socket) { + if (!is_resource($socket)) + continue; + if ($socket!=$listen && + isset($this->agents[$id]) && + isset($this->events['idle']) && + is_callable($func=$this->events['idle'])) + $func($this->agents[$id]); + } + $wait=$this->wait-microtime(TRUE)+$mark; + } + gc_collect_cycles(); + } + } + + /** + * @param $addr string + * @param $ctx resource + * @param $wait int + **/ + function __construct($addr,$ctx=NULL,$wait=60) { + $this->addr=$addr; + $this->ctx=$ctx?:stream_context_create(); + $this->wait=$wait; + $this->events=[]; + } + +} + +//! RFC6455 remote socket +class Agent { + + protected + $server, + $id, + $socket, + $flag, + $verb, + $uri, + $headers; + + /** + * Return server instance + * @return WS + **/ + function server() { + return $this->server; + } + + /** + * Return socket ID + * @return string + **/ + function id() { + return $this->id; + } + + /** + * Return socket + * @return resource + **/ + function socket() { + return $this->socket; + } + + /** + * Return request method + * @return string + **/ + function verb() { + return $this->verb; + } + + /** + * Return request URI + * @return string + **/ + function uri() { + return $this->uri; + } + + /** + * Return socket headers + * @return array + **/ + function headers() { + return $this->headers; + } + + /** + * Frame and transmit payload + * @return string|FALSE + * @param $op int + * @param $data string + **/ + function send($op,$data='') { + $server=$this->server; + $mask=WS::Finale | $op & WS::OpCode; + $len=strlen($data); + $buf=''; + if ($len>0xffff) + $buf=pack('CCNN',$mask,0x7f,$len); + elseif ($len>0x7d) + $buf=pack('CCn',$mask,0x7e,$len); + else + $buf=pack('CC',$mask,$len); + $buf.=$data; + if (is_bool($server->write($this->socket,$buf))) + return FALSE; + if (!in_array($op,[WS::Pong,WS::Close]) && + isset($this->server->events()['send']) && + is_callable($func=$this->server->events()['send'])) + $func($this,$op,$data); + return $data; + } + + /** + * Retrieve and unmask payload + * @return bool|NULL + **/ + function fetch() { + // Unmask payload + $server=$this->server; + if (is_bool($buf=$server->read($this->socket))) + return FALSE; + while($buf) { + $op=ord($buf[0]) & WS::OpCode; + $len=ord($buf[1]) & WS::Length; + $pos=2; + if ($len==0x7e) { + $len=ord($buf[2])*256+ord($buf[3]); + $pos+=2; + } + else + if ($len==0x7f) { + for ($i=0,$len=0;$i<8;++$i) + $len=$len*256+ord($buf[$i+2]); + $pos+=8; + } + for ($i=0,$mask=[];$i<4;++$i) + $mask[$i]=ord($buf[$pos+$i]); + $pos+=4; + if (strlen($buf)<$len+$pos) + return FALSE; + for ($i=0,$data='';$i<$len;++$i) + $data.=chr(ord($buf[$pos+$i])^$mask[$i%4]); + // Dispatch + switch ($op & WS::OpCode) { + case WS::Ping: + $this->send(WS::Pong); + break; + case WS::Close: + $server->close($this->socket); + break; + case WS::Text: + $data=trim($data); + case WS::Binary: + if (isset($this->server->events()['receive']) && + is_callable($func=$this->server->events()['receive'])) + $func($this,$op,$data); + break; + } + $buf = substr($buf, $len+$pos); + } + } + + /** + * Destroy object + **/ + function __destruct() { + if (isset($this->server->events()['disconnect']) && + is_callable($func=$this->server->events()['disconnect'])) + $func($this); + } + + /** + * @param $server WS + * @param $socket resource + * @param $verb string + * @param $uri string + * @param $hdrs array + **/ + function __construct($server,$socket,$verb,$uri,array $hdrs) { + $this->server=$server; + $this->id=stream_socket_get_name($socket,TRUE); + $this->socket=$socket; + $this->verb=$verb; + $this->uri=$uri; + $this->headers=$hdrs; + + if (isset($server->events()['connect']) && + is_callable($func=$server->events()['connect'])) + $func($this); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/code.css b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/code.css new file mode 100644 index 0000000..618703f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/code.css @@ -0,0 +1 @@ +code{word-wrap:break-word;color:black}.comment,.doc_comment,.ml_comment{color:dimgray;font-style:italic}.variable{color:blueviolet}.const,.constant_encapsed_string,.class_c,.dir,.file,.func_c,.halt_compiler,.line,.method_c,.lnumber,.dnumber{color:crimson}.string,.and_equal,.boolean_and,.boolean_or,.concat_equal,.dec,.div_equal,.inc,.is_equal,.is_greater_or_equal,.is_identical,.is_not_equal,.is_not_identical,.is_smaller_or_equal,.logical_and,.logical_or,.logical_xor,.minus_equal,.mod_equal,.mul_equal,.ns_c,.ns_separator,.or_equal,.plus_equal,.sl,.sl_equal,.sr,.sr_equal,.xor_equal,.start_heredoc,.end_heredoc,.object_operator,.paamayim_nekudotayim{color:black}.abstract,.array,.array_cast,.as,.break,.case,.catch,.class,.clone,.continue,.declare,.default,.do,.echo,.else,.elseif,.empty.enddeclare,.endfor,.endforach,.endif,.endswitch,.endwhile,.eval,.exit,.extends,.final,.for,.foreach,.function,.global,.goto,.if,.implements,.include,.include_once,.instanceof,.interface,.isset,.list,.namespace,.new,.print,.private,.public,.protected,.require,.require_once,.return,.static,.switch,.throw,.try,.unset,.use,.var,.while{color:royalblue}.open_tag,.open_tag_with_echo,.close_tag{color:orange}.ini_section{color:black}.ini_key{color:royalblue}.ini_value{color:crimson}.xml_tag{color:dodgerblue}.xml_attr{color:blueviolet}.xml_data{color:red}.section{color:black}.directive{color:blue}.data{color:dimgray} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/composer.json b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/composer.json new file mode 100644 index 0000000..45635d2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/composer.json @@ -0,0 +1,12 @@ +{ + "name": "bcosca/fatfree-core", + "description": "A powerful yet easy-to-use PHP micro-framework designed to help you build dynamic and robust Web applications - fast!", + "homepage": "http://fatfreeframework.com/", + "license": "GPL-3.0", + "require": { + "php": ">=7.2" + }, + "autoload": { + "classmap": ["."] + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/cursor.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/cursor.php new file mode 100644 index 0000000..e692ac1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/cursor.php @@ -0,0 +1,389 @@ +. + +*/ + +namespace DB; + +//! Simple cursor implementation +abstract class Cursor extends \Magic implements \IteratorAggregate { + + //@{ Error messages + const + E_Field='Undefined field %s'; + //@} + + protected + //! Query results + $query=[], + //! Current position + $ptr=0, + //! Event listeners + $trigger=[]; + + /** + * Return database type + * @return string + **/ + abstract function dbtype(); + + /** + * Return field names + * @return array + **/ + abstract function fields(); + + /** + * Return fields of mapper object as an associative array + * @return array + * @param $obj object + **/ + abstract function cast($obj=NULL); + + /** + * Return records (array of mapper objects) that match criteria + * @return array + * @param $filter string|array + * @param $options array + * @param $ttl int + **/ + abstract function find($filter=NULL,?array $options=NULL,$ttl=0); + + /** + * Count records that match criteria + * @return int + * @param $filter array + * @param $options array + * @param $ttl int + **/ + abstract function count($filter=NULL,?array $options=NULL,$ttl=0); + + /** + * Insert new record + * @return array + **/ + abstract function insert(); + + /** + * Update current record + * @return array + **/ + abstract function update(); + + /** + * Hydrate mapper object using hive array variable + * @return NULL + * @param $var array|string + * @param $func callback + **/ + abstract function copyfrom($var,$func=NULL); + + /** + * Populate hive array variable with mapper fields + * @return NULL + * @param $key string + **/ + abstract function copyto($key); + + /** + * Get cursor's equivalent external iterator + * Causes a fatal error in PHP 5.3.5 if uncommented + * return ArrayIterator + **/ + #[\ReturnTypeWillChange] + abstract function getiterator(); + + + /** + * Return TRUE if current cursor position is not mapped to any record + * @return bool + **/ + function dry() { + return empty($this->query[$this->ptr]); + } + + /** + * Return first record (mapper object) that matches criteria + * @return static|FALSE + * @param $filter string|array + * @param $options array + * @param $ttl int + **/ + function findone($filter=NULL,?array $options=NULL,$ttl=0) { + if (!$options) + $options=[]; + // Override limit + $options['limit']=1; + return ($data=$this->find($filter,$options,$ttl))?$data[0]:FALSE; + } + + /** + * Return array containing subset of records matching criteria, + * total number of records in superset, specified limit, number of + * subsets available, and actual subset position + * @return array + * @param $pos int + * @param $size int + * @param $filter string|array + * @param $options array + * @param $ttl int + * @param $bounce bool + **/ + function paginate( + $pos=0,$size=10,$filter=NULL,?array $options=NULL,$ttl=0,$bounce=TRUE) { + $total=$this->count($filter,$options,$ttl); + $count=(int)ceil($total/$size); + if ($bounce) + $pos=max(0,min($pos,$count-1)); + return [ + 'subset'=>($bounce || $pos<$count)?$this->find($filter, + array_merge( + $options?:[], + ['limit'=>$size,'offset'=>$pos*$size] + ), + $ttl + ):[], + 'total'=>$total, + 'limit'=>$size, + 'count'=>$count, + 'pos'=>$bounce?($pos<$count?$pos:0):$pos + ]; + } + + /** + * Map to first record that matches criteria + * @return \DB\SQL\Mapper|FALSE + * @param $filter string|array + * @param $options array + * @param $ttl int + **/ + function load($filter=NULL,?array $options=NULL,$ttl=0) { + $this->reset(); + return ($this->query=$this->find($filter,$options,$ttl)) && + $this->skip(0)?$this->query[$this->ptr]:FALSE; + } + + /** + * Return the count of records loaded + * @return int + **/ + function loaded() { + return count($this->query); + } + + /** + * Map to first record in cursor + * @return mixed + **/ + function first() { + return $this->skip(-$this->ptr); + } + + /** + * Map to last record in cursor + * @return mixed + **/ + function last() { + return $this->skip(($ofs=count($this->query)-$this->ptr)?$ofs-1:0); + } + + /** + * Map to nth record relative to current cursor position + * @return mixed + * @param $ofs int + **/ + function skip($ofs=1) { + $this->ptr+=$ofs; + return $this->ptr>-1 && $this->ptrquery)? + $this->query[$this->ptr]:FALSE; + } + + /** + * Map next record + * @return mixed + **/ + function next() { + return $this->skip(); + } + + /** + * Map previous record + * @return mixed + **/ + function prev() { + return $this->skip(-1); + } + + /** + * Return whether current iterator position is valid. + */ + function valid() { + return !$this->dry(); + } + + /** + * Save mapped record + * @return mixed + **/ + function save() { + return $this->query?$this->update():$this->insert(); + } + + /** + * Delete current record + * @return int|bool + **/ + function erase() { + $this->query=array_slice($this->query,0,$this->ptr,TRUE)+ + array_slice($this->query,$this->ptr,NULL,TRUE); + $this->skip(0); + } + + /** + * Define onload trigger + * @return callback + * @param $func callback + **/ + function onload($func) { + return $this->trigger['load']=$func; + } + + /** + * Define beforeinsert trigger + * @return callback + * @param $func callback + **/ + function beforeinsert($func) { + return $this->trigger['beforeinsert']=$func; + } + + /** + * Define afterinsert trigger + * @return callback + * @param $func callback + **/ + function afterinsert($func) { + return $this->trigger['afterinsert']=$func; + } + + /** + * Define oninsert trigger + * @return callback + * @param $func callback + **/ + function oninsert($func) { + return $this->afterinsert($func); + } + + /** + * Define beforeupdate trigger + * @return callback + * @param $func callback + **/ + function beforeupdate($func) { + return $this->trigger['beforeupdate']=$func; + } + + /** + * Define afterupdate trigger + * @return callback + * @param $func callback + **/ + function afterupdate($func) { + return $this->trigger['afterupdate']=$func; + } + + /** + * Define onupdate trigger + * @return callback + * @param $func callback + **/ + function onupdate($func) { + return $this->afterupdate($func); + } + + /** + * Define beforesave trigger + * @return callback + * @param $func callback + **/ + function beforesave($func) { + $this->trigger['beforeinsert']=$func; + $this->trigger['beforeupdate']=$func; + return $func; + } + + /** + * Define aftersave trigger + * @return callback + * @param $func callback + **/ + function aftersave($func) { + $this->trigger['afterinsert']=$func; + $this->trigger['afterupdate']=$func; + return $func; + } + + /** + * Define onsave trigger + * @return callback + * @param $func callback + **/ + function onsave($func) { + return $this->aftersave($func); + } + + /** + * Define beforeerase trigger + * @return callback + * @param $func callback + **/ + function beforeerase($func) { + return $this->trigger['beforeerase']=$func; + } + + /** + * Define aftererase trigger + * @return callback + * @param $func callback + **/ + function aftererase($func) { + return $this->trigger['aftererase']=$func; + } + + /** + * Define onerase trigger + * @return callback + * @param $func callback + **/ + function onerase($func) { + return $this->aftererase($func); + } + + /** + * Reset cursor + * @return NULL + **/ + function reset() { + $this->query=[]; + $this->ptr=0; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/jig.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/jig.php new file mode 100644 index 0000000..ab2b67f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/jig.php @@ -0,0 +1,178 @@ +. + +*/ + +namespace DB; + +//! In-memory/flat-file DB wrapper +class Jig { + + //@{ Storage formats + const + FORMAT_JSON=0, + FORMAT_Serialized=1; + //@} + + protected + //! UUID + $uuid, + //! Storage location + $dir, + //! Current storage format + $format, + //! Jig log + $log, + //! Memory-held data + $data, + //! lazy load/save files + $lazy; + + /** + * Read data from memory/file + * @return array + * @param $file string + **/ + function &read($file) { + if (!$this->dir || !is_file($dst=$this->dir.$file)) { + if (!isset($this->data[$file])) + $this->data[$file]=[]; + return $this->data[$file]; + } + if ($this->lazy && isset($this->data[$file])) + return $this->data[$file]; + $fw=\Base::instance(); + $raw=$fw->read($dst); + switch ($this->format) { + case self::FORMAT_JSON: + $data=json_decode($raw,TRUE); + break; + case self::FORMAT_Serialized: + $data=$fw->unserialize($raw); + break; + } + $this->data[$file] = $data; + return $this->data[$file]; + } + + /** + * Write data to memory/file + * @return int + * @param $file string + * @param $data array + **/ + function write($file,?array $data=NULL) { + if (!$this->dir || $this->lazy) + return count($this->data[$file]=$data); + $fw=\Base::instance(); + switch ($this->format) { + case self::FORMAT_JSON: + if(version_compare(PHP_VERSION, '7.2.0') >= 0) + $out=json_encode($data,JSON_PRETTY_PRINT | JSON_INVALID_UTF8_IGNORE); + else + $out=json_encode($data,JSON_PRETTY_PRINT | JSON_PARTIAL_OUTPUT_ON_ERROR); + break; + case self::FORMAT_Serialized: + $out=$fw->serialize($data); + break; + } + return $fw->write($this->dir.$file,$out); + } + + /** + * Return directory + * @return string + **/ + function dir() { + return $this->dir; + } + + /** + * Return UUID + * @return string + **/ + function uuid() { + return $this->uuid; + } + + /** + * Return profiler results (or disable logging) + * @param $flag bool + * @return string + **/ + function log($flag=TRUE) { + if ($flag) + return $this->log; + $this->log=FALSE; + } + + /** + * Jot down log entry + * @return NULL + * @param $frame string + **/ + function jot($frame) { + if ($frame) + $this->log.=date('r').' '.$frame.PHP_EOL; + } + + /** + * Clean storage + * @return NULL + **/ + function drop() { + if ($this->lazy) // intentional + $this->data=[]; + if (!$this->dir) + $this->data=[]; + elseif ($glob=@glob($this->dir.'/*',GLOB_NOSORT)) + foreach ($glob as $file) + @unlink($file); + } + + //! Prohibit cloning + private function __clone() { + } + + /** + * Instantiate class + * @param $dir string + * @param $format int + **/ + function __construct($dir=NULL,$format=self::FORMAT_JSON,$lazy=FALSE) { + if ($dir && !is_dir($dir)) + mkdir($dir,\Base::MODE,TRUE); + $this->uuid=\Base::instance()->hash($this->dir=$dir); + $this->format=$format; + $this->lazy=$lazy; + } + + /** + * save file on destruction + **/ + function __destruct() { + if ($this->lazy) { + $this->lazy = FALSE; + foreach ($this->data?:[] as $file => $data) + $this->write($file,$data); + } + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/jig/mapper.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/jig/mapper.php new file mode 100644 index 0000000..86711ab --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/jig/mapper.php @@ -0,0 +1,541 @@ +. + +*/ + +namespace DB\Jig; + +//! Flat-file DB mapper +class Mapper extends \DB\Cursor { + + protected + //! Flat-file DB wrapper + $db, + //! Data file + $file, + //! Document identifier + $id, + //! Document contents + $document=[], + //! field map-reduce handlers + $_reduce; + + /** + * Return database type + * @return string + **/ + function dbtype() { + return 'Jig'; + } + + /** + * Return TRUE if field is defined + * @return bool + * @param $key string + **/ + function exists($key) { + return array_key_exists($key,$this->document); + } + + /** + * Assign value to field + * @return scalar|FALSE + * @param $key string + * @param $val scalar + **/ + function set($key,$val) { + return ($key=='_id')?FALSE:($this->document[$key]=$val); + } + + /** + * Retrieve value of field + * @return scalar|FALSE + * @param $key string + **/ + function &get($key) { + if ($key=='_id') + return $this->id; + if (array_key_exists($key,$this->document)) + return $this->document[$key]; + throw new \Exception(sprintf(self::E_Field,$key)); + } + + /** + * Delete field + * @return NULL + * @param $key string + **/ + function clear($key) { + if ($key!='_id') + unset($this->document[$key]); + } + + /** + * Convert array to mapper object + * @return object + * @param $id string + * @param $row array + **/ + function factory($id,$row) { + $mapper=clone($this); + $mapper->reset(); + $mapper->id=$id; + foreach ($row as $field=>$val) + $mapper->document[$field]=$val; + $mapper->query=[clone($mapper)]; + if (isset($mapper->trigger['load'])) + \Base::instance()->call($mapper->trigger['load'],$mapper); + return $mapper; + } + + /** + * Return fields of mapper object as an associative array + * @return array + * @param $obj object + **/ + function cast($obj=NULL) { + if (!$obj) + $obj=$this; + return $obj->document+['_id'=>$this->id]; + } + + /** + * Convert tokens in string expression to variable names + * @return string + * @param $str string + **/ + function token($str) { + $str=preg_replace_callback( + '/(?stringify(substr($expr[1],1)): + (preg_match('/^\w+/', + $mix=$this->token($expr[2]))? + $fw->stringify($mix): + $mix)). + ']'; + }, + $token[1] + ); + }, + $str + ); + return trim($str); + } + + /** + * Return records that match criteria + * @return static[]|FALSE + * @param $filter array + * @param $options array + * @param $ttl int|array + * @param $log bool + **/ + function find($filter=NULL,?array $options=NULL,$ttl=0,$log=TRUE) { + if (!$options) + $options=[]; + $options+=[ + 'order'=>NULL, + 'limit'=>0, + 'offset'=>0, + 'group'=>NULL, + ]; + $fw=\Base::instance(); + $cache=\Cache::instance(); + $db=$this->db; + $now=microtime(TRUE); + $data=[]; + $tag=''; + if (is_array($ttl)) + list($ttl,$tag)=$ttl; + if (!$fw->CACHE || !$ttl || !($cached=$cache->exists( + $hash=$fw->hash($this->db->dir(). + $fw->stringify([$filter,$options])).($tag?'.'.$tag:'').'.jig',$data)) || + $cached[0]+$ttlread($this->file); + if (is_null($data)) + return FALSE; + foreach ($data as $id=>&$doc) { + $doc['_id']=$id; + unset($doc); + } + if ($filter) { + if (!is_array($filter)) + return FALSE; + // Normalize equality operator + $expr=preg_replace('/(?<=[^<>!=])=(?!=)/','==',$filter[0]); + // Prepare query arguments + $args=isset($filter[1]) && is_array($filter[1])? + $filter[1]: + array_slice($filter,1,NULL,TRUE); + $args=is_array($args)?$args:[1=>$args]; + $keys=$vals=[]; + $tokens=array_slice( + token_get_all('token($expr)),1); + $data=array_filter($data, + function($_row) use($fw,$args,$tokens) { + $_expr=''; + $ctr=0; + $named=FALSE; + foreach ($tokens as $token) { + if (is_string($token)) + if ($token=='?') { + // Positional + ++$ctr; + $key=$ctr; + } + else { + if ($token==':') + $named=TRUE; + else + $_expr.=$token; + continue; + } + elseif ($named && + token_name($token[0])=='T_STRING') { + $key=':'.$token[1]; + $named=FALSE; + } + else { + $_expr.=$token[1]; + continue; + } + $_expr.=$fw->stringify( + is_string($args[$key])? + addcslashes($args[$key],'\''): + $args[$key]); + } + // Avoid conflict with user code + unset($fw,$tokens,$args,$ctr,$token,$key,$named); + extract($_row); + // Evaluate pseudo-SQL expression + return eval('return '.$_expr.';'); + } + ); + } + if (isset($options['group'])) { + $cols=array_reverse($fw->split($options['group'])); + // sort into groups + $data=$this->sort($data,$options['group']); + foreach($data as $i=>&$row) { + if (!isset($prev)) { + $prev=$row; + $prev_i=$i; + } + $drop=false; + foreach ($cols as $col) + if ($prev_i!=$i && array_key_exists($col,$row) && + array_key_exists($col,$prev) && $row[$col]==$prev[$col]) + // reduce/modify + $drop=!isset($this->_reduce[$col]) || call_user_func_array( + $this->_reduce[$col][0],[&$prev,&$row])!==FALSE; + elseif (isset($this->_reduce[$col])) { + $null=null; + // initial + call_user_func_array($this->_reduce[$col][0],[&$row,&$null]); + } + if ($drop) + unset($data[$i]); + else { + $prev=&$row; + $prev_i=$i; + } + unset($row); + } + // finalize + if ($this->_reduce[$col][1]) + foreach($data as $i=>&$row) { + $row=call_user_func($this->_reduce[$col][1],$row); + if (!$row) + unset($data[$i]); + unset($row); + } + } + if (isset($options['order'])) + $data=$this->sort($data,$options['order']); + $data=array_slice($data, + $options['offset'],$options['limit']?:NULL,TRUE); + if ($fw->CACHE && $ttl) + // Save to cache backend + $cache->set($hash,$data,$ttl); + } + $out=[]; + foreach ($data as $id=>&$doc) { + unset($doc['_id']); + $out[]=$this->factory($id,$doc); + unset($doc); + } + if ($log && isset($args)) { + if ($filter) + foreach ($args as $key=>$val) { + $vals[]=$fw->stringify(is_array($val)?$val[0]:$val); + $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/'; + } + $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + $this->file.' [find] '. + ($filter?preg_replace($keys,$vals,$filter[0],1):'')); + } + return $out; + } + + /** + * Sort a collection + * @param $data + * @param $cond + * @return mixed + */ + protected function sort($data,$cond) { + $cols=\Base::instance()->split($cond); + uasort( + $data, + function($val1,$val2) use($cols) { + foreach ($cols as $col) { + $parts=explode(' ',$col,2); + $order=empty($parts[1])? + SORT_ASC: + constant($parts[1]); + $col=$parts[0]; + if (!array_key_exists($col,$val1)) + $val1[$col]=NULL; + if (!array_key_exists($col,$val2)) + $val2[$col]=NULL; + list($v1,$v2)=[$val1[$col],$val2[$col]]; + if ($out=strnatcmp($v1?:'',$v2?:'')* + (($order==SORT_ASC)*2-1)) + return $out; + } + return 0; + } + ); + return $data; + } + + /** + * Add reduce handler for grouped fields + * @param $key string + * @param $handler callback + * @param $finalize callback + */ + function reduce($key,$handler,$finalize=null){ + $this->_reduce[$key]=[$handler,$finalize]; + } + + /** + * Count records that match criteria + * @return int + * @param $filter array + * @param $options array + * @param $ttl int|array + **/ + function count($filter=NULL,?array $options=NULL,$ttl=0) { + $now=microtime(TRUE); + $out=count($this->find($filter,$options,$ttl,FALSE)); + $this->db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + $this->file.' [count] '.($filter?json_encode($filter):'')); + return $out; + } + + /** + * Return record at specified offset using criteria of previous + * load() call and make it active + * @return array + * @param $ofs int + **/ + function skip($ofs=1) { + $this->document=($out=parent::skip($ofs))?$out->document:[]; + $this->id=$out?$out->id:NULL; + if ($this->document && isset($this->trigger['load'])) + \Base::instance()->call($this->trigger['load'],$this); + return $out; + } + + /** + * Insert new record + * @return array + **/ + function insert() { + if ($this->id) + return $this->update(); + $db=$this->db; + $now=microtime(TRUE); + while (($id=uniqid('',TRUE)) && + ($data=&$db->read($this->file)) && isset($data[$id]) && + !connection_aborted()) + usleep(mt_rand(0,100)); + $this->id=$id; + $pkey=['_id'=>$this->id]; + if (isset($this->trigger['beforeinsert']) && + \Base::instance()->call($this->trigger['beforeinsert'], + [$this,$pkey])===FALSE) + return $this->document; + $data[$id]=$this->document; + $db->write($this->file,$data); + $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + $this->file.' [insert] '.json_encode($this->document)); + if (isset($this->trigger['afterinsert'])) + \Base::instance()->call($this->trigger['afterinsert'], + [$this,$pkey]); + $this->load(['@_id=?',$this->id]); + return $this->document; + } + + /** + * Update current record + * @return array + **/ + function update() { + $db=$this->db; + $now=microtime(TRUE); + $data=&$db->read($this->file); + if (isset($this->trigger['beforeupdate']) && + \Base::instance()->call($this->trigger['beforeupdate'], + [$this,['_id'=>$this->id]])===FALSE) + return $this->document; + $data[$this->id]=$this->document; + $db->write($this->file,$data); + $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + $this->file.' [update] '.json_encode($this->document)); + if (isset($this->trigger['afterupdate'])) + \Base::instance()->call($this->trigger['afterupdate'], + [$this,['_id'=>$this->id]]); + return $this->document; + } + + /** + * Delete current record + * @return bool + * @param $filter array + * @param $quick bool + **/ + function erase($filter=NULL,$quick=FALSE) { + $db=$this->db; + $now=microtime(TRUE); + $data=&$db->read($this->file); + $pkey=['_id'=>$this->id]; + if ($filter) { + foreach ($this->find($filter,NULL,FALSE) as $mapper) + if (!$mapper->erase(null,$quick)) + return FALSE; + return TRUE; + } + elseif (isset($this->id)) { + unset($data[$this->id]); + parent::erase(); + } + else + return FALSE; + if (!$quick && isset($this->trigger['beforeerase']) && + \Base::instance()->call($this->trigger['beforeerase'], + [$this,$pkey])===FALSE) + return FALSE; + $db->write($this->file,$data); + if ($filter) { + $args=isset($filter[1]) && is_array($filter[1])? + $filter[1]: + array_slice($filter,1,NULL,TRUE); + $args=is_array($args)?$args:[1=>$args]; + foreach ($args as $key=>$val) { + $vals[]=\Base::instance()-> + stringify(is_array($val)?$val[0]:$val); + $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/'; + } + } + $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + $this->file.' [erase] '. + ($filter?preg_replace($keys,$vals,$filter[0],1):'')); + if (!$quick && isset($this->trigger['aftererase'])) + \Base::instance()->call($this->trigger['aftererase'], + [$this,$pkey]); + return TRUE; + } + + /** + * Reset cursor + * @return NULL + **/ + function reset() { + $this->id=NULL; + $this->document=[]; + parent::reset(); + } + + /** + * Hydrate mapper object using hive array variable + * @return NULL + * @param $var array|string + * @param $func callback + **/ + function copyfrom($var,$func=NULL) { + if (is_string($var)) + $var=\Base::instance()->$var; + if ($func) + $var=call_user_func($func,$var); + foreach ($var as $key=>$val) + $this->set($key,$val); + } + + /** + * Populate hive array variable with mapper fields + * @return NULL + * @param $key string + **/ + function copyto($key) { + $var=&\Base::instance()->ref($key); + foreach ($this->document as $key=>$field) + $var[$key]=$field; + } + + /** + * Return field names + * @return array + **/ + function fields() { + return array_keys($this->document); + } + + /** + * Retrieve external iterator for fields + * @return object + **/ + function getiterator() { + return new \ArrayIterator($this->cast()); + } + + /** + * Instantiate class + * @return void + * @param $db object + * @param $file string + **/ + function __construct(\DB\Jig $db,$file) { + $this->db=$db; + $this->file=$file; + $this->reset(); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/jig/session.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/jig/session.php new file mode 100644 index 0000000..20f509b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/jig/session.php @@ -0,0 +1,207 @@ +. + +*/ + +namespace DB\Jig; + +use ReturnTypeWillChange; +use SessionAdapter; + +//! Jig-managed session handler +class Session extends Mapper { + + protected + //! Session ID + $sid, + //! Anti-CSRF token + $_csrf, + //! User agent + $_agent, + //! IP, + $_ip, + //! Suspect callback + $onsuspect; + + /** + * Open session + * @return TRUE + * @param $path string + * @param $name string + **/ + function open(string $path, string $name): bool + { + return TRUE; + } + + /** + * Close session + * @return TRUE + **/ + function close(): bool + { + $this->reset(); + $this->sid=NULL; + return TRUE; + } + + /** + * Return session data in serialized format + * @return string|false + * @param $id string + **/ + #[ReturnTypeWillChange] + function read($id) + { + $this->load(['@session_id=?',$this->sid=$id]); + if ($this->dry()) + return ''; + if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { + $fw=\Base::instance(); + if (!isset($this->onsuspect) || + $fw->call($this->onsuspect,[$this,$id])===FALSE) { + // NB: `session_destroy` can't be called at that stage; + // `session_start` not completed + $this->destroy($id); + $this->close(); + unset($fw->{'COOKIE.'.session_name()}); + $fw->error(403); + } + } + return $this->get('data'); + } + + /** + * Write session data + * @return TRUE + * @param $id string + * @param $data string + **/ + function write(string $id, string $data): bool + { + $this->set('session_id',$id); + $this->set('data',$data); + $this->set('ip',$this->_ip); + $this->set('agent',$this->_agent); + $this->set('stamp',time()); + $this->save(); + return TRUE; + } + + /** + * Destroy session + * @return TRUE + * @param $id string + **/ + function destroy($id): bool + { + $this->erase(['@session_id=?',$id]); + return TRUE; + } + + /** + * Garbage collector + **/ + #[ReturnTypeWillChange] + function gc(int $max_lifetime): int + { + return (int) $this->erase(['@stamp+?sid; + } + + /** + * Return anti-CSRF token + * @return string + **/ + function csrf() { + return $this->_csrf; + } + + /** + * Return IP address + * @return string + **/ + function ip() { + return $this->_ip; + } + + /** + * Return Unix timestamp + * @return string|FALSE + **/ + function stamp() { + if (!$this->sid) + session_start(); + return $this->dry()?FALSE:$this->get('stamp'); + } + + /** + * Return HTTP user agent + * @return string|FALSE + **/ + function agent() { + return $this->_agent; + } + + /** + * Instantiate class + * @param $db \DB\Jig + * @param $file string + * @param $onsuspect callback + * @param $key string + **/ + function __construct(\DB\Jig $db,$file='sessions',$onsuspect=NULL,$key=NULL) { + parent::__construct($db,$file); + $this->onsuspect=$onsuspect; + if (version_compare(PHP_VERSION, '8.4.0')>=0) { + // TODO: remove this when php7 support is dropped + session_set_save_handler(new SessionAdapter($this)); + } else { + session_set_save_handler( + [$this,'open'], + [$this,'close'], + [$this,'read'], + [$this,'write'], + [$this,'destroy'], + [$this,'gc'] + ); + } + register_shutdown_function('session_commit'); + $fw=\Base::instance(); + $headers=$fw->HEADERS; + $this->_csrf=$fw->hash($fw->SEED. + extension_loaded('openssl')? + implode(unpack('L',openssl_random_pseudo_bytes(4))): + mt_rand() + ); + if ($key) + $fw->$key=$this->_csrf; + $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; + $this->_ip=$fw->IP; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/mongo.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/mongo.php new file mode 100644 index 0000000..7b6ba92 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/mongo.php @@ -0,0 +1,145 @@ +. + +*/ + +namespace DB; + +//! MongoDB wrapper +class Mongo { + + //@{ + const + E_Profiler='MongoDB profiler is disabled'; + //@} + + protected + //! UUID + $uuid, + //! Data source name + $dsn, + //! MongoDB object + $db, + //! Legacy flag + $legacy, + //! MongoDB log + $log; + + /** + * Return data source name + * @return string + **/ + function dsn() { + return $this->dsn; + } + + /** + * Return UUID + * @return string + **/ + function uuid() { + return $this->uuid; + } + + /** + * Return MongoDB profiler results (or disable logging) + * @param $flag bool + * @return string + **/ + function log($flag=TRUE) { + if ($flag) { + $cursor=$this->db->selectcollection('system.profile')->find(); + foreach (iterator_to_array($cursor) as $frame) + if (!preg_match('/\.system\..+$/',$frame['ns'])) + $this->log.=date('r',$this->legacy() ? + $frame['ts']->sec : (round((string)$frame['ts'])/1000)). + ' ('.sprintf('%.1f',$frame['millis']).'ms) '. + $frame['ns'].' ['.$frame['op'].'] '. + (empty($frame['query'])? + '':json_encode($frame['query'])). + (empty($frame['command'])? + '':json_encode($frame['command'])). + PHP_EOL; + } else { + $this->log=FALSE; + if ($this->legacy) + $this->db->setprofilinglevel(-1); + else + $this->db->command(['profile'=>-1]); + } + return $this->log; + } + + /** + * Intercept native call to re-enable profiler + * @return int + **/ + function drop() { + $out=$this->db->drop(); + if ($this->log!==FALSE) { + if ($this->legacy) + $this->db->setprofilinglevel(2); + else + $this->db->command(['profile'=>2]); + } + return $out; + } + + /** + * Redirect call to MongoDB object + * @return mixed + * @param $func string + * @param $args array + **/ + function __call($func,array $args) { + return call_user_func_array([$this->db,$func],$args); + } + + /** + * Return TRUE if legacy driver is loaded + * @return bool + **/ + function legacy() { + return $this->legacy; + } + + //! Prohibit cloning + private function __clone() { + } + + /** + * Instantiate class + * @param $dsn string + * @param $dbname string + * @param $options array + **/ + function __construct($dsn,$dbname,?array $options=NULL) { + $this->uuid=\Base::instance()->hash($this->dsn=$dsn); + if ($this->legacy=class_exists('\MongoClient')) { + $this->db=new \MongoDB(new \MongoClient($dsn,$options?:[]),$dbname); + $this->db->setprofilinglevel(2); + } + else { + $this->db=(new \MongoDB\Client($dsn,$options?:[]))->$dbname; + $this->db->command(['profile'=>2]); + } + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/mongo/mapper.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/mongo/mapper.php new file mode 100644 index 0000000..d02093d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/mongo/mapper.php @@ -0,0 +1,405 @@ +. + +*/ + +namespace DB\Mongo; + +//! MongoDB mapper +class Mapper extends \DB\Cursor { + + protected + //! MongoDB wrapper + $db, + //! Legacy flag + $legacy, + //! Mongo collection + $collection, + //! Mongo document + $document=[], + //! Mongo cursor + $cursor, + //! Defined fields + $fields; + + /** + * Return database type + * @return string + **/ + function dbtype() { + return 'Mongo'; + } + + /** + * Return TRUE if field is defined + * @return bool + * @param $key string + **/ + function exists($key) { + return array_key_exists($key,$this->document); + } + + /** + * Assign value to field + * @return scalar|FALSE + * @param $key string + * @param $val scalar + **/ + function set($key,$val) { + return $this->document[$key]=$val; + } + + /** + * Retrieve value of field + * @return scalar|FALSE + * @param $key string + **/ + function &get($key) { + if ($this->exists($key)) + return $this->document[$key]; + throw new \Exception(sprintf(self::E_Field,$key)); + } + + /** + * Delete field + * @return NULL + * @param $key string + **/ + function clear($key) { + unset($this->document[$key]); + } + + /** + * Convert array to mapper object + * @return static + * @param $row array + **/ + function factory($row) { + $mapper=clone($this); + $mapper->reset(); + foreach ($row as $key=>$val) + $mapper->document[$key]=$val; + $mapper->query=[clone($mapper)]; + if (isset($mapper->trigger['load'])) + \Base::instance()->call($mapper->trigger['load'],$mapper); + return $mapper; + } + + /** + * Return fields of mapper object as an associative array + * @return array + * @param $obj object + **/ + function cast($obj=NULL) { + if (!$obj) + $obj=$this; + return $obj->document; + } + + /** + * Build query and execute + * @return static[] + * @param $fields string + * @param $filter array + * @param $options array + * @param $ttl int|array + **/ + function select($fields=NULL,$filter=NULL,?array $options=NULL,$ttl=0) { + if (!$options) + $options=[]; + $options+=[ + 'group'=>NULL, + 'order'=>NULL, + 'limit'=>0, + 'offset'=>0 + ]; + $tag=''; + if (is_array($ttl)) + list($ttl,$tag)=$ttl; + $fw=\Base::instance(); + $cache=\Cache::instance(); + if (!($cached=$cache->exists($hash=$fw->hash($this->db->dsn(). + $fw->stringify([$fields,$filter,$options])).($tag?'.'.$tag:'').'.mongo', + $result)) || !$ttl || $cached[0]+$ttlcollection->group( + $options['group']['keys'], + $options['group']['initial'], + $options['group']['reduce'], + [ + 'condition'=>$filter, + 'finalize'=>$options['group']['finalize'] + ] + ); + $tmp=$this->db->selectcollection( + $fw->HOST.'.'.$fw->BASE.'.'. + uniqid('',TRUE).'.tmp' + ); + $tmp->batchinsert($grp['retval'],['w'=>1]); + $filter=[]; + $collection=$tmp; + } + else { + $filter=$filter?:[]; + $collection=$this->collection; + } + if ($this->legacy) { + $this->cursor=$collection->find($filter,$fields?:[]); + if ($options['order']) + $this->cursor=$this->cursor->sort($options['order']); + if ($options['limit']) + $this->cursor=$this->cursor->limit($options['limit']); + if ($options['offset']) + $this->cursor=$this->cursor->skip($options['offset']); + $result=[]; + while ($this->cursor->hasnext()) + $result[]=$this->cursor->getnext(); + } + else { + $this->cursor=$collection->find($filter,[ + 'sort'=>$options['order'], + 'limit'=>$options['limit'], + 'skip'=>$options['offset'] + ]); + $result=$this->cursor->toarray(); + } + if ($options['group']) + $tmp->drop(); + if ($fw->CACHE && $ttl) + // Save to cache backend + $cache->set($hash,$result,$ttl); + } + $out=[]; + foreach ($result as $doc) + $out[]=$this->factory($doc); + return $out; + } + + /** + * Return records that match criteria + * @return static[] + * @param $filter array + * @param $options array + * @param $ttl int|array + **/ + function find($filter=NULL,?array $options=NULL,$ttl=0) { + if (!$options) + $options=[]; + $options+=[ + 'group'=>NULL, + 'order'=>NULL, + 'limit'=>0, + 'offset'=>0 + ]; + return $this->select($this->fields,$filter,$options,$ttl); + } + + /** + * Count records that match criteria + * @return int + * @param $filter array + * @param $options array + * @param $ttl int|array + **/ + function count($filter=NULL,?array $options=NULL,$ttl=0) { + $fw=\Base::instance(); + $cache=\Cache::instance(); + $tag=''; + if (is_array($ttl)) + list($ttl,$tag)=$ttl; + if (!($cached=$cache->exists($hash=$fw->hash($fw->stringify( + [$filter])).($tag?'.'.$tag:'').'.mongo',$result)) || !$ttl || + $cached[0]+$ttlcollection->count($filter?:[]); + if ($fw->CACHE && $ttl) + // Save to cache backend + $cache->set($hash,$result,$ttl); + } + return $result; + } + + /** + * Return record at specified offset using criteria of previous + * load() call and make it active + * @return array + * @param $ofs int + **/ + function skip($ofs=1) { + $this->document=($out=parent::skip($ofs))?$out->document:[]; + if ($this->document && isset($this->trigger['load'])) + \Base::instance()->call($this->trigger['load'],$this); + return $out; + } + + /** + * Insert new record + * @return array + **/ + function insert() { + if (isset($this->document['_id'])) + return $this->update(); + if (isset($this->trigger['beforeinsert']) && + \Base::instance()->call($this->trigger['beforeinsert'], + [$this,['_id'=>$this->document['_id']]])===FALSE) + return $this->document; + if ($this->legacy) { + $this->collection->insert($this->document); + $pkey=['_id'=>$this->document['_id']]; + } + else { + $result=$this->collection->insertone($this->document); + $pkey=['_id'=>$result->getinsertedid()]; + } + if (isset($this->trigger['afterinsert'])) + \Base::instance()->call($this->trigger['afterinsert'], + [$this,$pkey]); + $this->load($pkey); + return $this->document; + } + + /** + * Update current record + * @return array + **/ + function update() { + $pkey=['_id'=>$this->document['_id']]; + if (isset($this->trigger['beforeupdate']) && + \Base::instance()->call($this->trigger['beforeupdate'], + [$this,$pkey])===FALSE) + return $this->document; + $upsert=['upsert'=>TRUE]; + if ($this->legacy) + $this->collection->update($pkey,$this->document,$upsert); + else + $this->collection->replaceone($pkey,$this->document,$upsert); + if (isset($this->trigger['afterupdate'])) + \Base::instance()->call($this->trigger['afterupdate'], + [$this,$pkey]); + return $this->document; + } + + /** + * Delete current record + * @return bool + * @param $quick bool + * @param $filter array + **/ + function erase($filter=NULL,$quick=TRUE) { + if ($filter) { + if (!$quick) { + foreach ($this->find($filter) as $mapper) + if (!$mapper->erase()) + return FALSE; + return TRUE; + } + return $this->legacy? + $this->collection->remove($filter): + $this->collection->deletemany($filter); + } + $pkey=['_id'=>$this->document['_id']]; + if (isset($this->trigger['beforeerase']) && + \Base::instance()->call($this->trigger['beforeerase'], + [$this,$pkey])===FALSE) + return FALSE; + $result=$this->legacy? + $this->collection->remove(['_id'=>$this->document['_id']]): + $this->collection->deleteone(['_id'=>$this->document['_id']]); + parent::erase(); + if (isset($this->trigger['aftererase'])) + \Base::instance()->call($this->trigger['aftererase'], + [$this,$pkey]); + return $result; + } + + /** + * Reset cursor + * @return NULL + **/ + function reset() { + $this->document=[]; + parent::reset(); + } + + /** + * Hydrate mapper object using hive array variable + * @return NULL + * @param $var array|string + * @param $func callback + **/ + function copyfrom($var,$func=NULL) { + if (is_string($var)) + $var=\Base::instance()->$var; + if ($func) + $var=call_user_func($func,$var); + foreach ($var as $key=>$val) + $this->set($key,$val); + } + + /** + * Populate hive array variable with mapper fields + * @return NULL + * @param $key string + **/ + function copyto($key) { + $var=&\Base::instance()->ref($key); + foreach ($this->document as $key=>$field) + $var[$key]=$field; + } + + /** + * Return field names + * @return array + **/ + function fields() { + return array_keys($this->document); + } + + /** + * Return the cursor from last query + * @return object|NULL + **/ + function cursor() { + return $this->cursor; + } + + /** + * Retrieve external iterator for fields + * @return object + **/ + function getiterator() { + return new \ArrayIterator($this->cast()); + } + + /** + * Instantiate class + * @return void + * @param $db object + * @param $collection string + * @param $fields array + **/ + function __construct(\DB\Mongo $db,$collection,$fields=NULL) { + $this->db=$db; + $this->legacy=$db->legacy(); + $this->collection=$db->selectcollection($collection); + $this->fields=$fields; + $this->reset(); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/mongo/session.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/mongo/session.php new file mode 100644 index 0000000..f41f171 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/mongo/session.php @@ -0,0 +1,207 @@ +. + +*/ + +namespace DB\Mongo; + +use ReturnTypeWillChange; +use SessionAdapter; + +//! MongoDB-managed session handler +class Session extends Mapper { + + protected + //! Session ID + $sid, + //! Anti-CSRF token + $_csrf, + //! User agent + $_agent, + //! IP, + $_ip, + //! Suspect callback + $onsuspect; + + /** + * Open session + * @return TRUE + * @param $path string + * @param $name string + **/ + function open(string $path, string $name): bool + { + return TRUE; + } + + /** + * Close session + * @return TRUE + **/ + function close(): bool + { + $this->reset(); + $this->sid=NULL; + return TRUE; + } + + /** + * Return session data in serialized format + * @return string + * @param $id string + **/ + #[ReturnTypeWillChange] + function read(string $id) + { + $this->load(['session_id'=>$this->sid=$id]); + if ($this->dry()) + return ''; + if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { + $fw=\Base::instance(); + if (!isset($this->onsuspect) || + $fw->call($this->onsuspect,[$this,$id])===FALSE) { + // NB: `session_destroy` can't be called at that stage; + // `session_start` not completed + $this->destroy($id); + $this->close(); + unset($fw->{'COOKIE.'.session_name()}); + $fw->error(403); + } + } + return $this->get('data'); + } + + /** + * Write session data + * @return TRUE + * @param $id string + * @param $data string + **/ + function write(string $id, string $data): bool + { + $this->set('session_id',$id); + $this->set('data',$data); + $this->set('ip',$this->_ip); + $this->set('agent',$this->_agent); + $this->set('stamp',time()); + $this->save(); + return TRUE; + } + + /** + * Destroy session + * @return TRUE + * @param $id string + **/ + function destroy($id): bool + { + $this->erase(['session_id'=>$id]); + return TRUE; + } + + /** + * Garbage collector + **/ + #[ReturnTypeWillChange] + function gc(int $max_lifetime): int + { + return (int) $this->erase(['$where'=>'this.stamp+'.$max_lifetime.'<'.time()]); + } + + /** + * Return session id (if session has started) + * @return string|NULL + **/ + function sid() { + return $this->sid; + } + + /** + * Return anti-CSRF token + * @return string + **/ + function csrf() { + return $this->_csrf; + } + + /** + * Return IP address + * @return string + **/ + function ip() { + return $this->_ip; + } + + /** + * Return Unix timestamp + * @return string|FALSE + **/ + function stamp() { + if (!$this->sid) + session_start(); + return $this->dry()?FALSE:$this->get('stamp'); + } + + /** + * Return HTTP user agent + * @return string + **/ + function agent() { + return $this->_agent; + } + + /** + * Instantiate class + * @param $db \DB\Mongo + * @param $table string + * @param $onsuspect callback + * @param $key string + **/ + function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL,$key=NULL) { + parent::__construct($db,$table); + $this->onsuspect=$onsuspect; + if (version_compare(PHP_VERSION, '8.4.0')>=0) { + // TODO: remove this when php7 support is dropped + session_set_save_handler(new SessionAdapter($this)); + } else { + session_set_save_handler( + [$this,'open'], + [$this,'close'], + [$this,'read'], + [$this,'write'], + [$this,'destroy'], + [$this,'gc'] + ); + } + register_shutdown_function('session_commit'); + $fw=\Base::instance(); + $headers=$fw->HEADERS; + $this->_csrf=$fw->hash($fw->SEED. + extension_loaded('openssl')? + implode(unpack('L',openssl_random_pseudo_bytes(4))): + mt_rand() + ); + if ($key) + $fw->$key=$this->_csrf; + $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; + $this->_ip=$fw->IP; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/sql.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/sql.php new file mode 100644 index 0000000..fb89e2c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/sql.php @@ -0,0 +1,552 @@ +. + +*/ + +namespace DB; + +//! PDO wrapper +class SQL { + + //@{ Error messages + const + E_PKey='Table %s does not have a primary key'; + //@} + + const + PARAM_FLOAT='float'; + + protected + //! UUID + $uuid, + //! Raw PDO + $pdo, + //! Data source name + $dsn, + //! Database engine + $engine, + //! Database name + $dbname, + //! Transaction flag + $trans=FALSE, + //! Number of rows affected by query + $rows=0, + //! SQL log + $log; + + /** + * Begin SQL transaction + * @return bool + **/ + function begin() { + $out=$this->pdo->begintransaction(); + $this->trans=TRUE; + return $out; + } + + /** + * Rollback SQL transaction + * @return bool + **/ + function rollback() { + $out=FALSE; + if ($this->pdo->inTransaction()) + $out=$this->pdo->rollback(); + $this->trans=FALSE; + return $out; + } + + /** + * Commit SQL transaction + * @return bool + **/ + function commit() { + $out=FALSE; + if ($this->pdo->inTransaction()) + $out=$this->pdo->commit(); + $this->trans=FALSE; + return $out; + } + + /** + * Return transaction flag + * @return bool + **/ + function trans() { + return $this->trans; + } + + /** + * Map data type of argument to a PDO constant + * @return int + * @param $val scalar + **/ + function type($val) { + switch (gettype($val)) { + case 'NULL': + return \PDO::PARAM_NULL; + case 'boolean': + return \PDO::PARAM_BOOL; + case 'integer': + return \PDO::PARAM_INT; + case 'resource': + return \PDO::PARAM_LOB; + case 'float': + return self::PARAM_FLOAT; + default: + return \PDO::PARAM_STR; + } + } + + /** + * Cast value to PHP type + * @return mixed + * @param $type string + * @param $val mixed + **/ + function value($type,$val) { + switch ($type) { + case self::PARAM_FLOAT: + if (!is_string($val) && $val !== NULL) + $val=str_replace(',','.',(string) $val); + return $val; + case \PDO::PARAM_NULL: + return NULL; + case \PDO::PARAM_INT: + return (int)$val; + case \PDO::PARAM_BOOL: + return (bool)$val; + case \PDO::PARAM_STR: + return (string)$val; + case \PDO::PARAM_LOB: + return (binary)$val; + } + } + + /** + * Execute SQL statement(s) + * @return array|int|FALSE + * @param $cmds string|array + * @param $args string|array + * @param $ttl int|array + * @param $log bool + * @param $stamp bool + **/ + function exec($cmds,$args=NULL,$ttl=0,$log=TRUE,$stamp=FALSE) { + $tag=''; + if (is_array($ttl)) + list($ttl,$tag)=$ttl; + $auto=FALSE; + if (is_null($args)) + $args=[]; + elseif (is_scalar($args)) + $args=[1=>$args]; + if (is_array($cmds)) { + if (count($args)<($count=count($cmds))) + // Apply arguments to SQL commands + $args=array_fill(0,$count,$args); + if (!$this->trans) { + $this->begin(); + $auto=TRUE; + } + } + else { + $count=1; + $cmds=[$cmds]; + $args=[$args]; + } + if ($this->log===FALSE) + $log=FALSE; + $fw=\Base::instance(); + $cache=\Cache::instance(); + $result=FALSE; + for ($i=0;$i<$count;++$i) { + $cmd=$cmds[$i]; + $arg=$args[$i]; + // ensure 1-based arguments + if (array_key_exists(0,$arg)) { + array_unshift($arg,''); + unset($arg[0]); + } + if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd)) + continue; + $now=microtime(TRUE); + $keys=$vals=[]; + if ($fw->CACHE && $ttl && ($cached=$cache->exists( + $hash=$fw->hash($this->dsn.$cmd. + $fw->stringify($arg)).($tag?'.'.$tag:'').'.sql',$result)) && + $cached[0]+$ttl>microtime(TRUE)) { + foreach ($arg as $key=>$val) { + $vals[]=$fw->stringify(is_array($val)?$val[0]:$val); + $keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). + '/'; + } + if ($log) + $this->log.=($stamp?(date('r').' '):'').'('. + sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. + '[CACHED] '. + preg_replace($keys,$vals, + str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; + } + elseif (is_object($query=$this->pdo->prepare($cmd))) { + foreach ($arg as $key=>$val) { + if (is_array($val)) { + // User-specified data type + $query->bindvalue($key,$val[0], + $val[1]==self::PARAM_FLOAT?\PDO::PARAM_STR:$val[1]); + $vals[]=$fw->stringify($this->value($val[1],$val[0])); + } + else { + // Convert to PDO data type + $query->bindvalue($key,$val, + ($type=$this->type($val))==self::PARAM_FLOAT? + \PDO::PARAM_STR:$type); + $vals[]=$fw->stringify($this->value($type,$val)); + } + $keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). + '/'; + } + if ($log) + $this->log.=($stamp?(date('r').' '):'').'(-0ms) '. + preg_replace($keys,$vals, + str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; + $query->execute(); + if ($log) + $this->log=str_replace('(-0ms)', + '('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms)', + $this->log); + if (($error=$query->errorinfo()) && $error[0]!=\PDO::ERR_NONE) { + // Statement-level error occurred + if ($this->trans) + $this->rollback(); + throw new \Exception('PDOStatement: '.$error[2]); + } + if (preg_match('/(?:^[\s\(]*'. + '(?:WITH|EXPLAIN|SELECT|PRAGMA|SHOW)|RETURNING)\b/is',$cmd) || + (preg_match('/^\s*(?:CALL|EXEC)\b/is',$cmd) && + $query->columnCount())) { + $result=$query->fetchall(\PDO::FETCH_ASSOC); + // Work around SQLite quote bug + if (preg_match('/sqlite2?/',$this->engine)) + foreach ($result as $pos=>$rec) { + unset($result[$pos]); + $result[$pos]=[]; + foreach ($rec as $key=>$val) + $result[$pos][trim($key,'\'"[]`')]=$val; + } + $this->rows=count($result); + if ($fw->CACHE && $ttl) + // Save to cache backend + $cache->set($hash,$result,$ttl); + } + else + $this->rows=$result=$query->rowcount(); + $query->closecursor(); + unset($query); + } + elseif (($error=$this->pdo->errorInfo()) && $error[0]!=\PDO::ERR_NONE) { + // PDO-level error occurred + if ($this->trans) + $this->rollback(); + throw new \Exception('PDO: '.$error[2]); + } + + } + if ($this->trans && $auto) + $this->commit(); + return $result; + } + + /** + * Return number of rows affected by last query + * @return int + **/ + function count() { + return $this->rows; + } + + /** + * Return SQL profiler results (or disable logging) + * @return string + * @param $flag bool + **/ + function log($flag=TRUE) { + if ($flag) + return $this->log; + $this->log=FALSE; + } + + /** + * Return TRUE if table exists + * @return bool + * @param $table string + **/ + function exists($table) { + $mode=$this->pdo->getAttribute(\PDO::ATTR_ERRMODE); + $this->pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_SILENT); + $out=$this->pdo-> + query('SELECT 1 FROM '.$this->quotekey($table).' LIMIT 1'); + $this->pdo->setAttribute(\PDO::ATTR_ERRMODE,$mode); + return is_object($out); + } + + /** + * Retrieve schema of SQL table + * @return array|FALSE + * @param $table string + * @param $fields array|string + * @param $ttl int|array + **/ + function schema($table,$fields=NULL,$ttl=0) { + $fw=\Base::instance(); + $cache=\Cache::instance(); + if ($fw->CACHE && $ttl && + ($cached=$cache->exists( + $hash=$fw->hash($this->dsn.$table.(is_array($fields) ? implode(',',$fields) : $fields)).'.schema',$result)) && + $cached[0]+$ttl>microtime(TRUE)) + return $result; + if (strpos($table,'.')) + list($schema,$table)=explode('.',$table); + // Supported engines + // format: engine_name => array of: + // 0: query + // 1: field name of column name + // 2: field name of column type + // 3: field name of default value + // 4: field name of nullable value + // 5: expected field value to be nullable + // 6: field name of primary key flag + // 7: expected field value to be a primary key + // 8: field name of auto increment check (optional) + // 9: expected field value to be an auto-incremented identifier + $cmd=[ + 'sqlite2?'=>[ + 'SELECT * FROM pragma_table_info('.$this->quote($table).') JOIN ('. + 'SELECT sql FROM sqlite_master WHERE (type=\'table\' OR type=\'view\') AND '. + 'name='.$this->quote($table).')', + 'name','type','dflt_value','notnull',0,'pk',TRUE,'sql', + '/\W(%s)\W+[^,]+?AUTOINCREMENT\W/i'], + 'mysql'=>[ + 'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`', + 'Field','Type','Default','Null','YES','Key','PRI','Extra','auto_increment'], + 'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>[ + 'SELECT '. + 'C.COLUMN_NAME AS field,'. + 'C.DATA_TYPE AS type,'. + 'C.COLUMN_DEFAULT AS defval,'. + 'C.IS_NULLABLE AS nullable,'. + ($this->engine=='pgsql' + ?'COALESCE(POSITION(\'nextval\' IN C.COLUMN_DEFAULT),0) AS autoinc,' + :'columnproperty(object_id(C.TABLE_NAME),C.COLUMN_NAME,\'IsIdentity\')' + .' AS autoinc,'). + 'T.CONSTRAINT_TYPE AS pkey '. + 'FROM INFORMATION_SCHEMA.COLUMNS AS C '. + 'LEFT OUTER JOIN '. + 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS K '. + 'ON '. + 'C.TABLE_NAME=K.TABLE_NAME AND '. + 'C.COLUMN_NAME=K.COLUMN_NAME AND '. + 'C.TABLE_SCHEMA=K.TABLE_SCHEMA '. + ($this->dbname? + ('AND C.TABLE_CATALOG=K.TABLE_CATALOG '):''). + 'LEFT OUTER JOIN '. + 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS T ON '. + 'K.TABLE_NAME=T.TABLE_NAME AND '. + 'K.CONSTRAINT_NAME=T.CONSTRAINT_NAME AND '. + 'K.TABLE_SCHEMA=T.TABLE_SCHEMA '. + ($this->dbname? + ('AND K.TABLE_CATALOG=T.TABLE_CATALOG '):''). + 'WHERE '. + 'C.TABLE_NAME='.$this->quote($table). + (empty($schema) ? '' : ' AND C.TABLE_SCHEMA='.$this->quote($schema)). + ($this->dbname? + (' AND C.TABLE_CATALOG='. + $this->quote($this->dbname)):''), + 'field','type','defval','nullable','YES','pkey','PRIMARY KEY','autoinc',1], + 'oci'=>[ + 'SELECT c.column_name AS field, '. + 'c.data_type AS type, '. + 'c.data_default AS defval, '. + 'c.nullable AS nullable, '. + '(SELECT t.constraint_type '. + 'FROM all_cons_columns acc '. + 'LEFT OUTER JOIN all_constraints t '. + 'ON acc.constraint_name=t.constraint_name '. + 'WHERE acc.table_name='.$this->quote($table).' '. + 'AND acc.column_name=c.column_name '. + 'AND constraint_type='.$this->quote('P').') AS pkey '. + 'FROM all_tab_cols c '. + 'WHERE c.table_name='.$this->quote($table), + 'FIELD','TYPE','DEFVAL','NULLABLE','Y','PKEY','P'] + ]; + if (is_string($fields)) + $fields=\Base::instance()->split($fields); + $conv=[ + 'int\b|integer'=>\PDO::PARAM_INT, + 'bool'=>\PDO::PARAM_BOOL, + 'blob|bytea|image|binary'=>\PDO::PARAM_LOB, + 'float|real|double|decimal|numeric'=>self::PARAM_FLOAT, + '.+'=>\PDO::PARAM_STR + ]; + foreach ($cmd as $key=>$val) + if (preg_match('/'.$key.'/',$this->engine)) { + $rows=[]; + foreach ($this->exec($val[0],NULL) as $row) + if (!$fields || in_array($row[$val[1]],$fields)) { + foreach ($conv as $regex=>$type) + if (preg_match('/'.$regex.'/i',$row[$val[2]])) + break; + if (!isset($rows[$row[$val[1]]])) // handle duplicate rows in PgSQL + $rows[$row[$val[1]]]=[ + 'type'=>$row[$val[2]], + 'pdo_type'=>$type, + 'default'=>is_string($row[$val[3]])? + preg_replace('/^\s*([\'"])(.*)\1\s*/','\2', + $row[$val[3]]):$row[$val[3]], + 'nullable'=>$row[$val[4]]==$val[5], + 'pkey'=>$row[$val[6]]==$val[7], + 'auto_inc'=>isset($val[8]) && isset($row[$val[8]]) + ? ($this->engine=='sqlite'? + (bool) preg_match(sprintf($val[9],$row[$val[1]]), + $row[$val[8]]): + ($row[$val[8]]==$val[9]) + ) : NULL, + ]; + } + if ($fw->CACHE && $ttl) + // Save to cache backend + $cache->set($hash,$rows,$ttl); + return $rows; + } + throw new \Exception(sprintf(self::E_PKey,$table)); + } + + /** + * Quote string + * @return string + * @param $val mixed + * @param $type int + **/ + function quote($val,$type=\PDO::PARAM_STR) { + return $this->engine=='odbc'? + (is_string($val)? + \Base::instance()->stringify(str_replace('\'','\'\'',$val)): + $val): + $this->pdo->quote($val,$type); + } + + /** + * Return UUID + * @return string + **/ + function uuid() { + return $this->uuid; + } + + /** + * Return parent object + * @return \PDO + **/ + function pdo() { + return $this->pdo; + } + + /** + * Return database engine + * @return string + **/ + function driver() { + return $this->engine; + } + + /** + * Return server version + * @return string + **/ + function version() { + return $this->pdo->getattribute(\PDO::ATTR_SERVER_VERSION); + } + + /** + * Return database name + * @return string + **/ + function name() { + return $this->dbname; + } + + /** + * Return quoted identifier name + * @return string + * @param $key + * @param bool $split + **/ + function quotekey($key, $split=TRUE) { + $delims=[ + 'sqlite2?|mysql'=>'``', + 'pgsql|oci'=>'""', + 'mssql|sqlsrv|odbc|sybase|dblib'=>'[]' + ]; + $use=''; + foreach ($delims as $engine=>$delim) + if (preg_match('/'.$engine.'/',$this->engine)) { + $use=$delim; + break; + } + return $use[0].($split ? implode($use[1].'.'.$use[0],explode('.',$key)) + : $key).$use[1]; + } + + /** + * Redirect call to PDO object + * @return mixed + * @param $func string + * @param $args array + **/ + function __call($func,array $args) { + return call_user_func_array([$this->pdo,$func],$args); + } + + //! Prohibit cloning + private function __clone() { + } + + /** + * Instantiate class + * @param $dsn string + * @param $user string + * @param $pw string + * @param $options array + **/ + function __construct($dsn,$user=NULL,$pw=NULL,?array $options=NULL) { + $fw=\Base::instance(); + $this->uuid=$fw->hash($this->dsn=$dsn); + if (preg_match('/^.+?(?:dbname|database)=(.+?)(?=;|$)/is',$dsn,$parts)) + $this->dbname=str_replace('\\ ',' ',$parts[1]); + if (!$options) + $options=[]; + if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql') + $options+=[\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '. + strtolower(str_replace('-','',$fw->ENCODING)).';']; + $this->pdo=new \PDO($dsn,$user,$pw,$options); + $this->engine=$this->pdo->getattribute(\PDO::ATTR_DRIVER_NAME); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/sql/mapper.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/sql/mapper.php new file mode 100644 index 0000000..93f6158 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/sql/mapper.php @@ -0,0 +1,767 @@ +. + +*/ + +namespace DB\SQL; + +//! SQL data mapper +class Mapper extends \DB\Cursor { + + //@{ Error messages + const + E_PKey='Table %s does not have a primary key'; + //@} + + protected + //! PDO wrapper + $db, + //! Database engine + $engine, + //! SQL table + $source, + //! SQL table (quoted) + $table, + //! Alias for SQL table + $as, + //! Last insert ID + $_id, + //! Defined fields + $fields, + //! Adhoc fields + $adhoc=[], + //! Dynamic properties + $props=[]; + + /** + * Return database type + * @return string + **/ + function dbtype() { + return 'SQL'; + } + + /** + * Return mapped table + * @return string + **/ + function table() { + return $this->source; + } + + /** + * Return TRUE if any/specified field value has changed + * @return bool + * @param $key string + **/ + function changed($key=NULL) { + if (isset($key)) + return $this->fields[$key]['changed']; + foreach($this->fields as $key=>$field) + if ($field['changed']) + return TRUE; + return FALSE; + } + + /** + * Return TRUE if field is defined + * @return bool + * @param $key string + **/ + function exists($key) { + return array_key_exists($key,$this->fields+$this->adhoc); + } + + /** + * Assign value to field + * @return scalar + * @param $key string + * @param $val scalar + **/ + function set($key,$val) { + if (array_key_exists($key,$this->fields)) { + $val=is_null($val) && $this->fields[$key]['nullable']? + NULL:$this->db->value($this->fields[$key]['pdo_type'],$val); + if ($this->fields[$key]['initial']!==$val || + $this->fields[$key]['default']!==$val && is_null($val)) + $this->fields[$key]['changed']=TRUE; + return $this->fields[$key]['value']=$val; + } + // Adjust result on existing expressions + if (isset($this->adhoc[$key])) + $this->adhoc[$key]['value']=$val; + elseif (is_string($val)) + // Parenthesize expression in case it's a subquery + $this->adhoc[$key]=['expr'=>'('.$val.')','value'=>NULL]; + else + $this->props[$key]=$val; + return $val; + } + + /** + * Retrieve value of field + * @return scalar + * @param $key string + **/ + function &get($key) { + if ($key=='_id') + return $this->_id; + elseif (array_key_exists($key,$this->fields)) + return $this->fields[$key]['value']; + elseif (array_key_exists($key,$this->adhoc)) + return $this->adhoc[$key]['value']; + elseif (array_key_exists($key,$this->props)) + return $this->props[$key]; + throw new \Exception(sprintf(self::E_Field,$key)); + } + + /** + * Clear value of field + * @return NULL + * @param $key string + **/ + function clear($key) { + if (array_key_exists($key,$this->adhoc)) + unset($this->adhoc[$key]); + else + unset($this->props[$key]); + } + + /** + * Invoke dynamic method + * @return mixed + * @param $func string + * @param $args array + * @deprecated (this is only used for custom dynamic properties that are callables + **/ + function __call($func,$args) { + $callable = (array_key_exists($func,$this->props) ? $this->props[$func] : $this->$func); + return $callable ? call_user_func_array($callable,$args) : null; + } + + /** + * Convert array to mapper object + * @return static + * @param $row array + **/ + function factory($row) { + $mapper=clone($this); + $mapper->reset(); + foreach ($row as $key=>$val) { + if (array_key_exists($key,$this->fields)) + $var='fields'; + elseif (array_key_exists($key,$this->adhoc)) + $var='adhoc'; + else + continue; + $mapper->{$var}[$key]['value']=$val; + $mapper->{$var}[$key]['initial']=$val; + if ($var=='fields' && $mapper->{$var}[$key]['pkey']) + $mapper->{$var}[$key]['previous']=$val; + } + $mapper->query=[clone($mapper)]; + if (isset($mapper->trigger['load'])) + \Base::instance()->call($mapper->trigger['load'],$mapper); + return $mapper; + } + + /** + * Return fields of mapper object as an associative array + * @return array + * @param $obj object + **/ + function cast($obj=NULL) { + if (!$obj) + $obj=$this; + return array_map( + function($row) { + return $row['value']; + }, + $obj->fields+$obj->adhoc + ); + } + + /** + * Build query string and arguments + * @return array + * @param $fields string + * @param $filter string|array + * @param $options array + **/ + function stringify($fields,$filter=NULL,?array $options=NULL) { + if (!$options) + $options=[]; + $options+=[ + 'group'=>NULL, + 'order'=>NULL, + 'limit'=>0, + 'offset'=>0, + 'comment'=>NULL + ]; + $db=$this->db; + $sql='SELECT '.$fields.' FROM '.$this->table; + if (isset($this->as)) + $sql.=' AS '.$this->db->quotekey($this->as); + $args=[]; + if (is_array($filter) && !empty($filter)) { + $args=isset($filter[1]) && is_array($filter[1])? + $filter[1]: + array_slice($filter,1,NULL,TRUE); + $args=is_array($args)?$args:[1=>$args]; + list($filter)=$filter; + } + if ($filter) + $sql.=' WHERE '.$filter; + if ($options['group']) { + $sql.=' GROUP BY '.implode(',',array_map( + function($str) use($db) { + return preg_replace_callback( + '/\b(\w+[._\-\w]*)\h*(HAVING.+|$)/i', + function($parts) use($db) { + return $db->quotekey($parts[1]). + (isset($parts[2])?(' '.$parts[2]):''); + }, + $str + ); + }, + explode(',',$options['group']))); + } + if ($options['order']) { + $char=substr($db->quotekey(''),0,1);// quoting char + $order=' ORDER BY '.(is_bool(strpos($options['order'],$char))? + implode(',',array_map(function($str) use($db) { + return preg_match('/^\h*(\w+[._\-\w]*)'. + '(?:\h+((?:ASC|DESC)[\w\h]*))?\h*$/i', + $str,$parts)? + ($db->quotekey($parts[1]). + (isset($parts[2])?(' '.$parts[2]):'')):$str; + },explode(',',$options['order']))): + $options['order']); + } + // SQL Server fixes + if (preg_match('/mssql|sqlsrv|odbc/', $this->engine) && + ($options['limit'] || $options['offset'])) { + // order by pkey when no ordering option was given + if (!$options['order']) + foreach ($this->fields as $key=>$field) + if ($field['pkey']) { + $order=' ORDER BY '.$db->quotekey($key); + break; + } + $ofs=$options['offset']?(int)$options['offset']:0; + $lmt=$options['limit']?(int)$options['limit']:0; + if (strncmp($db->version(),'11',2)>=0) { + // SQL Server >= 2012 + $sql.=$order.' OFFSET '.$ofs.' ROWS'; + if ($lmt) + $sql.=' FETCH NEXT '.$lmt.' ROWS ONLY'; + } + else { + // SQL Server 2008 + $sql=preg_replace('/SELECT/', + 'SELECT '. + ($lmt>0?'TOP '.($ofs+$lmt):'').' ROW_NUMBER() '. + 'OVER ('.$order.') AS rnum,',$sql.$order,1); + $sql='SELECT * FROM ('.$sql.') x WHERE rnum > '.($ofs); + } + } + else { + if (isset($order)) + $sql.=$order; + if ($options['limit']) + $sql.=' LIMIT '.(int)$options['limit']; + if ($options['offset']) + $sql.=' OFFSET '.(int)$options['offset']; + } + if ($options['comment']) + $sql.="\n".' /* '.$options['comment'].' */'; + return [$sql,$args]; + } + + /** + * Build query string and execute + * @return static[] + * @param $fields string + * @param $filter string|array + * @param $options array + * @param $ttl int|array + **/ + function select($fields,$filter=NULL,?array $options=NULL,$ttl=0) { + list($sql,$args)=$this->stringify($fields,$filter,$options); + $result=$this->db->exec($sql,$args,$ttl); + $out=[]; + foreach ($result as &$row) { + foreach ($row as $field=>&$val) { + if (array_key_exists($field,$this->fields)) { + if (!is_null($val) || !$this->fields[$field]['nullable']) + $val=$this->db->value( + $this->fields[$field]['pdo_type'],$val); + } + unset($val); + } + $out[]=$this->factory($row); + unset($row); + } + return $out; + } + + /** + * Return records that match criteria + * @return static[] + * @param $filter string|array + * @param $options array + * @param $ttl int|array + **/ + function find($filter=NULL,?array $options=NULL,$ttl=0) { + if (!$options) + $options=[]; + $options+=[ + 'group'=>NULL, + 'order'=>NULL, + 'limit'=>0, + 'offset'=>0 + ]; + $adhoc=''; + foreach ($this->adhoc as $key=>$field) + $adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key); + return $this->select( + ($options['group'] && !preg_match('/mysql|sqlite/',$this->engine)? + $options['group']: + implode(',',array_map([$this->db,'quotekey'], + array_keys($this->fields)))).$adhoc,$filter,$options,$ttl); + } + + /** + * Count records that match criteria + * @return int + * @param $filter string|array + * @param $options array + * @param $ttl int|array + **/ + function count($filter=NULL,?array $options=NULL,$ttl=0) { + $adhoc=[]; + // with grouping involved, we need to wrap the actualy query and count the results + if ($subquery_mode=($options && !empty($options['group']))) { + $group_string=preg_replace('/HAVING.+$/i','',$options['group']); + $group_fields=array_flip(array_map('trim',explode(',',$group_string))); + foreach ($this->adhoc as $key=>$field) + // add adhoc fields that are used for grouping + if (isset($group_fields[$key])) + $adhoc[]=$field['expr'].' AS '.$this->db->quotekey($key); + $fields=implode(',',$adhoc); + if (empty($fields)) + // Select at least one field, ideally the grouping fields + // or sqlsrv fails + $fields=$group_string; + if (preg_match('/mssql|dblib|sqlsrv/',$this->engine)) + $fields='TOP 100 PERCENT '.$fields; + } else { + // for simple count just add a new adhoc counter + $fields='COUNT(*) AS '.$this->db->quotekey('_rows'); + } + // no need to order for a count query as that could include virtual + // field references that are not present here + unset($options['order']); + list($sql,$args)=$this->stringify($fields,$filter,$options); + if ($subquery_mode) + $sql='SELECT COUNT(*) AS '.$this->db->quotekey('_rows').' '. + 'FROM ('.$sql.') AS '.$this->db->quotekey('_temp'); + $result=$this->db->exec($sql,$args,$ttl); + unset($this->adhoc['_rows']); + return (int)$result[0]['_rows']; + } + /** + * Return record at specified offset using same criteria as + * previous load() call and make it active + * @return static + * @param $ofs int + **/ + function skip($ofs=1) { + $out=parent::skip($ofs); + $dry=$this->dry(); + foreach ($this->fields as $key=>&$field) { + $field['value']=$dry?NULL:$out->fields[$key]['value']; + $field['initial']=$field['value']; + $field['changed']=FALSE; + if ($field['pkey']) + $field['previous']=$dry?NULL:$out->fields[$key]['value']; + unset($field); + } + foreach ($this->adhoc as $key=>&$field) { + $field['value']=$dry?NULL:$out->adhoc[$key]['value']; + unset($field); + } + if (!$dry && isset($this->trigger['load'])) + \Base::instance()->call($this->trigger['load'],$this); + return $out; + } + + /** + * Insert new record + * @return static + **/ + function insert() { + $args=[]; + $actr=0; + $nctr=0; + $fields=''; + $values=''; + $filter=''; + $pkeys=[]; + $aikeys=[]; + $nkeys=[]; + $ckeys=[]; + $inc=NULL; + foreach ($this->fields as $key=>$field) + if ($field['pkey']) + $pkeys[$key]=$field['previous']; + if (isset($this->trigger['beforeinsert']) && + \Base::instance()->call($this->trigger['beforeinsert'], + [$this,$pkeys])===FALSE) + return $this; + if ($this->valid()) + // duplicate record + foreach ($this->fields as $key=>&$field) { + $field['changed']=true; + if ($field['pkey'] && !$inc && ($field['auto_inc'] === TRUE || + ($field['auto_inc'] === NULL && !$field['nullable'] + && $field['pdo_type']==\PDO::PARAM_INT) + )) + $inc=$key; + unset($field); + } + foreach ($this->fields as $key=>&$field) { + if ($field['auto_inc']) { + $aikeys[] = $key; + } + if ($field['pkey']) { + $field['previous']=$field['value']; + if (!$inc && empty($field['value']) && + ($field['auto_inc'] === TRUE || ($field['auto_inc'] === NULL + && $field['pdo_type']==\PDO::PARAM_INT && !$field['nullable'])) + ) + $inc=$key; + $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; + $nkeys[$nctr+1]=[$field['value'],$field['pdo_type']]; + ++$nctr; + } + if ($field['changed'] && $key!=$inc) { + $fields.=($actr?',':'').$this->db->quotekey($key); + $values.=($actr?',':'').'?'; + $args[$actr+1]=[$field['value'],$field['pdo_type']]; + ++$actr; + $ckeys[]=$key; + } + unset($field); + } + if ($fields) { + $add=$aik=''; + if ($this->engine=='pgsql' && !empty($pkeys)) { + $names=array_keys($pkeys); + $aik=end($names); + $add=' RETURNING '.$this->db->quotekey($aik); + } + $lID=$this->db->exec( + (preg_match('/mssql|dblib|sqlsrv/',$this->engine) && + array_intersect(array_keys($aikeys),$ckeys)? + 'SET IDENTITY_INSERT '.$this->table.' ON;':''). + 'INSERT INTO '.$this->table.' ('.$fields.') '. + 'VALUES ('.$values.')'.$add,$args + ); + if ($this->engine=='pgsql' && $lID && $aik) + $this->_id=$lID[0][$aik]; + elseif ($this->engine!='oci') + $this->_id=$this->db->lastinsertid(); + // Reload to obtain default and auto-increment field values + if ($reload=(($inc && $this->_id) || $filter)) + $this->load($inc? + [$inc.'=?',$this->db->value( + $this->fields[$inc]['pdo_type'],$this->_id)]: + [$filter,$nkeys]); + if (isset($this->trigger['afterinsert'])) + \Base::instance()->call($this->trigger['afterinsert'], + [$this,$pkeys]); + // reset changed flag after calling afterinsert + if (!$reload) + foreach ($this->fields as $key=>&$field) { + $field['changed']=FALSE; + $field['initial']=$field['value']; + unset($field); + } + } + return $this; + } + + /** + * Update current record + * @return static + **/ + function update() { + $args=[]; + $ctr=0; + $pairs=''; + $pkeys=[]; + foreach ($this->fields as $key=>$field) + if ($field['pkey']) + $pkeys[$key]=$field['previous']; + if (isset($this->trigger['beforeupdate']) && + \Base::instance()->call($this->trigger['beforeupdate'], + [$this,$pkeys])===FALSE) + return $this; + foreach ($this->fields as $key=>$field) + if ($field['changed']) { + $pairs.=($pairs?',':'').$this->db->quotekey($key).'=?'; + $args[++$ctr]=[$field['value'],$field['pdo_type']]; + } + if ($pairs) { + $filter=''; + foreach ($this->fields as $key=>$field) + if ($field['pkey']) { + $filter.=($filter?' AND ':' WHERE '). + $this->db->quotekey($key).'=?'; + $args[++$ctr]=[$field['previous'],$field['pdo_type']]; + } + if (!$filter) + throw new \Exception(sprintf(self::E_PKey,$this->source)); + $sql='UPDATE '.$this->table.' SET '.$pairs.$filter; + $this->db->exec($sql,$args); + } + if (isset($this->trigger['afterupdate'])) + \Base::instance()->call($this->trigger['afterupdate'], + [$this,$pkeys]); + // reset changed flag after calling afterupdate + foreach ($this->fields as $key=>&$field) { + $field['changed']=FALSE; + $field['initial']=$field['value']; + unset($field); + } + return $this; + } + + /** + * batch-update multiple records at once + * @param string|array $filter + * @return int + */ + function updateAll($filter=NULL) { + $args=[]; + $ctr=$out=0; + $pairs=''; + foreach ($this->fields as $key=>$field) + if ($field['changed']) { + $pairs.=($pairs?',':'').$this->db->quotekey($key).'=?'; + $args[++$ctr]=[$field['value'],$field['pdo_type']]; + } + if ($filter) + if (is_array($filter)) { + $cond=array_shift($filter); + $args=array_merge($args,$filter); + $filter=' WHERE '.$cond; + } else + $filter=' WHERE '.$filter; + if ($pairs) { + $sql='UPDATE '.$this->table.' SET '.$pairs.$filter; + $out = $this->db->exec($sql,$args); + } + // reset changed flag after calling afterupdate + foreach ($this->fields as $key=>&$field) { + $field['changed']=FALSE; + $field['initial']=$field['value']; + unset($field); + } + return $out; + } + + + /** + * Delete current record + * @return int + * @param $quick bool + * @param $filter string|array + **/ + function erase($filter=NULL,$quick=TRUE) { + if (isset($filter)) { + if (!$quick) { + $out=0; + foreach ($this->find($filter) as $mapper) + $out+=$mapper->erase(); + return $out; + } + $args=[]; + if (is_array($filter)) { + $args=isset($filter[1]) && is_array($filter[1])? + $filter[1]: + array_slice($filter,1,NULL,TRUE); + $args=is_array($args)?$args:[1=>$args]; + list($filter)=$filter; + } + return $this->db-> + exec('DELETE FROM '.$this->table. + ($filter?' WHERE '.$filter:'').';',$args); + } + $args=[]; + $ctr=0; + $filter=''; + $pkeys=[]; + foreach ($this->fields as $key=>&$field) { + if ($field['pkey']) { + $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; + $args[$ctr+1]=[$field['previous'],$field['pdo_type']]; + $pkeys[$key]=$field['previous']; + ++$ctr; + } + $field['value']=NULL; + $field['changed']=(bool)$field['default']; + if ($field['pkey']) + $field['previous']=NULL; + unset($field); + } + if (!$filter) + throw new \Exception(sprintf(self::E_PKey,$this->source)); + foreach ($this->adhoc as &$field) { + $field['value']=NULL; + unset($field); + } + parent::erase(); + if (isset($this->trigger['beforeerase']) && + \Base::instance()->call($this->trigger['beforeerase'], + [$this,$pkeys])===FALSE) + return 0; + $out=$this->db-> + exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args); + if (isset($this->trigger['aftererase'])) + \Base::instance()->call($this->trigger['aftererase'], + [$this,$pkeys]); + return $out; + } + + /** + * Reset cursor + * @return NULL + **/ + function reset() { + foreach ($this->fields as &$field) { + $field['value']=NULL; + $field['initial']=NULL; + $field['changed']=FALSE; + if ($field['pkey']) + $field['previous']=NULL; + unset($field); + } + foreach ($this->adhoc as &$field) { + $field['value']=NULL; + unset($field); + } + parent::reset(); + } + + /** + * Hydrate mapper object using hive array variable + * @return NULL + * @param $var array|string + * @param $func callback + **/ + function copyfrom($var,$func=NULL) { + if (is_string($var)) + $var=\Base::instance()->$var; + if ($func) + $var=call_user_func($func,$var); + foreach ($var as $key=>$val) + if (in_array($key,array_keys($this->fields))) + $this->set($key,$val); + } + + /** + * Populate hive array variable with mapper fields + * @return NULL + * @param $key string + **/ + function copyto($key) { + $var=&\Base::instance()->ref($key); + foreach ($this->fields+$this->adhoc as $key=>$field) + $var[$key]=$field['value']; + } + + /** + * Return schema and, if the first argument is provided, update it + * @return array + * @param $fields NULL|array + **/ + function schema($fields=null) { + if ($fields) + $this->fields = $fields; + return $this->fields; + } + + /** + * Return field names + * @return array + * @param $adhoc bool + **/ + function fields($adhoc=TRUE) { + return array_keys($this->fields+($adhoc?$this->adhoc:[])); + } + + /** + * Return TRUE if field is not nullable + * @return bool + * @param $field string + **/ + function required($field) { + return isset($this->fields[$field]) && + !$this->fields[$field]['nullable']; + } + + /** + * Retrieve external iterator for fields + * @return object + **/ + function getiterator() { + return new \ArrayIterator($this->cast()); + } + + /** + * Assign alias for table + * @param $alias string + **/ + function alias($alias) { + $this->as=$alias; + return $this; + } + + /** + * Instantiate class + * @param $db \DB\SQL + * @param $table string + * @param $fields array|string + * @param $ttl int|array + **/ + function __construct(\DB\SQL $db,$table,$fields=NULL,$ttl=60) { + $this->db=$db; + $this->engine=$db->driver(); + if ($this->engine=='oci') + $table=strtoupper($table); + $this->source=$table; + $this->table=$this->db->quotekey($table); + $this->fields=$db->schema($table,$fields,$ttl); + $this->reset(); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/sql/session.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/sql/session.php new file mode 100644 index 0000000..45c7137 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/db/sql/session.php @@ -0,0 +1,235 @@ +. + +*/ + +namespace DB\SQL; + +use ReturnTypeWillChange; +use SessionAdapter; + +//! SQL-managed session handler +class Session extends Mapper { + + protected + //! Session ID + $sid, + //! Anti-CSRF token + $_csrf, + //! User agent + $_agent, + //! IP, + $_ip, + //! Suspect callback + $onsuspect; + + /** + * Open session + * @return TRUE + * @param $path string + * @param $name string + **/ + function open(string $path, string $name): bool + { + return TRUE; + } + + /** + * Close session + * @return TRUE + **/ + function close(): bool + { + $this->reset(); + $this->sid=NULL; + return TRUE; + } + + /** + * Return session data in serialized format + * @return string|false + * @param $id string + **/ + #[ReturnTypeWillChange] + function read(string $id) + { + $this->load(['session_id=?',$this->sid=$id]); + if ($this->dry()) + return ''; + if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { + $fw=\Base::instance(); + if (!isset($this->onsuspect) || + $fw->call($this->onsuspect,[$this,$id])===FALSE) { + //NB: `session_destroy` can't be called at that stage (`session_start` not completed) + $this->destroy($id); + $this->close(); + unset($fw->{'COOKIE.'.session_name()}); + $fw->error(403); + } + } + return $this->get('data'); + } + + /** + * Write session data + * @return TRUE + * @param $id string + * @param $data string + **/ + function write(string $id, string $data): bool + { + $this->set('session_id',$id); + $this->set('data',$data); + $this->set('ip',$this->_ip); + $this->set('agent',$this->_agent); + $this->set('stamp',time()); + $this->save(); + return TRUE; + } + + /** + * Destroy session + * @return TRUE + * @param $id string + **/ + function destroy($id): bool + { + $this->erase(['session_id=?',$id]); + return TRUE; + } + + /** + * Garbage collector + **/ + #[ReturnTypeWillChange] + function gc(int $max_lifetime): int + { + return (int) $this->erase(['stamp+?sid; + } + + /** + * Return anti-CSRF token + * @return string + **/ + function csrf() { + return $this->_csrf; + } + + /** + * Return IP address + * @return string + **/ + function ip() { + return $this->_ip; + } + + /** + * Return Unix timestamp + * @return string|FALSE + **/ + function stamp() { + if (!$this->sid) + session_start(); + return $this->dry()?FALSE:$this->get('stamp'); + } + + /** + * Return HTTP user agent + * @return string + **/ + function agent() { + return $this->_agent; + } + + /** + * Instantiate class + * @param $db \DB\SQL + * @param $table string + * @param $force bool + * @param $onsuspect callback + * @param $key string + * @param $type string, column type for data field + **/ + function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL,$key=NULL,$type='TEXT') { + if ($force) { + $eol="\n"; + $tab="\t"; + $sqlsrv=preg_match('/mssql|sqlsrv|sybase/',$db->driver()); + $db->exec( + ($sqlsrv? + ('IF NOT EXISTS (SELECT * FROM sysobjects WHERE '. + 'name='.$db->quote($table).' AND xtype=\'U\') '. + 'CREATE TABLE dbo.'): + ('CREATE TABLE IF NOT EXISTS '. + ((($name=$db->name())&&$db->driver()!='pgsql')? + ($db->quotekey($name,FALSE).'.'):''))). + $db->quotekey($table,FALSE).' ('.$eol. + ($sqlsrv?$tab.$db->quotekey('id').' INT IDENTITY,'.$eol:''). + $tab.$db->quotekey('session_id').' VARCHAR(255),'.$eol. + $tab.$db->quotekey('data').' '.$type.','.$eol. + $tab.$db->quotekey('ip').' VARCHAR(45),'.$eol. + $tab.$db->quotekey('agent').' VARCHAR(300),'.$eol. + $tab.$db->quotekey('stamp').' INTEGER,'.$eol. + $tab.'PRIMARY KEY ('.$db->quotekey($sqlsrv?'id':'session_id').')'.$eol. + ($sqlsrv?',CONSTRAINT [UK_session_id] UNIQUE(session_id)':''). + ');' + ); + } + parent::__construct($db,$table); + $this->onsuspect=$onsuspect; + if (version_compare(PHP_VERSION, '8.4.0')>=0) { + // TODO: remove this when php7 support is dropped + session_set_save_handler(new SessionAdapter($this)); + } else { + session_set_save_handler( + [$this,'open'], + [$this,'close'], + [$this,'read'], + [$this,'write'], + [$this,'destroy'], + [$this,'gc'] + ); + } + register_shutdown_function('session_commit'); + $fw=\Base::instance(); + $headers=$fw->HEADERS; + $this->_csrf=$fw->hash($fw->SEED. + extension_loaded('openssl')? + implode(unpack('L',openssl_random_pseudo_bytes(4))): + mt_rand() + ); + if ($key) + $fw->$key=$this->_csrf; + $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; + if (strlen($this->_agent) > 300) { + $this->_agent = substr($this->_agent, 0, 300); + } + $this->_ip=$fw->IP; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/f3.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/f3.php new file mode 100644 index 0000000..ae95942 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/f3.php @@ -0,0 +1,42 @@ +. + +*/ + +//! Legacy mode enabler +class F3 { + + static + //! Framework instance + $fw; + + /** + * Forward function calls to framework + * @return mixed + * @param $func callback + * @param $args array + **/ + static function __callstatic($func,array $args) { + if (!self::$fw) + self::$fw=Base::instance(); + return call_user_func_array([self::$fw,$func],$args); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/image.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/image.php new file mode 100644 index 0000000..4d34dfd --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/image.php @@ -0,0 +1,617 @@ +. + +*/ + +//! Image manipulation tools +class Image { + + //@{ Messages + const + E_Color='Invalid color specified: %s', + E_File='File not found', + E_Font='CAPTCHA font not found', + E_TTF='No TrueType support in GD module', + E_Length='Invalid CAPTCHA length: %s'; + //@} + + //@{ Positional cues + const + POS_Left=1, + POS_Center=2, + POS_Right=4, + POS_Top=8, + POS_Middle=16, + POS_Bottom=32; + //@} + + protected + //! Source filename + $file, + //! Image resource + $data, + //! Enable/disable history + $flag=FALSE, + //! Filter count + $count=0; + + /** + * Convert RGB hex triad to array + * @return array|FALSE + * @param $color int|string + **/ + function rgb($color) { + if (is_string($color)) + $color=hexdec($color); + $hex=str_pad($hex=dechex($color),$color<4096?3:6,'0',STR_PAD_LEFT); + if (($len=strlen($hex))>6) + throw new \Exception(sprintf(self::E_Color,'0x'.$hex)); + $color=str_split($hex,$len/3); + foreach ($color as &$hue) { + $hue=hexdec(str_repeat($hue,6/$len)); + unset($hue); + } + return $color; + } + + /** + * Invert image + * @return object + **/ + function invert() { + imagefilter($this->data,IMG_FILTER_NEGATE); + return $this->save(); + } + + /** + * Adjust brightness (range:-255 to 255) + * @return object + * @param $level int + **/ + function brightness($level) { + imagefilter($this->data,IMG_FILTER_BRIGHTNESS,$level); + return $this->save(); + } + + /** + * Adjust contrast (range:-100 to 100) + * @return object + * @param $level int + **/ + function contrast($level) { + imagefilter($this->data,IMG_FILTER_CONTRAST,$level); + return $this->save(); + } + + /** + * Convert to grayscale + * @return object + **/ + function grayscale() { + imagefilter($this->data,IMG_FILTER_GRAYSCALE); + return $this->save(); + } + + /** + * Adjust smoothness + * @return object + * @param $level int + **/ + function smooth($level) { + imagefilter($this->data,IMG_FILTER_SMOOTH,$level); + return $this->save(); + } + + /** + * Emboss the image + * @return object + **/ + function emboss() { + imagefilter($this->data,IMG_FILTER_EMBOSS); + return $this->save(); + } + + /** + * Apply sepia effect + * @return object + **/ + function sepia() { + imagefilter($this->data,IMG_FILTER_GRAYSCALE); + imagefilter($this->data,IMG_FILTER_COLORIZE,90,60,45); + return $this->save(); + } + + /** + * Pixelate the image + * @return object + * @param $size int + **/ + function pixelate($size) { + imagefilter($this->data,IMG_FILTER_PIXELATE,$size,TRUE); + return $this->save(); + } + + /** + * Blur the image using Gaussian filter + * @return object + * @param $selective bool + **/ + function blur($selective=FALSE) { + imagefilter($this->data, + $selective?IMG_FILTER_SELECTIVE_BLUR:IMG_FILTER_GAUSSIAN_BLUR); + return $this->save(); + } + + /** + * Apply sketch effect + * @return object + **/ + function sketch() { + imagefilter($this->data,IMG_FILTER_MEAN_REMOVAL); + return $this->save(); + } + + /** + * Flip on horizontal axis + * @return object + **/ + function hflip() { + $tmp=imagecreatetruecolor( + $width=$this->width(),$height=$this->height()); + imagesavealpha($tmp,TRUE); + imagefill($tmp,0,0,IMG_COLOR_TRANSPARENT); + imagecopyresampled($tmp,$this->data, + 0,0,$width-1,0,$width,$height,-$width,$height); + imagedestroy($this->data); + $this->data=$tmp; + return $this->save(); + } + + /** + * Flip on vertical axis + * @return object + **/ + function vflip() { + $tmp=imagecreatetruecolor( + $width=$this->width(),$height=$this->height()); + imagesavealpha($tmp,TRUE); + imagefill($tmp,0,0,IMG_COLOR_TRANSPARENT); + imagecopyresampled($tmp,$this->data, + 0,0,0,$height-1,$width,$height,$width,-$height); + imagedestroy($this->data); + $this->data=$tmp; + return $this->save(); + } + + /** + * Crop the image + * @return object + * @param $x1 int + * @param $y1 int + * @param $x2 int + * @param $y2 int + **/ + function crop($x1,$y1,$x2,$y2) { + $tmp=imagecreatetruecolor($width=$x2-$x1+1,$height=$y2-$y1+1); + imagesavealpha($tmp,TRUE); + imagefill($tmp,0,0,IMG_COLOR_TRANSPARENT); + imagecopyresampled($tmp,$this->data, + 0,0,$x1,$y1,$width,$height,$width,$height); + imagedestroy($this->data); + $this->data=$tmp; + return $this->save(); + } + + /** + * Resize image (Maintain aspect ratio); Crop relative to center + * if flag is enabled; Enlargement allowed if flag is enabled + * @return object + * @param $width int + * @param $height int + * @param $crop bool + * @param $enlarge bool + **/ + function resize($width=NULL,$height=NULL,$crop=TRUE,$enlarge=TRUE) { + if (is_null($width) && is_null($height)) + return $this; + $origw=$this->width(); + $origh=$this->height(); + if (is_null($width)) + $width=round(($height/$origh)*$origw); + if (is_null($height)) + $height=round(($width/$origw)*$origh); + // Adjust dimensions; retain aspect ratio + $ratio=$origw/$origh; + if (!$crop) { + if ($width/$ratio<=$height) + $height=round($width/$ratio); + else + $width=round($height*$ratio); + } + if (!$enlarge) { + $width=min($origw,$width); + $height=min($origh,$height); + } + // Create blank image + $tmp=imagecreatetruecolor($width,$height); + imagesavealpha($tmp,TRUE); + imagefill($tmp,0,0,IMG_COLOR_TRANSPARENT); + // Resize + if ($crop) { + if ($width/$ratio<=$height) { + $cropw=round($origh*$width/$height); + imagecopyresampled($tmp,$this->data, + 0,0,round(($origw-$cropw)/2),0,$width,$height,$cropw,$origh); + } + else { + $croph=round($origw*$height/$width); + imagecopyresampled($tmp,$this->data, + 0,0,0,round(($origh-$croph)/2),$width,$height,$origw,$croph); + } + } + else + imagecopyresampled($tmp,$this->data, + 0,0,0,0,$width,$height,$origw,$origh); + imagedestroy($this->data); + $this->data=$tmp; + return $this->save(); + } + + /** + * Rotate image + * @return object + * @param $angle int + **/ + function rotate($angle) { + $this->data=imagerotate($this->data,$angle, + imagecolorallocatealpha($this->data,0,0,0,127)); + imagesavealpha($this->data,TRUE); + return $this->save(); + } + + /** + * Apply an image overlay + * @return object + * @param $img object + * @param $align int|array + * @param $alpha int + **/ + function overlay(Image $img,$align=NULL,$alpha=100) { + if (is_null($align)) + $align=self::POS_Right|self::POS_Bottom; + if (is_array($align)) { + list($posx,$posy)=$align; + $align = 0; + } + $ovr=imagecreatefromstring($img->dump()); + imagesavealpha($ovr,TRUE); + $imgw=$this->width(); + $imgh=$this->height(); + $ovrw=imagesx($ovr); + $ovrh=imagesy($ovr); + if ($align & self::POS_Left) + $posx=0; + if ($align & self::POS_Center) + $posx=round(($imgw-$ovrw)/2); + if ($align & self::POS_Right) + $posx=$imgw-$ovrw; + if ($align & self::POS_Top) + $posy=0; + if ($align & self::POS_Middle) + $posy=round(($imgh-$ovrh)/2); + if ($align & self::POS_Bottom) + $posy=$imgh-$ovrh; + if (empty($posx)) + $posx=0; + if (empty($posy)) + $posy=0; + if ($alpha==100) + imagecopy($this->data,$ovr,$posx,$posy,0,0,$ovrw,$ovrh); + else { + $cut=imagecreatetruecolor($ovrw,$ovrh); + imagecopy($cut,$this->data,0,0,$posx,$posy,$ovrw,$ovrh); + imagecopy($cut,$ovr,0,0,0,0,$ovrw,$ovrh); + imagecopymerge($this->data, + $cut,$posx,$posy,0,0,$ovrw,$ovrh,$alpha); + } + return $this->save(); + } + + /** + * Generate identicon + * @return object + * @param $str string + * @param $size int + * @param $blocks int + **/ + function identicon($str,$size=64,$blocks=4) { + $sprites=[ + [.5,1,1,0,1,1], + [.5,0,1,0,.5,1,0,1], + [.5,0,1,0,1,1,.5,1,1,.5], + [0,.5,.5,0,1,.5,.5,1,.5,.5], + [0,.5,1,0,1,1,0,1,1,.5], + [1,0,1,1,.5,1,1,.5,.5,.5], + [0,0,1,0,1,.5,0,0,.5,1,0,1], + [0,0,.5,0,1,.5,.5,1,0,1,.5,.5], + [.5,0,.5,.5,1,.5,1,1,.5,1,.5,.5,0,.5], + [0,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1], + [0,.5,.5,1,1,.5,.5,0,1,0,1,1,0,1], + [.5,0,1,0,1,1,.5,1,1,.75,.5,.5,1,.25], + [0,.5,.5,0,.5,.5,1,0,1,.5,.5,1,.5,.5,0,1], + [0,0,1,0,1,1,0,1,1,.5,.5,.25,.5,.75,0,.5,.5,.25], + [0,.5,.5,.5,.5,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1], + [0,0,1,0,.5,.5,.5,0,0,.5,1,.5,.5,1,.5,.5,0,1] + ]; + $hash=sha1($str); + $this->data=imagecreatetruecolor($size,$size); + list($r,$g,$b)=$this->rgb(hexdec(substr($hash,-3))); + $fg=imagecolorallocate($this->data,$r,$g,$b); + imagefill($this->data,0,0,IMG_COLOR_TRANSPARENT); + $ctr=count($sprites); + $dim=$blocks*floor($size/$blocks)*2/$blocks; + for ($j=0,$y=ceil($blocks/2);$j<$y;++$j) + for ($i=$j,$x=$blocks-1-$j;$i<$x;++$i) { + $sprite=imagecreatetruecolor($dim,$dim); + imagefill($sprite,0,0,IMG_COLOR_TRANSPARENT); + $block=$sprites[hexdec($hash[($j*$blocks+$i)*2])%$ctr]; + for ($k=0,$pts=count($block);$k<$pts;++$k) + $block[$k]*=$dim; + if (version_compare(PHP_VERSION, '8.1.0') >= 0) { + imagefilledpolygon($sprite,$block,$fg); + } else { + imagefilledpolygon($sprite,$block,$pts/2,$fg); + } + for ($k=0;$k<4;++$k) { + imagecopyresampled($this->data,$sprite, + round($i*$dim/2),round($j*$dim/2),0,0,round($dim/2),round($dim/2),$dim,$dim); + $this->data=imagerotate($this->data,90, + imagecolorallocatealpha($this->data,0,0,0,127)); + } + imagedestroy($sprite); + } + imagesavealpha($this->data,TRUE); + return $this->save(); + } + + /** + * Generate CAPTCHA image + * @return object|FALSE + * @param $font string + * @param $size int + * @param $len int + * @param $key string + * @param $path string + * @param $fg int + * @param $bg int + **/ + function captcha($font,$size=24,$len=5, + $key=NULL,$path='',$fg=0xFFFFFF,$bg=0x000000) { + if ((!$ssl=extension_loaded('openssl')) && ($len<4 || $len>13)) { + throw new \Exception(sprintf(self::E_Length,$len)); + } + if (!function_exists('imagettftext')) { + throw new \Exception(self::E_TTF); + } + $fw=Base::instance(); + foreach ($fw->split($path?:$fw->UI.';./') as $dir) + if (is_file($path=realpath($dir.$font))) { + $seed=strtoupper(substr( + $ssl?bin2hex(openssl_random_pseudo_bytes($len)):uniqid(), + -$len)); + $block=$size*3; + $tmp=[]; + for ($i=0,$width=0,$height=0;$i<$len;++$i) { + // Process at 2x magnification + $box=imagettfbbox($size*2,0,$path,$seed[$i]); + $w=$box[2]-$box[0]; + $h=$box[1]-$box[5]; + $char=imagecreatetruecolor($block,$block); + imagefill($char,0,0,$bg); + imagettftext($char,$size*2,0, + round(($block-$w)/2),round($block-($block-$h)/2), + $fg,$path,$seed[$i]); + $char=imagerotate($char,mt_rand(-30,30), + imagecolorallocatealpha($char,0,0,0,127)); + // Reduce to normal size + $tmp[$i]=imagecreatetruecolor( + round(($w=imagesx($char))/2),round(($h=imagesy($char))/2)); + imagefill($tmp[$i],0,0,IMG_COLOR_TRANSPARENT); + imagecopyresampled($tmp[$i], + $char,0,0,0,0,round($w/2),round($h/2),$w,$h); + imagedestroy($char); + $width+=$i+1<$len?$block/2:$w/2; + $height=max($height,$h/2); + } + $this->data=imagecreatetruecolor(round($width),round($height)); + imagefill($this->data,0,0,IMG_COLOR_TRANSPARENT); + for ($i=0;$i<$len;++$i) { + imagecopy($this->data,$tmp[$i], + round($i*$block/2),round(($height-imagesy($tmp[$i]))/2),0,0, + imagesx($tmp[$i]),imagesy($tmp[$i])); + imagedestroy($tmp[$i]); + } + imagesavealpha($this->data,TRUE); + if ($key) + $fw->$key=$seed; + return $this->save(); + } + throw new \Exception(self::E_Font); + } + + /** + * Return image width + * @return int + **/ + function width() { + return imagesx($this->data); + } + + /** + * Return image height + * @return int + **/ + function height() { + return imagesy($this->data); + } + + /** + * Send image to HTTP client + * @return NULL + **/ + function render() { + $args=func_get_args(); + $format=$args?array_shift($args):'png'; + if (PHP_SAPI!='cli') { + header('Content-Type: image/'.$format); + header('X-Powered-By: '.Base::instance()->PACKAGE); + } + call_user_func_array( + 'image'.$format, + array_merge([$this->data,NULL],$args) + ); + } + + /** + * Return image as a string + * @return string + **/ + function dump() { + $args=func_get_args(); + $format=$args?array_shift($args):'png'; + ob_start(); + call_user_func_array( + 'image'.$format, + array_merge([$this->data,NULL],$args) + ); + return ob_get_clean(); + } + + /** + * Return image resource + * @return resource + **/ + function data() { + return $this->data; + } + + /** + * Save current state + * @return object + **/ + function save() { + $fw=Base::instance(); + if ($this->flag) { + if (!is_dir($dir=$fw->TEMP)) + mkdir($dir,Base::MODE,TRUE); + ++$this->count; + $fw->write($dir.'/'.$fw->SEED.'.'. + $fw->hash($this->file).'-'.$this->count.'.png', + $this->dump()); + } + return $this; + } + + /** + * Revert to specified state + * @return object + * @param $state int + **/ + function restore($state=1) { + $fw=Base::instance(); + if ($this->flag && is_file($file=($path=$fw->TEMP. + $fw->SEED.'.'.$fw->hash($this->file).'-').$state.'.png')) { + if (is_resource($this->data)) + imagedestroy($this->data); + $this->data=imagecreatefromstring($fw->read($file)); + imagesavealpha($this->data,TRUE); + foreach (glob($path.'*.png',GLOB_NOSORT) as $match) + if (preg_match('/-(\d+)\.png/',$match,$parts) && + $parts[1]>$state) + @unlink($match); + $this->count=$state; + } + return $this; + } + + /** + * Undo most recently applied filter + * @return object + **/ + function undo() { + if ($this->flag) { + if ($this->count) + $this->count--; + return $this->restore($this->count); + } + return $this; + } + + /** + * Load string + * @return object|FALSE + * @param $str string + **/ + function load($str) { + if (!$this->data=@imagecreatefromstring($str)) + return FALSE; + imagesavealpha($this->data,TRUE); + $this->save(); + return $this; + } + + /** + * Instantiate image + * @param $file string + * @param $flag bool + * @param $path string + **/ + function __construct($file=NULL,$flag=FALSE,$path=NULL) { + $this->flag=$flag; + if ($file) { + $fw=Base::instance(); + // Create image from file + $this->file=$file; + if (!isset($path)) + $path=$fw->UI.';./'; + foreach ($fw->split($path,FALSE) as $dir) + if (is_file($dir.$file)) + return $this->load($fw->read($dir.$file)); + throw new \Exception(self::E_File); + } + } + + /** + * Wrap-up + * @return NULL + **/ + function __destruct() { + if (is_resource($this->data)) { + imagedestroy($this->data); + $fw=Base::instance(); + $path=$fw->TEMP.$fw->SEED.'.'.$fw->hash($this->file); + if ($glob=@glob($path.'*.png',GLOB_NOSORT)) + foreach ($glob as $match) + if (preg_match('/-(\d+)\.png/',$match)) + @unlink($match); + } + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/log.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/log.php new file mode 100644 index 0000000..5b7341d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/log.php @@ -0,0 +1,71 @@ +. + +*/ + +//! Custom logger +class Log { + + protected + //! File name + $file; + + /** + * Write specified text to log file + * @return string + * @param $text string + * @param $format string + **/ + function write($text,$format='r') { + $fw=Base::instance(); + foreach (preg_split('/\r?\n|\r/',trim($text)) as $line) + $fw->write( + $this->file, + date($format). + (isset($_SERVER['REMOTE_ADDR'])? + (' ['.$_SERVER['REMOTE_ADDR']. + (($fwd=filter_var($fw->get('HEADERS.X-Forwarded-For'), + FILTER_VALIDATE_IP))?(' ('.$fwd.')'):'') + .']'):'').' '. + trim($line).PHP_EOL, + TRUE + ); + } + + /** + * Erase log + * @return NULL + **/ + function erase() { + @unlink($this->file); + } + + /** + * Instantiate class + * @param $file string + **/ + function __construct($file) { + $fw=Base::instance(); + if (!is_dir($dir=$fw->LOGS)) + mkdir($dir,Base::MODE,TRUE); + $this->file=$dir.$file; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/magic.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/magic.php new file mode 100644 index 0000000..92398a5 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/magic.php @@ -0,0 +1,143 @@ +. + +*/ + +//! PHP magic wrapper +abstract class Magic implements ArrayAccess { + + /** + * Return TRUE if key is not empty + * @return bool + * @param $key string + **/ + abstract function exists($key); + + /** + * Bind value to key + * @return mixed + * @param $key string + * @param $val mixed + **/ + abstract function set($key,$val); + + /** + * Retrieve contents of key + * @return mixed + * @param $key string + **/ + abstract function &get($key); + + /** + * Unset key + * @return NULL + * @param $key string + **/ + abstract function clear($key); + + /** + * Convenience method for checking property value + * @return mixed + * @param $key string + **/ + #[\ReturnTypeWillChange] + function offsetexists($key) { + return Base::instance()->visible($this,$key)? + isset($this->$key): + ($this->exists($key) && $this->get($key)!==NULL); + } + + /** + * Convenience method for assigning property value + * @return mixed + * @param $key string + * @param $val mixed + **/ + #[\ReturnTypeWillChange] + function offsetset($key,$val) { + return Base::instance()->visible($this,$key)? + ($this->$key=$val):$this->set($key,$val); + } + + /** + * Convenience method for retrieving property value + * @return mixed + * @param $key string + **/ + #[\ReturnTypeWillChange] + function &offsetget($key) { + if (Base::instance()->visible($this,$key)) + $val=&$this->$key; + else + $val=&$this->get($key); + return $val; + } + + /** + * Convenience method for removing property value + * @return NULL + * @param $key string + **/ + #[\ReturnTypeWillChange] + function offsetunset($key) { + if (Base::instance()->visible($this,$key)) + unset($this->$key); + else + $this->clear($key); + } + + /** + * Alias for offsetexists() + * @return mixed + * @param $key string + **/ + function __isset($key) { + return $this->offsetexists($key); + } + + /** + * Alias for offsetset() + * @return mixed + * @param $key string + * @param $val mixed + **/ + function __set($key,$val) { + return $this->offsetset($key,$val); + } + + /** + * Alias for offsetget() + * @return mixed + * @param $key string + **/ + function &__get($key) { + $val=&$this->offsetget($key); + return $val; + } + + /** + * Alias for offsetunset() + * @param $key string + **/ + function __unset($key) { + $this->offsetunset($key); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/markdown.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/markdown.php new file mode 100644 index 0000000..cb03d14 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/markdown.php @@ -0,0 +1,569 @@ +. + +*/ + +//! Markdown-to-HTML converter +class Markdown extends Prefab { + + protected + //! Parsing rules + $blocks, + //! Special characters + $special; + + /** + * Process blockquote + * @return string + * @param $str string + **/ + protected function _blockquote($str) { + $str=preg_replace('/(?<=^|\n)\h?>\h?(.*?(?:\n+|$))/','\1',$str); + return strlen($str)? + ('
'.$this->build($str).'
'."\n\n"):''; + } + + /** + * Process whitespace-prefixed code block + * @return string + * @param $str string + **/ + protected function _pre($str) { + $str=preg_replace('/(?<=^|\n)(?: {4}|\t)(.+?(?:\n+|$))/','\1', + $this->esc($str)); + return strlen($str)? + ('
'.
+				$this->esc($this->snip($str)).
+			'
'."\n\n"): + ''; + } + + /** + * Process fenced code block + * @return string + * @param $hint string + * @param $str string + **/ + protected function _fence($hint,$str) { + $str=$this->snip($str); + $fw=Base::instance(); + if ($fw->HIGHLIGHT) { + switch (strtolower($hint)) { + case 'php': + $str=$fw->highlight($str); + break; + case 'apache': + preg_match_all('/(?<=^|\n)(\h*)'. + '(?:(<\/?)(\w+)((?:\h+[^>]+)*)(>)|'. + '(?:(\w+)(\h.+?)))(\h*(?:\n+|$))/', + $str,$matches,PREG_SET_ORDER); + $out=''; + foreach ($matches as $match) + $out.=$match[1]. + ($match[3]? + (''. + $this->esc($match[2]).$match[3]. + ''. + ($match[4]? + (''. + $this->esc($match[4]). + ''): + ''). + ''. + $this->esc($match[5]). + ''): + (''. + $match[6]. + ''. + ''. + $this->esc($match[7]). + '')). + $match[8]; + $str=''.$out.''; + break; + case 'html': + preg_match_all( + '/(?:(?:<(\/?)(\w+)'. + '((?:\h+(?:\w+\h*=\h*)?".+?"|[^>]+)*|'. + '\h+.+?)(\h*\/?)>)|(.+?))/s', + $str,$matches,PREG_SET_ORDER + ); + $out=''; + foreach ($matches as $match) { + if ($match[2]) { + $out.='<'. + $match[1].$match[2].''; + if ($match[3]) { + preg_match_all( + '/(?:\h+(?:(?:(\w+)\h*=\h*)?'. + '(".+?")|(.+)))/', + $match[3],$parts,PREG_SET_ORDER + ); + foreach ($parts as $part) + $out.=' '. + (empty($part[3])? + ((empty($part[1])? + '': + (''. + $part[1].'=')). + ''. + $part[2].''): + (''. + $part[3].'')); + } + $out.=''. + $match[4].'>'; + } + else + $out.=$this->esc($match[5]); + } + $str=''.$out.''; + break; + case 'ini': + preg_match_all( + '/(?<=^|\n)(?:'. + '(;[^\n]*)|(?:<\?php.+?\?>?)|'. + '(?:\[(.+?)\])|'. + '(.+?)(\h*=\h*)'. + '((?:\\\\\h*\r?\n|.+?)*)'. + ')((?:\r?\n)+|$)/', + $str,$matches,PREG_SET_ORDER + ); + $out=''; + foreach ($matches as $match) { + if ($match[1]) + $out.=''.$match[1]. + ''; + elseif ($match[2]) + $out.='['.$match[2].']'. + ''; + elseif ($match[3]) + $out.=''.$match[3]. + ''.$match[4]. + ($match[5]? + (''. + $match[5].''):''); + else + $out.=$match[0]; + if (isset($match[6])) + $out.=$match[6]; + } + $str=''.$out.''; + break; + default: + $str=''.$this->esc($str).''; + break; + } + } + else + $str=''.$this->esc($str).''; + return '
'.$str.'
'."\n\n"; + } + + /** + * Process horizontal rule + * @return string + **/ + protected function _hr() { + return '
'."\n\n"; + } + + /** + * Process atx-style heading + * @return string + * @param $type string + * @param $str string + **/ + protected function _atx($type,$str) { + $level=strlen($type); + return ''. + $this->scan($str).''."\n\n"; + } + + /** + * Process setext-style heading + * @return string + * @param $str string + * @param $type string + **/ + protected function _setext($str,$type) { + $level=strpos('=-',$type)+1; + return ''. + $this->scan($str).''."\n\n"; + } + + /** + * Process ordered/unordered list + * @return string + * @param $str string + **/ + protected function _li($str) { + // Initialize list parser + $len=strlen($str); + $ptr=0; + $dst=''; + $first=TRUE; + $tight=TRUE; + $type='ul'; + // Main loop + while ($ptr<$len) { + if (preg_match('/^\h*[*\-](?:\h?[*\-]){2,}(?:\n+|$)/', + substr($str,$ptr),$match)) { + $ptr+=strlen($match[0]); + // Embedded horizontal rule + return (strlen($dst)? + ('<'.$type.'>'."\n".$dst.''."\n\n"):''). + '
'."\n\n".$this->build(substr($str,$ptr)); + } + elseif (preg_match('/(?<=^|\n)([*+\-]|\d+\.)\h'. + '(.+?(?:\n+|$))((?:(?: {4}|\t)+.+?(?:\n+|$))*)/s', + substr($str,$ptr),$match)) { + $match[3]=preg_replace('/(?<=^|\n)(?: {4}|\t)/','',$match[3]); + $found=FALSE; + foreach (array_slice($this->blocks,0,-1) as $regex) + if (preg_match($regex,$match[3])) { + $found=TRUE; + break; + } + // List + if ($first) { + // First pass + if (is_numeric($match[1])) + $type='ol'; + if (preg_match('/\n{2,}$/',$match[2]. + ($found?'':$match[3]))) + // Loose structure; Use paragraphs + $tight=FALSE; + $first=FALSE; + } + // Strip leading whitespaces + $ptr+=strlen($match[0]); + $tmp=$this->snip($match[2].$match[3]); + if ($tight) { + if ($found) + $tmp=$match[2].$this->build($this->snip($match[3])); + } + else + $tmp=$this->build($tmp); + $dst.='
  • '.$this->scan(trim($tmp)).'
  • '."\n"; + } + } + return strlen($dst)? + ('<'.$type.'>'."\n".$dst.''."\n\n"):''; + } + + /** + * Ignore raw HTML + * @return string + * @param $str string + **/ + protected function _raw($str) { + return $str; + } + + /** + * Process paragraph + * @return string + * @param $str string + **/ + protected function _p($str) { + $str=trim($str); + if (strlen($str)) { + if (preg_match('/^(.+?\n)([>#].+)$/s',$str,$parts)) + return $this->_p($parts[1]).$this->build($parts[2]); + $str=preg_replace_callback( + '/([^<>\[]+)?(<[\?%].+?[\?%]>|<.+?>|\[.+?\]\s*\(.+?\))|'. + '(.+)/s', + function($expr) { + $tmp=''; + if (isset($expr[4])) + $tmp.=$this->esc($expr[4]); + else { + if (isset($expr[1])) + $tmp.=$this->esc($expr[1]); + $tmp.=$expr[2]; + if (isset($expr[3])) + $tmp.=$this->esc($expr[3]); + } + return $tmp; + }, + $str + ); + $str=preg_replace('/\s{2}\r?\n/','
    ',$str); + return '

    '.$this->scan($str).'

    '."\n\n"; + } + return ''; + } + + /** + * Process strong/em/strikethrough spans + * @return string + * @param $str string + **/ + protected function _text($str) { + $tmp=''; + while ($str!=$tmp) + $str=preg_replace_callback( + '/(?<=\s|^)(?'.$expr[4].'
    '; + if ($expr[2]) + return ''.$expr[4].''; + return ''.$expr[4].''; + }, + preg_replace( + '/(?\1', + $tmp=$str + ) + ); + return $str; + } + + /** + * Process image span + * @return string + * @param $str string + **/ + protected function _img($str) { + return preg_replace_callback( + '/!(?:\[(.+?)\])?\h*\(?(?:\h*"(.*?)"\h*)?\)/', + function($expr) { + return ''.$this->esc($expr[1]).''; + }, + $str + ); + } + + /** + * Process anchor span + * @return string + * @param $str string + **/ + protected function _a($str) { + return preg_replace_callback( + '/(??(?:\h*"(.*?)"\h*)?\)/', + function($expr) { + return ''.$this->scan($expr[1]).''; + }, + $str + ); + } + + /** + * Auto-convert links + * @return string + * @param $str string + **/ + protected function _auto($str) { + return preg_replace_callback( + '/`.*?<(.+?)>.*?`|<(.+?)>/', + function($expr) { + if (empty($expr[1]) && parse_url($expr[2],PHP_URL_SCHEME)) { + $expr[2]=$this->esc($expr[2]); + return ''.$expr[2].''; + } + return $expr[0]; + }, + $str + ); + } + + /** + * Process code span + * @return string + * @param $str string + **/ + protected function _code($str) { + return preg_replace_callback( + '/`` (.+?) ``|(?'. + $this->esc(empty($expr[1])?$expr[2]:$expr[1]).''; + }, + $str + ); + } + + /** + * Convert characters to HTML entities + * @return string + * @param $str string + **/ + function esc($str) { + if (!$this->special) + $this->special=[ + '...'=>'…', + '(tm)'=>'™', + '(r)'=>'®', + '(c)'=>'©' + ]; + foreach ($this->special as $key=>$val) + $str=preg_replace('/'.preg_quote($key,'/').'/i',$val,$str); + return htmlspecialchars($str,ENT_COMPAT, + Base::instance()->ENCODING,FALSE); + } + + /** + * Reduce multiple line feeds + * @return string + * @param $str string + **/ + protected function snip($str) { + return preg_replace('/(?:(?<=\n)\n+)|\n+$/',"\n",$str); + } + + /** + * Scan line for convertible spans + * @return string + * @param $str string + **/ + function scan($str) { + $inline=['img','a','text','auto','code']; + foreach ($inline as $func) + $str=$this->{'_'.$func}($str); + return $str; + } + + /** + * Assemble blocks + * @return string + * @param $str string + **/ + protected function build($str) { + if (!$this->blocks) { + // Regexes for capturing entire blocks + $this->blocks=[ + 'blockquote'=>'/^(?:\h?>\h?.*?(?:\n+|$))+/', + 'pre'=>'/^(?:(?: {4}|\t).+?(?:\n+|$))+/', + 'fence'=>'/^`{3}\h*(\w+)?.*?[^\n]*\n+(.+?)`{3}[^\n]*'. + '(?:\n+|$)/s', + 'hr'=>'/^\h*[*_\-](?:\h?[\*_\-]){2,}\h*(?:\n+|$)/', + 'atx'=>'/^\h*(#{1,6})\h?(.+?)\h*(?:#.*)?(?:\n+|$)/u', + 'setext'=>'/^\h*(.+?)\h*\n([=\-])+\h*(?:\n+|$)/u', + 'li'=>'/^(?:(?:[*+\-]|\d+\.)\h.+?(?:\n+|$)'. + '(?:(?: {4}|\t)+.+?(?:\n+|$))*)+/s', + 'raw'=>'/^((?:|'. + '<(address|article|aside|audio|blockquote|canvas|dd|'. + 'div|dl|fieldset|figcaption|figure|footer|form|h\d|'. + 'header|hgroup|hr|noscript|object|ol|output|p|pre|'. + 'section|table|tfoot|ul|video).*?'. + '(?:\/>|>(?:(?>[^><]+)|(?R))*<\/\2>))'. + '\h*(?:\n{2,}|\n*$)|<[\?%].+?[\?%]>\h*(?:\n?$|\n*))/s', + 'p'=>'/^(.+?(?:\n{2,}|\n*$))/s' + ]; + } + // Treat lines with nothing but whitespaces as empty lines + $str=preg_replace('/\n\h+(?=\n)/',"\n",$str); + // Initialize block parser + $len=strlen($str); + $ptr=0; + $dst=''; + // Main loop + while ($ptr<$len) { + if (preg_match('/^ {0,3}\[([^\[\]]+)\]:\s*?\s*'. + '(?:"([^\n]*)")?(?:\n+|$)/s',substr($str,$ptr),$match)) { + // Reference-style link; Backtrack + $ptr+=strlen($match[0]); + $tmp=''; + // Catch line breaks in title attribute + $ref=preg_replace('/\h/','\s',preg_quote($match[1],'/')); + while ($dst!=$tmp) { + $dst=preg_replace_callback( + '/(?esc($match[2]).'"'. + (empty($match[3])? + '': + (' title="'. + $this->esc($match[3]).'"')).'>'. + // Link + $this->scan( + empty($expr[3])? + (empty($expr[1])? + $expr[4]: + $expr[1]): + $expr[3] + ).''): + // Image + (''.
+										$this->esc($expr[3]).''); + }, + $tmp=$dst + ); + } + } + else + foreach ($this->blocks as $func=>$regex) + if (preg_match($regex,substr($str,$ptr),$match)) { + $ptr+=strlen($match[0]); + $dst.=call_user_func_array( + [$this,'_'.$func], + count($match)>1?array_slice($match,1):$match + ); + break; + } + } + return $dst; + } + + /** + * Render HTML equivalent of markdown + * @return string + * @param $txt string + **/ + function convert($txt) { + $txt=preg_replace_callback( + '/(.+?<\/code>|'. + '<[^>\n]+>|\([^\n\)]+\)|"[^"\n]+")|'. + '\\\\(.)/s', + function($expr) { + // Process escaped characters + return empty($expr[1])?$expr[2]:$expr[1]; + }, + $this->build(preg_replace('/\r\n|\r/',"\n",$txt)) + ); + return $this->snip($txt); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/matrix.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/matrix.php new file mode 100644 index 0000000..6c22bae --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/matrix.php @@ -0,0 +1,139 @@ +. + +*/ + +//! Generic array utilities +class Matrix extends Prefab { + + /** + * Retrieve values from a specified column of a multi-dimensional + * array variable + * @return array + * @param $var array + * @param $col mixed + **/ + function pick(array $var,$col) { + return array_map( + function($row) use($col) { + return $row[$col]; + }, + $var + ); + } + + /** + * select a subset of fields from an input array + * @param string|array $fields splittable string or array + * @param string|array $data hive key or array + * @return array + */ + function select($fields, $data) { + return array_intersect_key(is_array($data) ? $data : \Base::instance()->get($data), + array_flip(is_array($fields) ? $fields : \Base::instance()->split($fields))); + } + + /** + * walk with a callback function through a subset of fields from an input array + * the callback receives the value, index-key and the full input array as parameters + * set value parameter as reference and you're able to modify the data as well + * @param string|array $fields splittable string or array of fields + * @param string|array $data hive key or input array + * @param callable $callback (mixed &$value, string $key, array $data) + * @return array modified subset data + */ + function walk($fields, $data, $callback) { + $subset=$this->select($fields, $data); + array_walk($subset, $callback, $data); + return $subset; + } + + /** + * Rotate a two-dimensional array variable + * @return NULL + * @param $var array + **/ + function transpose(array &$var) { + $out=[]; + foreach ($var as $keyx=>$cols) + foreach ($cols as $keyy=>$valy) + $out[$keyy][$keyx]=$valy; + $var=$out; + } + + /** + * Sort a multi-dimensional array variable on a specified column + * @return bool + * @param $var array + * @param $col mixed + * @param $order int + **/ + function sort(array &$var,$col,$order=SORT_ASC) { + uasort( + $var, + function($val1,$val2) use($col,$order) { + list($v1,$v2)=[$val1[$col],$val2[$col]]; + $out=is_numeric($v1) && is_numeric($v2)? + Base::instance()->sign($v1-$v2):strcmp($v1,$v2); + if ($order==SORT_DESC) + $out=-$out; + return $out; + } + ); + $var=array_values($var); + } + + /** + * Change the key of a two-dimensional array element + * @return NULL + * @param $var array + * @param $old string + * @param $new string + **/ + function changekey(array &$var,$old,$new) { + $keys=array_keys($var); + $vals=array_values($var); + $keys[array_search($old,$keys)]=$new; + $var=array_combine($keys,$vals); + } + + /** + * Return month calendar of specified date, with optional setting for + * first day of week (0 for Sunday) + * @return array + * @param $date string|int + * @param $first int + **/ + function calendar($date='now',$first=0) { + $out=FALSE; + if (extension_loaded('calendar')) { + if (is_string($date)) + $date=strtotime($date); + $parts=getdate($date); + $days=cal_days_in_month(CAL_GREGORIAN,$parts['mon'],$parts['year']); + $ref=date('w',strtotime(date('Y-m',$parts[0]).'-01'))+(7-$first)%7; + $out=[]; + for ($i=0;$i<$days;++$i) + $out[floor(($ref+$i)/7)][($ref+$i)%7]=$i+1; + } + return $out; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/session.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/session.php new file mode 100644 index 0000000..e4eb82a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/session.php @@ -0,0 +1,231 @@ +. + +*/ + +//! Cache-based session handler +class Session extends Magic { + + protected + //! Session ID + $sid, + //! Anti-CSRF token + $_csrf, + //! User agent + $_agent, + //! IP, + $_ip, + //! Suspect callback + $onsuspect, + //! Cache instance + $_cache, + //! Session meta data + $_data=[]; + + /** + * Open session + * @return TRUE + * @param $path string + * @param $name string + **/ + function open(string $path, string $name): bool + { + return TRUE; + } + + /** + * Close session + * @return TRUE + **/ + function close(): bool + { + $this->sid=NULL; + $this->_data=[]; + return TRUE; + } + + /** + * Return session data in serialized format + * @return false|string + * @param $id string + **/ + #[ReturnTypeWillChange] + function read(string $id) + { + $this->sid=$id; + if (!$data=$this->_cache->get($id.'.@')) + return ''; + $this->_data = $data; + if ($data['ip']!=$this->_ip || $data['agent']!=$this->_agent) { + $fw=Base::instance(); + if (!isset($this->onsuspect) || + $fw->call($this->onsuspect,[$this,$id])===FALSE) { + //NB: `session_destroy` can't be called at that stage (`session_start` not completed) + $this->destroy($id); + $this->close(); + unset($fw->{'COOKIE.'.session_name()}); + $fw->error(403); + } + } + return $data['data']; + } + + /** + * Write session data + */ + function write(string $id, string $data): bool + { + $fw=Base::instance(); + $jar=$fw->JAR; + $this->_cache->set($id.'.@', + [ + 'data'=>$data, + 'ip'=>$this->_ip, + 'agent'=>$this->_agent, + 'stamp'=>time() + ], + $jar['expire'] + ); + return TRUE; + } + + /** + * Destroy session + */ + function destroy(string $id): bool + { + $this->_cache->clear($id.'.@'); + return TRUE; + } + + /** + * Garbage collector + **/ + #[ReturnTypeWillChange] + function gc(int $max_lifetime) + { + $this->_cache->reset('.@',$max_lifetime); + return TRUE; + } + + /** + * Return session id (if session has started) + * @return string|NULL + **/ + function sid() { + return $this->sid; + } + + /** + * Return anti-CSRF token + * @return string + **/ + function csrf() { + return $this->_csrf; + } + + /** + * Return IP address + * @return string + **/ + function ip() { + return $this->_ip; + } + + /** + * Return Unix timestamp + * @return string|FALSE + **/ + function stamp() { + if (!$this->sid) + session_start(); + return $this->_cache->exists($this->sid.'.@',$data)? + $data['stamp']:FALSE; + } + + /** + * Return HTTP user agent + * @return string + **/ + function agent() { + return $this->_agent; + } + + /** + * Instantiate class + * @param $onsuspect callback + * @param $key string + **/ + function __construct($onsuspect=NULL,$key=NULL,$cache=null) { + $this->onsuspect=$onsuspect; + $this->_cache=$cache?:Cache::instance(); + if (version_compare(PHP_VERSION, '8.4.0')>=0) { + // TODO: remove this when php7 support is dropped + session_set_save_handler(new SessionAdapter($this)); + } else { + session_set_save_handler( + [$this,'open'], + [$this,'close'], + [$this,'read'], + [$this,'write'], + [$this,'destroy'], + [$this,'gc'] + ); + } + register_shutdown_function('session_commit'); + $fw=\Base::instance(); + $headers=$fw->HEADERS; + $this->_csrf=$fw->hash($fw->SEED. + extension_loaded('openssl')? + implode(unpack('L',openssl_random_pseudo_bytes(4))): + mt_rand() + ); + if ($key) + $fw->$key=$this->_csrf; + $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; + $this->_ip=$fw->IP; + } + + /** + * check latest meta data existence + * @param string $key + * @return bool + */ + function exists($key) { + return isset($this->_data[$key]); + } + + /** + * get meta data from latest session + * @param string $key + * @return mixed + */ + function &get($key) { + return $this->_data[$key]; + } + + function set($key,$val) { + trigger_error('Unable to set data on previous session'); + } + + function clear($key) { + trigger_error('Unable to clear data on previous session'); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/sessionadapter.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/sessionadapter.php new file mode 100644 index 0000000..8713118 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/sessionadapter.php @@ -0,0 +1,46 @@ +_handler = $handler; + } + + public function close(): bool + { + return $this->_handler->close(); + } + + public function destroy(string $id): bool + { + return $this->_handler->destroy($id); + } + + #[ReturnTypeWillChange] + public function gc(int $max_lifetime): int + { + return $this->_handler->gc($max_lifetime); + } + + public function open(string $path, string $name): bool + { + return $this->_handler->open($path, $name); + } + + #[ReturnTypeWillChange] + public function read(string $id): string + { + return $this->_handler->read($id); + } + + public function write(string $id, string $data): bool + { + return $this->_handler->write($id, $data); + } +} \ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/smtp.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/smtp.php new file mode 100644 index 0000000..2a31e9d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/smtp.php @@ -0,0 +1,363 @@ +. + +*/ + +//! SMTP plug-in +class SMTP extends Magic { + + //@{ Locale-specific error/exception messages + const + E_Header='%s: header is required', + E_Blank='Message must not be blank', + E_Attach='Attachment %s not found', + E_DIALOG='SMTP dialog error: %s'; + //@} + + protected + //! Message properties + $headers, + //! E-mail attachments + $attachments, + //! SMTP host + $host, + //! SMTP port + $port, + //! TLS/SSL + $scheme, + //! User ID + $user, + //! Password + $pw, + //! TLS/SSL stream context + $context, + //! TCP/IP socket + $socket, + //! Server-client conversation + $log; + + /** + * Fix header + * @return string + * @param $key string + **/ + protected function fixheader($key) { + return str_replace(' ','-', + ucwords(preg_replace('/[_\-]/',' ',strtolower($key)))); + } + + /** + * Return TRUE if header exists + * @return bool + * @param $key + **/ + function exists($key) { + $key=$this->fixheader($key); + return isset($this->headers[$key]); + } + + /** + * Bind value to e-mail header + * @return string + * @param $key string + * @param $val string + **/ + function set($key,$val) { + $key=$this->fixheader($key); + return $this->headers[$key]=$val; + } + + /** + * Return value of e-mail header + * @return string|NULL + * @param $key string + **/ + function &get($key) { + $key=$this->fixheader($key); + if (isset($this->headers[$key])) + $val=&$this->headers[$key]; + else + $val=NULL; + return $val; + } + + /** + * Remove header + * @return NULL + * @param $key string + **/ + function clear($key) { + $key=$this->fixheader($key); + unset($this->headers[$key]); + } + + /** + * Return client-server conversation history + * @return string + **/ + function log() { + return str_replace("\n",PHP_EOL,$this->log); + } + + /** + * Send SMTP command and record server response + * @return string + * @param $cmd string + * @param $log bool|string + * @param $mock bool + **/ + protected function dialog($cmd=NULL,$log=TRUE,$mock=FALSE) { + $reply=''; + if ($mock) { + $host=str_replace('ssl://','',$this->host); + switch ($cmd) { + case NULL: + $reply='220 '.$host.' ESMTP ready'."\n"; + break; + case 'DATA': + $reply='354 Go ahead'."\n"; + break; + case 'QUIT': + $reply='221 '.$host.' closing connection'."\n"; + break; + default: + $reply='250 OK'."\n"; + break; + } + } + else { + $socket=&$this->socket; + if ($cmd) + fputs($socket,$cmd."\r\n"); + while (!feof($socket) && ($info=stream_get_meta_data($socket)) && + !$info['timed_out'] && $str=fgets($socket,4096)) { + $reply.=$str; + if (preg_match('/(?:^|\n)\d{3} .+?\r\n/s',$reply)) + break; + } + } + if ($log) { + if ($cmd) + $this->log.=$cmd."\n"; + $this->log.=str_replace("\r",'',$reply); + } + if (preg_match('/^(4|5)\d{2}\s.*$/', $reply)) + throw new \Exception(sprintf(self::E_DIALOG,$reply)); + return $reply; + } + + /** + * Add e-mail attachment + * @return NULL + * @param $file string + * @param $alias string + * @param $cid string + **/ + function attach($file,$alias=NULL,$cid=NULL) { + if (!is_file($file)) + throw new \Exception(sprintf(self::E_Attach,$file)); + if ($alias) + $file=[$alias,$file]; + $this->attachments[]=['filename'=>$file,'cid'=>$cid]; + } + + /** + * Transmit message + * @return bool + * @param $message string + * @param $log bool|string + * @param $mock bool + **/ + function send($message,$log=TRUE,$mock=FALSE) { + if ($this->scheme=='ssl' && !extension_loaded('openssl')) + return FALSE; + // Message should not be blank + if (!$message) + throw new \Exception(self::E_Blank); + $fw=Base::instance(); + // Retrieve headers + $headers=$this->headers; + // Connect to the server + if (!$mock) { + $socket=&$this->socket; + $socket=@stream_socket_client($this->host.':'.$this->port, + $errno,$errstr,ini_get('default_socket_timeout'), + STREAM_CLIENT_CONNECT,$this->context); + if (!$socket) { + $fw->error(500,$errstr); + return FALSE; + } + stream_set_blocking($socket,TRUE); + } + // Get server's initial response + $this->dialog(NULL,$log,$mock); + // Announce presence + $reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock); + if (strtolower($this->scheme)=='tls') { + $this->dialog('STARTTLS',$log,$mock); + if (!$mock) { + $method=STREAM_CRYPTO_METHOD_TLS_CLIENT; + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { + $method|=STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + $method|=STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + } + stream_socket_enable_crypto($socket,TRUE,$method); + } + $reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock); + } + $message=wordwrap($message,998); + if (preg_match('/8BITMIME/',$reply)) + $headers['Content-Transfer-Encoding']='8bit'; + else { + $headers['Content-Transfer-Encoding']='quoted-printable'; + $message=preg_replace('/^\.(.+)/m', + '..$1',quoted_printable_encode($message)); + } + if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) { + // Authenticate + $this->dialog('AUTH LOGIN',$log,$mock); + $this->dialog(base64_encode($this->user),$log,$mock); + $reply=$this->dialog(base64_encode($this->pw),$log,$mock); + if (!preg_match('/^235\s.*/',$reply)) { + $this->dialog('QUIT',$log,$mock); + if (!$mock && $socket) + fclose($socket); + return FALSE; + } + } + if (empty($headers['Message-Id'])) + $headers['Message-Id']='<'.uniqid('',TRUE).'@'.$this->host.'>'; + if (empty($headers['Date'])) + $headers['Date']=date('r'); + // Required headers + $reqd=['From','To','Subject']; + foreach ($reqd as $id) + if (empty($headers[$id])) + throw new \Exception(sprintf(self::E_Header,$id)); + $eol="\r\n"; + // Stringify headers + foreach ($headers as $key=>&$val) { + if (in_array($key,['From','To','Cc','Bcc'])) { + $email=''; + preg_match_all('/(?:".+?" |=\?.+?\?= )?(?:<.+?>|[^ ,]+)/', + $val,$matches,PREG_SET_ORDER); + foreach ($matches as $raw) + $email.=($email?', ':''). + (preg_match('/<.+?>/',$raw[0])? + $raw[0]: + ('<'.$raw[0].'>')); + $val=$email; + } + unset($val); + } + $from=isset($headers['Sender'])?$headers['Sender']:strstr($headers['From'],'<'); + unset($headers['Sender']); + // Start message dialog + $this->dialog('MAIL FROM: '.$from,$log,$mock); + foreach ($fw->split($headers['To']. + (isset($headers['Cc'])?(';'.$headers['Cc']):''). + (isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst) { + $this->dialog('RCPT TO: '.strstr($dst,'<'),$log,$mock); + } + $this->dialog('DATA',$log,$mock); + if ($this->attachments) { + // Replace Content-Type + $type=$headers['Content-Type']; + unset($headers['Content-Type']); + $enc=$headers['Content-Transfer-Encoding']; + unset($headers['Content-Transfer-Encoding']); + $hash=uniqid('',TRUE); + // Send mail headers + $out='Content-Type: multipart/mixed; boundary="'.$hash.'"'.$eol; + foreach ($headers as $key=>$val) + if ($key!='Bcc') + $out.=$key.': '.$val.$eol; + $out.=$eol; + $out.='This is a multi-part message in MIME format'.$eol; + $out.=$eol; + $out.='--'.$hash.$eol; + $out.='Content-Type: '.$type.$eol; + $out.='Content-Transfer-Encoding: '.$enc.$eol; + $out.=$eol; + $out.=$message.$eol; + foreach ($this->attachments as $attachment) { + if (is_array($attachment['filename'])) + list($alias,$file)=$attachment['filename']; + else + $alias=basename($file=$attachment['filename']); + $out.='--'.$hash.$eol; + $out.='Content-Type: application/octet-stream'.$eol; + $out.='Content-Transfer-Encoding: base64'.$eol; + if ($attachment['cid']) + $out.='Content-Id: '.$attachment['cid'].$eol; + $out.='Content-Disposition: attachment; '. + 'filename="'.$alias.'"'.$eol; + $out.=$eol; + $out.=chunk_split(base64_encode( + file_get_contents($file))).$eol; + } + $out.=$eol; + $out.='--'.$hash.'--'.$eol; + $out.='.'; + $this->dialog($out,preg_match('/verbose/i',$log),$mock); + } + else { + // Send mail headers + $out=''; + foreach ($headers as $key=>$val) + if ($key!='Bcc') + $out.=$key.': '.$val.$eol; + $out.=$eol; + $out.=$message.$eol; + $out.='.'; + // Send message + $this->dialog($out,preg_match('/verbose/i',$log),$mock); + } + $this->dialog('QUIT',$log,$mock); + if (!$mock && $socket) + fclose($socket); + return TRUE; + } + + /** + * Instantiate class + * @param $host string + * @param $port int + * @param $scheme string + * @param $user string + * @param $pw string + * @param $ctx resource + **/ + function __construct( + $host='localhost',$port=25,$scheme=NULL,$user=NULL,$pw=NULL,$ctx=NULL) { + $this->headers=[ + 'MIME-Version'=>'1.0', + 'Content-Type'=>'text/plain; '. + 'charset='.Base::instance()->ENCODING + ]; + $this->host=strtolower((($this->scheme=strtolower($scheme?:''))=='ssl'? + 'ssl':'tcp').'://'.$host); + $this->port=$port; + $this->user=$user; + $this->pw=$pw; + $this->context=stream_context_create($ctx); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/template.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/template.php new file mode 100644 index 0000000..6250ce0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/template.php @@ -0,0 +1,353 @@ +. + +*/ + +//! XML-style template engine +class Template extends Preview { + + //@{ Error messages + const + E_Method='Call to undefined method %s()'; + //@} + + protected + //! Template tags + $tags, + //! Custom tag handlers + $custom=[]; + + /** + * Template -set- tag handler + * @return string + * @param $node array + **/ + protected function _set(array $node) { + $out=''; + foreach ($node['@attrib'] as $key=>$val) + $out.='$'.$key.'='. + (preg_match('/\{\{(.+?)\}\}/',$val?:'')? + $this->token($val): + Base::instance()->stringify($val)).'; '; + return ''; + } + + /** + * Template -include- tag handler + * @return string + * @param $node array + **/ + protected function _include(array $node) { + $attrib=$node['@attrib']; + $hive=isset($attrib['with']) && + ($attrib['with']=$this->token($attrib['with'])) && + preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/', + $attrib['with'],$pairs,PREG_SET_ORDER)? + ('['.implode(',', + array_map(function($pair) { + return '\''.$pair[1].'\'=>'. + (preg_match('/^\'.*\'$/',$pair[2]) || + preg_match('/\$/',$pair[2])? + $pair[2]:Base::instance()->stringify( + Base::instance()->cast($pair[2]))); + },$pairs)).']+get_defined_vars()'): + 'get_defined_vars()'; + $ttl=isset($attrib['ttl'])?(int)$attrib['ttl']:0; + return + 'token($attrib['if']).') '):''). + ('echo $this->render('. + (preg_match('/^\{\{(.+?)\}\}$/',$attrib['href'])? + $this->token($attrib['href']): + Base::instance()->stringify($attrib['href'])).','. + 'NULL,'.$hive.','.$ttl.'); ?>'); + } + + /** + * Template -exclude- tag handler + * @return string + **/ + protected function _exclude() { + return ''; + } + + /** + * Template -ignore- tag handler + * @return string + * @param $node array + **/ + protected function _ignore(array $node) { + return $node[0]; + } + + /** + * Template -loop- tag handler + * @return string + * @param $node array + **/ + protected function _loop(array $node) { + $attrib=$node['@attrib']; + unset($node['@attrib']); + return + 'token($attrib['from']).';'. + $this->token($attrib['to']).';'. + $this->token($attrib['step']).'): ?>'. + $this->build($node). + ''; + } + + /** + * Template -repeat- tag handler + * @return string + * @param $node array + **/ + protected function _repeat(array $node) { + $attrib=$node['@attrib']; + unset($node['@attrib']); + return + 'token($attrib['counter'])).'=0; '):''). + 'foreach (('. + $this->token($attrib['group']).'?:[]) as '. + (isset($attrib['key'])? + ($this->token($attrib['key']).'=>'):''). + $this->token($attrib['value']).'):'. + (isset($ctr)?(' '.$ctr.'++;'):'').' ?>'. + $this->build($node). + ''; + } + + /** + * Template -check- tag handler + * @return string + * @param $node array + **/ + protected function _check(array $node) { + $attrib=$node['@attrib']; + unset($node['@attrib']); + // Grab and blocks + foreach ($node as $pos=>$block) + if (isset($block['true'])) + $true=[$pos,$block]; + elseif (isset($block['false'])) + $false=[$pos,$block]; + if (isset($true,$false) && $true[0]>$false[0]) + // Reverse and blocks + list($node[$true[0]],$node[$false[0]])=[$false[1],$true[1]]; + return + 'token($attrib['if']).'): ?>'. + $this->build($node). + ''; + } + + /** + * Template -true- tag handler + * @return string + * @param $node array + **/ + protected function _true(array $node) { + return $this->build($node); + } + + /** + * Template -false- tag handler + * @return string + * @param $node array + **/ + protected function _false(array $node) { + return ''.$this->build($node); + } + + /** + * Template -switch- tag handler + * @return string + * @param $node array + **/ + protected function _switch(array $node) { + $attrib=$node['@attrib']; + unset($node['@attrib']); + foreach ($node as $pos=>$block) + if (is_string($block) && !preg_replace('/\s+/','',$block)) + unset($node[$pos]); + return + 'token($attrib['expr']).'): ?>'. + $this->build($node). + ''; + } + + /** + * Template -case- tag handler + * @return string + * @param $node array + **/ + protected function _case(array $node) { + $attrib=$node['@attrib']; + unset($node['@attrib']); + return + 'token($attrib['value']): + Base::instance()->stringify($attrib['value'])).': ?>'. + $this->build($node). + 'token($attrib['break']).') ':''). + 'break; ?>'; + } + + /** + * Template -default- tag handler + * @return string + * @param $node array + **/ + protected function _default(array $node) { + return + ''. + $this->build($node). + ''; + } + + /** + * Assemble markup + * @return string + * @param $node array|string + **/ + function build($node) { + if (is_string($node)) + return parent::build($node); + $out=''; + foreach ($node as $key=>$val) + $out.=is_int($key)?$this->build($val):$this->{'_'.$key}($val); + return $out; + } + + /** + * Extend template with custom tag + * @return NULL + * @param $tag string + * @param $func callback + **/ + function extend($tag,$func) { + $this->tags.='|'.$tag; + $this->custom['_'.$tag]=$func; + } + + /** + * Call custom tag handler + * @return string|FALSE + * @param $func string + * @param $args array + **/ + function __call($func,array $args) { + if ($func[0]=='_') + return call_user_func_array($this->custom[$func],$args); + if (method_exists($this,$func)) + return call_user_func_array([$this,$func],$args); + throw new \Exception(sprintf(self::E_Method,$func)); + } + + /** + * Parse string for template directives and tokens + * @return array + * @param $text string + **/ + function parse($text) { + $text=parent::parse($text); + // Build tree structure + for ($ptr=0,$w=5,$len=strlen($text),$tree=[],$tmp='';$ptr<$len;) + if (preg_match('/^(.{0,'.$w.'}?)<(\/?)(?:F3:)?'. + '('.$this->tags.')\b((?:\s+[\w.:@!\-]+'. + '(?:\h*=\h*(?:"(?:.*?)"|\'(?:.*?)\'))?|'. + '\h*\{\{.+?\}\})*)\s*(\/?)>/is', + substr($text,$ptr),$match)) { + if (strlen($tmp) || isset($match[1])) + $tree[]=$tmp.$match[1]; + // Element node + if ($match[2]) { + // Find matching start tag + $stack=[]; + for($i=count($tree)-1;$i>=0;--$i) { + $item=$tree[$i]; + if (is_array($item) && + array_key_exists($k=strtolower($match[3]),$item) && + !isset($item[$k][0])) { + // Start tag found + $tree[$i][$k]+=array_reverse($stack); + $tree=array_slice($tree,0,$i+1); + break; + } + else $stack[]=$item; + } + } + else { + // Start tag + $node=&$tree[][strtolower($match[3])]; + $node=[]; + if ($match[4]) { + // Process attributes + preg_match_all( + '/(?:(\{\{.+?\}\})|([^\s\/"\'=]+))'. + '\h*(?:=\h*(?:"(.*?)"|\'(.*?)\'))?/s', + $match[4],$attr,PREG_SET_ORDER); + foreach ($attr as $kv) + if (!empty($kv[1]) && !isset($kv[3]) && !isset($kv[4])) + $node['@attrib'][]=$kv[1]; + else + $node['@attrib'][$kv[1]?:$kv[2]]= + (isset($kv[3]) && $kv[3]!==''? + $kv[3]: + (isset($kv[4]) && $kv[4]!==''? + $kv[4]:NULL)); + } + } + $tmp=''; + $ptr+=strlen($match[0]); + $w=5; + } + else { + // Text node + $tmp.=substr($text,$ptr,$w); + $ptr+=$w; + if ($w<50) + ++$w; + } + if (strlen($tmp)) + // Append trailing text + $tree[]=$tmp; + // Break references + unset($node); + return $tree; + } + + /** + * Class constructor + * return object + **/ + function __construct() { + $ref=new ReflectionClass(get_called_class()); + $this->tags=''; + foreach ($ref->getmethods() as $method) + if (preg_match('/^_(?=[[:alpha:]])/',$method->name)) + $this->tags.=(strlen($this->tags)?'|':''). + substr($method->name,1); + parent::__construct(); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/test.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/test.php new file mode 100644 index 0000000..d45bb18 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/test.php @@ -0,0 +1,98 @@ +. + +*/ + +//! Unit test kit +class Test { + + //@{ Reporting level + const + FLAG_False=0, + FLAG_True=1, + FLAG_Both=2; + //@} + + protected + //! Test results + $data=[], + //! Success indicator + $passed=TRUE, + //! Reporting level + $level; + + /** + * Return test results + * @return array + **/ + function results() { + return $this->data; + } + + /** + * Return FALSE if at least one test case fails + * @return bool + **/ + function passed() { + return $this->passed; + } + + /** + * Evaluate condition and save test result + * @return object + * @param $cond bool + * @param $text string + **/ + function expect($cond,$text=NULL) { + $out=(bool)$cond; + if ($this->level==$out || $this->level==self::FLAG_Both) { + $data=['status'=>$out,'text'=>$text,'source'=>NULL]; + foreach (debug_backtrace() as $frame) + if (isset($frame['file'])) { + $data['source']=Base::instance()-> + fixslashes($frame['file']).':'.$frame['line']; + break; + } + $this->data[]=$data; + } + if (!$out && $this->passed) + $this->passed=FALSE; + return $this; + } + + /** + * Append message to test results + * @return NULL + * @param $text string + **/ + function message($text) { + $this->expect(TRUE,$text); + } + + /** + * Class constructor + * @return NULL + * @param $level int + **/ + function __construct($level=self::FLAG_Both) { + $this->level=$level; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/utf.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/utf.php new file mode 100644 index 0000000..34b8230 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/utf.php @@ -0,0 +1,199 @@ +. + +*/ + +//! Unicode string manager +class UTF extends Prefab { + + /** + * Get string length + * @return int + * @param $str string + **/ + function strlen($str) { + preg_match_all('/./us',$str,$parts); + return count($parts[0]); + } + + /** + * Reverse a string + * @return string + * @param $str string + **/ + function strrev($str) { + preg_match_all('/./us',$str,$parts); + return implode('',array_reverse($parts[0])); + } + + /** + * Find position of first occurrence of a string (case-insensitive) + * @return int|FALSE + * @param $stack string + * @param $needle string + * @param $ofs int + **/ + function stripos($stack,$needle,$ofs=0) { + return $this->strpos($stack,$needle,$ofs,TRUE); + } + + /** + * Find position of first occurrence of a string + * @return int|FALSE + * @param $stack string + * @param $needle string + * @param $ofs int + * @param $case bool + **/ + function strpos($stack,$needle,$ofs=0,$case=FALSE) { + return preg_match('/^(.{'.$ofs.'}.*?)'. + preg_quote($needle,'/').'/us'.($case?'i':''),$stack,$match)? + $this->strlen($match[1]):FALSE; + } + + /** + * Returns part of haystack string from the first occurrence of + * needle to the end of haystack (case-insensitive) + * @return string|FALSE + * @param $stack string + * @param $needle string + * @param $before bool + **/ + function stristr($stack,$needle,$before=FALSE) { + return $this->strstr($stack,$needle,$before,TRUE); + } + + /** + * Returns part of haystack string from the first occurrence of + * needle to the end of haystack + * @return string|FALSE + * @param $stack string + * @param $needle string + * @param $before bool + * @param $case bool + **/ + function strstr($stack,$needle,$before=FALSE,$case=FALSE) { + if (!$needle) + return FALSE; + preg_match('/^(.*?)'.preg_quote($needle,'/').'/us'.($case?'i':''), + $stack,$match); + return isset($match[1])? + ($before? + $match[1]: + $this->substr($stack,$this->strlen($match[1]))): + FALSE; + } + + /** + * Return part of a string + * @return string|FALSE + * @param $str string + * @param $start int + * @param $len int + **/ + function substr($str,$start,$len=0) { + if ($start<0) + $start=$this->strlen($str)+$start; + if (!$len) + $len=$this->strlen($str)-$start; + return preg_match('/^.{'.$start.'}(.{0,'.$len.'})/us',$str,$match)? + $match[1]:FALSE; + } + + /** + * Count the number of substring occurrences + * @return int + * @param $stack string + * @param $needle string + **/ + function substr_count($stack,$needle) { + preg_match_all('/'.preg_quote($needle,'/').'/us',$stack, + $matches,PREG_SET_ORDER); + return count($matches); + } + + /** + * Strip whitespaces from the beginning of a string + * @return string + * @param $str string + **/ + function ltrim($str) { + return preg_replace('/^[\pZ\pC]+/u','',$str); + } + + /** + * Strip whitespaces from the end of a string + * @return string + * @param $str string + **/ + function rtrim($str) { + return preg_replace('/[\pZ\pC]+$/u','',$str); + } + + /** + * Strip whitespaces from the beginning and end of a string + * @return string + * @param $str string + **/ + function trim($str) { + return preg_replace('/^[\pZ\pC]+|[\pZ\pC]+$/u','',$str); + } + + /** + * Return UTF-8 byte order mark + * @return string + **/ + function bom() { + return chr(0xef).chr(0xbb).chr(0xbf); + } + + /** + * Convert code points to Unicode symbols + * @return string + * @param $str string + **/ + function translate($str) { + return html_entity_decode( + preg_replace('/\\\\u([[:xdigit:]]+)/i','&#x\1;',$str)); + } + + /** + * Translate emoji tokens to Unicode font-supported symbols + * @return string + * @param $str string + **/ + function emojify($str) { + $map=[ + ':('=>'\u2639', // frown + ':)'=>'\u263a', // smile + '<3'=>'\u2665', // heart + ':D'=>'\u1f603', // grin + 'XD'=>'\u1f606', // laugh + ';)'=>'\u1f609', // wink + ':P'=>'\u1f60b', // tongue + ':,'=>'\u1f60f', // think + ':/'=>'\u1f623', // skeptic + '8O'=>'\u1f632', // oops + ]+Base::instance()->EMOJI; + return $this->translate(str_replace(array_keys($map), + array_values($map),$str)); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web.php new file mode 100644 index 0000000..afecb3d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web.php @@ -0,0 +1,1024 @@ +. + +*/ + +//! Wrapper for various HTTP utilities +class Web extends Prefab { + + //@{ Error messages + const + E_Request='No suitable HTTP request engine found'; + //@} + + protected + //! HTTP request engine + $wrapper; + + /** + * Detect MIME type using file extension or file inspection + * @return string + * @param $file string + * @param $inspect bool + **/ + function mime($file, $inspect=FALSE) { + if ($inspect) { + if (is_file($file) && is_readable($file)) { + // physical files + if (extension_loaded('fileinfo')) + $mime=mime_content_type($file); + elseif (preg_match('/Darwin/i',PHP_OS)) + $mime=trim(exec('file -bI '.escapeshellarg($file))); + elseif (!preg_match('/^win/i',PHP_OS)) + $mime=trim(exec('file -bi '.escapeshellarg($file))); + if (isset($mime) && !empty($mime)){ + // cut charset information if any + $exp=explode(';',$mime,2); + $mime=$exp[0]; + } + } + else { + // remote and stream files + if (ini_get('allow_url_fopen') && ($fhandle=fopen($file,'rb'))) { + // only get head bytes instead of whole file + $bytes=fread($fhandle,20); + fclose($fhandle); + } + elseif (($response=$this->request($file,['method' => 'HEAD'])) + && preg_grep('/HTTP\/[\d.]{1,3} 200/',$response['headers']) + && ($type = preg_grep('/^Content-Type:/i',$response['headers']))) { + // get mime type directly from response header + return preg_replace('/^Content-Type:\s*/i','',array_pop($type)); + } + else // load whole file + $bytes=file_get_contents($file); + if (extension_loaded('fileinfo')) { + // get mime from fileinfo + $finfo=finfo_open(FILEINFO_MIME_TYPE); + $mime=finfo_buffer($finfo,$bytes); + } + elseif ($bytes) { + // magic number header fallback + $map=[ + '\x64\x6E\x73\x2E'=>'audio/basic', + '\x52\x49\x46\x46.{4}\x41\x56\x49\x20\x4C\x49\x53\x54'=>'video/avi', + '\x42\x4d'=>'image/bmp', + '\x42\x5A\x68'=>'application/x-bzip2', + '\x07\x64\x74\x32\x64\x64\x74\x64'=>'application/xml-dtd', + '\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1'=>'application/msword', + '\x50\x4B\x03\x04\x14\x00\x06\x00'=>'application/msword', + '\x0D\x44\x4F\x43'=>'application/msword', + 'GIF\d+a'=>'image/gif', + '\x1F\x8B'=>'application/x-gzip', + '\xff\xd8\xff'=>'image/jpeg', + '\x49\x46\x00'=>'image/jpeg', + '\xFF\xFB'=>'audio/mpeg', + '\x49\x44\x33'=>'audio/mpeg', + '\x00\x00\x01\xBA'=>'video/mpeg', + '\x4F\x67\x67\x53\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00'=>'audio/vorbis', + '\x25\x50\x44\x46'=>'application/pdf', + '\x89PNG\x0d\x0a'=>'image/png', + '.{4}\x6D\x6F\x6F\x76\x'=>'video/quicktime', + '\x53\x49\x54\x21\x00'=>'application/x-stuffit', + '\x43\x57\x53'=>'application/x-shockwave-flash', + '\x1F\x8B\x08'=>'application/x-tar', + '\x49\x20\x49'=>'image/tiff', + '\x52\x49\x46\x46.{4}\x57\x41\x56\x45\x66\x6D\x74\x20'=>'audio/wav', + '\xFD\xFF\xFF\xFF\x20\x00\x00\x00'=>'application/vnd.ms-excel', + '\x50\x4B\x03\x04'=>'application/x-zip-compressed', + '[ -~]+$'=>'text/plain', + ]; + foreach ($map as $key=>$val) + if (preg_match('/^'.$key.'/',substr($bytes,0,128))) + return $val; + } + } + if (isset($mime) && !empty($mime)) + return $mime; + // Fallback to file extension-based check if no mime was found yet + } + if (preg_match('/\w+$/',$file,$ext)) { + $map=[ + 'au'=>'audio/basic', + 'avi'=>'video/avi', + 'bmp'=>'image/bmp', + 'bz2'=>'application/x-bzip2', + 'css'=>'text/css', + 'dtd'=>'application/xml-dtd', + 'doc'=>'application/msword', + 'gif'=>'image/gif', + 'gz'=>'application/x-gzip', + 'hqx'=>'application/mac-binhex40', + 'html?'=>'text/html', + 'jar'=>'application/java-archive', + 'jpe?g|jfif?'=>'image/jpeg', + 'js'=>'application/x-javascript', + 'midi'=>'audio/x-midi', + 'mp3'=>'audio/mpeg', + 'mpe?g'=>'video/mpeg', + 'ogg'=>'audio/vorbis', + 'pdf'=>'application/pdf', + 'png'=>'image/png', + 'ppt'=>'application/vnd.ms-powerpoint', + 'ps'=>'application/postscript', + 'qt'=>'video/quicktime', + 'ram?'=>'audio/x-pn-realaudio', + 'rdf'=>'application/rdf', + 'rtf'=>'application/rtf', + 'sgml?'=>'text/sgml', + 'sit'=>'application/x-stuffit', + 'svg'=>'image/svg+xml', + 'swf'=>'application/x-shockwave-flash', + 'tgz'=>'application/x-tar', + 'tiff'=>'image/tiff', + 'txt'=>'text/plain', + 'wav'=>'audio/wav', + 'xls'=>'application/vnd.ms-excel', + 'xml'=>'application/xml', + 'zip'=>'application/x-zip-compressed' + ]; + foreach ($map as $key=>$val) + if (preg_match('/'.$key.'/',strtolower($ext[0]))) + return $val; + } + return 'application/octet-stream'; + } + + /** + * Return the MIME types stated in the HTTP Accept header as an array; + * If a list of MIME types is specified, return the best match; or + * FALSE if none found + * @return array|string|FALSE + * @param $list string|array + **/ + function acceptable($list=NULL) { + $accept=[]; + foreach (explode(',',str_replace(' ','',@$_SERVER['HTTP_ACCEPT'])) + as $mime) + if (preg_match('/(.+?)(?:;q=([\d\.]+)|$)/',$mime,$parts)) + $accept[$parts[1]]=isset($parts[2])?$parts[2]:1; + if (!$accept) + $accept['*/*']=1; + else { + krsort($accept); + arsort($accept); + } + if ($list) { + if (is_string($list)) + $list=explode(',',$list); + foreach ($accept as $mime=>$q) + if ($q && $out=preg_grep('/'. + str_replace('\*','.*',preg_quote($mime,'/')).'/',$list)) + return current($out); + return FALSE; + } + return $accept; + } + + /** + * Transmit file to HTTP client; Return file size if successful, + * FALSE otherwise + * @return int|FALSE + * @param $file string + * @param $mime string + * @param $kbps int + * @param $force bool + * @param $name string + * @param $flush bool + **/ + function send($file,$mime=NULL,$kbps=0,$force=TRUE,$name=NULL,$flush=TRUE) { + if (!is_file($file)) + return FALSE; + $size=filesize($file); + if (PHP_SAPI!='cli') { + header('Content-Type: '.($mime?:$this->mime($file))); + if ($force) + header('Content-Disposition: attachment; '. + 'filename="'.($name!==NULL?$name:basename($file)).'"'); + header('Accept-Ranges: bytes'); + header('Content-Length: '.$size); + header('X-Powered-By: '.Base::instance()->PACKAGE); + } + if (!$kbps && $flush) { + while (ob_get_level()) + ob_end_clean(); + readfile($file); + } + else { + $ctr=0; + $handle=fopen($file,'rb'); + $start=microtime(TRUE); + while (!feof($handle) && + ($info=stream_get_meta_data($handle)) && + !$info['timed_out'] && !connection_aborted()) { + if ($kbps) { + // Throttle output + ++$ctr; + if ($ctr/$kbps>$elapsed=microtime(TRUE)-$start) + usleep(round(1e6*($ctr/$kbps-$elapsed))); + } + // Send 1KiB and reset timer + echo fread($handle,1024); + if ($flush) { + ob_flush(); + flush(); + } + } + fclose($handle); + } + return $size; + } + + /** + * Receive file(s) from HTTP client + * @return array|bool + * @param $func callback + * @param $overwrite bool + * @param $slug callback|bool + **/ + function receive($func=NULL,$overwrite=FALSE,$slug=TRUE) { + $fw=Base::instance(); + $dir=$fw->UPLOADS; + if (!is_dir($dir)) + mkdir($dir,Base::MODE,TRUE); + if ($fw->VERB=='PUT') { + $tmp=$fw->TEMP.$fw->SEED.'.'.$fw->hash(uniqid()); + if (!$fw->RAW) + $fw->write($tmp,$fw->BODY); + else { + $src=@fopen('php://input','r'); + $dst=@fopen($tmp,'w'); + if (!$src || !$dst) + return FALSE; + while (!feof($src) && + ($info=stream_get_meta_data($src)) && + !$info['timed_out'] && $str=fgets($src,4096)) + fputs($dst,$str,strlen($str)); + fclose($dst); + fclose($src); + } + $base=basename($fw->URI); + $file=[ + 'name'=>$dir. + ($slug && preg_match('/(.+?)(\.\w+)?$/',$base,$parts)? + (is_callable($slug)? + $slug($base): + ($this->slug($parts[1]). + (isset($parts[2])?$parts[2]:''))): + $base), + 'tmp_name'=>$tmp, + 'type'=>$this->mime($base), + 'size'=>filesize($tmp) + ]; + return (!file_exists($file['name']) || $overwrite) && + (!$func || $fw->call($func,[$file])!==FALSE) && + rename($tmp,$file['name']); + } + $fetch=function($arr) use(&$fetch) { + if (!is_array($arr)) + return [$arr]; + $data=[]; + foreach($arr as $k=>$sub) + $data=array_merge($data,$fetch($sub)); + return $data; + }; + $out=[]; + foreach ($_FILES as $name=>$item) { + $files=[]; + foreach ($item as $k=>$mix) + foreach ($fetch($mix) as $i=>$val) + $files[$i][$k]=$val; + foreach ($files as $file) { + if (empty($file['name'])) + continue; + $base=basename($file['name']); + $file['name']=$dir. + ($slug && preg_match('/(.+?)(\.\w+)?$/',$base,$parts)? + (is_callable($slug)? + $slug($base,$name): + ($this->slug($parts[1]). + (isset($parts[2])?$parts[2]:''))): + $base); + $out[$file['name']]=!$file['error'] && + (!file_exists($file['name']) || $overwrite) && + (!$func || $fw->call($func,[$file,$name])!==FALSE) && + move_uploaded_file($file['tmp_name'],$file['name']); + } + } + return $out; + } + + /** + * Return upload progress in bytes, FALSE on failure + * @return int|FALSE + * @param $id string + **/ + function progress($id) { + // ID returned by session.upload_progress.name + return ini_get('session.upload_progress.enabled') && + isset($_SESSION[$id]['bytes_processed'])? + $_SESSION[$id]['bytes_processed']:FALSE; + } + + /** + * HTTP request via cURL + * @return array + * @param $url string + * @param $options array + **/ + protected function _curl($url,$options) { + $curl=curl_init($url); + if (!$open_basedir=ini_get('open_basedir')) + curl_setopt($curl,CURLOPT_FOLLOWLOCATION, + $options['follow_location']); + curl_setopt($curl,CURLOPT_MAXREDIRS, + $options['max_redirects']); + curl_setopt($curl,CURLOPT_PROTOCOLS,CURLPROTO_HTTP|CURLPROTO_HTTPS); + curl_setopt($curl,CURLOPT_REDIR_PROTOCOLS,CURLPROTO_HTTP|CURLPROTO_HTTPS); + curl_setopt($curl,CURLOPT_CUSTOMREQUEST,$options['method']); + if (isset($options['header'])) + curl_setopt($curl,CURLOPT_HTTPHEADER,$options['header']); + if (isset($options['content'])) + curl_setopt($curl,CURLOPT_POSTFIELDS,$options['content']); + if (isset($options['proxy'])) + curl_setopt($curl,CURLOPT_PROXY,$options['proxy']); + curl_setopt($curl,CURLOPT_ENCODING,isset($options['encoding']) + ? $options['encoding'] : 'gzip,deflate'); + $timeout=isset($options['timeout'])? + $options['timeout']: + ini_get('default_socket_timeout'); + curl_setopt($curl,CURLOPT_CONNECTTIMEOUT,$timeout); + curl_setopt($curl,CURLOPT_TIMEOUT,$timeout); + $headers=[]; + curl_setopt($curl,CURLOPT_HEADERFUNCTION, + // Callback for response headers + function($curl,$line) use(&$headers) { + if ($trim=trim($line)) + $headers[]=$trim; + return strlen($line); + } + ); + curl_setopt($curl,CURLOPT_SSL_VERIFYHOST,2); + curl_setopt($curl,CURLOPT_SSL_VERIFYPEER,FALSE); + ob_start(); + curl_exec($curl); + $err=curl_error($curl); + curl_close($curl); + $body=ob_get_clean(); + if (!$err && + $options['follow_location'] && $open_basedir && + preg_grep('/HTTP\/[\d.]{1,3} 3\d{2}/',$headers) && + preg_match('/^Location: (.+)$/m',implode(PHP_EOL,$headers),$loc)) { + --$options['max_redirects']; + if($loc[1][0] == '/') { + $parts=parse_url($url); + $loc[1]=$parts['scheme'].'://'.$parts['host']. + ((isset($parts['port']) && !in_array($parts['port'],[80,443])) + ?':'.$parts['port']:'').$loc[1]; + } + return $this->request($loc[1],$options); + } + return [ + 'body'=>$body, + 'headers'=>$headers, + 'engine'=>'cURL', + 'cached'=>FALSE, + 'error'=>$err + ]; + } + + /** + * HTTP request via PHP stream wrapper + * @return array + * @param $url string + * @param $options array + **/ + protected function _stream($url,$options) { + $eol="\r\n"; + if (isset($options['proxy'])) { + $options['proxy']=preg_replace('/https?/i','tcp',$options['proxy']); + $options['request_fulluri']=true; + if (preg_match('/socks4?/i',$options['proxy'])) + return $this->_socket($url,$options); + } + $options['header']=implode($eol,$options['header']); + $body=@file_get_contents($url,FALSE, + stream_context_create(['http'=>$options])); + $headers=isset($http_response_header)? + $http_response_header:[]; + $err=''; + if (is_string($body)) { + $match=NULL; + foreach ($headers as $header) + if (preg_match('/Content-Encoding: (.+)/i',$header,$match)) + break; + if ($match) + switch ($match[1]) { + case 'gzip': + $body=gzdecode($body); + break; + case 'deflate': + $body=gzuncompress($body); + break; + } + } + else { + $tmp=error_get_last(); + $err=$tmp['message']; + } + return [ + 'body'=>$body, + 'headers'=>$headers, + 'engine'=>'stream', + 'cached'=>FALSE, + 'error'=>$err + ]; + } + + /** + * HTTP request via low-level TCP/IP socket + * @return array + * @param $url string + * @param $options array + **/ + protected function _socket($url,$options) { + $eol="\r\n"; + $headers=[]; + $body=''; + $parts=parse_url($url); + $hostname=$parts['host']; + $proxy=false; + if ($parts['scheme']=='https') + $parts['host']='ssl://'.$parts['host']; + if (empty($parts['port'])) + $parts['port']=$parts['scheme']=='https'?443:80; + if (empty($parts['path'])) + $parts['path']='/'; + if (empty($parts['query'])) + $parts['query']=''; + if (isset($options['proxy'])) { + $req=$url; + $pp=parse_url($options['proxy']); + $proxy=$pp['scheme']; + if ($pp['scheme']=='https') + $pp['host']='ssl://'.$pp['host']; + if (empty($pp['port'])) + $pp['port']=$pp['scheme']=='https'?443:80; + $socket=@fsockopen($pp['host'],$pp['port'],$code,$err); + } else { + $req=$parts['path'].($parts['query']?('?'.$parts['query']):''); + $socket=@fsockopen($parts['host'],$parts['port'],$code,$err); + } + if ($socket) { + stream_set_blocking($socket,TRUE); + stream_set_timeout($socket,isset($options['timeout'])? + $options['timeout']:ini_get('default_socket_timeout')); + if ($proxy=='socks4') { + // SOCKS4; http://en.wikipedia.org/wiki/SOCKS#Protocol + $packet="\x04\x01".pack("n", $parts['port']). + pack("H*",dechex(ip2long(gethostbyname($hostname))))."\0"; + fputs($socket, $packet, strlen($packet)); + $response=fread($socket, 9); + if (strlen($response)==8 && (ord($response[0])==0 || ord($response[0])==4) + && ord($response[1])==90) { + $options['header'][]='Host: '.$hostname; + } else + $err='Socket Status '.ord($response[1]); + } + fputs($socket,$options['method'].' '.$req.' HTTP/1.0'.$eol); + fputs($socket,implode($eol,$options['header']).$eol.$eol); + if (isset($options['content'])) + fputs($socket,$options['content'].$eol); + // Get response + $content=''; + while (!feof($socket) && + ($info=stream_get_meta_data($socket)) && + !$info['timed_out'] && !connection_aborted() && + $str=fgets($socket,4096)) + $content.=$str; + fclose($socket); + $html=explode($eol.$eol,$content,2); + $body=isset($html[1])?$html[1]:''; + $headers=array_merge($headers,$current=explode($eol,$html[0])); + $match=NULL; + foreach ($current as $header) + if (preg_match('/Content-Encoding: (.+)/i',$header,$match)) + break; + if ($match) + switch ($match[1]) { + case 'gzip': + $body=gzdecode($body); + break; + case 'deflate': + $body=gzuncompress($body); + break; + } + if ($options['follow_location'] && + preg_grep('/HTTP\/[\d.]{1,3} 3\d{2}/',$headers) && + preg_match('/Location: (.+?)'.preg_quote($eol).'/', + $html[0],$loc)) { + --$options['max_redirects']; + return $this->request($loc[1],$options); + } + } + return [ + 'body'=>$body, + 'headers'=>$headers, + 'engine'=>'socket', + 'cached'=>FALSE, + 'error'=>$err + ]; + } + + /** + * Specify the HTTP request engine to use; If not available, + * fall back to an applicable substitute + * @return string + * @param $arg string + **/ + function engine($arg='curl') { + $arg=strtolower($arg); + $flags=[ + 'curl'=>extension_loaded('curl'), + 'stream'=>ini_get('allow_url_fopen'), + 'socket'=>function_exists('fsockopen') + ]; + if ($flags[$arg]) + return $this->wrapper=$arg; + foreach ($flags as $key=>$val) + if ($val) + return $this->wrapper=$key; + throw new \Exception(self::E_Request); + } + + /** + * Replace old headers with new elements + * @return NULL + * @param $old array + * @param $new string|array + **/ + function subst(array &$old,$new) { + if (is_string($new)) + $new=[$new]; + foreach ($new as $hdr) { + $old=preg_grep('/'.preg_quote(strstr($hdr,':',TRUE),'/').':.+/', + $old,PREG_GREP_INVERT); + array_push($old,$hdr); + } + } + + /** + * Submit HTTP request; Use HTTP context options (described in + * http://www.php.net/manual/en/context.http.php) if specified; + * Cache the page as instructed by remote server + * @return array|FALSE + * @param $url string + * @param $options array + **/ + function request($url,?array $options=NULL) { + $fw=Base::instance(); + $parts=parse_url($url); + if (empty($parts['scheme'])) { + // Local URL + $url=$fw->SCHEME.'://'.$fw->HOST. + (in_array($fw->PORT,[80,443])?'':(':'.$fw->PORT)). + ($url[0]!='/'?($fw->BASE.'/'):'').$url; + $parts=parse_url($url); + } + elseif (!preg_match('/https?/',$parts['scheme'])) + return FALSE; + if (!is_array($options)) + $options=[]; + if (empty($options['header'])) + $options['header']=[]; + elseif (is_string($options['header'])) + $options['header']=[$options['header']]; + if (!$this->wrapper) + $this->engine(); + if ($this->wrapper!='stream') { + // PHP streams can't cope with redirects when Host header is set + $this->subst($options['header'],'Host: '.$parts['host']); + } + $this->subst($options['header'], + [ + 'Accept-Encoding: '.(isset($options['encoding'])? + $options['encoding']:'gzip,deflate'), + 'User-Agent: '.(isset($options['user_agent'])? + $options['user_agent']: + 'Mozilla/5.0 (compatible; '.php_uname('s').')'), + 'Connection: close' + ] + ); + if (isset($options['content']) && is_string($options['content'])) { + if ($options['method']=='POST' && + !preg_grep('/^Content-Type:/i',$options['header'])) + $this->subst($options['header'], + 'Content-Type: application/x-www-form-urlencoded'); + $this->subst($options['header'], + 'Content-Length: '.strlen($options['content'])); + } + if (isset($parts['user'],$parts['pass'])) + $this->subst($options['header'], + 'Authorization: Basic '. + base64_encode($parts['user'].':'.$parts['pass']) + ); + $options+=[ + 'method'=>'GET', + 'header'=>$options['header'], + 'follow_location'=>TRUE, + 'max_redirects'=>20, + 'ignore_errors'=>FALSE + ]; + $eol="\r\n"; + if ($fw->CACHE && + preg_match('/GET|HEAD/',$options['method'])) { + $cache=Cache::instance(); + if ($cache->exists( + $hash=$fw->hash($options['method'].' '.$url).'.url',$data)) { + if (preg_match('/Last-Modified: (.+?)'.preg_quote($eol).'/', + implode($eol,$data['headers']),$mod)) + $this->subst($options['header'], + 'If-Modified-Since: '.$mod[1]); + } + } + $result=$this->{'_'.$this->wrapper}($url,$options); + if ($result && isset($cache)) { + if (preg_match('/HTTP\/[\d.]{1,3} 304/', + implode($eol,$result['headers']))) { + $result=$cache->get($hash); + $result['cached']=TRUE; + } + elseif (preg_match('/Cache-Control:(?:.*)max-age=(\d+)(?:,?.*'. + preg_quote($eol).')/i',implode($eol,$result['headers']),$exp)) + $cache->set($hash,$result,$exp[1]); + } + $req=[$options['method'].' '.$url]; + foreach ($options['header'] as $header) + array_push($req,$header); + return array_merge(['request'=>$req],$result); + } + + /** + * Strip Javascript/CSS files of extraneous whitespaces and comments; + * Return combined output as a minified string + * @return string + * @param $files string|array + * @param $mime string + * @param $header bool + * @param $path string + **/ + function minify($files,$mime=NULL,$header=TRUE,$path=NULL) { + $fw=Base::instance(); + if (is_string($files)) + $files=$fw->split($files); + if (!$mime) + $mime=$this->mime($files[0]); + preg_match('/\w+$/',$files[0],$ext); + $cache=Cache::instance(); + $dst=''; + if (!isset($path)) + $path=$fw->UI.';./'; + foreach (array_unique($fw->split($path,FALSE)) as $dir) + foreach ($files as $i=>$file) + if (is_file($save=$fw->fixslashes($dir.$file)) && + is_bool(strpos($save,'../')) && + preg_match('/\.(css|js)$/i',$file)) { + unset($files[$i]); + if ($fw->CACHE && + ($cached=$cache->exists( + $hash=$fw->hash($save).'.'.$ext[0],$data)) && + $cached[0]>filemtime($save)) + $dst.=$data; + else { + $data=''; + $src=$fw->read($save); + for ($ptr=0,$len=strlen($src);$ptr<$len;) { + if (preg_match('/^@import\h+url'. + '\(\h*([\'"])((?!(?:https?:)?\/\/).+?)\1\h*\)[^;]*;/', + substr($src,$ptr),$parts)) { + $path=dirname($file); + $data.=$this->minify( + ($path?($path.'/'):'').$parts[2], + $mime,$header + ); + $ptr+=strlen($parts[0]); + continue; + } + if ($ext[0]=='css'&&preg_match('/^url\(([^\'"].*?[^\'"])\)/i', + substr($src,$ptr),$parts)) { + $data.=$parts[0]; + $ptr+=strlen($parts[0]); + continue; + } + if ($src[$ptr]=='/') { + if ($src[$ptr+1]=='*') { + // Multiline comment + $str=strstr( + substr($src,$ptr+2),'*/',TRUE); + $ptr+=strlen($str)+4; + } + elseif ($src[$ptr+1]=='/') { + // Single-line comment + $str=strstr( + substr($src,$ptr+2),"\n",TRUE); + $ptr+=(empty($str))? + strlen(substr($src,$ptr)):strlen($str)+2; + } + else { + // Presume it's a regex pattern + $regex=TRUE; + // Backtrack and validate + for ($ofs=$ptr;$ofs;--$ofs) { + // Pattern should be preceded by + // open parenthesis, colon, + // object property or operator + if (preg_match( + '/(return|[(:=!+\-*&|])$/', + substr($src,0,$ofs))) { + $data.='/'; + ++$ptr; + while ($ptr<$len) { + $data.=$src[$ptr]; + ++$ptr; + if ($src[$ptr-1]=='\\') { + $data.=$src[$ptr]; + ++$ptr; + } + elseif ($src[$ptr-1]=='/') + break; + } + break; + } + elseif (!ctype_space($src[$ofs-1])) { + // Not a regex pattern + $regex=FALSE; + break; + } + } + if (!$regex) { + // Division operator + $data.=$src[$ptr]; + ++$ptr; + } + } + continue; + } + if (in_array($src[$ptr],['\'','"','`'])) { + $match=$src[$ptr]; + $data.=$match; + ++$ptr; + // String literal + while ($ptr<$len) { + $data.=$src[$ptr]; + ++$ptr; + if ($src[$ptr-1]=='\\') { + $data.=$src[$ptr]; + ++$ptr; + } + elseif ($src[$ptr-1]==$match) + break; + } + continue; + } + if (ctype_space($src[$ptr])) { + if ($ptr+1CACHE) + $cache->set($hash,$data); + $dst.=$data; + } + } + if (PHP_SAPI!='cli' && $header) + header('Content-Type: '.$mime.'; charset='.$fw->ENCODING); + return $dst; + } + + /** + * Retrieve RSS feed and return as an array + * @return array|FALSE + * @param $url string + * @param $max int + * @param $tags string + **/ + function rss($url,$max=10,$tags=NULL) { + if (!$data=$this->request($url)) + return FALSE; + // Suppress errors caused by invalid XML structures + libxml_use_internal_errors(TRUE); + $xml=simplexml_load_string($data['body'], + NULL,LIBXML_NOBLANKS|LIBXML_NOERROR); + if (!is_object($xml)) + return FALSE; + $out=[]; + if (isset($xml->channel)) { + $out['source']=(string)$xml->channel->title; + $max=min($max,count($xml->channel->item)); + for ($i=0;$i<$max;++$i) { + $item=$xml->channel->item[$i]; + $list=[''=>NULL]+$item->getnamespaces(TRUE); + $fields=[]; + foreach ($list as $ns=>$uri) + foreach ($item->children($uri) as $key=>$val) + $fields[$ns.($ns?':':'').$key]=(string)$val; + $out['feed'][]=$fields; + } + } + else + return FALSE; + Base::instance()->scrub($out,$tags); + return $out; + } + + /** + * Retrieve information from whois server + * @return string|FALSE + * @param $addr string + * @param $server string + **/ + function whois($addr,$server='whois.internic.net') { + $socket=@fsockopen($server,43,$errno,$errstr); + if (!$socket) + // Can't establish connection + return FALSE; + // Set connection timeout parameters + stream_set_blocking($socket,FALSE); + stream_set_timeout($socket,ini_get('default_socket_timeout')); + // Send request + fputs($socket,$addr."\r\n"); + $info=stream_get_meta_data($socket); + // Get response + $response=''; + while (!feof($socket) && !$info['timed_out']) { + $response.=fgets($socket,4096); // MDFK97 + $info=stream_get_meta_data($socket); + } + fclose($socket); + return $info['timed_out']?FALSE:trim($response); + } + + /** + * Return preset diacritics translation table + * @return array + **/ + function diacritics() { + return [ + 'Ǎ'=>'A','А'=>'A','Ā'=>'A','Ă'=>'A','Ą'=>'A','Å'=>'A', + 'Ǻ'=>'A','Ä'=>'Ae','Á'=>'A','À'=>'A','Ã'=>'A','Â'=>'A', + 'Æ'=>'AE','Ǽ'=>'AE','Б'=>'B','Ç'=>'C','Ć'=>'C','Ĉ'=>'C', + 'Č'=>'C','Ċ'=>'C','Ц'=>'C','Ч'=>'Ch','Ð'=>'Dj','Đ'=>'Dj', + 'Ď'=>'Dj','Д'=>'Dj','É'=>'E','Ę'=>'E','Ё'=>'E','Ė'=>'E', + 'Ê'=>'E','Ě'=>'E','Ē'=>'E','È'=>'E','Е'=>'E','Э'=>'E', + 'Ë'=>'E','Ĕ'=>'E','Ф'=>'F','Г'=>'G','Ģ'=>'G','Ġ'=>'G', + 'Ĝ'=>'G','Ğ'=>'G','Х'=>'H','Ĥ'=>'H','Ħ'=>'H','Ï'=>'I', + 'Ĭ'=>'I','İ'=>'I','Į'=>'I','Ī'=>'I','Í'=>'I','Ì'=>'I', + 'И'=>'I','Ǐ'=>'I','Ĩ'=>'I','Î'=>'I','IJ'=>'IJ','Ĵ'=>'J', + 'Й'=>'J','Я'=>'Ja','Ю'=>'Ju','К'=>'K','Ķ'=>'K','Ĺ'=>'L', + 'Л'=>'L','Ł'=>'L','Ŀ'=>'L','Ļ'=>'L','Ľ'=>'L','М'=>'M', + 'Н'=>'N','Ń'=>'N','Ñ'=>'N','Ņ'=>'N','Ň'=>'N','Ō'=>'O', + 'О'=>'O','Ǿ'=>'O','Ǒ'=>'O','Ơ'=>'O','Ŏ'=>'O','Ő'=>'O', + 'Ø'=>'O','Ö'=>'Oe','Õ'=>'O','Ó'=>'O','Ò'=>'O','Ô'=>'O', + 'Œ'=>'OE','П'=>'P','Ŗ'=>'R','Р'=>'R','Ř'=>'R','Ŕ'=>'R', + 'Ŝ'=>'S','Ş'=>'S','Š'=>'S','Ș'=>'S','Ś'=>'S','С'=>'S', + 'Ш'=>'Sh','Щ'=>'Shch','Ť'=>'T','Ŧ'=>'T','Ţ'=>'T','Ț'=>'T', + 'Т'=>'T','Ů'=>'U','Ű'=>'U','Ŭ'=>'U','Ũ'=>'U','Ų'=>'U', + 'Ū'=>'U','Ǜ'=>'U','Ǚ'=>'U','Ù'=>'U','Ú'=>'U','Ü'=>'Ue', + 'Ǘ'=>'U','Ǖ'=>'U','У'=>'U','Ư'=>'U','Ǔ'=>'U','Û'=>'U', + 'В'=>'V','Ŵ'=>'W','Ы'=>'Y','Ŷ'=>'Y','Ý'=>'Y','Ÿ'=>'Y', + 'Ź'=>'Z','З'=>'Z','Ż'=>'Z','Ž'=>'Z','Ж'=>'Zh','á'=>'a', + 'ă'=>'a','â'=>'a','à'=>'a','ā'=>'a','ǻ'=>'a','å'=>'a', + 'ä'=>'ae','ą'=>'a','ǎ'=>'a','ã'=>'a','а'=>'a','ª'=>'a', + 'æ'=>'ae','ǽ'=>'ae','б'=>'b','č'=>'c','ç'=>'c','ц'=>'c', + 'ċ'=>'c','ĉ'=>'c','ć'=>'c','ч'=>'ch','ð'=>'dj','ď'=>'dj', + 'д'=>'dj','đ'=>'dj','э'=>'e','é'=>'e','ё'=>'e','ë'=>'e', + 'ê'=>'e','е'=>'e','ĕ'=>'e','è'=>'e','ę'=>'e','ě'=>'e', + 'ė'=>'e','ē'=>'e','ƒ'=>'f','ф'=>'f','ġ'=>'g','ĝ'=>'g', + 'ğ'=>'g','г'=>'g','ģ'=>'g','х'=>'h','ĥ'=>'h','ħ'=>'h', + 'ǐ'=>'i','ĭ'=>'i','и'=>'i','ī'=>'i','ĩ'=>'i','į'=>'i', + 'ı'=>'i','ì'=>'i','î'=>'i','í'=>'i','ï'=>'i','ij'=>'ij', + 'ĵ'=>'j','й'=>'j','я'=>'ja','ю'=>'ju','ķ'=>'k','к'=>'k', + 'ľ'=>'l','ł'=>'l','ŀ'=>'l','ĺ'=>'l','ļ'=>'l','л'=>'l', + 'м'=>'m','ņ'=>'n','ñ'=>'n','ń'=>'n','н'=>'n','ň'=>'n', + 'ʼn'=>'n','ó'=>'o','ò'=>'o','ǒ'=>'o','ő'=>'o','о'=>'o', + 'ō'=>'o','º'=>'o','ơ'=>'o','ŏ'=>'o','ô'=>'o','ö'=>'oe', + 'õ'=>'o','ø'=>'o','ǿ'=>'o','œ'=>'oe','п'=>'p','р'=>'r', + 'ř'=>'r','ŕ'=>'r','ŗ'=>'r','ſ'=>'s','ŝ'=>'s','ș'=>'s', + 'š'=>'s','ś'=>'s','с'=>'s','ş'=>'s','ш'=>'sh','щ'=>'shch', + 'ß'=>'ss','ţ'=>'t','т'=>'t','ŧ'=>'t','ť'=>'t','ț'=>'t', + 'у'=>'u','ǘ'=>'u','ŭ'=>'u','û'=>'u','ú'=>'u','ų'=>'u', + 'ù'=>'u','ű'=>'u','ů'=>'u','ư'=>'u','ū'=>'u','ǚ'=>'u', + 'ǜ'=>'u','ǔ'=>'u','ǖ'=>'u','ũ'=>'u','ü'=>'ue','в'=>'v', + 'ŵ'=>'w','ы'=>'y','ÿ'=>'y','ý'=>'y','ŷ'=>'y','ź'=>'z', + 'ž'=>'z','з'=>'z','ż'=>'z','ж'=>'zh','ь'=>'','ъ'=>'', + 'њ'=>'nj','љ'=>'lj','ђ'=>'dj','џ'=>'dz','ћ'=>'c','ј'=>'j', + '\''=>'', + ]; + } + + /** + * Return a URL/filesystem-friendly version of string + * @return string + * @param $text string + * @param $separator string + **/ + function slug($text, $separator = '-') + { + return trim(strtolower(preg_replace( + '/([^\pL\pN])+/u', + $separator, + trim(strtr($text, Base::instance()->DIACRITICS + $this->diacritics())) + )), $separator); + } + + /** + * Return chunk of text from standard Lorem Ipsum passage + * @return string + * @param $count int + * @param $max int + * @param $std bool + **/ + function filler($count=1,$max=20,$std=TRUE) { + $out=''; + if ($std) + $out='Lorem ipsum dolor sit amet, consectetur adipisicing elit, '. + 'sed do eiusmod tempor incididunt ut labore et dolore magna '. + 'aliqua.'; + $rnd=explode(' ', + 'a ab ad accusamus adipisci alias aliquam amet animi aperiam '. + 'architecto asperiores aspernatur assumenda at atque aut beatae '. + 'blanditiis cillum commodi consequatur corporis corrupti culpa '. + 'cum cupiditate debitis delectus deleniti deserunt dicta '. + 'dignissimos distinctio dolor ducimus duis ea eaque earum eius '. + 'eligendi enim eos error esse est eum eveniet ex excepteur '. + 'exercitationem expedita explicabo facere facilis fugiat harum '. + 'hic id illum impedit in incidunt ipsa iste itaque iure iusto '. + 'laborum laudantium libero magnam maiores maxime minim minus '. + 'modi molestiae mollitia nam natus necessitatibus nemo neque '. + 'nesciunt nihil nisi nobis non nostrum nulla numquam occaecati '. + 'odio officia omnis optio pariatur perferendis perspiciatis '. + 'placeat porro possimus praesentium proident quae quia quibus '. + 'quo ratione recusandae reiciendis rem repellat reprehenderit '. + 'repudiandae rerum saepe sapiente sequi similique sint soluta '. + 'suscipit tempora tenetur totam ut ullam unde vel veniam vero '. + 'vitae voluptas'); + for ($i=0,$add=$count-(int)$std;$i<$add;++$i) { + shuffle($rnd); + $words=array_slice($rnd,0,mt_rand(3,$max)); + $out.=(!$std&&$i==0?'':' ').ucfirst(implode(' ',$words)).'.'; + } + return $out; + } + +} + +if (!function_exists('gzdecode')) { + + /** + * Decode gzip-compressed string + * @param $str string + * @return string + **/ + function gzdecode($str) { + $fw=Base::instance(); + if (!is_dir($tmp=$fw->TEMP)) + mkdir($tmp,Base::MODE,TRUE); + file_put_contents($file=$tmp.'/'.$fw->SEED.'.'. + $fw->hash(uniqid('',TRUE)).'.gz',$str,LOCK_EX); + ob_start(); + readgzfile($file); + $out=ob_get_clean(); + @unlink($file); + return $out; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/geo.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/geo.php new file mode 100644 index 0000000..98ba204 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/geo.php @@ -0,0 +1,111 @@ +. + +*/ + +namespace Web; + +//! Geo plug-in +class Geo extends \Prefab { + + /** + * Return information about specified Unix time zone + * @return array + * @param $zone string + **/ + function tzinfo($zone) { + $ref=new \DateTimeZone($zone); + $loc=$ref->getLocation(); + $trn=$ref->getTransitions($now=time(),$now); + $out=[ + 'offset'=>$ref-> + getOffset(new \DateTime('now',new \DateTimeZone('UTC')))/3600, + 'country'=>$loc['country_code'], + 'latitude'=>$loc['latitude'], + 'longitude'=>$loc['longitude'], + 'dst'=>$trn[0]['isdst'] + ]; + unset($ref); + return $out; + } + + /** + * Return geolocation data based on specified/auto-detected IP address + * @return array|FALSE + * @param $ip string + **/ + function location($ip=NULL) { + $fw=\Base::instance(); + $web=\Web::instance(); + if (!$ip) + $ip=$fw->IP; + $public=filter_var($ip,FILTER_VALIDATE_IP, + FILTER_FLAG_IPV4|FILTER_FLAG_IPV6| + FILTER_FLAG_NO_RES_RANGE|FILTER_FLAG_NO_PRIV_RANGE); + if (function_exists('geoip_db_avail') && + geoip_db_avail(GEOIP_CITY_EDITION_REV1) && + $out=@geoip_record_by_name($ip)) { + $out['request']=$ip; + $out['region_code']=$out['region']; + $out['region_name']=''; + if (!empty($out['country_code']) && !empty($out['region'])) + $out['region_name']=geoip_region_name_by_code( + $out['country_code'],$out['region'] + ); + unset($out['country_code3'],$out['region'],$out['postal_code']); + return $out; + } + if (($req=$web->request('http://www.geoplugin.net/json.gp'. + ($public?('?ip='.$ip):''))) && + $data=json_decode($req['body'],TRUE)) { + $out=[]; + foreach ($data as $key=>$val) + if (!strpos($key,'currency') && $key!=='geoplugin_status' + && $key!=='geoplugin_region') + $out[$fw->snakecase(substr($key, 10))]=$val; + return $out; + } + return FALSE; + } + + /** + * Return weather data based on specified latitude/longitude + * @return array|FALSE + * @param $latitude float + * @param $longitude float + * @param $key string + **/ + function weather($latitude,$longitude,$key) { + $fw=\Base::instance(); + $web=\Web::instance(); + $query=[ + 'lat'=>$latitude, + 'lon'=>$longitude, + 'APPID'=>$key, + 'units'=>'metric' + ]; + return ($req=$web->request( + 'http://api.openweathermap.org/data/2.5/weather?'. + http_build_query($query)))? + json_decode($req['body'],TRUE): + FALSE; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/google/recaptcha.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/google/recaptcha.php new file mode 100644 index 0000000..38fd2d0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/google/recaptcha.php @@ -0,0 +1,58 @@ +. + +*/ + +namespace Web\Google; + +//! Google ReCAPTCHA v2 plug-in +class Recaptcha { + + const + //! API URL + URL_Recaptcha='https://www.google.com/recaptcha/api/siteverify'; + + /** + * Verify reCAPTCHA response + * @param string $secret + * @param string $response + * @return bool + **/ + static function verify($secret,$response=NULL) { + $fw=\Base::instance(); + if (!isset($response)) + $response=$fw->{'POST.g-recaptcha-response'}; + if (!$response) + return FALSE; + $web=\Web::instance(); + $out=$web->request(self::URL_Recaptcha,[ + 'method'=>'POST', + 'content'=>http_build_query([ + 'secret'=>$secret, + 'response'=>$response, + 'remoteip'=>$fw->IP + ]), + ]); + return isset($out['body']) && + ($json=json_decode($out['body'],TRUE)) && + isset($json['success']) && $json['success']; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/google/staticmap.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/google/staticmap.php new file mode 100644 index 0000000..023103d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/google/staticmap.php @@ -0,0 +1,65 @@ +. + +*/ + +namespace Web\Google; + +//! Google Static Maps API v2 plug-in +class StaticMap { + + const + //! API URL + URL_Static='http://maps.googleapis.com/maps/api/staticmap'; + + protected + //! Query arguments + $query=array(); + + /** + * Specify API key-value pair via magic call + * @return object + * @param $func string + * @param $args array + **/ + function __call($func,array $args) { + $this->query[]=array($func,$args[0]); + return $this; + } + + /** + * Generate map + * @return string + **/ + function dump() { + $fw=\Base::instance(); + $web=\Web::instance(); + $out=''; + return ($req=$web->request( + self::URL_Static.'?'.array_reduce( + $this->query, + function($out,$item) { + return ($out.=($out?'&':''). + urlencode($item[0]).'='.urlencode($item[1])); + } + ))) && $req['body']?$req['body']:FALSE; + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/oauth2.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/oauth2.php new file mode 100644 index 0000000..1e242a6 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/oauth2.php @@ -0,0 +1,163 @@ +. + +*/ + +namespace Web; + +//! Lightweight OAuth2 client +class OAuth2 extends \Magic { + + protected + //! Scopes and claims + $args=[], + //! Encoding + $enc_type = PHP_QUERY_RFC1738; + + /** + * Return OAuth2 authentication URI + * @return string + * @param $endpoint string + * @param $query bool + **/ + function uri($endpoint,$query=TRUE) { + return $endpoint.($query?('?'. + http_build_query($this->args,'','&',$this->enc_type)):''); + } + + /** + * Send request to API/token endpoint + * @return string|array|FALSE + * @param $uri string + * @param $method string + * @param $token string + **/ + function request($uri,$method,$token=NULL) { + $web=\Web::instance(); + $options=[ + 'method'=>$method, + 'content'=>http_build_query($this->args,'','&',$this->enc_type), + 'header'=>['Accept: application/json'] + ]; + if ($token) + array_push($options['header'],'Authorization: Bearer '.$token); + elseif ($method=='POST' && isset($this->args['client_id'])) + array_push($options['header'],'Authorization: Basic '. + base64_encode( + $this->args['client_id'].':'. + $this->args['client_secret'] + ) + ); + $response=$web->request($uri,$options); + if ($response['error']) + throw new \Exception($response['error']); + if (isset($response['body'])) { + if (preg_grep('/^Content-Type:.*application\/json/i', + $response['headers'])) { + $token=json_decode($response['body'],TRUE); + if (isset($token['error_description'])) + throw new \Exception($token['error_description']); + if (isset($token['error'])) + throw new \Exception($token['error']); + return $token; + } + else + return $response['body']; + } + return FALSE; + } + + /** + * Parse JSON Web token + * @return array + * @param $token string + **/ + function jwt($token) { + return json_decode( + base64_decode( + str_replace(['-','_'],['+','/'],explode('.',$token)[1]) + ), + TRUE + ); + } + + /** + * change default url encoding type, i.E. PHP_QUERY_RFC3986 + * @param $type + */ + function setEncoding($type) { + $this->enc_type = $type; + } + + /** + * URL-safe base64 encoding + * @return array + * @param $data string + **/ + function b64url($data) { + return trim(strtr(base64_encode($data),'+/','-_'),'='); + } + + /** + * Return TRUE if scope/claim exists + * @return bool + * @param $key string + **/ + function exists($key) { + return isset($this->args[$key]); + } + + /** + * Bind value to scope/claim + * @return string + * @param $key string + * @param $val string + **/ + function set($key,$val) { + return $this->args[$key]=$val; + } + + /** + * Return value of scope/claim + * @return mixed + * @param $key string + **/ + function &get($key) { + if (isset($this->args[$key])) + $val=&$this->args[$key]; + else + $val=NULL; + return $val; + } + + /** + * Remove scope/claim + * @return NULL + * @param $key string + **/ + function clear($key=NULL) { + if ($key) + unset($this->args[$key]); + else + $this->args=[]; + } + +} + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/openid.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/openid.php new file mode 100644 index 0000000..96b8c2b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/openid.php @@ -0,0 +1,248 @@ +. + +*/ + +namespace Web; + +//! OpenID consumer +class OpenID extends \Magic { + + protected + //! OpenID provider endpoint URL + $url, + //! HTTP request parameters + $args=[]; + + /** + * Determine OpenID provider + * @return string|FALSE + * @param $proxy string + **/ + protected function discover($proxy) { + // Normalize + if (!preg_match('/https?:\/\//i',$this->args['endpoint'])) + $this->args['endpoint']='http://'.$this->args['endpoint']; + $url=parse_url($this->args['endpoint']); + // Remove fragment; reconnect parts + $this->args['endpoint']=$url['scheme'].'://'. + (isset($url['user'])? + ($url['user']. + (isset($url['pass'])?(':'.$url['pass']):'').'@'):''). + strtolower($url['host']).(isset($url['path'])?$url['path']:'/'). + (isset($url['query'])?('?'.$url['query']):''); + // HTML-based discovery of OpenID provider + $req=\Web::instance()-> + request($this->args['endpoint'],['proxy'=>$proxy]); + if (!$req) + return FALSE; + $type=array_values(preg_grep('/Content-Type:/',$req['headers'])); + if ($type && + preg_match('/application\/xrds\+xml|text\/xml/',$type[0]) && + ($sxml=simplexml_load_string($req['body'])) && + ($xrds=json_decode(json_encode($sxml),TRUE)) && + isset($xrds['XRD'])) { + // XRDS document + $svc=$xrds['XRD']['Service']; + if (isset($svc[0])) + $svc=$svc[0]; + $svc_type=is_array($svc['Type'])?$svc['Type']:array($svc['Type']); + if (preg_grep('/http:\/\/specs\.openid\.net\/auth\/2.0\/'. + '(?:server|signon)/',$svc_type)) { + $this->args['provider']=$svc['URI']; + if (isset($svc['LocalID'])) + $this->args['localidentity']=$svc['LocalID']; + elseif (isset($svc['CanonicalID'])) + $this->args['localidentity']=$svc['CanonicalID']; + } + $this->args['server']=$svc['URI']; + if (isset($svc['Delegate'])) + $this->args['delegate']=$svc['Delegate']; + } + else { + $len=strlen($req['body']); + $ptr=0; + // Parse document + while ($ptr<$len) + if (preg_match( + '/^/is', + substr($req['body'],$ptr),$parts)) { + if ($parts[1] && + // Process attributes + preg_match_all('/\b(rel|href)\h*=\h*'. + '(?:"(.+?)"|\'(.+?)\')/s',$parts[1],$attr, + PREG_SET_ORDER)) { + $node=[]; + foreach ($attr as $kv) + $node[$kv[1]]=isset($kv[2])?$kv[2]:$kv[3]; + if (isset($node['rel']) && + preg_match('/openid2?\.(\w+)/', + $node['rel'],$var) && + isset($node['href'])) + $this->args[$var[1]]=$node['href']; + + } + $ptr+=strlen($parts[0]); + } + else + ++$ptr; + } + // Get OpenID provider's endpoint URL + if (isset($this->args['provider'])) { + // OpenID 2.0 + $this->args['ns']='http://specs.openid.net/auth/2.0'; + if (isset($this->args['localidentity'])) + $this->args['identity']=$this->args['localidentity']; + if (isset($this->args['trust_root'])) + $this->args['realm']=$this->args['trust_root']; + } + elseif (isset($this->args['server'])) { + // OpenID 1.1 + $this->args['ns']='http://openid.net/signon/1.1'; + if (isset($this->args['delegate'])) + $this->args['identity']=$this->args['delegate']; + } + if (isset($this->args['provider'])) { + // OpenID 2.0 + if (empty($this->args['claimed_id'])) + $this->args['claimed_id']=$this->args['identity']; + return $this->args['provider']; + } + elseif (isset($this->args['server'])) + // OpenID 1.1 + return $this->args['server']; + else + return FALSE; + } + + /** + * Initiate OpenID authentication sequence; Return FALSE on failure + * or redirect to OpenID provider URL + * @return bool + * @param $proxy string + * @param $attr array + * @param $reqd string|array + **/ + function auth($proxy=NULL,$attr=[],?array $reqd=NULL) { + $fw=\Base::instance(); + $root=$fw->SCHEME.'://'.$fw->HOST; + if (empty($this->args['trust_root'])) + $this->args['trust_root']=$root.$fw->BASE.'/'; + if (empty($this->args['return_to'])) + $this->args['return_to']=$root.$_SERVER['REQUEST_URI']; + $this->args['mode']='checkid_setup'; + if ($this->url=$this->discover($proxy)) { + if ($attr) { + $this->args['ns.ax']='http://openid.net/srv/ax/1.0'; + $this->args['ax.mode']='fetch_request'; + foreach ($attr as $key=>$val) + $this->args['ax.type.'.$key]=$val; + $this->args['ax.required']=is_string($reqd)? + $reqd:implode(',',$reqd); + } + $var=[]; + foreach ($this->args as $key=>$val) + $var['openid.'.$key]=$val; + $fw->reroute($this->url.'?'.http_build_query($var)); + } + return FALSE; + } + + /** + * Return TRUE if OpenID verification was successful + * @return bool + * @param $proxy string + **/ + function verified($proxy=NULL) { + preg_match_all('/(?<=^|&)openid\.([^=]+)=([^&]+)/', + $_SERVER['QUERY_STRING'],$matches,PREG_SET_ORDER); + foreach ($matches as $match) + $this->args[$match[1]]=urldecode($match[2]); + if (isset($this->args['mode']) && + $this->args['mode']!='error' && + $this->url=$this->discover($proxy)) { + $this->args['mode']='check_authentication'; + $var=[]; + foreach ($this->args as $key=>$val) + $var['openid.'.$key]=$val; + $req=\Web::instance()->request( + $this->url, + [ + 'method'=>'POST', + 'content'=>http_build_query($var), + 'proxy'=>$proxy + ] + ); + return (bool)preg_match('/is_valid:true/i',$req['body']); + } + return FALSE; + } + + /** + * Return OpenID response fields + * @return array + **/ + function response() { + return $this->args; + } + + /** + * Return TRUE if OpenID request parameter exists + * @return bool + * @param $key string + **/ + function exists($key) { + return isset($this->args[$key]); + } + + /** + * Bind value to OpenID request parameter + * @return string + * @param $key string + * @param $val string + **/ + function set($key,$val) { + return $this->args[$key]=$val; + } + + /** + * Return value of OpenID request parameter + * @return mixed + * @param $key string + **/ + function &get($key) { + if (isset($this->args[$key])) + $val=&$this->args[$key]; + else + $val=NULL; + return $val; + } + + /** + * Remove OpenID request parameter + * @return NULL + * @param $key + **/ + function clear($key) { + unset($this->args[$key]); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/pingback.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/pingback.php new file mode 100644 index 0000000..ef05582 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/bcosca/fatfree-core/web/pingback.php @@ -0,0 +1,176 @@ +. + +*/ + +namespace Web; + +//! Pingback 1.0 protocol (client and server) implementation +class Pingback extends \Prefab { + + protected + //! Transaction history + $log; + + /** + * Return TRUE if URL points to a pingback-enabled resource + * @return bool + * @param $url + **/ + protected function enabled($url) { + $web=\Web::instance(); + $req=$web->request($url); + $found=FALSE; + if ($req['body']) { + // Look for pingback header + foreach ($req['headers'] as $header) + if (preg_match('/^X-Pingback:\h*(.+)/',$header,$href)) { + $found=$href[1]; + break; + } + if (!$found && + // Scan page for pingback link tag + preg_match('//i',$req['body'],$parts) && + preg_match('/rel\h*=\h*"pingback"/i',$parts[1]) && + preg_match('/href\h*=\h*"\h*(.+?)\h*"/i',$parts[1],$href)) + $found=$href[1]; + } + return $found; + } + + /** + * Load local page contents, parse HTML anchor tags, find permalinks, + * and send XML-RPC calls to corresponding pingback servers + * @return NULL + * @param $source string + **/ + function inspect($source) { + $fw=\Base::instance(); + $web=\Web::instance(); + $parts=parse_url($source); + if (empty($parts['scheme']) || empty($parts['host']) || + $parts['host']==$fw->HOST) { + $req=$web->request($source); + $doc=new \DOMDocument('1.0',$fw->ENCODING); + $doc->strictErrorChecking=FALSE; + $doc->recover=TRUE; + if (@$doc->loadhtml($req['body'])) { + // Parse anchor tags + $links=$doc->getelementsbytagname('a'); + foreach ($links as $link) { + $permalink=$link->getattribute('href'); + // Find pingback-enabled resources + if ($permalink && $found=$this->enabled($permalink)) { + $req=$web->request($found, + [ + 'method'=>'POST', + 'header'=>'Content-Type: application/xml', + 'content'=>xmlrpc_encode_request( + 'pingback.ping', + [$source,$permalink], + ['encoding'=>$fw->ENCODING] + ) + ] + ); + if ($req['body']) + $this->log.=date('r').' '. + $permalink.' [permalink:'.$found.']'.PHP_EOL. + $req['body'].PHP_EOL; + } + } + } + unset($doc); + } + } + + /** + * Receive ping, check if local page is pingback-enabled, verify + * source contents, and return XML-RPC response + * @return string + * @param $func callback + * @param $path string + **/ + function listen($func,$path=NULL) { + $fw=\Base::instance(); + if (PHP_SAPI!='cli') { + header('X-Powered-By: '.$fw->PACKAGE); + header('Content-Type: application/xml; '. + 'charset='.$charset=$fw->ENCODING); + } + if (!$path) + $path=$fw->BASE; + $web=\Web::instance(); + $args=xmlrpc_decode_request($fw->BODY,$method,$charset); + $options=['encoding'=>$charset]; + if ($method=='pingback.ping' && isset($args[0],$args[1])) { + list($source,$permalink)=$args; + $doc=new \DOMDocument('1.0',$fw->ENCODING); + // Check local page if pingback-enabled + $parts=parse_url($permalink); + if ((empty($parts['scheme']) || + $parts['host']==$fw->HOST) && + preg_match('/^'.preg_quote($path,'/').'/'. + ($fw->CASELESS?'i':''),$parts['path']) && + $this->enabled($permalink)) { + // Check source + $parts=parse_url($source); + if ((empty($parts['scheme']) || + $parts['host']==$fw->HOST) && + ($req=$web->request($source)) && + $doc->loadhtml($req['body'])) { + $links=$doc->getelementsbytagname('a'); + foreach ($links as $link) { + if ($link->getattribute('href')==$permalink) { + call_user_func_array($func,[$source,$req['body']]); + // Success + die(xmlrpc_encode_request(NULL,$source,$options)); + } + } + // No link to local page + die(xmlrpc_encode_request(NULL,0x11,$options)); + } + // Source failure + die(xmlrpc_encode_request(NULL,0x10,$options)); + } + // Doesn't exist (or not pingback-enabled) + die(xmlrpc_encode_request(NULL,0x21,$options)); + } + // Access denied + die(xmlrpc_encode_request(NULL,0x31,$options)); + } + + /** + * Return transaction history + * @return string + **/ + function log() { + return $this->log; + } + + /** + * Instantiate class + * @return object + **/ + function __construct() { + // Suppress errors caused by invalid HTML structures + libxml_use_internal_errors(TRUE); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/index.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/index.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/index.php @@ -0,0 +1,3 @@ +cache = $cache; + } + + /** + * @inheritDoc + */ + public function fetch(string $id) + { + return $this->cache->fetch($id); + } + + /** + * @inheritDoc + */ + public function contains(string $id): bool + { + return $this->cache->contains($id); + } + + /** + * @inheritDoc + */ + public function save(string $id, $data, int $lifeTime = 0): bool + { + return $this->cache->save($id, $data, $lifeTime); + } + + /** + * @inheritDoc + */ + public function delete(string $id): bool + { + return $this->cache->delete($id); + } + + /** + * @inheritDoc + */ + public function flushAll(): bool + { + return $this->cache->flushAll(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Cache/LaravelCache.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Cache/LaravelCache.php new file mode 100644 index 0000000..61d5100 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Cache/LaravelCache.php @@ -0,0 +1,58 @@ +cache = $cache; + } + + /** + * @inheritDoc + */ + public function fetch(string $id) + { + return $this->cache->get($id, false); + } + + /** + * @inheritDoc + */ + public function contains(string $id): bool + { + return $this->cache->has($id); + } + + /** + * @inheritDoc + */ + public function save(string $id, $data, int $lifeTime = 0): bool + { + return $this->cache->set($id, $data, \func_num_args() < 3 ? null : $lifeTime); + } + + /** + * @inheritDoc + */ + public function delete(string $id): bool + { + return $this->cache->delete($id); + } + + /** + * @inheritDoc + */ + public function flushAll(): bool + { + return $this->cache->clear(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Cache/PSR6Bridge.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Cache/PSR6Bridge.php new file mode 100644 index 0000000..c11d105 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Cache/PSR6Bridge.php @@ -0,0 +1,81 @@ +pool = $pool; + } + + /** + * @inheritDoc + */ + public function fetch(string $id) + { + $item = $this->pool->getItem($id); + + return $item->isHit() ? $item->get() : false; + } + + /** + * @inheritDoc + */ + public function contains(string $id): bool + { + return $this->pool->hasItem($id); + } + + /** + * @inheritDoc + */ + public function save(string $id, $data, int $lifeTime = 0): bool + { + $item = $this->pool->getItem($id); + $item->set($data); + + if (\func_num_args() > 2) { + $item->expiresAfter($lifeTime); + } + + return $this->pool->save($item); + } + + /** + * @inheritDoc + */ + public function delete(string $id): bool + { + return $this->pool->deleteItem($id); + } + + /** + * @inheritDoc + */ + public function flushAll(): bool + { + return $this->pool->clear(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Cache/StaticCache.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Cache/StaticCache.php new file mode 100644 index 0000000..f2790e1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Cache/StaticCache.php @@ -0,0 +1,74 @@ +contains($id) ? self::$staticCache[$id] : false; + } + + /** + * @inheritdoc + */ + public function contains(string $id): bool + { + return isset(self::$staticCache[$id]) || \array_key_exists($id, self::$staticCache); + } + + /** + * @inheritdoc + */ + public function save(string $id, $data, int $lifeTime = 0): bool + { + self::$staticCache[$id] = $data; + + return true; + } + + /** + * @inheritdoc + */ + public function delete(string $id): bool + { + unset(self::$staticCache[$id]); + + return true; + } + + /** + * @inheritdoc + */ + public function flushAll(): bool + { + self::$staticCache = []; + + return true; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/ClientHints.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/ClientHints.php new file mode 100644 index 0000000..cd98c07 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/ClientHints.php @@ -0,0 +1,373 @@ +model = $model; + $this->platform = $platform; + $this->platformVersion = $platformVersion; + $this->uaFullVersion = $uaFullVersion; + $this->fullVersionList = $fullVersionList; + $this->mobile = $mobile; + $this->architecture = $architecture; + $this->bitness = $bitness; + $this->app = $app; + $this->formFactors = $formFactors; + } + + /** + * Magic method to directly allow accessing the protected properties + * + * @param string $variable + * + * @return mixed + * + * @throws \Exception + */ + public function __get(string $variable) + { + if (\property_exists($this, $variable)) { + return $this->$variable; + } + + throw new \Exception('Invalid ClientHint property requested.'); + } + + /** + * Returns if the client hints + * + * @return bool + */ + public function isMobile(): bool + { + return $this->mobile; + } + + /** + * Returns the Architecture + * + * @return string + */ + public function getArchitecture(): string + { + return $this->architecture; + } + + /** + * Returns the Bitness + * + * @return string + */ + public function getBitness(): string + { + return $this->bitness; + } + + /** + * Returns the device model + * + * @return string + */ + public function getModel(): string + { + return $this->model; + } + + /** + * Returns the Operating System + * + * @return string + */ + public function getOperatingSystem(): string + { + return $this->platform; + } + + /** + * Returns the Operating System version + * + * @return string + */ + public function getOperatingSystemVersion(): string + { + return $this->platformVersion; + } + + /** + * Returns the Browser name + * + * @return array + */ + public function getBrandList(): array + { + if (\is_array($this->fullVersionList) && \count($this->fullVersionList)) { + $brands = \array_column($this->fullVersionList, 'brand'); + $versions = \array_column($this->fullVersionList, 'version'); + + if (\count($brands) === \count($versions)) { + // @phpstan-ignore-next-line + return \array_combine($brands, $versions); + } + } + + return []; + } + + /** + * Returns the Browser version + * + * @return string + */ + public function getBrandVersion(): string + { + if (!empty($this->uaFullVersion)) { + return $this->uaFullVersion; + } + + return ''; + } + + /** + * Returns the Android app id + * + * @return string + */ + public function getApp(): string + { + return $this->app; + } + + /** + * Returns the formFactor device type name + * + * @return array + */ + public function getFormFactors(): array + { + return $this->formFactors; + } + + /** + * Factory method to easily instantiate this class using an array containing all available (client hint) headers + * + * @param array $headers + * + * @return ClientHints + */ + public static function factory(array $headers): ClientHints + { + $model = $platform = $platformVersion = $uaFullVersion = $architecture = $bitness = ''; + $app = ''; + $mobile = false; + $fullVersionList = []; + $formFactors = []; + + foreach ($headers as $name => $value) { + if (empty($value)) { + continue; + } + + switch (\str_replace('_', '-', \strtolower((string) $name))) { + case 'http-sec-ch-ua-arch': + case 'sec-ch-ua-arch': + case 'arch': + case 'architecture': + $architecture = \trim($value, '"'); + + break; + case 'http-sec-ch-ua-bitness': + case 'sec-ch-ua-bitness': + case 'bitness': + $bitness = \trim($value, '"'); + + break; + case 'http-sec-ch-ua-mobile': + case 'sec-ch-ua-mobile': + case 'mobile': + $mobile = true === $value || '1' === $value || '?1' === $value; + + break; + case 'http-sec-ch-ua-model': + case 'sec-ch-ua-model': + case 'model': + $model = \trim($value, '"'); + + break; + case 'http-sec-ch-ua-full-version': + case 'sec-ch-ua-full-version': + case 'uafullversion': + $uaFullVersion = \trim($value, '"'); + + break; + case 'http-sec-ch-ua-platform': + case 'sec-ch-ua-platform': + case 'platform': + $platform = \trim($value, '"'); + + break; + case 'http-sec-ch-ua-platform-version': + case 'sec-ch-ua-platform-version': + case 'platformversion': + $platformVersion = \trim($value, '"'); + + break; + case 'brands': + if (!empty($fullVersionList)) { + break; + } + // use this only if no other header already set the list + case 'fullversionlist': + $fullVersionList = \is_array($value) ? $value : $fullVersionList; + + break; + case 'http-sec-ch-ua': + case 'sec-ch-ua': + if (!empty($fullVersionList)) { + break; + } + // use this only if no other header already set the list + case 'http-sec-ch-ua-full-version-list': + case 'sec-ch-ua-full-version-list': + $reg = '/^"([^"]+)"; ?v="([^"]+)"(?:, )?/'; + $list = []; + + while (\preg_match($reg, $value, $matches)) { + $list[] = ['brand' => $matches[1], 'version' => $matches[2]]; + $value = \substr($value, \strlen($matches[0])); + } + + if (\count($list)) { + $fullVersionList = $list; + } + + break; + case 'http-x-requested-with': + case 'x-requested-with': + if ('xmlhttprequest' !== \strtolower($value)) { + $app = $value; + } + + break; + case 'formfactors': + case 'http-sec-ch-ua-form-factors': + case 'sec-ch-ua-form-factors': + if (\is_array($value)) { + $formFactors = \array_map('\strtolower', $value); + } elseif (\preg_match_all('~"([a-z]+)"~i', \strtolower($value), $matches)) { + $formFactors = $matches[1]; + } + + break; + } + } + + return new self( + $model, + $platform, + $platformVersion, + $uaFullVersion, + $fullVersionList, + $mobile, + $architecture, + $bitness, + $app, + $formFactors + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/DeviceDetector.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/DeviceDetector.php new file mode 100644 index 0000000..dac9900 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/DeviceDetector.php @@ -0,0 +1,1183 @@ + + */ + protected $clientParsers = []; + + /** + * @var array + */ + protected $deviceParsers = []; + + /** + * @var array + */ + public $botParsers = []; + + /** + * @var bool + */ + private $parsed = false; + + /** + * Constructor + * + * @param string $userAgent UA to parse + * @param ClientHints $clientHints Browser client hints to parse + */ + public function __construct(string $userAgent = '', ?ClientHints $clientHints = null) + { + if ('' !== $userAgent) { + $this->setUserAgent($userAgent); + } + + if ($clientHints instanceof ClientHints) { + $this->setClientHints($clientHints); + } + + $this->addClientParser(new FeedReader()); + $this->addClientParser(new MobileApp()); + $this->addClientParser(new MediaPlayer()); + $this->addClientParser(new PIM()); + $this->addClientParser(new Browser()); + $this->addClientParser(new Library()); + + $this->addDeviceParser(new HbbTv()); + $this->addDeviceParser(new ShellTv()); + $this->addDeviceParser(new Notebook()); + $this->addDeviceParser(new Console()); + $this->addDeviceParser(new CarBrowser()); + $this->addDeviceParser(new Camera()); + $this->addDeviceParser(new PortableMediaPlayer()); + $this->addDeviceParser(new Mobile()); + + $this->addBotParser(new Bot()); + } + + /** + * @param string $methodName + * @param array $arguments + * + * @return bool + */ + public function __call(string $methodName, array $arguments): bool + { + foreach (AbstractDeviceParser::getAvailableDeviceTypes() as $deviceName => $deviceType) { + if (\strtolower($methodName) === 'is' . \strtolower(\str_replace(' ', '', $deviceName))) { + return $this->getDevice() === $deviceType; + } + } + + foreach ($this->clientTypes as $client) { + if (\strtolower($methodName) === 'is' . \strtolower(\str_replace(' ', '', $client))) { + return $this->getClient('type') === $client; + } + } + + throw new \BadMethodCallException("Method {$methodName} not found"); + } + + /** + * Sets the useragent to be parsed + * + * @param string $userAgent + */ + public function setUserAgent(string $userAgent): void + { + if ($this->userAgent !== $userAgent) { + $this->reset(); + } + + $this->userAgent = $userAgent; + } + + /** + * Sets the browser client hints to be parsed + * + * @param ?ClientHints $clientHints + */ + public function setClientHints(?ClientHints $clientHints = null): void + { + if ($this->clientHints !== $clientHints) { + $this->reset(); + } + + $this->clientHints = $clientHints; + } + + /** + * @param AbstractClientParser $parser + * + * @throws \Exception + */ + public function addClientParser(AbstractClientParser $parser): void + { + $this->clientParsers[] = $parser; + $this->clientTypes[] = $parser->getName(); + } + + /** + * @return array + */ + public function getClientParsers(): array + { + return $this->clientParsers; + } + + /** + * @param AbstractDeviceParser $parser + * + * @throws \Exception + */ + public function addDeviceParser(AbstractDeviceParser $parser): void + { + $this->deviceParsers[] = $parser; + } + + /** + * @return array + */ + public function getDeviceParsers(): array + { + return $this->deviceParsers; + } + + /** + * @param AbstractBotParser $parser + */ + public function addBotParser(AbstractBotParser $parser): void + { + $this->botParsers[] = $parser; + } + + /** + * @return array + */ + public function getBotParsers(): array + { + return $this->botParsers; + } + + /** + * Sets whether to discard additional bot information + * If information is discarded it's only possible check whether UA was detected as bot or not. + * (Discarding information speeds up the detection a bit) + * + * @param bool $discard + */ + public function discardBotInformation(bool $discard = true): void + { + $this->discardBotInformation = $discard; + } + + /** + * Sets whether to skip bot detection. + * It is needed if we want bots to be processed as a simple clients. So we can detect if it is mobile client, + * or desktop, or enything else. By default all this information is not retrieved for the bots. + * + * @param bool $skip + */ + public function skipBotDetection(bool $skip = true): void + { + $this->skipBotDetection = $skip; + } + + /** + * Returns if the parsed UA was identified as a Bot + * + * @return bool + * + * @see bots.yml for a list of detected bots + * + */ + public function isBot(): bool + { + return !empty($this->bot); + } + + /** + * Returns if the parsed UA was identified as a touch enabled device + * + * Note: That only applies to windows 8 tablets + * + * @return bool + */ + public function isTouchEnabled(): bool + { + $regex = 'Touch'; + + return !!$this->matchUserAgent($regex); + } + + /** + * Returns if the parsed UA is detected as a mobile device + * + * @return bool + */ + public function isMobile(): bool + { + // Client hints indicate a mobile device + if ($this->clientHints instanceof ClientHints && $this->clientHints->isMobile()) { + return true; + } + + // Mobile device types + if (\in_array($this->device, [ + AbstractDeviceParser::DEVICE_TYPE_FEATURE_PHONE, + AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE, + AbstractDeviceParser::DEVICE_TYPE_TABLET, + AbstractDeviceParser::DEVICE_TYPE_PHABLET, + AbstractDeviceParser::DEVICE_TYPE_CAMERA, + AbstractDeviceParser::DEVICE_TYPE_PORTABLE_MEDIA_PAYER, + ]) + ) { + return true; + } + + // non mobile device types + if (\in_array($this->device, [ + AbstractDeviceParser::DEVICE_TYPE_TV, + AbstractDeviceParser::DEVICE_TYPE_SMART_DISPLAY, + AbstractDeviceParser::DEVICE_TYPE_CONSOLE, + ]) + ) { + return false; + } + + // Check for browsers available for mobile devices only + if ($this->usesMobileBrowser()) { + return true; + } + + $osName = $this->getOs('name'); + + if (empty($osName) || self::UNKNOWN === $osName) { + return false; + } + + return !$this->isBot() && !$this->isDesktop(); + } + + /** + * Returns if the parsed UA was identified as desktop device + * Desktop devices are all devices with an unknown type that are running a desktop os + * + * @return bool + * + * @see OperatingSystem::$desktopOsArray + * + */ + public function isDesktop(): bool + { + $osName = $this->getOsAttribute('name'); + + if (empty($osName) || self::UNKNOWN === $osName) { + return false; + } + + // Check for browsers available for mobile devices only + if ($this->usesMobileBrowser()) { + return false; + } + + return OperatingSystem::isDesktopOs($osName); + } + + /** + * Returns the operating system data extracted from the parsed UA + * + * If $attr is given only that property will be returned + * + * @param string $attr property to return(optional) + * + * @return array|string|null + */ + public function getOs(string $attr = '') + { + if ('' === $attr) { + return $this->os; + } + + return $this->getOsAttribute($attr); + } + + /** + * Returns the client data extracted from the parsed UA + * + * If $attr is given only that property will be returned + * + * @param string $attr property to return(optional) + * + * @return array|string|null + */ + public function getClient(string $attr = '') + { + if ('' === $attr) { + return $this->client; + } + + return $this->getClientAttribute($attr); + } + + /** + * Returns the device type extracted from the parsed UA + * + * @return int|null + * + * @see AbstractDeviceParser::$deviceTypes for available device types + * + */ + public function getDevice(): ?int + { + return $this->device; + } + + /** + * Returns the device type extracted from the parsed UA + * + * @return string + * + * @see AbstractDeviceParser::$deviceTypes for available device types + * + */ + public function getDeviceName(): string + { + if (null !== $this->getDevice()) { + return AbstractDeviceParser::getDeviceName($this->getDevice()); + } + + return ''; + } + + /** + * Returns the device brand extracted from the parsed UA + * + * @return string + * + * @see self::$deviceBrand for available device brands + * + * @deprecated since 4.0 - short codes might be removed in next major release + */ + public function getBrand(): string + { + return AbstractDeviceParser::getShortCode($this->brand); + } + + /** + * Returns the full device brand name extracted from the parsed UA + * + * @return string + * + * @see self::$deviceBrand for available device brands + * + */ + public function getBrandName(): string + { + return $this->brand; + } + + /** + * Returns the device model extracted from the parsed UA + * + * @return string + */ + public function getModel(): string + { + return $this->model; + } + + /** + * Returns the user agent that is set to be parsed + * + * @return string + */ + public function getUserAgent(): string + { + return $this->userAgent; + } + + /** + * Returns the client hints that is set to be parsed + * + * @return ?ClientHints + */ + public function getClientHints(): ?ClientHints + { + return $this->clientHints; + } + + /** + * Returns the bot extracted from the parsed UA + * + * @return array|bool|null + */ + public function getBot() + { + return $this->bot; + } + + /** + * Returns true, if userAgent was already parsed with parse() + * + * @return bool + */ + public function isParsed(): bool + { + return $this->parsed; + } + + /** + * Triggers the parsing of the current user agent + */ + public function parse(): void + { + if ($this->isParsed()) { + return; + } + + $this->parsed = true; + + // skip parsing for empty useragents or those not containing any letter (if no client hints were provided) + if ((empty($this->userAgent) || !\preg_match('/([a-z])/i', $this->userAgent)) + && empty($this->clientHints) + ) { + return; + } + + $this->parseBot(); + + if ($this->isBot()) { + return; + } + + $this->parseOs(); + + /** + * Parse Clients + * Clients might be browsers, Feed Readers, Mobile Apps, Media Players or + * any other application accessing with an parseable UA + */ + $this->parseClient(); + + $this->parseDevice(); + } + + /** + * Parses a useragent and returns the detected data + * + * ATTENTION: Use that method only for testing or very small applications + * To get fast results from DeviceDetector you need to make your own implementation, + * that should use one of the caching mechanisms. See README.md for more information. + * + * @param string $ua UserAgent to parse + * @param ?ClientHints $clientHints Client Hints to parse + * + * @return array + * + * @deprecated + * + * @internal + * + */ + public static function getInfoFromUserAgent(string $ua, ?ClientHints $clientHints = null): array + { + static $deviceDetector; + + if (!($deviceDetector instanceof DeviceDetector)) { + $deviceDetector = new DeviceDetector(); + } + + $deviceDetector->setUserAgent($ua); + $deviceDetector->setClientHints($clientHints); + + $deviceDetector->parse(); + + if ($deviceDetector->isBot()) { + return [ + 'user_agent' => $deviceDetector->getUserAgent(), + 'bot' => $deviceDetector->getBot(), + ]; + } + + /** @var array $client */ + $client = $deviceDetector->getClient(); + $browserFamily = 'Unknown'; + + if ($deviceDetector->isBrowser() + && true === \is_array($client) + && true === \array_key_exists('family', $client) + && null !== $client['family'] + ) { + $browserFamily = $client['family']; + } + + unset($client['short_name'], $client['family']); + + /** @var array $os */ + $os = $deviceDetector->getOs(); + $osFamily = $os['family'] ?? 'Unknown'; + + unset($os['short_name'], $os['family']); + + return [ + 'user_agent' => $deviceDetector->getUserAgent(), + 'os' => $os, + 'client' => $client, + 'device' => [ + 'type' => $deviceDetector->getDeviceName(), + 'brand' => $deviceDetector->getBrandName(), + 'model' => $deviceDetector->getModel(), + ], + 'os_family' => $osFamily, + 'browser_family' => $browserFamily, + ]; + } + + /** + * Sets the Cache class + * + * @param CacheInterface $cache + */ + public function setCache(CacheInterface $cache): void + { + $this->cache = $cache; + } + + /** + * Returns Cache object + * + * @return CacheInterface + */ + public function getCache(): CacheInterface + { + if (!empty($this->cache)) { + return $this->cache; + } + + return new StaticCache(); + } + + /** + * Sets the Yaml Parser class + * + * @param YamlParser $yamlParser + */ + public function setYamlParser(YamlParser $yamlParser): void + { + $this->yamlParser = $yamlParser; + } + + /** + * Returns Yaml Parser object + * + * @return YamlParser + */ + public function getYamlParser(): YamlParser + { + if (!empty($this->yamlParser)) { + return $this->yamlParser; + } + + return new Spyc(); + } + + /** + * @param string $attr + * + * @return string + */ + protected function getClientAttribute(string $attr): string + { + if (!isset($this->client[$attr])) { + return self::UNKNOWN; + } + + return $this->client[$attr]; + } + + /** + * @param string $attr + * + * @return string + */ + protected function getOsAttribute(string $attr): string + { + if (!isset($this->os[$attr])) { + return self::UNKNOWN; + } + + return $this->os[$attr]; + } + + /** + * Returns if the parsed UA contains the 'Android; Tablet;' fragment + * + * @return bool + */ + protected function hasAndroidTableFragment(): bool + { + $regex = 'Android( [.0-9]+)?; Tablet;|Tablet(?! PC)|.*\-tablet$'; + + return !!$this->matchUserAgent($regex); + } + + /** + * Returns if the parsed UA contains the 'Android; Mobile;' fragment + * + * @return bool + */ + protected function hasAndroidMobileFragment(): bool + { + $regex = 'Android( [.0-9]+)?; Mobile;|.*\-mobile$'; + + return !!$this->matchUserAgent($regex); + } + + /** + * Returns if the parsed UA contains the 'Android; Mobile VR;' fragment + * + * @return bool + */ + protected function hasAndroidVRFragment(): bool + { + $regex = 'Android( [.0-9]+)?; Mobile VR;| VR '; + + return !!$this->matchUserAgent($regex); + } + + /** + * Returns if the parsed UA contains the 'Desktop;', 'Desktop x32;', 'Desktop x64;' or 'Desktop WOW64;' fragment + * + * @return bool + */ + protected function hasDesktopFragment(): bool + { + $regex = 'Desktop(?: (x(?:32|64)|WOW64))?;'; + + return !!$this->matchUserAgent($regex); + } + + /** + * Returns if the parsed UA contains usage of a mobile only browser + * + * @return bool + */ + protected function usesMobileBrowser(): bool + { + return 'browser' === $this->getClient('type') + && Browser::isMobileOnlyBrowser($this->getClientAttribute('name')); + } + + /** + * Parses the UA for bot information using the Bot parser + */ + protected function parseBot(): void + { + if ($this->skipBotDetection) { + $this->bot = false; + + return; + } + + $parsers = $this->getBotParsers(); + + foreach ($parsers as $parser) { + $parser->setYamlParser($this->getYamlParser()); + $parser->setCache($this->getCache()); + $parser->setUserAgent($this->getUserAgent()); + $parser->setClientHints($this->getClientHints()); + + if ($this->discardBotInformation) { + $parser->discardDetails(); + } + + $bot = $parser->parse(); + + if (!empty($bot)) { + $this->bot = $bot; + + break; + } + } + } + + /** + * Tries to detect the client (e.g. browser, mobile app, ...) + */ + protected function parseClient(): void + { + $parsers = $this->getClientParsers(); + + foreach ($parsers as $parser) { + $parser->setYamlParser($this->getYamlParser()); + $parser->setCache($this->getCache()); + $parser->setUserAgent($this->getUserAgent()); + $parser->setClientHints($this->getClientHints()); + $client = $parser->parse(); + + if (!empty($client)) { + $this->client = $client; + + break; + } + } + } + + /** + * Tries to detect the device type, model and brand + */ + protected function parseDevice(): void + { + $parsers = $this->getDeviceParsers(); + + foreach ($parsers as $parser) { + $parser->setYamlParser($this->getYamlParser()); + $parser->setCache($this->getCache()); + $parser->setUserAgent($this->getUserAgent()); + $parser->setClientHints($this->getClientHints()); + + if ($parser->parse()) { + $this->device = $parser->getDeviceType(); + $this->model = $parser->getModel(); + $this->brand = $parser->getBrand(); + + break; + } + } + + /** + * If no model could be parsed from useragent, we use the one from client hints if available + */ + if ($this->clientHints instanceof ClientHints && empty($this->model)) { + $this->model = $this->clientHints->getModel(); + } + + /** + * If no brand has been assigned try to match by known vendor fragments + */ + if (empty($this->brand)) { + $vendorParser = new VendorFragment($this->getUserAgent()); + $vendorParser->setYamlParser($this->getYamlParser()); + $vendorParser->setCache($this->getCache()); + $this->brand = $vendorParser->parse()['brand'] ?? ''; + } + + $osName = $this->getOsAttribute('name'); + $osFamily = $this->getOsAttribute('family'); + $osVersion = $this->getOsAttribute('version'); + $clientName = $this->getClientAttribute('name'); + $appleOsNames = ['iPadOS', 'tvOS', 'watchOS', 'iOS', 'Mac']; + + /** + * if it's fake UA then it's best not to identify it as Apple running Android OS or GNU/Linux + */ + if ('Apple' === $this->brand && !\in_array($osName, $appleOsNames)) { + $this->device = null; + $this->brand = ''; + $this->model = ''; + } + + /** + * Assume all devices running iOS / Mac OS are from Apple + */ + if (empty($this->brand) && \in_array($osName, $appleOsNames)) { + $this->brand = 'Apple'; + } + + /** + * All devices containing VR fragment are assumed to be a wearable + */ + if (null === $this->device && $this->hasAndroidVRFragment()) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_WEARABLE; + } + + /** + * Chrome on Android passes the device type based on the keyword 'Mobile' + * If it is present the device should be a smartphone, otherwise it's a tablet + * See https://developer.chrome.com/multidevice/user-agent#chrome_for_android_user_agent + * Note: We do not check for browser (family) here, as there might be mobile apps using Chrome, that won't have + * a detected browser, but can still be detected. So we check the useragent for Chrome instead. + */ + if (null === $this->device && 'Android' === $osFamily + && $this->matchUserAgent('Chrome/[.0-9]*') + ) { + if ($this->matchUserAgent('(?:Mobile|eliboM)')) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE; + } else { + $this->device = AbstractDeviceParser::DEVICE_TYPE_TABLET; + } + } + + /** + * Some UA contain the fragment 'Pad/APad', so we assume those devices as tablets + */ + if (AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE === $this->device && $this->matchUserAgent('Pad/APad')) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_TABLET; + } + + /** + * Some UA contain the fragment 'Android; Tablet;' or 'Opera Tablet', so we assume those devices as tablets + */ + if (null === $this->device && ($this->hasAndroidTableFragment() + || $this->matchUserAgent('Opera Tablet')) + ) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_TABLET; + } + + /** + * Some user agents simply contain the fragment 'Android; Mobile;', so we assume those devices as smartphones + */ + if (null === $this->device && $this->hasAndroidMobileFragment()) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE; + } + + /** + * Android up to 3.0 was designed for smartphones only. But as 3.0, which was tablet only, was published + * too late, there were a bunch of tablets running with 2.x + * With 4.0 the two trees were merged and it is for smartphones and tablets + * + * So were are expecting that all devices running Android < 2 are smartphones + * Devices running Android 3.X are tablets. Device type of Android 2.X and 4.X+ are unknown + */ + if (null === $this->device && 'Android' === $osName && '' !== $osVersion) { + if (-1 === \version_compare($osVersion, '2.0')) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE; + } elseif (\version_compare($osVersion, '3.0') >= 0 + && -1 === \version_compare($osVersion, '4.0') + ) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_TABLET; + } + } + + /** + * All detected feature phones running android are more likely a smartphone + */ + if (AbstractDeviceParser::DEVICE_TYPE_FEATURE_PHONE === $this->device && 'Android' === $osFamily) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE; + } + + /** + * All unknown devices under running Java ME are more likely features phones + */ + if ('Java ME' === $osName && null === $this->device) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_FEATURE_PHONE; + } + + /** + * All devices running KaiOS are more likely features phones + */ + if ('KaiOS' === $osName) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_FEATURE_PHONE; + } + + /** + * According to http://msdn.microsoft.com/en-us/library/ie/hh920767(v=vs.85).aspx + * Internet Explorer 10 introduces the "Touch" UA string token. If this token is present at the end of the + * UA string, the computer has touch capability, and is running Windows 8 (or later). + * This UA string will be transmitted on a touch-enabled system running Windows 8 (RT) + * + * As most touch enabled devices are tablets and only a smaller part are desktops/notebooks we assume that + * all Windows 8 touch devices are tablets. + */ + + if (null === $this->device && ('Windows RT' === $osName || ('Windows' === $osName + && \version_compare($osVersion, '8') >= 0)) && $this->isTouchEnabled() + ) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_TABLET; + } + + /** + * All devices running Puffin Secure Browser that contain letter 'D' are assumed to be desktops + */ + if (null === $this->device && $this->matchUserAgent('Puffin/(?:\d+[.\d]+)[LMW]D')) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_DESKTOP; + } + + /** + * All devices running Puffin Web Browser that contain letter 'P' are assumed to be smartphones + */ + if (null === $this->device && $this->matchUserAgent('Puffin/(?:\d+[.\d]+)[AIFLW]P')) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE; + } + + /** + * All devices running Puffin Web Browser that contain letter 'T' are assumed to be tablets + */ + if (null === $this->device && $this->matchUserAgent('Puffin/(?:\d+[.\d]+)[AILW]T')) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_TABLET; + } + + /** + * All devices running Opera TV Store are assumed to be a tv + */ + if ($this->matchUserAgent('Opera TV Store| OMI/')) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_TV; + } + + /** + * All devices running Coolita OS are assumed to be a tv + */ + if ('Coolita OS' === $osName) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_TV; + $this->brand = 'coocaa'; + } + + /** + * All devices that contain Andr0id in string are assumed to be a tv + */ + $hasDeviceTvType = false === \in_array($this->device, [ + AbstractDeviceParser::DEVICE_TYPE_TV, + AbstractDeviceParser::DEVICE_TYPE_PERIPHERAL, + ]) && $this->matchUserAgent('Andr0id|(?:Android(?: UHD)?|Google) TV|\(lite\) TV|BRAVIA|Firebolt| TV$'); + + if ($hasDeviceTvType) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_TV; + } + + /** + * All devices running Tizen TV or SmartTV are assumed to be a tv + */ + if (null === $this->device && $this->matchUserAgent('SmartTV|Tizen.+ TV .+$')) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_TV; + } + + /** + * Devices running those clients are assumed to be a TV + */ + if (\in_array($clientName, [ + 'Kylo', 'Espial TV Browser', 'LUJO TV Browser', 'LogicUI TV Browser', 'Open TV Browser', 'Seraphic Sraf', + 'Opera Devices', 'Crow Browser', 'Vewd Browser', 'TiviMate', 'Quick Search TV', 'QJY TV Browser', 'TV Bro', + ]) + ) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_TV; + } + + /** + * All devices containing TV fragment are assumed to be a tv + */ + if (null === $this->device && $this->matchUserAgent('\(TV;')) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_TV; + } + + /** + * Set device type desktop if string ua contains desktop + */ + $hasDesktop = AbstractDeviceParser::DEVICE_TYPE_DESKTOP !== $this->device + && false !== \strpos($this->userAgent, 'Desktop') + && $this->hasDesktopFragment(); + + if ($hasDesktop) { + $this->device = AbstractDeviceParser::DEVICE_TYPE_DESKTOP; + } + + // set device type to desktop for all devices running a desktop os that were not detected as another device type + if (null !== $this->device || !$this->isDesktop()) { + return; + } + + $this->device = AbstractDeviceParser::DEVICE_TYPE_DESKTOP; + } + + /** + * Tries to detect the operating system + */ + protected function parseOs(): void + { + $osParser = new OperatingSystem(); + $osParser->setUserAgent($this->getUserAgent()); + $osParser->setClientHints($this->getClientHints()); + $osParser->setYamlParser($this->getYamlParser()); + $osParser->setCache($this->getCache()); + $this->os = $osParser->parse(); + } + + /** + * @param string $regex + * + * @return array|null + */ + protected function matchUserAgent(string $regex): ?array + { + $regex = '/(?:^|[^A-Z_-])(?:' . \str_replace('/', '\/', $regex) . ')/i'; + + if (\preg_match($regex, $this->userAgent, $matches)) { + return $matches; + } + + return null; + } + + /** + * Resets all detected data + */ + protected function reset(): void + { + $this->bot = null; + $this->client = null; + $this->device = null; + $this->os = null; + $this->brand = ''; + $this->model = ''; + $this->parsed = false; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/LICENSE b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/LICENSE new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser 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 +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/AbstractBotParser.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/AbstractBotParser.php new file mode 100644 index 0000000..ea5c7e5 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/AbstractBotParser.php @@ -0,0 +1,26 @@ +> + */ + protected static $clientHintMapping = []; + + /** + * Holds an array with method that should be available global + * @var array + */ + protected $globalMethods; + + /** + * Holds an array with regexes to parse, if already loaded + * @var array + */ + protected $regexList; + + /** + * Holds the concatenated regex for all items in regex list + * @var string + */ + protected $overAllMatch; + + /** + * Indicates how deep versioning will be detected + * if $maxMinorParts is 0 only the major version will be returned + * @var int + */ + protected static $maxMinorParts = 1; + + /** + * Versioning constant used to set max versioning to major version only + * Version examples are: 3, 5, 6, 200, 123, ... + */ + public const VERSION_TRUNCATION_MAJOR = 0; + + /** + * Versioning constant used to set max versioning to minor version + * Version examples are: 3.4, 5.6, 6.234, 0.200, 1.23, ... + */ + public const VERSION_TRUNCATION_MINOR = 1; + + /** + * Versioning constant used to set max versioning to path level + * Version examples are: 3.4.0, 5.6.344, 6.234.2, 0.200.3, 1.2.3, ... + */ + public const VERSION_TRUNCATION_PATCH = 2; + + /** + * Versioning constant used to set versioning to build number + * Version examples are: 3.4.0.12, 5.6.334.0, 6.234.2.3, 0.200.3.1, 1.2.3.0, ... + */ + public const VERSION_TRUNCATION_BUILD = 3; + + /** + * Versioning constant used to set versioning to unlimited (no truncation) + */ + public const VERSION_TRUNCATION_NONE = -1; + + /** + * @var CacheInterface|null + */ + protected $cache = null; + + /** + * @var YamlParser|null + */ + protected $yamlParser = null; + + /** + * parses the currently set useragents and returns possible results + * + * @return array|null + */ + abstract public function parse(): ?array; + + /** + * AbstractParser constructor. + * + * @param string $ua + * @param ?ClientHints $clientHints + */ + public function __construct(string $ua = '', ?ClientHints $clientHints = null) + { + $this->setUserAgent($ua); + $this->setClientHints($clientHints); + } + + /** + * @inheritdoc + */ + public function restoreUserAgentFromClientHints(): void + { + if (null === $this->clientHints) { + return; + } + + $deviceModel = $this->clientHints->getModel(); + + if ('' === $deviceModel) { + return; + } + + // Restore Android User Agent + if ($this->hasUserAgentClientHintsFragment()) { + $osVersion = $this->clientHints->getOperatingSystemVersion(); + $this->setUserAgent((string) \preg_replace( + '(Android (?:10[.\d]*; K|1[1-5]))', + \sprintf('Android %s; %s', '' !== $osVersion ? $osVersion : '10', $deviceModel), + $this->userAgent + )); + } + + // Restore Desktop User Agent + if (!$this->hasDesktopFragment()) { + return; + } + + $this->setUserAgent((string) \preg_replace( + '(X11; Linux x86_64)', + \sprintf('X11; Linux x86_64; %s', $deviceModel), + $this->userAgent + )); + } + + /** + * Set how DeviceDetector should return versions + * @param int $type Any of the VERSION_TRUNCATION_* constants + */ + public static function setVersionTruncation(int $type): void + { + if (!\in_array($type, [ + self::VERSION_TRUNCATION_BUILD, + self::VERSION_TRUNCATION_NONE, + self::VERSION_TRUNCATION_MAJOR, + self::VERSION_TRUNCATION_MINOR, + self::VERSION_TRUNCATION_PATCH, + ]) + ) { + return; + } + + static::$maxMinorParts = $type; + } + + /** + * Sets the user agent to parse + * + * @param string $ua user agent + */ + public function setUserAgent(string $ua): void + { + $this->userAgent = $ua; + } + + /** + * Sets the client hints to parse + * + * @param ?ClientHints $clientHints client hints + */ + public function setClientHints(?ClientHints $clientHints): void + { + $this->clientHints = $clientHints; + } + + /** + * Returns the internal name of the parser + * + * @return string + */ + public function getName(): string + { + return $this->parserName; + } + + /** + * Sets the Cache class + * + * @param CacheInterface $cache + */ + public function setCache(CacheInterface $cache): void + { + $this->cache = $cache; + } + + /** + * Returns Cache object + * + * @return CacheInterface + */ + public function getCache(): CacheInterface + { + if (!empty($this->cache)) { + return $this->cache; + } + + return new StaticCache(); + } + + /** + * Sets the YamlParser class + * + * @param YamlParser $yamlParser + */ + public function setYamlParser(YamlParser $yamlParser): void + { + $this->yamlParser = $yamlParser; + } + + /** + * Returns YamlParser object + * + * @return YamlParser + */ + public function getYamlParser(): YamlParser + { + if (!empty($this->yamlParser)) { + return $this->yamlParser; + } + + return new Spyc(); + } + + /** + * Returns the result of the parsed yml file defined in $fixtureFile + * + * @return array + */ + protected function getRegexes(): array + { + if (empty($this->regexList)) { + $cacheKey = 'DeviceDetector-' . DeviceDetector::VERSION . 'regexes-' . $this->getName(); + $cacheKey = (string) \preg_replace('/([^a-z0-9_-]+)/i', '', $cacheKey); + $cacheContent = $this->getCache()->fetch($cacheKey); + + if (\is_array($cacheContent)) { + $this->regexList = $cacheContent; + } + + if (empty($this->regexList)) { + $parsedContent = $this->getYamlParser()->parseFile( + $this->getRegexesDirectory() . DIRECTORY_SEPARATOR . $this->fixtureFile + ); + + if (!\is_array($parsedContent)) { + $parsedContent = []; + } + + $this->regexList = $parsedContent; + $this->getCache()->save($cacheKey, $this->regexList); + } + } + + return $this->regexList; + } + + /** + * Returns the provided name after applying client hint mappings. + * This is used to map names provided in client hints to the names we use. + * + * @param string $name + * + * @return string + */ + protected function applyClientHintMapping(string $name): string + { + foreach (static::$clientHintMapping as $mappedName => $clientHints) { + foreach ($clientHints as $clientHint) { + if (\strtolower($name) === \strtolower($clientHint)) { + return $mappedName; + } + } + } + + return $name; + } + + /** + * @return string + */ + protected function getRegexesDirectory(): string + { + return \dirname(__DIR__); + } + + /** + * Returns if the parsed UA contains the 'Windows NT;' or 'X11; Linux x86_64' fragments + * + * @return bool + */ + protected function hasDesktopFragment(): bool + { + $regexExcludeDesktopFragment = \implode('|', [ + 'CE-HTML', + ' Mozilla/|Andr[o0]id|Tablet|Mobile|iPhone|Windows Phone|ricoh|OculusBrowser', + 'PicoBrowser|Lenovo|compatible; MSIE|Trident/|Tesla/|XBOX|FBMD/|ARM; ?([^)]+)', + ]); + + return + $this->matchUserAgent('(?:Windows (?:NT|IoT)|X11; Linux x86_64)') && + !$this->matchUserAgent($regexExcludeDesktopFragment); + } + + /** + * Returns if the parsed UA contains the 'Android 10 K;' or Android 10 K Build/` fragment + * + * @return bool + */ + protected function hasUserAgentClientHintsFragment(): bool + { + return (bool) \preg_match('~Android (?:10[.\d]*; K(?: Build/|[;)])|1[1-5]\)) AppleWebKit~i', $this->userAgent); + } + + /** + * Matches the useragent against the given regex + * + * @param string $regex + * + * @return ?array + * + * @throws \Exception + */ + protected function matchUserAgent(string $regex): ?array + { + $matches = []; + + // only match if useragent begins with given regex or there is no letter before it + $regex = '/(?:^|[^A-Z0-9_-]|[^A-Z0-9-]_|sprd-|MZ-)(?:' . \str_replace('/', '\/', $regex) . ')/i'; + + try { + if (\preg_match($regex, $this->userAgent, $matches)) { + return $matches; + } + } catch (\Exception $exception) { + throw new \Exception( + \sprintf("%s\nRegex: %s", $exception->getMessage(), $regex), + $exception->getCode(), + $exception + ); + } + + return null; + } + + /** + * @param string $item + * @param array $matches + * + * @return string + */ + protected function buildByMatch(string $item, array $matches): string + { + $search = []; + $replace = []; + + for ($nb = 1; $nb <= \count($matches); $nb++) { + $search[] = '$' . $nb; + $replace[] = $matches[$nb] ?? ''; + } + + return \trim(\str_replace($search, $replace, $item)); + } + + /** + * Builds the version with the given $versionString and $matches + * + * Example: + * $versionString = 'v$2' + * $matches = ['version_1_0_1', '1_0_1'] + * return value would be v1.0.1 + * + * @param string $versionString + * @param array $matches + * + * @return string + */ + protected function buildVersion(string $versionString, array $matches): string + { + $versionString = $this->buildByMatch($versionString, $matches); + $versionString = \str_replace('_', '.', $versionString); + + if (self::VERSION_TRUNCATION_NONE !== static::$maxMinorParts + && \substr_count($versionString, '.') > static::$maxMinorParts + ) { + $versionParts = \explode('.', $versionString); + $versionParts = \array_slice($versionParts, 0, 1 + static::$maxMinorParts); + $versionString = \implode('.', $versionParts); + } + + return \trim($versionString, ' .'); + } + + /** + * Tests the useragent against a combination of all regexes + * + * All regexes returned by getRegexes() will be reversed and concatenated with '|' + * Afterwards the big regex will be tested against the user agent + * + * Method can be used to speed up detections by making a big check before doing checks for every single regex + * + * @return ?array + */ + protected function preMatchOverall(): ?array + { + $regexes = $this->getRegexes(); + + $cacheKey = $this->parserName . DeviceDetector::VERSION . '-all'; + $cacheKey = (string) \preg_replace('/([^a-z0-9_-]+)/i', '', $cacheKey); + + if (empty($this->overAllMatch)) { + $overAllMatch = $this->getCache()->fetch($cacheKey); + + if (\is_string($overAllMatch)) { + $this->overAllMatch = $overAllMatch; + } + } + + if (empty($this->overAllMatch)) { + // reverse all regexes, so we have the generic one first, which already matches most patterns + $this->overAllMatch = \array_reduce(\array_reverse($regexes), static function ($val1, $val2) { + return !empty($val1) ? $val1 . '|' . $val2['regex'] : $val2['regex']; + }); + $this->getCache()->save($cacheKey, $this->overAllMatch); + } + + return $this->matchUserAgent($this->overAllMatch); + } + + /** + * Compares if two strings equals after lowering their case and removing spaces + * + * @param string $value1 + * @param string $value2 + * + * @return bool + */ + protected function fuzzyCompare(string $value1, string $value2): bool + { + return \str_replace(' ', '', \strtolower($value1)) === + \str_replace(' ', '', \strtolower($value2)); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Bot.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Bot.php new file mode 100644 index 0000000..49348e9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Bot.php @@ -0,0 +1,88 @@ +discardDetails = true; + } + + /** + * Parses the current UA and checks whether it contains bot information + * + * @see bots.yml for list of detected bots + * + * Step 1: Build a big regex containing all regexes and match UA against it + * -> If no matches found: return + * -> Otherwise: + * Step 2: Walk through the list of regexes in bots.yml and try to match every one + * -> Return the matched data + * + * If $discardDetails is set to TRUE, the Step 2 will be skipped + * $bot will be set to TRUE instead + * + * NOTE: Doing the big match before matching every single regex speeds up the detection + * + * @return array|null + */ + public function parse(): ?array + { + $result = null; + + if ($this->preMatchOverall()) { + if ($this->discardDetails) { + return [true]; + } + + foreach ($this->getRegexes() as $regex) { + $matches = $this->matchUserAgent($regex['regex']); + + if ($matches) { + unset($regex['regex']); + $result = $regex; + + break; + } + } + } + + return $result; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/AbstractClientParser.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/AbstractClientParser.php new file mode 100644 index 0000000..984c6c7 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/AbstractClientParser.php @@ -0,0 +1,106 @@ + If no matches found: return + * -> Otherwise: + * Step 2: Walk through the list of regexes in feed_readers.yml and try to match every one + * -> Return the matched feed reader + * + * NOTE: Doing the big match before matching every single regex speeds up the detection + * + * @return array|null + */ + public function parse(): ?array + { + $result = null; + + if ($this->preMatchOverall()) { + foreach ($this->getRegexes() as $regex) { + $matches = $this->matchUserAgent($regex['regex']); + + if ($matches) { + $result = [ + 'type' => $this->parserName, + 'name' => $this->buildByMatch($regex['name'], $matches), + 'version' => $this->buildVersion((string) $regex['version'], $matches), + ]; + + break; + } + } + } + + return $result; + } + + /** + * Returns all names defined in the regexes + * + * Attention: This method might not return all names of detected clients + * + * @return array + */ + public static function getAvailableClients(): array + { + $instance = new static(); // @phpstan-ignore-line + $regexes = $instance->getRegexes(); + $names = []; + + foreach ($regexes as $regex) { + if (false !== \strpos($regex['name'], '$1')) { + continue; + } + + $names[] = $regex['name']; + } + + if (static::class === MobileApp::class) { + $names = \array_merge($names, [ + // Microsoft Office $1 + 'Microsoft Office Access', 'Microsoft Office Excel', 'Microsoft Office OneDrive for Business', + 'Microsoft Office OneNote', 'Microsoft Office PowerPoint', 'Microsoft Office Project', + 'Microsoft Office Publisher', 'Microsoft Office Visio', 'Microsoft Office Word', + // Podkicker$1 + 'Podkicker', 'Podkicker Pro', 'Podkicker Classic', + // radio.$1 + 'radio.at', 'radio.de', 'radio.dk', 'radio.es', 'radio.fr', + 'radio.it', 'radio.pl', 'radio.pt', 'radio.se', 'radio.net', + ]); + } + + \natcasesort($names); + + return \array_unique($names); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Browser.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Browser.php new file mode 100644 index 0000000..c502849 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Browser.php @@ -0,0 +1,1282 @@ + 'Via', + '1P' => 'Pure Mini Browser', + '4P' => 'Pure Lite Browser', + '1R' => 'Raise Fast Browser', + 'R1' => 'Rabbit Private Browser', + 'FQ' => 'Fast Browser UC Lite', + 'FJ' => 'Fast Explorer', + '1L' => 'Lightning Browser', + '1C' => 'Cake Browser', + '1I' => 'IE Browser Fast', + '1V' => 'Vegas Browser', + '1O' => 'OH Browser', + '3O' => 'OH Private Browser', + '1X' => 'XBrowser Mini', + '1S' => 'Sharkee Browser', + '2L' => 'Lark Browser', + '3P' => 'Pluma', + '1A' => 'Anka Browser', + 'AZ' => 'Azka Browser', + '1D' => 'Dragon Browser', + '1E' => 'Easy Browser', + 'DW' => 'Dark Web Browser', + 'D6' => 'Dark Browser', + '18' => '18+ Privacy Browser', + '1B' => '115 Browser', + 'DM' => '1DM Browser', + '1M' => '1DM+ Browser', + '2B' => '2345 Browser', + '3B' => '360 Secure Browser', + '36' => '360 Phone Browser', + '7B' => '7654 Browser', + 'AA' => 'Avant Browser', + 'AB' => 'ABrowse', + '4A' => 'Acoo Browser', + 'BW' => 'AdBlock Browser', + 'A7' => 'Adult Browser', + '8A' => 'Ai Browser', + 'A9' => 'Airfind Secure Browser', + 'AF' => 'ANT Fresco', + 'AG' => 'ANTGalio', + 'AL' => 'Aloha Browser', + 'AH' => 'Aloha Browser Lite', + 'A8' => 'ALVA', + '9A' => 'AltiBrowser', + 'AM' => 'Amaya', + 'A3' => 'Amaze Browser', + 'A5' => 'Amerigo', + 'AO' => 'Amigo', + 'AN' => 'Android Browser', + '3A' => 'AOL Explorer', + 'AE' => 'AOL Desktop', + 'AD' => 'AOL Shield', + 'A4' => 'AOL Shield Pro', + '2A' => 'Aplix', + 'A6' => 'AppBrowzer', + '0A' => 'AppTec Secure Browser', + 'AP' => 'APUS Browser', + 'AR' => 'Arora', + 'AX' => 'Arctic Fox', + 'AV' => 'Amiga Voyager', + 'AW' => 'Amiga Aweb', + 'PN' => 'APN Browser', + '6A' => 'Arachne', + 'RA' => 'Arc Search', + 'R5' => 'Armorfly Browser', + 'AI' => 'Arvin', + 'AK' => 'Ask.com', + 'AU' => 'Asus Browser', + 'A0' => 'Atom', + 'AT' => 'Atomic Web Browser', + 'A2' => 'Atlas', + 'AS' => 'Avast Secure Browser', + 'VG' => 'AVG Secure Browser', + 'AC' => 'Avira Secure Browser', + 'A1' => 'AwoX', + '7A' => 'Awesomium', + '5B' => 'Basic Web Browser', + 'BA' => 'Beaker Browser', + 'BM' => 'Beamrise', + 'F7' => 'BF Browser', + 'BB' => 'BlackBerry Browser', + '6B' => 'Bluefy', + 'H1' => 'BrowseHere', + 'B8' => 'Browser Hup Pro', + 'BD' => 'Baidu Browser', + 'BS' => 'Baidu Spark', + 'BG' => 'Bang', + 'B9' => 'Bangla Browser', + 'BI' => 'Basilisk', + 'BV' => 'Belva Browser', + 'B5' => 'Beyond Private Browser', + 'BE' => 'Beonex', + 'B2' => 'Berry Browser', + 'BT' => 'Bitchute Browser', + '9B' => 'BizBrowser', + 'BH' => 'BlackHawk', + 'B0' => 'Bloket', + 'BJ' => 'Bunjalloo', + 'BL' => 'B-Line', + 'B6' => 'Black Lion Browser', + 'BU' => 'Blue Browser', + 'BO' => 'Bonsai', + 'BN' => 'Borealis Navigator', + 'BR' => 'Brave', + 'BK' => 'BriskBard', + 'K2' => 'BroKeep Browser', + 'B3' => 'Browspeed Browser', + 'BX' => 'BrowseX', + 'BZ' => 'Browzar', + 'B7' => 'Browlser', + 'M9' => 'Browser Mini', + '4B' => 'BrowsBit', + 'BY' => 'Biyubi', + 'BF' => 'Byffox', + 'B4' => 'BXE Browser', + 'CA' => 'Camino', + '5C' => 'Catalyst', + 'XP' => 'Catsxp', + '0C' => 'Cave Browser', + 'CL' => 'CCleaner', + 'C8' => 'CG Browser', + 'CJ' => 'ChanjetCloud', + 'C6' => 'Chedot', + 'C9' => 'Cherry Browser', + 'C0' => 'Centaury', + 'CQ' => 'Cliqz', + 'CC' => 'Coc Coc', + 'C4' => 'CoolBrowser', + 'C2' => 'Colibri', + '6C' => 'Columbus Browser', + 'CD' => 'Comodo Dragon', + 'C1' => 'Coast', + 'CX' => 'Charon', + 'CE' => 'CM Browser', + 'C7' => 'CM Mini', + 'CF' => 'Chrome Frame', + 'HC' => 'Headless Chrome', + 'CH' => 'Chrome', + 'CI' => 'Chrome Mobile iOS', + 'CK' => 'Conkeror', + 'CM' => 'Chrome Mobile', + '3C' => 'Chowbo', + '7C' => 'Classilla', + 'CN' => 'CoolNovo', + '4C' => 'Colom Browser', + 'CO' => 'CometBird', + '2C' => 'Comfort Browser', + 'CB' => 'COS Browser', + 'CW' => 'Cornowser', + 'C3' => 'Chim Lac', + 'CP' => 'ChromePlus', + 'CR' => 'Chromium', + 'C5' => 'Chromium GOST', + 'CY' => 'Cyberfox', + 'CS' => 'Cheshire', + '8C' => 'Cromite', + 'RC' => 'Crow Browser', + 'CT' => 'Crusta', + 'CG' => 'Craving Explorer', + 'CZ' => 'Crazy Browser', + 'CU' => 'Cunaguaro', + 'CV' => 'Chrome Webview', + 'YC' => 'CyBrowser', + 'DB' => 'dbrowser', + 'PD' => 'Peeps dBrowser', + 'DK' => 'Dark Web', + 'DP' => 'Dark Web Private', + 'D1' => 'Debuggable Browser', + 'DC' => 'Decentr', + 'DE' => 'Deepnet Explorer', + 'DG' => 'deg-degan', + 'DA' => 'Deledao', + 'DT' => 'Delta Browser', + 'D0' => 'Desi Browser', + 'DS' => 'DeskBrowse', + 'D3' => 'Dezor', + 'II' => 'Diigo Browser', + 'D2' => 'DoCoMo', + 'DF' => 'Dolphin', + 'DZ' => 'Dolphin Zero', + 'DO' => 'Dorado', + 'DR' => 'Dot Browser', + 'DL' => 'Dooble', + 'DI' => 'Dillo', + 'DU' => 'DUC Browser', + 'DD' => 'DuckDuckGo Privacy Browser', + 'E1' => 'East Browser', + 'EC' => 'Ecosia', + 'EW' => 'Edge WebView', + 'EV' => 'Every Browser', + 'EI' => 'Epic', + 'EL' => 'Elinks', + 'EN' => 'EinkBro', + 'EB' => 'Element Browser', + 'EE' => 'Elements Browser', + 'EO' => 'Eolie', + 'EX' => 'Explore Browser', + 'EZ' => 'eZ Browser', + 'E2' => 'EudoraWeb', + 'EU' => 'EUI Browser', + 'EP' => 'GNOME Web', + 'G1' => 'G Browser', + 'ES' => 'Espial TV Browser', + 'FG' => 'fGet', + 'FA' => 'Falkon', + 'FX' => 'Faux Browser', + 'F8' => 'Fire Browser', + 'F4' => 'Fiery Browser', + 'F1' => 'Firefox Mobile iOS', + 'FB' => 'Firebird', + 'FD' => 'Fluid', + 'FE' => 'Fennec', + 'FF' => 'Firefox', + 'FK' => 'Firefox Focus', + 'FY' => 'Firefox Reality', + 'FR' => 'Firefox Rocket', + '1F' => 'Firefox Klar', + 'F0' => 'Float Browser', + 'FL' => 'Flock', + 'FP' => 'Floorp', + 'FO' => 'Flow', + 'F2' => 'Flow Browser', + 'FM' => 'Firefox Mobile', + 'FW' => 'Fireweb', + 'FN' => 'Fireweb Navigator', + 'FH' => 'Flash Browser', + 'FS' => 'Flast', + 'F5' => 'Flyperlink', + 'F9' => 'FOSS Browser', + 'FU' => 'FreeU', + 'F6' => 'Freedom Browser', + 'FT' => 'Frost', + 'F3' => 'Frost+', + 'FI' => 'Fulldive', + 'GA' => 'Galeon', + 'G8' => 'Gener8', + 'GH' => 'Ghostery Privacy Browser', + 'GI' => 'GinxDroid Browser', + 'GB' => 'Glass Browser', + 'GD' => 'Godzilla Browser', + 'G3' => 'Good Browser', + 'GE' => 'Google Earth', + 'GP' => 'Google Earth Pro', + 'GO' => 'GOG Galaxy', + 'GR' => 'GoBrowser', + 'GK' => 'GoKu', + 'G2' => 'GO Browser', + 'RN' => 'GreenBrowser', + 'HW' => 'Habit Browser', + 'H7' => 'Halo Browser', + 'HB' => 'Harman Browser', + 'HS' => 'HasBrowser', + 'HA' => 'Hawk Turbo Browser', + 'HQ' => 'Hawk Quick Browser', + 'HE' => 'Helio', + 'HN' => 'Herond Browser', + 'HX' => 'Hexa Web Browser', + 'HI' => 'Hi Browser', + 'HO' => 'hola! Browser', + 'H4' => 'Holla Web Browser', + 'H5' => 'HotBrowser', + 'HJ' => 'HotJava', + 'H6' => 'HONOR Browser', + 'HT' => 'HTC Browser', + 'HU' => 'Huawei Browser Mobile', + 'HP' => 'Huawei Browser', + 'H3' => 'HUB Browser', + 'IO' => 'iBrowser', + 'IS' => 'iBrowser Mini', + 'IB' => 'IBrowse', + 'I6' => 'iDesktop PC Browser', + 'IC' => 'iCab', + 'I2' => 'iCab Mobile', + '4I' => 'iNet Browser', + 'I1' => 'Iridium', + 'I3' => 'Iron Mobile', + 'I4' => 'IceCat', + 'ID' => 'IceDragon', + 'IV' => 'Isivioo', + 'I8' => 'IVVI Browser', + 'IW' => 'Iceweasel', + '2I' => 'Impervious Browser', + 'N3' => 'Incognito Browser', + 'IN' => 'Inspect Browser', + 'I9' => 'Insta Browser', + 'IE' => 'Internet Explorer', + 'I7' => 'Internet Browser Secure', + '5I' => 'Internet Webbrowser', + '3I' => 'Intune Managed Browser', + 'I5' => 'Indian UC Mini Browser', + 'Z0' => 'InBrowser', + 'IG' => 'Involta Go', + 'IM' => 'IE Mobile', + 'IR' => 'Iron', + 'JB' => 'Japan Browser', + 'JS' => 'Jasmine', + 'JA' => 'JavaFX', + 'JL' => 'Jelly', + 'JI' => 'Jig Browser', + 'JP' => 'Jig Browser Plus', + 'JO' => 'JioSphere', + 'JZ' => 'JUZI Browser', + 'KB' => 'K.Browser', + 'KF' => 'Keepsafe Browser', + 'K7' => 'KeepSolid Browser', + 'KS' => 'Kids Safe Browser', + 'KI' => 'Kindle Browser', + 'KM' => 'K-meleon', + 'KJ' => 'K-Ninja', + 'KO' => 'Konqueror', + 'KP' => 'Kapiko', + 'KE' => 'Keyboard Browser', + 'KN' => 'Kinza', + 'K4' => 'Kitt', + 'KW' => 'Kiwi', + 'KD' => 'Kode Browser', + 'KU' => 'KUN', + 'KT' => 'KUTO Mini Browser', + 'KY' => 'Kylo', + 'KZ' => 'Kazehakase', + 'LB' => 'Cheetah Browser', + 'LD' => 'Ladybird', + 'LA' => 'Lagatos Browser', + 'GN' => 'Legan Browser', + 'LR' => 'Lexi Browser', + 'LV' => 'Lenovo Browser', + 'LF' => 'LieBaoFast', + 'LG' => 'LG Browser', + 'LH' => 'Light', + 'L4' => 'Lightning Browser Plus', + 'L1' => 'Lilo', + 'LI' => 'Links', + 'RI' => 'Liri Browser', + 'LC' => 'LogicUI TV Browser', + 'IF' => 'Lolifox', + 'L3' => 'Lotus', + 'LO' => 'Lovense Browser', + 'LT' => 'LT Browser', + 'LU' => 'LuaKit', + 'LJ' => 'LUJO TV Browser', + 'LL' => 'Lulumi', + 'LS' => 'Lunascape', + 'LN' => 'Lunascape Lite', + 'LX' => 'Lynx', + 'L2' => 'Lynket Browser', + 'MD' => 'Mandarin', + 'MP' => 'Maple', + 'M5' => 'MarsLab Web Browser', + 'M7' => 'MaxBrowser', + 'M1' => 'mCent', + 'MB' => 'MicroB', + 'MC' => 'NCSA Mosaic', + 'MZ' => 'Meizu Browser', + 'ME' => 'Mercury', + 'M2' => 'Me Browser', + 'MF' => 'Mobile Safari', + 'MI' => 'Midori', + 'M3' => 'Midori Lite', + 'M6' => 'MixerBox AI', + 'MO' => 'Mobicip', + 'MU' => 'Mi Browser', + 'MS' => 'Mobile Silk', + 'MK' => 'Mogok Browser', + 'M8' => 'Motorola Internet Browser', + 'MN' => 'Minimo', + 'MT' => 'Mint Browser', + 'MX' => 'Maxthon', + 'M4' => 'MaxTube Browser', + 'MA' => 'Maelstrom', + '3M' => 'Mises', + 'MM' => 'Mmx Browser', + 'NM' => 'MxNitro', + 'MY' => 'Mypal', + 'MR' => 'Monument Browser', + 'MW' => 'MAUI WAP Browser', + 'N7' => 'Naenara Browser', + 'NW' => 'Navigateur Web', + 'NK' => 'Naked Browser', + 'NA' => 'Naked Browser Pro', + 'NR' => 'NFS Browser', + 'N5' => 'Ninetails', + 'NB' => 'Nokia Browser', + 'NO' => 'Nokia OSS Browser', + 'NV' => 'Nokia Ovi Browser', + 'N2' => 'Norton Private Browser', + 'NX' => 'Nox Browser', + 'N1' => 'NOMone VR Browser', + 'N6' => 'NOOK Browser', + 'NE' => 'NetSurf', + 'NF' => 'NetFront', + 'NL' => 'NetFront Life', + 'NP' => 'NetPositive', + 'NS' => 'Netscape', + 'WR' => 'NextWord Browser', + 'N8' => 'Ninesky', + 'NT' => 'NTENT Browser', + 'NU' => 'Nuanti Meta', + 'NI' => 'Nuviu', + 'O9' => 'Ocean Browser', + 'OC' => 'Oculus Browser', + 'O6' => 'Odd Browser', + 'O1' => 'Opera Mini iOS', + 'OB' => 'Obigo', + 'O2' => 'Odin', + '2O' => 'Odin Browser', + 'H2' => 'OceanHero', + 'OD' => 'Odyssey Web Browser', + 'OF' => 'Off By One', + 'O5' => 'Office Browser', + 'HH' => 'OhHai Browser', + 'OL' => 'OnBrowser Lite', + 'OE' => 'ONE Browser', + 'N4' => 'Onion Browser', + '1N' => 'ONIONBrowser', + 'Y1' => 'Opera Crypto', + 'OX' => 'Opera GX', + 'OG' => 'Opera Neon', + 'OH' => 'Opera Devices', + 'OI' => 'Opera Mini', + 'OM' => 'Opera Mobile', + 'OP' => 'Opera', + 'ON' => 'Opera Next', + 'OO' => 'Opera Touch', + 'OU' => 'Orbitum', + 'OA' => 'Orca', + 'OS' => 'Ordissimo', + 'OR' => 'Oregano', + 'O0' => 'Origin In-Game Overlay', + 'OY' => 'Origyn Web Browser', + 'O8' => 'OrNET Browser', + 'OV' => 'Openwave Mobile Browser', + 'O3' => 'OpenFin', + 'O4' => 'Open Browser', + '4U' => 'Open Browser 4U', + '5G' => 'Open Browser fast 5G', + '5O' => 'Open Browser Lite', + 'O7' => 'Open TV Browser', + 'OW' => 'OmniWeb', + 'OT' => 'Otter Browser', + '4O' => 'Owl Browser', + 'JR' => 'OJR Browser', + 'PL' => 'Palm Blazer', + 'PM' => 'Pale Moon', + 'PY' => 'Polypane', + '8P' => 'Prism', + 'PP' => 'Oppo Browser', + 'P6' => 'Opus Browser', + 'PR' => 'Palm Pre', + '2E' => 'Pocket Internet Explorer', + '7I' => 'Puffin Cloud Browser', + '6I' => 'Puffin Incognito Browser', + 'PU' => 'Puffin Secure Browser', + '2P' => 'Puffin Web Browser', + 'PW' => 'Palm WebPro', + 'PA' => 'Palmscape', + 'P7' => 'Pawxy', + '0P' => 'Peach Browser', + 'PE' => 'Perfect Browser', + 'K6' => 'Perk', + 'P1' => 'Phantom.me', + 'PH' => 'Phantom Browser', + 'PX' => 'Phoenix', + 'PB' => 'Phoenix Browser', + '5P' => 'Photon', + 'N9' => 'Pintar Browser', + 'P9' => 'PirateBrowser', + 'P8' => 'PICO Browser', + 'PF' => 'PlayFree Browser', + 'PK' => 'PocketBook Browser', + 'PO' => 'Polaris', + 'PT' => 'Polarity', + 'LY' => 'PolyBrowser', + '9P' => 'Presearch', + 'BP' => 'Privacy Browser', + 'PI' => 'PrivacyWall', + 'P4' => 'Privacy Explorer Fast Safe', + 'X5' => 'Privacy Pioneer Browser', + 'P3' => 'Private Internet Browser', + 'P5' => 'Proxy Browser', + '7P' => 'Proxyium', + '6P' => 'Proxynet', + '2F' => 'ProxyFox', + '2M' => 'ProxyMax', + 'P2' => 'Pi Browser', + 'P0' => 'PronHub Browser', + 'PC' => 'PSI Secure Browser', + 'RW' => 'Reqwireless WebViewer', + 'RO' => 'Roccat', + 'PS' => 'Microsoft Edge', + 'QA' => 'Qazweb', + 'QI' => 'Qiyu', + 'QJ' => 'QJY TV Browser', + 'Q3' => 'Qmamu', + 'Q4' => 'Quick Search TV', + 'Q2' => 'QQ Browser Lite', + 'Q1' => 'QQ Browser Mini', + 'QQ' => 'QQ Browser', + 'QS' => 'Quick Browser', + 'QT' => 'Qutebrowser', + 'QU' => 'Quark', + 'Q6' => 'QuarkPC', + 'Q7' => 'Quetta', + 'QZ' => 'QupZilla', + 'QM' => 'Qwant Mobile', + 'Q5' => 'QtWeb', + 'QW' => 'QtWebEngine', + 'R3' => 'Rakuten Browser', + 'R4' => 'Rakuten Web Search', + 'R2' => 'Raspbian Chromium', + 'RT' => 'RCA Tor Explorer', + 'RE' => 'Realme Browser', + 'RK' => 'Rekonq', + 'RM' => 'RockMelt', + 'RB' => 'Roku Browser', + 'SB' => 'Samsung Browser', + '3L' => 'Samsung Browser Lite', + 'SA' => 'Sailfish Browser', + 'R0' => 'SberBrowser', + 'S8' => 'Seewo Browser', + 'SC' => 'SEMC-Browser', + 'SE' => 'Sogou Explorer', + 'SO' => 'Sogou Mobile Browser', + 'RF' => 'SOTI Surf', + '2S' => 'Soul Browser', + 'T0' => 'Soundy Browser', + 'SF' => 'Safari', + 'PV' => 'Safari Technology Preview', + 'S5' => 'Safe Exam Browser', + 'SW' => 'SalamWeb', + 'VN' => 'Savannah Browser', + 'SD' => 'SavySoda', + 'S9' => 'Secure Browser', + 'SV' => 'SFive', + 'SH' => 'Shiira', + 'K1' => 'Sidekick', + 'S1' => 'SimpleBrowser', + '3S' => 'SilverMob US', + 'ZB' => 'Singlebox', + 'SY' => 'Sizzy', + 'K3' => 'Skye', + 'SK' => 'Skyfire', + 'KL' => 'SkyLeap', + 'SS' => 'Seraphic Sraf', + 'KK' => 'SiteKiosk', + 'SL' => 'Sleipnir', + '8B' => 'SlimBoat', + 'S6' => 'Slimjet', + 'S7' => 'SP Browser', + '9S' => 'Sony Small Browser', + '8S' => 'Secure Private Browser', + 'X2' => 'SecureX', + 'T1' => 'Stampy Browser', + '7S' => '7Star', + 'SQ' => 'Smart Browser', + '6S' => 'Smart Search & Web Browser', + 'LE' => 'Smart Lenovo Browser', + 'OZ' => 'Smooz', + 'SN' => 'Snowshoe', + 'K5' => 'Spark', + 'B1' => 'Spectre Browser', + 'S2' => 'Splash', + 'SI' => 'Sputnik Browser', + 'SR' => 'Sunrise', + '0S' => 'Sunflower Browser', + 'SP' => 'SuperBird', + 'SU' => 'Super Fast Browser', + '5S' => 'SuperFast Browser', + 'HR' => 'Sushi Browser', + 'S3' => 'surf', + '4S' => 'Surf Browser', + 'RY' => 'Surfy Browser', + 'SG' => 'Stargon', + 'S0' => 'START Internet Browser', + '5A' => 'Stealth Browser', + 'S4' => 'Steam In-Game Overlay', + 'ST' => 'Streamy', + 'SX' => 'Swiftfox', + 'W7' => 'Swiftweasel', + 'SZ' => 'Seznam Browser', + 'W1' => 'Sweet Browser', + '2X' => 'SX Browser', + 'TP' => 'T+Browser', + 'TR' => 'T-Browser', + 'TO' => 't-online.de Browser', + 'TT' => 'TalkTo', + 'TA' => 'Tao Browser', + 'T2' => 'tararia', + 'TH' => 'Thor', + '1T' => 'Tor Browser', + 'TF' => 'TenFourFox', + 'TB' => 'Tenta Browser', + 'TE' => 'Tesla Browser', + 'TZ' => 'Tizen Browser', + 'TI' => 'Tint Browser', + 'TL' => 'TrueLocation Browser', + 'TC' => 'TUC Mini Browser', + 'TK' => 'TUSK', + 'TU' => 'Tungsten', + 'TG' => 'ToGate', + 'T3' => 'Total Browser', + 'TQ' => 'TQ Browser', + 'TS' => 'TweakStyle', + 'TV' => 'TV Bro', + 'T4' => 'TV-Browser Internet', + 'U0' => 'U Browser', + 'UB' => 'UBrowser', + 'UC' => 'UC Browser', + 'UH' => 'UC Browser HD', + 'UM' => 'UC Browser Mini', + 'UT' => 'UC Browser Turbo', + 'UI' => 'Ui Browser Mini', + 'UP' => 'UPhone Browser', + 'UR' => 'UR Browser', + 'UZ' => 'Uzbl', + 'UE' => 'Ume Browser', + 'V0' => 'vBrowser', + 'VA' => 'Vast Browser', + 'V3' => 'VD Browser', + 'VR' => 'Veera', + 'VE' => 'Venus Browser', + 'WD' => 'Vewd Browser', + 'V5' => 'VibeMate', + 'N0' => 'Nova Video Downloader Pro', + 'VS' => 'Viasat Browser', + 'VI' => 'Vivaldi', + 'VV' => 'vivo Browser', + 'V2' => 'Vivid Browser Mini', + 'VB' => 'Vision Mobile Browser', + 'V4' => 'Vertex Surf', + 'VM' => 'VMware AirWatch', + 'V6' => 'VMS Mosaic', + 'VK' => 'Vonkeror', + 'VU' => 'Vuhuv', + 'WI' => 'Wear Internet Browser', + 'WP' => 'Web Explorer', + 'W3' => 'Web Browser & Explorer', + 'W5' => 'Webian Shell', + 'W4' => 'WebDiscover', + 'WE' => 'WebPositive', + 'W6' => 'Weltweitimnetz Browser', + 'WX' => 'Wexond', + 'WF' => 'Waterfox', + 'WB' => 'Wave Browser', + 'WA' => 'Wavebox', + 'WH' => 'Whale Browser', + 'W2' => 'Whale TV Browser', + 'WO' => 'wOSBrowser', + '3W' => 'w3m', + 'WT' => 'WeTab Browser', + '1W' => 'World Browser', + 'WL' => 'Wolvic', + 'WK' => 'Wukong Browser', + 'WY' => 'Wyzo', + 'YG' => 'YAGI', + 'YJ' => 'Yahoo! Japan Browser', + 'YA' => 'Yandex Browser', + 'Y4' => 'Yandex Browser Corp', + 'YL' => 'Yandex Browser Lite', + 'YN' => 'Yaani Browser', + 'Y2' => 'Yo Browser', + 'YB' => 'Yolo Browser', + 'YO' => 'YouCare', + 'Y3' => 'YouBrowser', + 'YZ' => 'Yuzu Browser', + 'XR' => 'xBrowser', + 'X3' => 'MMBOX XBrowser', + 'XB' => 'X Browser Lite', + 'X0' => 'X-VPN', + 'X1' => 'xBrowser Pro Super Fast', + 'XN' => 'XNX Browser', + 'XT' => 'XtremeCast', + 'XS' => 'xStand', + 'XI' => 'Xiino', + 'X4' => 'XnBrowse', + 'XO' => 'Xooloo Internet', + 'XV' => 'Xvast', + 'ZE' => 'Zetakey', + 'ZV' => 'Zvu', + 'ZI' => 'Zirco Browser', + 'ZR' => 'Zordo Browser', + 'ZT' => 'ZTE Browser', + + // detected browsers in older versions + // 'IA' => 'Iceape', => pim + // 'SM' => 'SeaMonkey', => pim + ]; + + /** + * Browser families mapped to the short codes of the associated browsers + * + * @var array + */ + protected static $browserFamilies = [ + 'Android Browser' => ['AN'], + 'BlackBerry Browser' => ['BB'], + 'Baidu' => ['BD', 'BS', 'H6'], + 'Amiga' => ['AV', 'AW'], + 'Chrome' => [ + 'CH', '2B', '7S', 'A0', 'AC', 'A4', 'AE', 'AH', 'AI', + 'AO', 'AS', 'BA', 'BM', 'BR', 'C2', 'C3', 'C5', 'C4', + 'C6', 'CC', 'CD', 'CE', 'CF', 'CG', '1B', 'CI', 'CL', + 'CM', 'CN', 'CP', 'CR', 'CV', 'CW', 'DA', 'DD', 'DG', + 'DR', 'EC', 'EE', 'EU', 'EW', 'FA', 'FS', 'GB', 'GI', + 'H2', 'HA', 'HE', 'HH', 'HS', 'I3', 'IR', 'JB', 'KN', + 'KW', 'LF', 'LL', 'LO', 'M1', 'MA', 'MD', 'MR', 'MS', + 'MT', 'MZ', 'NM', 'NR', 'O0', 'O2', 'O3', 'OC', 'PB', + 'PT', 'QU', 'QW', 'RM', 'S4', 'S6', 'S8', 'S9', 'SB', + 'SG', 'SS', 'SU', 'SV', 'SW', 'SY', 'SZ', 'T1', 'TA', + 'TB', 'TG', 'TR', 'TS', 'TU', 'TV', 'UB', 'UR', 'VE', + 'VG', 'VI', 'VM', 'WP', 'WH', 'XV', 'YJ', 'YN', 'FH', + 'B1', 'BO', 'HB', 'PC', 'LA', 'LT', 'PD', 'HR', 'HU', + 'HP', 'IO', 'TP', 'CJ', 'HQ', 'HI', 'PN', 'BW', 'YO', + 'DC', 'G8', 'DT', 'AP', 'AK', 'UI', 'SD', 'VN', '4S', + '2S', 'RF', 'LR', 'SQ', 'BV', 'L1', 'F0', 'KS', 'V0', + 'C8', 'AZ', 'MM', 'BT', 'N0', 'P0', 'F3', 'VS', 'DU', + 'D0', 'P1', 'O4', '8S', 'H3', 'TE', 'WB', 'K1', 'P2', + 'XO', 'U0', 'B0', 'VA', 'X0', 'NX', 'O5', 'R1', 'I1', + 'HO', 'A5', 'X1', '18', 'B5', 'B6', 'TC', 'A6', '2X', + 'F4', 'YG', 'WR', 'NA', 'DM', '1M', 'A7', 'XN', 'XT', + 'XB', 'W1', 'HT', 'B8', 'F5', 'B9', 'WA', 'T0', 'HC', + 'O6', 'P7', 'LJ', 'LC', 'O7', 'N2', 'A8', 'P8', 'RB', + '1W', 'EV', 'I9', 'V4', 'H4', '1T', 'M5', '0S', '0C', + 'ZR', 'D6', 'F6', 'RC', 'WD', 'P3', 'FT', 'A9', 'X2', + 'N3', 'GD', 'O9', 'Q3', 'F7', 'K2', 'P5', 'H5', 'V3', + 'K3', 'Q4', 'G2', 'R2', 'WX', 'XP', '3I', 'BG', 'R0', + 'JO', 'OL', 'GN', 'W4', 'QI', 'E1', 'RI', '8B', '5B', + 'K4', 'WK', 'T3', 'K5', 'MU', '9P', 'K6', 'VR', 'N9', + 'M9', 'F9', '0P', '0A', 'JR', 'D3', 'TK', 'BP', '2F', + '2M', 'K7', '1N', '8A', 'H7', 'X3', 'T4', 'X4', '5O', + '8C', '3M', '6I', '2P', 'PU', '7I', 'X5', 'AL', '3P', + 'W2', 'ZB', 'HN', 'Q6', 'Q7', + ], + 'Firefox' => [ + 'FF', 'BI', 'BF', 'BH', 'BN', 'C0', 'CU', 'EI', 'F1', + 'FB', 'FE', 'AX', 'FM', 'FR', 'FY', 'I4', 'IF', '8P', + 'IW', 'LH', 'LY', 'MB', 'MN', 'MO', 'MY', 'OA', 'OS', + 'PI', 'PX', 'QA', 'S5', 'SX', 'TF', 'TO', 'WF', 'ZV', + 'FP', 'AD', '2I', 'P9', 'KJ', 'WY', 'VK', 'W5', + '7C', 'N7', 'W7', + ], + 'Internet Explorer' => ['IE', 'CZ', 'BZ', 'IM', 'PS', '3A', '4A', 'RN', '2E'], + 'Konqueror' => ['KO'], + 'NetFront' => ['NF'], + 'NetSurf' => ['NE'], + 'Nokia Browser' => ['NB', 'DO', 'NO', 'NV'], + 'Opera' => ['OP', 'OG', 'OH', 'OI', 'OM', 'ON', 'OO', 'O1', 'OX', 'Y1'], + 'Safari' => ['SF', 'S7', 'MF', 'SO', 'PV'], + 'Sailfish Browser' => ['SA'], + ]; + + /** + * Browsers that are available for mobile devices only + * + * @var array + */ + protected static $mobileOnlyBrowsers = [ + '36', 'AH', 'AI', 'BL', 'C1', 'C4', 'CB', 'CW', 'DB', + '3M', 'DT', 'EU', 'EZ', 'FK', 'FM', 'FR', 'FX', 'GH', + 'GI', 'GR', 'HA', 'HU', 'IV', 'JB', 'KD', 'M1', 'MF', + 'MN', 'MZ', 'NX', 'OC', 'OI', 'OM', 'OZ', '2P', 'PI', + 'PE', 'QU', 'RE', 'S0', 'S7', 'SA', 'SB', 'SG', 'SK', + 'ST', 'SU', 'T1', 'UH', 'UM', 'UT', 'VE', 'VV', 'WI', + 'WP', 'YN', 'IO', 'IS', 'HQ', 'RW', 'HI', 'PN', 'BW', + 'YO', 'PK', 'MR', 'AP', 'AK', 'UI', 'SD', 'VN', '4S', + 'RF', 'LR', 'SQ', 'BV', 'L1', 'F0', 'KS', 'V0', 'C8', + 'AZ', 'MM', 'BT', 'N0', 'P0', 'F3', 'DU', 'D0', 'P1', + 'O4', 'XO', 'U0', 'B0', 'VA', 'X0', 'A5', 'X1', '18', + 'B5', 'B6', 'TC', 'A6', '2X', 'F4', 'YG', 'WR', 'NA', + 'DM', '1M', 'A7', 'XN', 'XT', 'XB', 'W1', 'HT', 'B7', + 'B9', 'T0', 'I8', 'O6', 'P7', 'O8', '4B', 'A8', 'P8', + '1W', 'EV', 'Z0', 'I9', 'V4', 'H4', 'M5', '0S', '0C', + 'ZR', 'D6', 'F6', 'P3', 'FT', 'A9', 'X2', 'NI', 'FG', + 'TH', 'N3', 'GD', 'O9', 'Q3', 'F7', 'K2', 'N4', 'P5', + 'H5', 'V3', 'G2', 'BG', 'OL', 'II', 'TL', 'M6', 'Y3', + 'M7', 'GN', 'JR', 'IG', 'HW', '4O', 'OU', '5P', 'KE', + '5A', 'TT', '6P', 'G3', '7P', 'VU', 'F8', 'L4', 'DK', + 'DP', 'KL', 'K4', 'N6', 'KU', 'WK', 'M8', 'UP', 'ZT', + '9P', 'N8', 'VR', 'N9', 'M9', 'F9', '0P', '0A', '2F', + '2M', 'K7', '1N', '8A', 'H7', 'X3', 'X4', '5O', '6I', + '7I', 'X5', '3P', '2E', + ]; + + /** + * Contains a list of mappings from OS names we use to known client hint values + * + * @var array> + */ + protected static $clientHintMapping = [ + 'Chrome' => ['Google Chrome'], + 'Chrome Webview' => ['Android WebView'], + 'DuckDuckGo Privacy Browser' => ['DuckDuckGo'], + 'Edge WebView' => ['Microsoft Edge WebView2'], + 'Mi Browser' => ['Miui Browser', 'XiaoMiBrowser'], + 'Microsoft Edge' => ['Edge'], + 'Norton Private Browser' => ['Norton Secure Browser'], + 'Vewd Browser' => ['Vewd Core'], + ]; + + /** + * Browser constructor. + * + * @param string $ua + * @param ClientHints|null $clientHints + */ + public function __construct(string $ua = '', ?ClientHints $clientHints = null) + { + $this->browserHints = new BrowserHints($ua, $clientHints); + parent::__construct($ua, $clientHints); + } + + /** + * Sets the client hints to parse + * + * @param ?ClientHints $clientHints client hints + */ + public function setClientHints(?ClientHints $clientHints): void + { + parent::setClientHints($clientHints); + $this->browserHints->setClientHints($clientHints); + } + + /** + * Sets the user agent to parse + * + * @param string $ua user agent + */ + public function setUserAgent(string $ua): void + { + parent::setUserAgent($ua); + $this->browserHints->setUserAgent($ua); + } + + /** + * Sets the Cache class + * + * @param CacheInterface $cache + */ + public function setCache(CacheInterface $cache): void + { + parent::setCache($cache); + $this->browserHints->setCache($cache); + } + + /** + * Returns list of all available browsers + * @return array + */ + public static function getAvailableBrowsers(): array + { + return self::$availableBrowsers; + } + + /** + * Returns list of all available browser families + * @return array + */ + public static function getAvailableBrowserFamilies(): array + { + return self::$browserFamilies; + } + + /** + * @param string $name name browser + * + * @return string + */ + public static function getBrowserShortName(string $name): ?string + { + foreach (self::getAvailableBrowsers() as $browserShort => $browserName) { + if (\strtolower($name) === \strtolower($browserName)) { + return (string) $browserShort; + } + } + + return null; + } + + /** + * @param string $browserLabel name or short name + * + * @return string|null If null, "Unknown" + */ + public static function getBrowserFamily(string $browserLabel): ?string + { + if (\in_array($browserLabel, self::$availableBrowsers)) { + $browserLabel = \array_search($browserLabel, self::$availableBrowsers); + } + + foreach (self::$browserFamilies as $browserFamily => $browserLabels) { + if (\in_array($browserLabel, $browserLabels)) { + return $browserFamily; + } + } + + return null; + } + + /** + * Returns if the given browser is mobile only + * + * @param string $browser Label or name of browser + * + * @return bool + */ + public static function isMobileOnlyBrowser(string $browser): bool + { + return \in_array($browser, self::$mobileOnlyBrowsers) || (\in_array($browser, self::$availableBrowsers) + && \in_array(\array_search($browser, self::$availableBrowsers), self::$mobileOnlyBrowsers)); + } + + /** + * Sets the YamlParser class + * + * @param YamlParser $yamlParser + */ + public function setYamlParser(YamlParser $yamlParser): void + { + parent::setYamlParser($yamlParser); + $this->browserHints->setYamlParser($this->getYamlParser()); + } + + /** + * @inheritdoc + */ + public function parse(): ?array + { + $browserFromClientHints = $this->parseBrowserFromClientHints(); + $browserFromUserAgent = $this->parseBrowserFromUserAgent(); + + // use client hints in favor of user agent data if possible + if (!empty($browserFromClientHints['name']) && !empty($browserFromClientHints['version'])) { + $name = $browserFromClientHints['name']; + $version = $browserFromClientHints['version']; + $short = $browserFromClientHints['short_name']; + $engine = ''; + $engineVersion = ''; + + // If the version reported from the client hints is YYYY or YYYY.MM (e.g., 2022 or 2022.04), + // then it is the Iridium browser + // https://iridiumbrowser.de/news/ + if (\preg_match('/^202[0-4]/', $version)) { + $name = 'Iridium'; + $short = 'I1'; + } + + // https://bbs.360.cn/thread-16096544-1-1.html + if (\preg_match('/^15/', $version) && \preg_match('/^114/', $browserFromUserAgent['version'])) { + $name = '360 Secure Browser'; + $short = '3B'; + $engine = $browserFromUserAgent['engine'] ?? ''; + $engineVersion = $browserFromUserAgent['engine_version'] ?? ''; + } + + // If client hints report the following browsers, we use the version from useragent + if (!empty($browserFromUserAgent['version']) + && \in_array($short, ['A0', 'AL', 'HP', 'JR', 'MU', 'OM', 'OP', 'VR']) + ) { + $version = $browserFromUserAgent['version']; + } + + if ('Vewd Browser' === $name) { + $engine = $browserFromUserAgent['engine'] ?? ''; + $engineVersion = $browserFromUserAgent['engine_version'] ?? ''; + } + + // If client hints report Chromium, but user agent detects a Chromium based browser, we favor this instead + if (('Chromium' === $name || 'Chrome Webview' === $name) + && !empty($browserFromUserAgent['name']) + && !\in_array($browserFromUserAgent['short_name'], ['CR', 'CV', 'AN']) + ) { + $name = $browserFromUserAgent['name']; + $short = $browserFromUserAgent['short_name']; + $version = $browserFromUserAgent['version']; + } + + // Fix mobile browser names e.g. Chrome => Chrome Mobile + if ($name . ' Mobile' === $browserFromUserAgent['name']) { + $name = $browserFromUserAgent['name']; + $short = $browserFromUserAgent['short_name']; + } + + // If user agent detects another browser, but the family matches, we use the detected engine from user agent + if ($name !== $browserFromUserAgent['name'] + && self::getBrowserFamily($name) === self::getBrowserFamily($browserFromUserAgent['name']) + ) { + $engine = $browserFromUserAgent['engine'] ?? ''; + $engineVersion = $browserFromUserAgent['engine_version'] ?? ''; + } + + if ($name === $browserFromUserAgent['name']) { + $engine = $browserFromUserAgent['engine'] ?? ''; + $engineVersion = $browserFromUserAgent['engine_version'] ?? ''; + } + + // In case the user agent reports a more detailed version, we try to use this instead + if (!empty($browserFromUserAgent['version']) + && 0 === \strpos($browserFromUserAgent['version'], $version) + && \version_compare($version, $browserFromUserAgent['version'], '<') + ) { + $version = $browserFromUserAgent['version']; + } + + if ('DuckDuckGo Privacy Browser' === $name) { + $version = ''; + } + + // In case client hints report a more detailed engine version, we try to use this instead + if ('Blink' === $engine && 'Iridium' !== $name + && \version_compare($engineVersion, $browserFromClientHints['version'], '<') + ) { + $engineVersion = $browserFromClientHints['version']; + } + } else { + $name = $browserFromUserAgent['name']; + $version = $browserFromUserAgent['version']; + $short = $browserFromUserAgent['short_name']; + $engine = $browserFromUserAgent['engine']; + $engineVersion = $browserFromUserAgent['engine_version']; + } + + $family = self::getBrowserFamily((string) $short); + $appHash = $this->browserHints->parse(); + + if (null !== $appHash && $name !== $appHash['name']) { + $name = $appHash['name']; + $version = ''; + $short = self::getBrowserShortName($name); + + if (\preg_match('~Chrome/.+ Safari/537.36~i', $this->userAgent)) { + $engine = 'Blink'; + $family = self::getBrowserFamily((string) $short) ?? 'Chrome'; + $engineVersion = $this->buildEngineVersion($engine); + } + + if (null === $short) { + // This Exception should never be thrown. If so a defined browser name is missing in $availableBrowsers + throw new \Exception(\sprintf( + 'Detected browser name "%s" was not found in $availableBrowsers. Tried to parse user agent: %s', + $name, + $this->userAgent + )); // @codeCoverageIgnore + } + } + + if (empty($name) || 1 === \preg_match('/Cypress|PhantomJS/', $this->userAgent)) { + return []; + } + + // exclude Blink engine version for browsers + if ('Blink' === $engine && 'Flow Browser' === $name) { + $engineVersion = ''; + } + + // the browser simulate ua for Android OS + if ('Every Browser' === $name) { + $family = 'Chrome'; + $engine = 'Blink'; + $engineVersion = ''; + } + + // This browser simulates user-agent of Firefox + if ('TV-Browser Internet' === $name && 'Gecko' === $engine) { + $family = 'Chrome'; + $engine = 'Blink'; + $engineVersion = ''; + } + + if ('Wolvic' === $name && 'Blink' === $engine) { + $family = 'Chrome'; + } + + if ('Wolvic' === $name && 'Gecko' === $engine) { + $family = 'Firefox'; + } + + return [ + 'type' => 'browser', + 'name' => $name, + 'short_name' => $short, + 'version' => $version, + 'engine' => $engine, + 'engine_version' => $engineVersion, + 'family' => $family, + ]; + } + + /** + * Returns the browser that can be safely detected from client hints + * + * @return array + */ + protected function parseBrowserFromClientHints(): array + { + $name = $version = $short = ''; + + if ($this->clientHints instanceof ClientHints && $this->clientHints->getBrandList()) { + $brands = $this->clientHints->getBrandList(); + + foreach ($brands as $brand => $brandVersion) { + $brand = $this->applyClientHintMapping($brand); + + foreach (self::$availableBrowsers as $browserShort => $browserName) { + if ($this->fuzzyCompare("{$brand}", $browserName) + || $this->fuzzyCompare($brand . ' Browser', $browserName) + || $this->fuzzyCompare("{$brand}", $browserName . ' Browser') + ) { + $name = $browserName; + $short = $browserShort; + $version = $brandVersion; + + break; + } + } + + // If we detected a brand, that is not Chromium, we will use it, otherwise we will look further + if ('' !== $name && 'Chromium' !== $name && 'Microsoft Edge' !== $name) { + break; + } + } + + $version = $this->clientHints->getBrandVersion() ?: $version; + } + + return [ + 'name' => $name, + 'short_name' => $short, + 'version' => $this->buildVersion($version, []), + ]; + } + + /** + * Returns the browser that can be detected from user agent + * + * @return array + * + * @throws \Exception + */ + protected function parseBrowserFromUserAgent(): array + { + foreach ($this->getRegexes() as $regex) { + $matches = $this->matchUserAgent($regex['regex']); + + if ($matches) { + break; + } + } + + if (empty($matches) || empty($regex)) { + return [ + 'name' => '', + 'short_name' => '', + 'version' => '', + 'engine' => '', + 'engine_version' => '', + ]; + } + + $name = $this->buildByMatch($regex['name'], $matches); + $browserShort = self::getBrowserShortName($name); + + if (null !== $browserShort) { + $version = $this->buildVersion((string) $regex['version'], $matches); + $engine = $this->buildEngine($regex['engine'] ?? [], $version); + $engineVersion = $this->buildEngineVersion($engine); + + return [ + 'name' => $name, + 'short_name' => $browserShort, + 'version' => $version, + 'engine' => $engine, + 'engine_version' => $engineVersion, + ]; + } + + // This Exception should never be thrown. If so a defined browser name is missing in $availableBrowsers + throw new \Exception(\sprintf( + 'Detected browser name "%s" was not found in $availableBrowsers. Tried to parse user agent: %s', + $name, + $this->userAgent + )); // @codeCoverageIgnore + } + + /** + * @param array $engineData + * @param string $browserVersion + * + * @return string + */ + protected function buildEngine(array $engineData, string $browserVersion): string + { + $engine = ''; + + // if an engine is set as default + if (isset($engineData['default'])) { + $engine = $engineData['default']; + } + + // check if engine is set for browser version + if (\array_key_exists('versions', $engineData) && \is_array($engineData['versions'])) { + foreach ($engineData['versions'] as $version => $versionEngine) { + if (\version_compare($browserVersion, (string) $version) < 0) { + continue; + } + + $engine = $versionEngine; + } + } + + // try to detect the engine using the regexes + if (empty($engine)) { + $engineParser = new Engine(); + $engineParser->setYamlParser($this->getYamlParser()); + $engineParser->setCache($this->getCache()); + $engineParser->setUserAgent($this->userAgent); + $result = $engineParser->parse(); + $engine = $result['engine'] ?? ''; + } + + return $engine; + } + + /** + * @param string $engine + * + * @return string + */ + protected function buildEngineVersion(string $engine): string + { + $engineVersionParser = new Engine\Version($this->userAgent, $engine); + $result = $engineVersionParser->parse(); + + return $result['version'] ?? ''; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Browser/Engine.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Browser/Engine.php new file mode 100644 index 0000000..6bf0e91 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Browser/Engine.php @@ -0,0 +1,104 @@ +getRegexes() as $regex) { + $matches = $this->matchUserAgent($regex['regex']); + + if ($matches) { + break; + } + } + + if (empty($matches) || empty($regex)) { + return null; + } + + $name = $this->buildByMatch($regex['name'], $matches); + + foreach (self::getAvailableEngines() as $engineName) { + if (\strtolower($name) === \strtolower($engineName)) { + return ['engine' => $engineName]; + } + } + + // This Exception should never be thrown. If so a defined browser name is missing in $availableEngines + throw new \Exception(\sprintf( + 'Detected browser engine was not found in $availableEngines. Tried to parse user agent: %s', + $this->userAgent + )); // @codeCoverageIgnore + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Browser/Engine/Version.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Browser/Engine/Version.php new file mode 100644 index 0000000..b62a512 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Browser/Engine/Version.php @@ -0,0 +1,85 @@ +engine = $engine; + } + + /** + * @inheritdoc + */ + public function parse(): ?array + { + if (empty($this->engine)) { + return null; + } + + if ('Gecko' === $this->engine || 'Clecko' === $this->engine) { + $pattern = '~[ ](?:rv[: ]([0-9.]+)).*(?:g|cl)ecko/[0-9]{8,10}~i'; + + if (\preg_match($pattern, $this->userAgent, $matches)) { + return ['version' => \array_pop($matches)]; + } + } + + $engineToken = $this->engine; + + if ('Blink' === $this->engine) { + $engineToken = 'Chr[o0]me|Chromium|Cronet'; + } + + if ('Arachne' === $this->engine) { + $engineToken = 'Arachne\/5\.'; + } + + if ('LibWeb' === $this->engine) { + $engineToken = 'LibWeb\+LibJs'; + } + + \preg_match( + "~(?:{$engineToken})\s*[/_]?\s*((?(?=\d+\.\d)\d+[.\d]*|\d{1,7}(?=(?:\D|$))))~i", + $this->userAgent, + $matches + ); + + if (!$matches) { + return null; + } + + return ['version' => \array_pop($matches)]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/FeedReader.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/FeedReader.php new file mode 100644 index 0000000..900263c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/FeedReader.php @@ -0,0 +1,31 @@ +clientHints) { + return null; + } + + $appId = $this->clientHints->getApp(); + $name = $this->getRegexes()[$appId] ?? null; + + if ('' === (string) $name) { + return null; + } + + return [ + 'name' => $name, + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Hints/BrowserHints.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Hints/BrowserHints.php new file mode 100644 index 0000000..d1252b9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Hints/BrowserHints.php @@ -0,0 +1,51 @@ +clientHints) { + return null; + } + + $appId = $this->clientHints->getApp(); + $name = $this->getRegexes()[$appId] ?? null; + + if ('' === (string) $name) { + return null; + } + + return [ + 'name' => $name, + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Library.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Library.php new file mode 100644 index 0000000..a4c84fe --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/Library.php @@ -0,0 +1,31 @@ +appHints = new AppHints($ua, $clientHints); + parent::__construct($ua, $clientHints); + } + + /** + * Sets the client hints to parse + * + * @param ?ClientHints $clientHints client hints + */ + public function setClientHints(?ClientHints $clientHints): void + { + parent::setClientHints($clientHints); + $this->appHints->setClientHints($clientHints); + } + + /** + * Sets the user agent to parse + * + * @param string $ua user agent + */ + public function setUserAgent(string $ua): void + { + parent::setUserAgent($ua); + $this->appHints->setUserAgent($ua); + } + + /** + * Sets the Cache class + * + * @param CacheInterface $cache + */ + public function setCache(CacheInterface $cache): void + { + parent::setCache($cache); + $this->appHints->setCache($cache); + } + + /** + * Sets the YamlParser class + * + * @param YamlParser $yamlParser + */ + public function setYamlParser(YamlParser $yamlParser): void + { + parent::setYamlParser($yamlParser); + $this->appHints->setYamlParser($this->getYamlParser()); + } + + /** + * Parses the current UA and checks whether it contains any client information + * See parent::parse() for more details. + * + * @return array|null + */ + public function parse(): ?array + { + $result = parent::parse(); + $name = $result['name'] ?? ''; + $version = $result['version'] ?? ''; + $appHash = $this->appHints->parse(); + + if (null !== $appHash && $appHash['name'] !== $name) { + $name = $appHash['name']; + $version = ''; + } + + if (empty($name)) { + return null; + } + + return [ + 'type' => $this->parserName, + 'name' => $name, + 'version' => $version, + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/PIM.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/PIM.php new file mode 100644 index 0000000..0db3269 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Client/PIM.php @@ -0,0 +1,31 @@ + self::DEVICE_TYPE_DESKTOP, + 'smartphone' => self::DEVICE_TYPE_SMARTPHONE, + 'tablet' => self::DEVICE_TYPE_TABLET, + 'feature phone' => self::DEVICE_TYPE_FEATURE_PHONE, + 'console' => self::DEVICE_TYPE_CONSOLE, + 'tv' => self::DEVICE_TYPE_TV, + 'car browser' => self::DEVICE_TYPE_CAR_BROWSER, + 'smart display' => self::DEVICE_TYPE_SMART_DISPLAY, + 'camera' => self::DEVICE_TYPE_CAMERA, + 'portable media player' => self::DEVICE_TYPE_PORTABLE_MEDIA_PAYER, + 'phablet' => self::DEVICE_TYPE_PHABLET, + 'smart speaker' => self::DEVICE_TYPE_SMART_SPEAKER, + 'wearable' => self::DEVICE_TYPE_WEARABLE, + 'peripheral' => self::DEVICE_TYPE_PERIPHERAL, + ]; + + /** + * Known device brands + * + * Note: Before using a new brand in on of the regex files, it needs to be added here + * + * @var array + */ + public static $deviceBrands = [ + '5E' => '2E', + '5IV' => '5IVE', + '2F' => 'F2 Mobile', + '3Q' => '3Q', + 'J7' => '7 Mobile', + '2Q' => '3GNET', + '4G' => '4Good', + '27' => '3GO', + '04' => '4ife', + '36' => '360', + '88' => '8848', + '10M' => '10moons', + '41' => 'A1', + 'AK1' => 'A&K', + '00' => 'Accent', + 'ACC' => 'Accesstyle', + 'ACD' => 'ACD', + 'AE' => 'Ace', + 'AC' => 'Acer', + 'ACL' => 'Aceline', + 'ACP' => 'Acepad', + '3K' => 'Acteck', + 'ACT' => 'actiMirror', + 'A9' => 'Advan', + 'AD' => 'Advance', + 'ADV' => 'Advantage Air', + '76' => 'Adronix', + 'ADR' => 'Adreamer', + 'AF' => 'AfriOne', + 'FY' => 'AFFIX', + 'A3' => 'AGM', + 'AEE' => 'AEEZO', + 'J0' => 'AG Mobile', + 'AJI' => 'Ajib', + 'AZ' => 'Ainol', + 'AIR' => 'Airis', + 'AI' => 'Airness', + 'ARP' => 'Airpha', + 'AT' => 'Airties', + '7U' => 'Airtel', + 'AIT' => 'AirTouch', + 'U0' => 'AIRON', + '0A' => 'AIS', + 'AW' => 'Aiwa', + '85' => 'Aiuto', + 'U7' => 'AIDATA', + 'AL1' => 'AileTV', + 'AK' => 'Akai', + 'Q3' => 'AKIRA', + '1A' => 'Alba', + 'AL' => 'Alcatel', + 'AL0' => 'Alienware', + '20' => 'Alcor', + 'XY' => 'Alps', + 'XYA' => 'XY Auto', + 'AAU' => 'AAUW', + '7L' => 'ALDI NORD', + '6L' => 'ALDI SÜD', + '3L' => 'Alfawise', + '4A' => 'Aligator', + 'ALS' => 'All Star', + 'AA' => 'AllCall', + '3A' => 'AllDocube', + 'ALL' => 'allente', + 'A2' => 'Allview', + 'ALI' => 'ALLINmobile', + 'A7' => 'Allwinner', + 'ALP' => 'alpsmart', + 'A1' => 'Altech UEC', + '66' => 'Altice', + 'ALM' => 'Altimo', + 'A5' => 'altron', + 'ALB' => 'Altibox', + 'ALT' => 'Altus', + 'KN' => 'Amazon', + 'AMZ' => 'Amazon Basics', + 'AMA' => 'AMA', + 'AG' => 'AMGOO', + '9A' => 'Amigoo', + 'AO' => 'Amoi', + '3J' => 'Amino', + '54' => 'AMCV', + '60' => 'Andowl', + 'ANX' => 'ANXONIT', + 'ANL' => 'ANCEL', + 'ANC' => 'ANBERNIC', + 'AND' => 'andersson', + '6J' => 'Angelcare', + 'ANG' => 'AngelTech', + '7A' => 'Anry', + 'A0' => 'ANS', + '74' => 'Anker', + '3N' => 'Aoson', + 'O8' => 'AOC', + 'J2' => 'AOYODKG', + '55' => 'AOpen', + 'RW' => 'Aoro', + '9Y' => 'Aocos', + 'AOW' => 'Aocwei', + 'AP' => 'Apple', + 'APS' => 'ApoloSign', + 'ARC' => 'Arçelik', + 'AR' => 'Archos', + 'AB' => 'Arian Space', + 'ARI' => 'Arival', + 'A6' => 'Ark', + '5A' => 'ArmPhone', + 'AN' => 'Arnova', + 'AS' => 'ARRIS', + 'AQ' => 'Aspera', + 'HJ' => 'Aquarius', + '40' => 'Artel', + '21' => 'Artizlee', + '59' => 'ArtLine', + '8A' => 'Asano', + '90' => 'Asanzo', + '1U' => 'Astro (UA)', + 'AST' => 'astro (MY)', + 'A4' => 'Ask', + 'A8' => 'Assistant', + 'ASS' => 'ASSE', + 'AU' => 'Asus', + '6A' => 'AT&T', + 'ATH' => 'Athesi', + 'ATE' => 'Atlantic Electrics', + '5Q' => 'Atmaca Elektronik', + 'YH' => 'ATMAN', + 'ATM' => 'ATMPC', + '2A' => 'Atom', + 'AT1' => 'Atozee', + 'ATO' => 'ATOL', + 'Z2' => 'Atvio', + 'ATI' => 'Attila', + 'ATU' => 'Atouch', + 'AX' => 'Audiovox', + 'AJ' => 'AURIS', + 'YZ' => 'Autan', + 'AUP' => 'AUPO', + 'ZA' => 'Avenzo', + 'AH' => 'AVH', + 'AV' => 'Avvio', + 'AVA' => 'Avaya', + 'AXE' => 'AXEN', + 'AY' => 'Axxion', + 'AXX' => 'AXXA', + 'AYA' => 'AYA', + 'YR' => 'AYYA', + 'XA' => 'Axioo', + 'AM' => 'Azumi Mobile', + 'AZE' => 'Azeyou', + 'AZU' => 'Azupik', + 'AZO' => 'AZOM', + 'WW' => 'Awow', + 'AWO' => 'AWOX', + 'XU' => 'AUX', + 'BAC' => 'Backcell', + 'BFF' => 'BAFF', + 'BO' => 'BangOlufsen', + 'BN' => 'Barnes & Noble', + 'BAR' => 'BARTEC', + 'BAS' => 'BASE', + 'BAU' => 'BAUHN', + 'BB' => 'BBK', + '0B' => 'BB Mobile', + 'B6' => 'BDF', + 'QD' => 'BDQ', + '8Z' => 'BDsharing', + 'BEF' => 'Beafon', + 'BE' => 'Becker', + 'B5' => 'Beeline', + 'B0' => 'Beelink', + 'BL' => 'Beetel', + 'BEK' => 'Beko', + '2X' => 'Benco', + 'BQ' => 'BenQ', + 'BQL' => 'Bqeel', + 'BS' => 'BenQ-Siemens', + '4Y' => 'Benzo', + 'XJ' => 'Benesse', + 'BEN' => 'BenWee', + 'YB' => 'Beista', + 'BY' => 'BS Mobile', + 'BZ' => 'Bezkam', + 'BEL' => 'Bell', + '9B' => 'Bellphone', + '63' => 'Beyond', + 'BG' => 'BGH', + '6B' => 'Bigben', + 'B8' => 'BIHEE', + '1B' => 'Billion', + 'BA' => 'BilimLand', + 'BIL' => 'Billow', + 'BH' => 'BioRugged', + 'BI' => 'Bird', + 'BT' => 'Bitel', + 'B7' => 'Bitmore', + 'ZB' => 'Bittium', + 'BIE' => 'Biegedy', + 'BK' => 'Bkav', + '5B' => 'Black Bear', + 'BLK' => 'Black Box', + 'BF' => 'Black Fox', + 'BPC' => 'Blackpcs', + 'BLP' => 'Blackphone', + 'BLT' => 'Blackton', + 'B2' => 'Blackview', + '2Y' => 'b2m', + 'BP' => 'Blaupunkt', + 'BU' => 'Blu', + 'BUS' => 'BluSlate', + 'BUZ' => 'BuzzTV', + 'B3' => 'Bluboo', + '2B' => 'Bluedot', + 'BD' => 'Bluegood', + 'LB' => 'Bluewave', + 'J8' => 'Bluebird', + 'BSS' => 'BlueSky', + '7B' => 'Blloc', + 'UB' => 'Bleck', + 'Q2' => 'Blow', + 'BLI' => 'BLISS', + 'BM' => 'Bmobile', + 'Y5' => 'BMAX', + 'BMX' => 'BMXC', + 'B9' => 'Bobarry', + 'B4' => 'bogo', + 'BOL' => 'Bolva', + 'BW' => 'Boway', + 'BOO' => 'Boost', + 'BOK' => 'Bookeen', + 'BOT' => 'Botech', + 'BX' => 'bq', + '8B' => 'Brandt', + 'BRA' => 'BrandCode', + 'BV' => 'Bravis', + 'BRV' => 'BRAVE', + 'BRG' => 'Brigmton', + 'BR' => 'Brondi', + 'XF' => 'BROR', + 'BJ' => 'BrightSign', + 'B1' => 'Bush', + '4Q' => 'Bundy', + 'Y8' => 'Bubblegum', + 'BMW' => 'BMW', + 'BYJ' => 'BYJU\'S', + 'BYY' => 'BYYBUO', + 'BYD' => 'BYD', + 'C9' => 'CAGI', + 'CAD' => 'CADENA', + 'CAL' => 'CALME', + 'CAI' => 'Caixun', + 'CT' => 'Capitel', + 'GRD' => 'G-Guard', + 'GVL' => 'G-Vill', + 'G3' => 'CG Mobile', + '37' => 'CGV', + 'CP' => 'Captiva', + 'CPD' => 'CPDEVICE', + 'CF' => 'Carrefour', + 'CA1' => 'Carbon Mobile', + 'CS' => 'Casio', + 'R4' => 'Casper', + 'CA' => 'Cat', + 'BC' => 'Camfone', + 'CJ' => 'Cavion', + 'CNM' => 'Canaima', + 'CAN' => 'Canal+', + '4D' => 'Canal Digital', + 'CNG' => 'Canguro', + 'CNT' => 'CCIT', + 'CEC' => 'Cecotec', + 'CEP' => 'CEPTER', + 'CEI' => 'Ceibal', + '02' => 'Cell-C', + 'CEL' => 'Cellacom', + '34' => 'CellAllure', + '7C' => 'Celcus', + 'CE' => 'Celkon', + 'CG' => 'Cellution', + '62' => 'Centric', + 'CEN' => 'CENTEK', + 'C2' => 'Changhong', + 'CHA' => 'Chainway', + 'CHG' => 'ChiliGreen', + 'CH' => 'Cherry Mobile', + 'C3' => 'China Mobile', + 'U9' => 'China Telecom', + 'CI' => 'Chico Mobile', + 'CHC' => 'CHCNAV', + 'CIA' => 'C Idea', + 'CIP' => 'CipherLab', + 'CIT' => 'Citycall', + '1C' => 'Chuwi', + 'L8' => 'Clarmin', + '25' => 'Claresta', + 'CLA' => 'CLAYTON', + 'CLT' => 'Clovertek', + '1J' => 'Cloud', + 'CD' => 'Cloudfone', + '6C' => 'Cloudpad', + 'C0' => 'Clout', + 'CN' => 'CnM', + 'CY' => 'Coby Kyros', + 'XC' => 'Cobalt', + 'C6' => 'Comio', + 'COM' => 'CommScope', + 'CL' => 'Compal', + 'CQ' => 'Compaq', + 'C7' => 'ComTrade Tesla', + '7Z' => 'COMPUMAX', + 'C8' => 'Concord', + 'CC' => 'ConCorde', + 'C5' => 'Condor', + 'C5M' => 'C5 Mobile', + 'COE' => 'Cogeco', + '4C' => 'Conquest', + 'COG' => 'CONSUNG', + '3C' => 'Contixo', + '8C' => 'Connex', + '53' => 'Connectce', + 'CON' => 'Conceptum', + 'CED' => 'Continental Edison', + '9C' => 'Colors', + 'COL' => 'COLORROOM', + 'CAA' => 'coocaa', + 'CO' => 'Coolpad', + 'COO' => 'Coopers', + 'CDE' => 'COOD-E', + '4R' => 'CORN', + '1O' => 'Cosmote', + 'CW' => 'Cowon', + '75' => 'Covia', + 'QG' => 'COYOTE', + 'CKK' => 'CKK Mobile', + 'YW' => 'ClearPHONE', + '33' => 'Clementoni', + 'CR' => 'CreNova', + 'CX' => 'Crescent', + 'CRE' => 'Crestron', + 'CK' => 'Cricket', + 'CM' => 'Crius Mea', + 'CMF' => 'CMF', + '0C' => 'Crony', + 'C1' => 'Crosscall', + '4W' => 'Crown', + 'CTR' => 'Ctroniq', + 'CU' => 'Cube', + 'CB' => 'CUBOT', + 'CUI' => 'Cuiud', + 'CUL' => 'Cultraview', + 'CV' => 'CVTE', + 'CWO' => 'Cwowdefu', + 'CX0' => 'CX', + 'C4' => 'Cyrus', + 'D5' => 'Daewoo', + 'DA' => 'Danew', + 'DAN' => 'Dany', + 'DA1' => 'DangcapHD', + 'DAR' => 'Daria', + 'DT' => 'Datang', + 'D7' => 'Datawind', + '7D' => 'Datamini', + '6D' => 'Datalogic', + 'D1' => 'Datsun', + 'DZ' => 'Dazen', + 'DAS' => 'DASS', + 'DAW' => 'Dawlance', + 'DB' => 'Dbtel', + 'DBP' => 'DbPhone', + 'DCO' => 'Dcode', + 'DL' => 'Dell', + 'DL0' => 'DL', + 'DE' => 'Denver', + 'DS' => 'Desay', + 'DSD' => 'DSDevices', + 'DSI' => 'DSIC', + 'DW' => 'DeWalt', + 'DX' => 'DEXP', + 'DEY' => 'DEYI', + 'DEN' => 'Denali', + 'DEA' => 'DEALDIG', + 'DNK' => 'Denka', + '8D' => 'DF', + 'DGT' => 'DGTEC', + 'DG' => 'Dialog', + 'DIA' => 'DIALN', + 'DI' => 'Dicam', + 'D4' => 'Digi', + 'D3' => 'Digicel', + 'DDG' => 'Digidragon', + 'DH' => 'Digihome', + 'DD' => 'Digiland', + 'DIJ' => 'DIJITSU', + 'DIG' => 'Digit4G', + 'DIC' => 'DIGICOM', + 'Q0' => 'DIGIFORS', + 'DIK' => 'DIKOM', + 'DQ' => 'DISH', + 'DIS' => 'Disney', + '9D' => 'Ditecma', + 'D2' => 'Digma', + '1D' => 'Diva', + 'DIV' => 'DiverMax', + 'D6' => 'Divisat', + 'DIO' => 'DIORA', + 'DIF' => 'Diofox', + 'X6' => 'DIXON', + 'DIM' => 'DIMO', + '5D' => 'DING DING', + 'DIN' => 'Dinax', + 'DNA' => 'Dinalink', + 'DM' => 'DMM', + 'DMO' => 'DMOAO', + 'DN' => 'DNS', + 'DC' => 'DoCoMo', + 'DF' => 'Doffler', + 'D9' => 'Dolamee', + 'DO' => 'Doogee', + 'D0' => 'Doopro', + 'DV' => 'Doov', + 'DOM' => 'Dom.ru', + 'DP' => 'Dopod', + 'DPA' => 'DPA', + 'JQ' => 'Doppio', + 'DR' => 'Doro', + 'DOR' => 'Dora', + 'ZD' => 'DORLAND', + 'DRO' => 'Droidlogic', + 'D8' => 'Droxio', + 'DJ' => 'Dragon Touch', + 'DRA' => 'DRAGON', + 'DY' => 'Dreamgate', + 'DRE' => 'DreamTab', + 'DR1' => 'DreamStar', + 'DTA' => 'Dtac', + 'DU' => 'Dune HD', + 'DUO' => 'DuoTV', + 'UD' => 'DUNNS Mobile', + 'DUU' => 'Duubee', + 'DUR' => 'Durabook', + 'DUD' => 'DUDU AUTO', + 'DYO' => 'Dyon', + 'DYM' => 'Dykemann', + 'DTE' => 'D-Tech', + 'DLI' => 'D-Link', + 'ENO' => 'eNOVA', + 'IN2' => 'iNOVA', + 'IN3' => 'inovo', + 'INH' => 'Inhon', + 'IND' => 'Indurama', + 'EB' => 'E-Boda', + 'EJ' => 'Engel', + 'ENA' => 'ENACOM', + 'END' => 'ENDURO', + 'ENI' => 'ENIE', + '2E' => 'E-Ceros', + 'E8' => 'E-tel', + 'ETH' => 'E-TACHI', + 'EAS' => 'EAS Electric', + 'EP' => 'Easypix', + 'EQ' => 'Eagle', + 'EGS' => 'EagleSoar', + 'EA' => 'EBEST', + 'YC' => 'EBEN', + 'E4' => 'Echo Mobiles', + 'EQ1' => 'Equator', + 'ES' => 'ECS', + '35' => 'ECON', + 'ECC' => 'ECOO', + 'ZZ' => 'ecom', + 'ECS' => 'EcoStar', + 'EDE' => 'Edenwood', + 'E6' => 'EE', + 'GW' => 'EGL', + 'EGO' => 'EGOTEK', + 'EFT' => 'EFT', + 'EK' => 'EKO', + 'EY' => 'Einstein', + 'EM' => 'Eks Mobility', + 'UE' => 'Ematic', + 'EMR' => 'Emporia', + '4K' => 'EKT', + 'EKI' => 'EKINOX', + '7E' => 'ELARI', + '03' => 'Electroneum', + 'Z8' => 'ELECTRONIA', + 'ELG' => 'ELE-GATE', + 'EL1' => 'Elecson', + 'ELK' => 'Elektroland', + 'L0' => 'Element', + 'EG' => 'Elenberg', + 'EL' => 'Elephone', + 'JE' => 'Elekta', + 'ELE' => 'Elevate', + 'ELS' => 'Elista', + 'ELT' => 'elit', + '4E' => 'Eltex', + 'ELM' => 'Elong Mobile', + 'ED' => 'Energizer', + 'E1' => 'Energy Sistem', + '3E' => 'Enot', + 'ENT' => 'Entity', + 'ENV' => 'Envizen', + '8E' => 'Epik One', + 'EPK' => 'Epic', + 'XP' => 'Epson', + 'EPH' => 'Ephone', + 'EPL' => 'Eplutus', + 'E7' => 'Ergo', + 'EC' => 'Ericsson', + '05' => 'Erisson', + 'ER' => 'Ericy', + 'EE' => 'Essential', + 'E2' => 'Essentielb', + '6E' => 'eSTAR', + 'ETO' => 'ETOE', + 'EN' => 'Eton', + 'ET' => 'eTouch', + '1E' => 'Etuline', + 'EHL' => 'Ehlel', + 'EU' => 'Eurostar', + '4J' => 'Eurocase', + 'EUR' => 'EUROLUX', + 'EUD' => 'Eudora', + 'E9' => 'Evercoss', + 'EV' => 'Evertek', + 'EVE' => 'Everest', + 'EV1' => 'Everex', + 'EVR' => 'Everis', + 'EVF' => 'Everfine', + 'E3' => 'Evolio', + 'EO' => 'Evolveo', + '0Q' => 'Evoo', + '5U' => 'EVPAD', + 'EVV' => 'evvoli', + 'E0' => 'EvroMedia', + 'XE' => 'ExMobile', + '4Z' => 'Exmart', + 'EH' => 'EXO', + 'EX' => 'Explay', + 'EXP' => 'Express LUCK', + 'E5' => 'Extrem', + 'EXL' => 'ExtraLink', + 'EF' => 'EXCEED', + 'QE' => 'EWIS', + 'EI' => 'Ezio', + 'EZ' => 'Ezze', + 'UF' => 'EYU', + 'EYE' => 'Eyemoo', + 'UE1' => 'UE', + '5F' => 'F150', + 'FPS' => 'F+', + 'F6' => 'Facebook', + 'FAC' => 'Facetel', + 'FA1' => 'Facime', + 'FA' => 'Fairphone', + 'FM' => 'Famoco', + 'FAM' => 'Famous', + '17' => 'FarEasTone', + '9R' => 'FaRao Pro', + 'FAR' => 'Farassoo', + 'FB' => 'Fantec', + 'FAN' => 'Fanvace', + 'FE' => 'Fengxiang', + 'FEN' => 'Fenoti', + 'F7' => 'Fero', + '67' => 'FEONAL', + 'FI' => 'FiGO', + 'J9' => 'FiGi', + 'FIG' => 'Figgers', + 'F9' => 'FiiO', + 'F1' => 'FinePower', + 'FX' => 'Finlux', + 'F3' => 'FireFly Mobile', + 'F8' => 'FISE', + 'FIS' => 'Fision', + 'FIT' => 'FITCO', + 'FLM' => 'Filimo', + 'FIL' => 'FILIX', + 'FIN' => 'FINIX', + 'FL' => 'Fly', + 'QC' => 'FLYCAT', + 'FLY' => 'FLYCOAY', + 'FLU' => 'Fluo', + 'FN' => 'FNB', + 'FOB' => 'Fobem', + 'FD' => 'Fondi', + '0F' => 'Fourel', + '44' => 'Four Mobile', + 'F0' => 'Fonos', + 'F0N' => 'FONTEL', + 'F2' => 'FORME', + 'FRM' => 'Formovie', + 'F5' => 'Formuler', + 'FR' => 'Forstar', + 'RF' => 'Fortis', + 'FRT' => 'FortuneShip', + 'FO' => 'Foxconn', + 'FOD' => 'FoxxD', + 'FJ' => 'FOODO', + 'FOS' => 'FOSSiBOT', + 'FRE' => 'free', + 'FT' => 'Freetel', + 'FTH' => 'FRESH', + 'FEY' => 'FreeYond', + 'FRU' => 'Frunsi', + 'F4' => 'F&U', + '1F' => 'FMT', + 'FPT' => 'FPT', + 'FG' => 'Fuego', + 'FUJ' => 'FUJICOM', + 'FU' => 'Fujitsu', + '4F' => 'Funai', + '5J' => 'Fusion5', + 'FF' => 'Future Mobile Technology', + 'FFF' => 'FFF SmartLife', + 'FW' => 'FNF', + 'FXT' => 'Fxtec', + 'GT' => 'G-TiDE', + 'G9' => 'G-Touch', + 'GFO' => 'Gfone', + 'GTM' => 'GTMEDIA', + 'GTX' => 'GTX', + 'GDL' => 'GDL', + '0G' => 'GFive', + 'GM' => 'Garmin-Asus', + 'GA' => 'Gateway', + '99' => 'Galaxy Innovations', + 'GA1' => 'Galactic', + 'GAT' => 'Galatec', + 'GAM' => 'Gamma', + 'GAZ' => 'Gazer', + 'GAL' => 'Gazal', + 'GEA' => 'Geanee', + 'GEN' => 'Geant', + 'GD' => 'Gemini', + 'GN' => 'General Mobile', + '2G' => 'Genesis', + 'GEP' => 'Geo Phone', + 'G2' => 'GEOFOX', + 'GE' => 'Geotel', + 'Q4' => 'Geotex', + 'GEO' => 'GEOZON', + 'GNO' => 'Getnord', + 'GER' => 'Gear Mobile', + 'GH' => 'Ghia', + '2C' => 'Ghong', + 'GJ' => 'Ghost', + 'GG' => 'Gigabyte', + 'GS' => 'Gigaset', + 'GZ' => 'Ginzzu', + '1G' => 'Gini', + 'GI' => 'Gionee', + 'GIR' => 'GIRASOLE', + 'G4' => 'Globex', + 'GL2' => 'GlobalSec', + 'GLB' => 'Globmall', + 'GME' => 'GlocalMe', + '38' => 'GLONYX', + 'U6' => 'Glofiish', + 'GLO' => 'Glory Star', + 'GNE' => 'GN Electronics', + 'G7' => 'GoGEN', + 'GC' => 'GOCLEVER', + '5G' => 'Gocomma', + 'GB' => 'Gol Mobile', + 'GL' => 'Goly', + 'GOL' => 'GoldMaster', + 'GOS' => 'GoldStar', + 'GOB' => 'GOLDBERG', + 'GX' => 'GLX', + 'G5' => 'Gome', + 'G1' => 'GoMobile', + 'GO' => 'Google', + 'G0' => 'Goophone', + '6G' => 'Gooweel', + 'GOO' => 'GOODTEL', + 'GO1' => 'GOtv', + 'GPL' => 'G-PLUS', + '8G' => 'Gplus', + 'GR' => 'Gradiente', + 'GRE' => 'Graetz', + 'GP' => 'Grape', + 'G6' => 'Gree', + 'GRA' => 'Great Asia', + '3G' => 'Greentel', + 'GRO' => 'Green Orange', + 'GRL' => 'Green Lion', + 'GR1' => 'GroBerwert', + 'GF' => 'Gretel', + '82' => 'Gresso', + 'GRB' => 'Grünberg', + 'GU' => 'Grundig', + 'GV' => 'Gtel', + 'CUO' => 'Guophone', + 'CUD' => 'CUD', + 'GVC' => 'GVC Pro', + 'H13' => 'H133', + '9Z' => 'H96', + 'HF' => 'Hafury', + '9F' => 'HAOVM', + 'HAQ' => 'HAOQIN', + 'HA' => 'Haier', + 'HEC' => 'HEC', + 'XH' => 'Haipai', + 'XHU' => 'Haixu', + 'HAN' => 'Handheld', + 'HE' => 'HannSpree', + 'HNS' => 'Hanseatic', + 'HA2' => 'Hanson', + 'HK' => 'Hardkernel', + 'HAR' => 'Harper', + 'HA1' => 'Hartens', + 'HS' => 'Hasee', + '8H' => 'Hamlet', + 'HAM' => 'Hammer', + 'HAT' => 'Hathway', + 'HAV' => 'HAVIT', + 'HEM' => 'Hemilton', + 'H6' => 'Helio', + 'HQ' => 'HERO', + 'ZH' => 'Hezire', + 'HEX' => 'HexaByte', + 'HEW' => 'HeadWolf', + 'HEI' => 'Heimat', + 'HL' => 'Hi-Level', + '3H' => 'Hi', + 'HIB' => 'Hiberg', + 'HBY' => 'HiBy', + 'HIH' => 'HiHi', + 'HIK' => 'HiKing', + 'H2' => 'Highscreen', + 'Q1' => 'High Q', + 'HI1' => 'HIGH1ONE', + 'HIG' => 'HiGrace', + '1H' => 'Hipstreet', + 'HIR' => 'Hiremco', + 'HI' => 'Hisense', + 'HIP' => 'HIPER', + 'HC' => 'Hitachi', + 'H8' => 'Hitech', + 'W3' => 'HiMax', + '8X' => 'Hi Nova', + 'HLL' => 'HLLO', + 'HKC' => 'HKC', + 'HMD' => 'HMD', + '8W' => 'HKPro', + 'HOF' => 'HOFER', + 'HOC' => 'hoco', + 'H1' => 'Hoffmann', + 'HOM' => 'Homatics', + 'H0' => 'Hometech', + 'HLB' => 'HOLLEBERG', + 'HM' => 'Homtom', + 'HM1' => 'HOMII', + 'HOP' => 'Hopeland', + 'HZ' => 'Hoozo', + 'HOR' => 'Horion', + 'H7' => 'Horizon', + '4H' => 'Horizont', + 'HO' => 'Hosin', + 'H3' => 'Hotel', + 'HV' => 'Hotwav', + 'U8' => 'Hot Pepper', + 'HOT' => 'HOTACK', + 'JH' => 'HOTREALS', + 'HW' => 'How', + 'WH' => 'Honeywell', + 'HON' => 'HongTop', + 'HOG' => 'HONKUAHG', + 'HP' => 'HP', + 'HDC' => 'HDC', + 'HT' => 'HTC', + 'QZ' => 'Huagan', + 'HD' => 'Huadoo', + 'HG' => 'Huavi', + 'HU' => 'Huawei', + 'HX' => 'Humax', + 'HUM' => 'Humanware', + 'HME' => 'HUMElab', + 'HR' => 'Hurricane', + 'H5' => 'Huskee', + 'HUG' => 'Hugerock', + 'HY' => 'Hyrican', + 'HN' => 'Hyundai', + '7H' => 'Hyve', + 'HYT' => 'Hytera', + 'HYK' => 'Hykker', + 'HYA' => 'Hyatta', + 'IKL' => 'I KALL', + '3I' => 'i-Cherry', + 'IJ' => 'i-Joy', + 'IM' => 'i-mate', + 'IO' => 'i-mobile', + 'INN' => 'I-INN', + 'IPL' => 'I-Plus', + 'OF' => 'iOutdoor', + 'IB' => 'iBall', + 'IY' => 'iBerry', + '7I' => 'iBrit', + 'IBO' => 'ibowin', + 'I2' => 'IconBIT', + 'ING' => 'Icone Gold', + 'IC' => 'iDroid', + 'IDI' => 'iDino', + '6Z' => 'iData', + 'IDC' => 'IDC', + 'IG' => 'iGet', + 'IHL' => 'iHome Life', + 'IH' => 'iHunt', + 'IA' => 'Ikea', + 'IYO' => 'iYou', + '8I' => 'IKU Mobile', + '2K' => 'IKI Mobile', + 'IK' => 'iKoMo', + '58' => 'iKon', + '588' => 'iKonia', + 'I7' => 'iLA', + '2I' => 'iLife', + '1I' => 'iMars', + 'IMI' => 'iMI', + 'U4' => 'iMan', + 'ILE' => 'iLepo', + 'IL' => 'IMO Mobile', + 'IMA' => 'Imaq', + 'IM1' => 'Imose', + 'I3' => 'Impression', + 'FC' => 'INCAR', + '2H' => 'Inch', + '6I' => 'Inco', + 'INK' => 'Inka', + 'IW' => 'iNew', + 'IF' => 'Infinix', + 'INF' => 'Infiniton', + 'IPR' => 'InfinityPro', + 'I0' => 'InFocus', + 'IN1' => 'InFone', + 'II' => 'Inkti', + 'MIR' => 'Infomir', + '81' => 'InfoKit', + 'I5' => 'InnJoo', + '26' => 'Innos', + 'IN' => 'Innostream', + 'I4' => 'Inoi', + 'INO' => 'iNo Mobile', + 'IQ' => 'INQ', + 'QN' => 'iQ&T', + 'IS' => 'Insignia', + 'YI' => 'INSYS', + 'IT' => 'Intek', + 'INT' => 'Intel', + 'IX' => 'Intex', + 'IV' => 'Inverto', + '32' => 'Invens', + '4I' => 'Invin', + 'IFT' => 'iFIT', + 'INA' => 'iNavi', + 'I1' => 'iOcean', + 'IMU' => 'iMuz', + 'IP' => 'iPro', + 'X9' => 'iPEGTOP', + '8Q' => 'IQM', + 'Q8' => 'IRA', + 'I6' => 'Irbis', + '5I' => 'Iris', + 'IRE' => 'iReplace', + 'IR' => 'iRola', + 'IU' => 'iRulu', + 'IRO' => 'iRobot', + '9I' => 'iSWAG', + '9J' => 'iSafe Mobile', + 'IST' => 'iStar', + '86' => 'IT', + 'IZ' => 'iTel', + '0I' => 'iTruck', + 'I8' => 'iVA', + 'IE' => 'iView', + '0J' => 'iVooMi', + 'UI' => 'ivvi', + 'QW' => 'iWaylink', + 'I9' => 'iZotron', + 'IXT' => 'iXTech', + 'IOT' => 'IOTWE', + 'JA' => 'JAY-Tech', + 'JAM' => 'Jambo', + 'KJ' => 'Jiake', + 'JD' => 'Jedi', + 'JEE' => 'Jeep', + 'J6' => 'Jeka', + 'JF' => 'JFone', + 'JI' => 'Jiayu', + 'JG' => 'Jinga', + 'JIN' => 'Jin Tu', + 'JX' => 'Jio', + 'VJ' => 'Jivi', + 'JK' => 'JKL', + 'JR1' => 'JREN', + 'JO' => 'Jolla', + 'JP' => 'Joy', + 'JOY' => 'JoySurf', + 'UJ' => 'Juniper Systems', + 'J5' => 'Just5', + 'JUS' => 'JUSYEA', + '7J' => 'Jumper', + 'JPA' => 'JPay', + 'JV' => 'JVC', + 'JXD' => 'JXD', + 'JS' => 'Jesy', + 'KT' => 'K-Touch', + 'KLT' => 'K-Lite', + 'K4' => 'Kaan', + 'K7' => 'Kaiomy', + 'KL' => 'Kalley', + 'K6' => 'Kanji', + 'KA' => 'Karbonn', + 'K5' => 'KATV1', + 'KAP' => 'Kapsys', + 'K0' => 'Kata', + 'KZ' => 'Kazam', + '9K' => 'Kazuna', + 'KD' => 'KDDI', + 'KHA' => 'Khadas', + 'KS' => 'Kempler & Strauss', + 'K3' => 'Keneksi', + 'KHI' => 'KENSHI', + 'KNW' => 'KENWOOD', + 'KX' => 'Kenxinda', + 'KEN' => 'Kenbo', + 'KND' => 'Kendo', + 'KZG' => 'KZG', + 'K1' => 'Kiano', + 'KID' => 'kidiby', + '5W' => 'Kingbox', + 'KI' => 'Kingsun', + 'KIS' => 'Kinstone', + 'KF' => 'KINGZONE', + 'KIN' => 'Kingstar', + '46' => 'Kiowa', + 'KV' => 'Kivi', + '64' => 'Kvant', + 'KVA' => 'KVADRA', + '0K' => 'Klipad', + 'KNM' => 'KN Mobile', + 'KC' => 'Kocaso', + 'KK' => 'Kodak', + 'KG' => 'Kogan', + 'KGT' => 'KGTEL', + 'KM' => 'Komu', + 'KMC' => 'KMC', + 'KO' => 'Konka', + 'KW' => 'Konrow', + 'KB' => 'Koobee', + '7K' => 'Koolnee', + 'K9' => 'Kooper', + 'KP' => 'KOPO', + 'KR' => 'Koridy', + 'XK' => 'Koslam', + 'K2' => 'KRONO', + 'KRX' => 'Korax', + 'KE' => 'Krüger&Matz', + '5K' => 'KREZ', + 'WK' => 'KRIP', + 'KRA' => 'Kraft', + 'KH' => 'KT-Tech', + 'KTC' => 'KTC', + 'Z6' => 'KUBO', + 'KUG' => 'KuGou', + 'K8' => 'Kuliao', + '8K' => 'Kult', + 'KU' => 'Kumai', + '6K' => 'Kurio', + 'KYD' => 'Kydos', + 'KY' => 'Kyocera', + 'KQ' => 'Kyowon', + '1K' => 'Kzen', + 'LQ' => 'LAIQ', + 'L6' => 'Land Rover', + 'L2' => 'Landvo', + 'LA' => 'Lanix', + 'LA1' => 'Lanin', + 'LK' => 'Lark', + 'Z3' => 'Laurus', + 'LEC' => 'Lectrus', + 'LAS' => 'Laser', + 'LV' => 'Lava', + 'LVI' => 'Lville', + 'LC' => 'LCT', + 'L5' => 'Leagoo', + 'U3' => 'Leben', + 'LEB' => 'LeBest', + 'LD' => 'Ledstar', + 'LEE' => 'Leelbox', + 'L1' => 'LeEco', + '4B' => 'Leff', + 'LEG' => 'Legend', + 'L4' => 'Lemhoov', + 'W9' => 'LEMFO', + 'LEM' => 'Lemco', + 'LN' => 'Lenco', + 'LE' => 'Lenovo', + 'LT' => 'Leotec', + 'LP' => 'Le Pan', + 'ZJ' => 'Leke', + 'L7' => 'Lephone', + 'LZ' => 'Lesia', + 'L3' => 'Lexand', + 'LX' => 'Lexibook', + 'LG' => 'LG', + '39' => 'Liberton', + '5L' => 'Linsar', + 'LIN' => 'Linsay', + 'LF' => 'Lifemaxx', + 'LI' => 'Lingwin', + 'LIB' => 'Lingbo', + 'LIM' => 'Lime', + 'LJ' => 'L-Max', + 'LW' => 'Linnex', + 'JJ' => 'Listo', + 'LNM' => 'LNMBBS', + 'LO' => 'Loewe', + 'LNG' => 'LongTV', + 'YL' => 'Loview', + 'LOV' => 'Lovme', + 'LGN' => 'LOGAN', + '1L' => 'Logic', + 'LH' => 'Logic Instrument', + 'LM' => 'Logicom', + 'LOG' => 'Logik', + 'LGT' => 'Logitech', + 'GY' => 'LOKMAT', + 'LPX' => 'LPX-G', + '0L' => 'Lumigon', + 'LU' => 'Lumus', + 'LUM' => 'Lumitel', + 'L9' => 'Luna', + 'LNN' => 'LUNNEN', + 'LUO' => 'LUO', + 'LR' => 'Luxor', + 'LY' => 'LYF', + 'LL' => 'Leader Phone', + 'LTL' => 'LYOTECH LABS', + 'QL' => 'LT Mobile', + 'LW1' => 'LW', + 'MQ' => 'M.T.T.', + 'MN' => 'M4tel', + 'XM' => 'Macoox', + '92' => 'MAC AUDIO', + 'MJ' => 'Majestic', + 'FQ' => 'Mafe', + 'MAG' => 'MAG', + 'MA2' => 'MAGCH', + '6Y' => 'Magicsee', + 'MA4' => 'Magenta', + '23' => 'Magnus', + 'NH' => 'Manhattan', + 'MAN' => 'Mango', + '5M' => 'Mann', + 'MA' => 'Manta Multimedia', + 'Z0' => 'Mantra', + 'J4' => 'Mara', + 'MAR' => 'Marshal', + '8Y' => 'Massgo', + 'MA1' => 'Mascom', + '2M' => 'Masstel', + '3X' => 'Mastertech', + 'MAS' => 'Master-G', + '50' => 'Matrix', + 'MAT' => 'Matco Tools', + '7M' => 'Maxcom', + '7M1' => 'Maxfone', + 'ZM' => 'Maximus', + '6X' => 'Maxtron', + '0D' => 'MAXVI', + 'MAX' => 'Maxwell', + 'XZ' => 'MAXX', + 'MW' => 'Maxwest', + 'M0' => 'Maze', + 'YM' => 'Maze Speed', + '87' => 'Malata', + 'MAU' => 'Maunfeld', + 'MCL' => 'McLaut', + '28' => 'MBOX', + 'FK' => 'MBI', + 'MBK' => 'MBK', + '3D' => 'MDC Store', + '1Y' => 'MDTV', + '09' => 'meanIT', + 'M3' => 'Mecer', + 'M3M' => 'M3 Mobile', + '0M' => 'Mecool', + 'MEC' => 'MECHEN', + 'MEM' => 'MeMobile', + 'MC' => 'Mediacom', + 'MD' => 'Medion', + 'M2' => 'MEEG', + 'MEG' => 'MEGA VISION', + 'MCA' => 'Megacable', + 'MP' => 'MegaFon', + 'MGX' => 'MEGAMAX', + 'X0' => 'mPhone', + '3M' => 'Meitu', + 'M1' => 'Meizu', + '0E' => 'Melrose', + 'MU' => 'Memup', + 'ME' => 'Metz', + 'MEO' => 'MEO', + 'MX' => 'MEU', + 'MES' => 'MESWAO', + 'MI' => 'MicroMax', + 'MIP' => 'mipo', + 'MS' => 'Microsoft', + '6Q' => 'Microtech', + 'MIG' => 'Mightier', + '1X' => 'Minix', + 'OM' => 'Mintt', + 'MIN' => 'Mint', + 'MO' => 'Mio', + 'MOD' => 'Moondrop', + 'MOR' => 'MORTAL', + 'X7' => 'Mione', + 'M7' => 'Miray', + 'MIT' => 'Mitchell & Brown', + '8M' => 'Mito', + 'MT' => 'Mitsubishi', + '0Y' => 'Mitsui', + 'M5' => 'MIXC', + '2D' => 'MIVO', + '1Z' => 'MiXzo', + 'MIW' => 'MIWANG', + 'ML' => 'MLLED', + 'LS' => 'MLS', + 'MLA' => 'MLAB', + '5H' => 'MMI', + '4M' => 'Mobicel', + 'M6' => 'Mobiistar', + 'MOK' => 'Mobile Kingdom', + 'MH' => 'Mobiola', + 'MB' => 'Mobistel', + 'ID' => 'MobiIoT', + '6W' => 'MobiWire', + '9M' => 'Mobo', + 'MOB' => 'Mobell', + 'MVO' => 'Mobvoi', + 'M4' => 'Modecom', + 'MEP' => 'Mode Mobile', + 'MF' => 'Mofut', + 'MR' => 'Motorola', + 'MTS' => 'Motorola Solutions', + 'MIV' => 'Motiv', + 'MV' => 'Movic', + 'MOV' => 'Movitel', + 'MO1' => 'MOVISUN', + 'MOS' => 'Mosimosi', + 'MOX' => 'Moxee', + 'MM' => 'Mpman', + 'MZ' => 'MSI', + '3R' => 'MStar', + 'M9' => 'MTC', + 'N4' => 'MTN', + '72' => 'M-Tech', + '9H' => 'M-Horse', + 'MKP' => 'M-KOPA', + 'MLB' => 'multibox', + '1R' => 'Multilaser', + 'MPS' => 'MultiPOS', + 'MLT' => 'MULTYNET', + '1M' => 'MYFON', + 'MY1' => 'myPhone (PL)', + 'MY' => 'MyPhone (PH)', + '51' => 'Myros', + 'M8' => 'Myria', + '6M' => 'Mystery', + '3T' => 'MyTab', + 'MG' => 'MyWigo', + 'J3' => 'Mymaga', + 'MYM' => 'MyMobile', + '07' => 'MyGica', + 'MYG' => 'MygPad', + 'MWA' => 'MwalimuPlus', + 'NEO' => 'neoCore', + 'NER' => 'Neoregent', + '08' => 'Nabi', + 'N7' => 'National', + 'NC' => 'Navcity', + '6N' => 'Navitech', + '7V' => 'Navitel', + 'N3' => 'Navon', + '7R' => 'NavRoad', + 'NAB' => 'NABO', + 'NAS' => 'NASCO', + 'NP' => 'Naomi Phone', + 'NAN' => 'Nanho', + 'NE' => 'NEC', + 'NDP' => 'Nedaphone', + '8N' => 'Necnot', + 'NF' => 'Neffos', + '9X' => 'Neo', + 'NEK' => 'NEKO', + '1N' => 'Neomi', + '7Q' => 'Neon IQ', + '8F' => 'Neolix', + 'NES' => 'Nesons', + 'NET' => 'NetBox', + 'NWT' => 'NETWIT', + 'NA' => 'Netgear', + 'NEM' => 'Netmak', + 'NU' => 'NeuImage', + 'NEU' => 'NeuTab', + 'NEV' => 'NEVIR', + 'NW' => 'Newgen', + 'N9' => 'Newland', + '0N' => 'Newman', + 'NS' => 'NewsMy', + 'ND' => 'Newsday', + 'HB' => 'New Balance', + 'BRI' => 'New Bridge', + 'NEW' => 'Newal', + 'XB' => 'NEXBOX', + 'NX' => 'Nexian', + '7X' => 'Nexa', + '7XR' => 'Nexar', + 'N8' => 'NEXON', + 'N2' => 'Nextbit', + 'NT' => 'NextBook', + 'NTT' => 'NTT West', + '4N' => 'NextTab', + 'NEX' => 'NEXT', + 'NST' => 'Next & NextStar', + 'NJO' => 'nJoy', + 'NG' => 'NGM', + 'NZ' => 'NG Optics', + 'NZP' => 'NGpon', + 'NN' => 'Nikon', + 'NIL' => 'NILAIT', + 'NI' => 'Nintendo', + 'NIN' => 'NINETEC', + 'NI1' => 'NINETOLOGY', + 'N5' => 'NOA', + 'N1' => 'Noain', + 'N6' => 'Nobby', + 'NOC' => 'Novacom', + 'NOS' => 'NoviSea', + 'NO2' => 'NOVIS', + 'NOV' => 'Novey', + 'NO1' => 'NOVO', + '57' => 'Nubia', + 'JN' => 'NOBUX', + 'NB' => 'Noblex', + 'OG' => 'NOGA', + 'NK' => 'Nokia', + 'NM' => 'Nomi', + '2N' => 'Nomu', + '6H' => 'Noontec', + 'NR' => 'Nordmende', + 'NRD' => 'Nordfrost', + 'NOR' => 'NORMANDE', + '7N' => 'NorthTech', + 'NOT' => 'Nothing', + '5N' => 'Nos', + 'NO' => 'Nous', + 'NQ' => 'Novex', + 'NOD' => 'noDROPOUT', + 'NJ' => 'NuAns', + 'NL' => 'NUU Mobile', + 'N0' => 'Nuvo', + 'NUV' => 'NuVision', + 'NV' => 'Nvidia', + 'NY' => 'NYX Mobile', + 'NON' => 'N-one', + 'O3' => 'O+', + 'OT' => 'O2', + 'O7' => 'Oale', + 'OC' => 'OASYS', + 'OB' => 'Obi', + 'OBR' => 'Ober', + 'OQ' => 'Meta', + 'O1' => 'Odys', + 'ODP' => 'Odotpad', + 'O9' => 'ok.', + 'OKA' => 'Okapi', + 'OA' => 'Okapia', + 'OKI' => 'Oking', + 'OLA' => 'Olax', + 'OLK' => 'Olkya', + 'OLY' => 'Olympia', + 'OCE' => 'OCEANIC', + 'OLT' => 'OLTO', + 'OJ' => 'Ookee', + 'OD' => 'Onda', + 'ON' => 'OnePlus', + 'ONC' => 'OneClick', + 'ONL' => 'OneLern', + 'OAN' => 'Oangcc', + 'OX' => 'Onix', + 'OIN' => 'Onida', + '3O' => 'ONYX BOOX', + 'O4' => 'ONN', + '9Q' => 'Onkyo', + 'ONV' => 'ONVO', + 'OOR' => 'Ooredoo', + '2O' => 'OpelMobile', + 'OH' => 'Openbox', + '7Y' => 'Obabox', + 'OP' => 'OPPO', + 'OO' => 'Opsson', + 'OPT' => 'Optoma', + 'OPH' => 'Ophone', + 'OR' => 'Orange', + 'ORP' => 'Orange Pi', + 'ORA' => 'Orava', + 'O5' => 'Orbic', + 'Y6' => 'Orbita', + 'ORB' => 'Orbsmart', + 'OS' => 'Ordissimo', + '8O' => 'Orion', + 'OTT' => 'OTTO', + 'OK' => 'Ouki', + '0O' => 'OINOM', + 'OIL' => 'Oilsky', + 'QK' => 'OKWU', + 'QQ' => 'OMIX', + '56' => 'OKSI', + 'OE' => 'Oukitel', + 'OU' => 'OUYA', + 'JB' => 'OUJIA', + 'OV' => 'Overmax', + '30' => 'Ovvi', + 'O2' => 'Owwo', + 'OSC' => 'OSCAL', + 'OXT' => 'OX TAB', + 'OY' => 'Oysters', + 'QF' => 'OYSIN', + 'O6' => 'Oyyu', + 'OZ' => 'OzoneHD', + 'OLL' => 'Ollee', + '7P' => 'P-UP', + 'PRA' => 'Pacific Research Alliance', + 'PAG' => 'PAGRAER', + 'PAD' => 'Padpro', + 'YP' => 'Paladin', + 'PM' => 'Palm', + 'PN' => 'Panacom', + 'PA' => 'Panasonic', + 'PNV' => 'Panavox', + 'PT' => 'Pantech', + 'PAN' => 'Pano', + 'PND' => 'Panodic', + 'PA1' => 'Panoramic', + 'PLT' => 'Platoon', + 'PLD' => 'PLDT', + '94' => 'Packard Bell', + 'H9' => 'Parrot Mobile', + 'PAR' => 'Partner Mobile', + 'PAP' => 'PAPYRE', + 'PB' => 'PCBOX', + 'PCS' => 'PC Smart', + 'PC' => 'PCD', + 'PD' => 'PCD Argentina', + 'PE' => 'PEAQ', + 'PEN' => 'Penta', + 'PG' => 'Pentagram', + 'PQ' => 'Pendoo', + '93' => 'Perfeo', + '8J' => 'Pelitt', + '1P' => 'Phicomm', + '4P' => 'Philco', + 'PH' => 'Philips', + '5P' => 'Phonemax', + 'PO' => 'phoneOne', + 'PI' => 'Pioneer', + 'PIC' => 'Pioneer Computers', + 'PJ' => 'PiPO', + '8P' => 'Pixelphone', + '9O' => 'Pixela', + 'PX' => 'Pixus', + 'PIX' => 'PIXPRO', + 'QP' => 'Pico', + 'PIR' => 'PIRANHA', + 'PIN' => 'PINE', + '9P' => 'Planet Computers', + 'PLA' => 'Play Now', + 'PY' => 'Ployer', + 'P4' => 'Plum', + 'PLU' => 'PlusStyle', + '22' => 'Pluzz', + 'P8' => 'PocketBook', + '0P' => 'POCO', + 'FH' => 'Point Mobile', + 'PV' => 'Point of View', + 'PVB' => 'PVBox', + 'PL' => 'Polaroid', + 'Q6' => 'Polar', + '97' => 'PolarLine', + 'PP' => 'PolyPad', + 'P5' => 'Polytron', + 'P2' => 'Pomp', + 'P0' => 'Poppox', + '0X' => 'POPTEL', + 'PS' => 'Positivo', + '3P' => 'Positivo BGH', + '3F' => 'Porsche', + 'PRT' => 'Portfolio', + 'PPD' => 'PPDS', + 'P3' => 'PPTV', + 'FP' => 'Premio', + 'PR1' => 'Premier', + 'PR' => 'Prestigio', + 'P9' => 'Primepad', + 'PRM' => 'PRIME', + '6P' => 'Primux', + '2P' => 'Prixton', + 'PRI' => 'Pritom', + 'PRP' => 'PRISM+', + 'PF' => 'PROFiLO', + 'P6' => 'Proline', + '5O' => 'Prology', + 'P1' => 'ProScan', + 'PRO' => 'PROSONIC', + 'P7' => 'Protruly', + 'R0' => 'ProVision', + '7O' => 'Polestar', + 'PU' => 'PULID', + 'UP' => 'Purism', + 'PUN' => 'Punos', + 'QFX' => 'QFX', + 'Q7' => 'Q-Box', + 'QH' => 'Q-Touch', + 'QB' => 'Q.Bell', + 'QI' => 'Qilive', + 'QIN' => 'QIN', + 'QIW' => 'Qiuwoky', + 'QM' => 'QMobile', + 'QT' => 'Qtek', + 'Q9' => 'QTECH', + 'QA' => 'Quantum', + 'QUE' => 'Quest', + 'QUA' => 'Quatro', + 'QU' => 'Quechua', + 'QUI' => 'Quipus', + 'QO' => 'Qumo', + 'UQ' => 'Qubo', + 'QUP' => 'Qupi', + 'YQ' => 'QLink', + 'QY' => 'Qnet Mobile', + 'WJ' => 'Qware', + 'QWT' => 'QWATT', + 'R2' => 'R-TV', + 'R3D' => 'R3Di', + 'RA' => 'Ramos', + '0R' => 'Raspberry', + 'R9' => 'Ravoz', + 'RZ' => 'Razer', + 'RAZ' => 'RAZZ', + '95' => 'Rakuten', + 'RAY' => 'Raylandz', + 'RC' => 'RCA Tablets', + 'RCT' => 'RCT', + '2R' => 'Reach', + 'RLX' => 'Realix', + 'REL' => 'RelNAT', + 'RE4' => 'Relndoo', + 'RB' => 'Readboy', + 'RE' => 'Realme', + 'RE1' => 'Redbean', + 'R8' => 'RED', + 'RDX' => 'RED-X', + 'REW' => 'Redway', + '6F' => 'Redfox', + 'RE2' => 'RedLine', + 'RD' => 'Reeder', + 'Z9' => 'REGAL', + 'RH' => 'Remdun', + 'RP' => 'Revo', + 'REV' => 'Revomovil', + '8R' => 'Retroid Pocket', + 'REN' => 'Renova', + 'RE3' => 'RENSO', + 'REP' => 'rephone', + 'RHI' => 'Rhino', + 'RIC' => 'Ricoh', + 'RI' => 'Rikomagic', + 'RM' => 'RIM', + 'RN' => 'Rinno', + 'RNB' => 'Ringing Bells', + 'RX' => 'Ritmix', + 'R7' => 'Ritzviva', + 'RV' => 'Riviera', + '6R' => 'Rivo', + 'RIZ' => 'Rizzen', + 'RR' => 'Roadrover', + 'QR' => 'ROADMAX', + 'ROH' => 'ROCH', + 'ROC' => 'Roam Cat', + 'ROT' => 'Rocket', + 'R1' => 'Rokit', + 'ROI' => 'ROiK', + 'RK' => 'Roku', + 'R3' => 'Rombica', + 'RUA' => 'Romsat', + 'R5' => 'Ross&Moor', + 'RO' => 'Rover', + 'R6' => 'RoverPad', + 'RQ' => 'RoyQueen', + 'RJ' => 'Royole', + 'RT' => 'RT Project', + 'RTK' => 'RTK', + 'RG' => 'RugGear', + 'RUG' => 'Ruggex', + 'RUT' => 'RuggeTech', + 'RU' => 'Runbo', + 'RUN' => 'RunGee', + 'RUP' => 'Rupa', + 'RL' => 'Ruio', + 'RY' => 'Ryte', + 'X5' => 'Saba', + '8L' => 'S-TELL', + '8L1' => 'S-Color', + '4O' => 'S2Tel', + '89' => 'Seatel', + 'SEW' => 'Sewoo', + 'SE1' => 'SEEWO', + 'SEN' => 'SENNA', + 'SER' => 'SERVO', + 'Y7' => 'Saiet', + 'SLF' => 'SAILF', + 'X1' => 'Safaricom', + 'SG' => 'Sagem', + 'SAG' => 'Sagemcom', + '4L' => 'Salora', + 'SA' => 'Samsung', + 'SAT' => 'Samtech', + 'SAM' => 'Samtron', + 'SAB' => 'Sambox', + 'SNA' => 'SNAMI', + 'S0' => 'Sanei', + '12' => 'Sansui', + 'SAK' => 'Sankey', + 'SQ' => 'Santin', + 'SY' => 'Sanyo', + 'SAN' => 'SANY', + 'S9' => 'Savio', + 'Y4' => 'TCL SCBC', + 'SCH' => 'SCHAUB LORENZ', + 'CZ' => 'Schneider', + 'SHO' => 'SCHONTECH', + 'SCO' => 'Scosmos', + 'SC1' => 'Scoole', + 'ZG' => 'Schok', + 'G8' => 'SEG', + 'SEX' => 'SEHMAX', + 'SD' => 'Sega', + '0U' => 'Selecline', + '9G' => 'Selenga', + 'SV' => 'Selevision', + 'SL' => 'Selfix', + '0S' => 'SEMP TCL', + 'S1' => 'Sencor', + 'SNM' => 'Sencrom', + 'SN' => 'Sendo', + '01' => 'Senkatel', + 'S6' => 'Senseit', + 'EW' => 'Senwa', + '24' => 'Seeken', + 'SEB' => 'SEBBE', + '61' => 'Seuic', + 'SX' => 'SFR', + 'SGI' => 'SGIN', + 'SH' => 'Sharp', + 'JU' => 'Shanling', + '7S' => 'Shift Phones', + '78' => 'Shivaki', + 'RS' => 'Shtrikh-M', + '3S' => 'Shuttle', + '13' => 'Sico', + 'SI' => 'Siemens', + '1S' => 'Sigma', + '70' => 'Silelis', + 'SJ' => 'Silent Circle', + 'SIL' => 'Silva Schneider', + '10' => 'Simbans', + '98' => 'Simply', + 'SIM' => 'simfer', + '52' => 'Singtech', + 'SIN' => 'SINGER', + '31' => 'Siragon', + 'SIS' => 'Siswoo', + '83' => 'Sirin Labs', + '5Z' => 'SK Broadband', + 'GK' => 'SKG', + 'SW' => 'Sky', + 'SK' => 'Skyworth', + 'SKY' => 'Skyline', + 'SK1' => 'SkyStream', + 'SKT' => 'Skytech', + 'SKK' => 'SKK Mobile', + '14' => 'Smadl', + '19' => 'Smailo', + 'SR' => 'Smart Electronic', + 'SMA' => 'Smart Kassel', + 'STE' => 'Smart Tech', + '49' => 'Smart', + '47' => 'SmartBook', + '3B' => 'Smartab', + '80' => 'SMARTEC', + 'SM1' => 'Smartex', + 'SC' => 'Smartfren', + 'S7' => 'Smartisan', + 'SMU' => 'SMUX', + 'SMT' => 'SMT Telecom', + 'JR' => 'Sylvania', + 'SYH' => 'SYH', + '3Y' => 'Smarty', + 'HH' => 'Smooth Mobile', + '1Q' => 'Smotreshka', + 'SF' => 'Softbank', + '9L' => 'SOLE', + 'JL' => 'SOLO', + 'SOS' => 'SOSH', + 'SOD' => 'Soda', + 'SOL' => 'Solas', + '16' => 'Solone', + 'OI' => 'Sonim', + 'SVE' => 'Sveon', + 'SO' => 'Sony', + 'SE' => 'Sony Ericsson', + 'X2' => 'Soundmax', + 'SUL' => 'SoulLink', + '8S' => 'Soyes', + '77' => 'SONOS', + '68' => 'Soho Style', + 'SOB' => 'SobieTech', + 'SOW' => 'SOWLY', + 'PK' => 'Spark', + 'SPX' => 'Sparx', + 'FS' => 'SPC', + '6S' => 'Spectrum', + '43' => 'Spectralink', + 'SP' => 'Spice', + 'SPD' => 'Spider', + '84' => 'Sprint', + 'SPU' => 'SPURT', + 'QS' => 'SQOOL', + 'S4' => 'Star', + 'OL' => 'Starlight', + 'STA' => 'Star-Light', + '18' => 'Starmobile', + '2S' => 'Starway', + '45' => 'Starwind', + 'SB' => 'STF Mobile', + 'S8' => 'STK', + 'GQ' => 'STG Telecom', + 'S2' => 'Stonex', + 'ST' => 'Storex', + 'STR' => 'Stream', + '71' => 'StrawBerry', + '96' => 'STRONG', + '69' => 'Stylo', + 'STI' => 'Stilevs', + '9S' => 'Sugar', + 'SUR' => 'Surge', + 'SUF' => 'Surfans', + '06' => 'Subor', + 'SUT' => 'SULPICE TV', + 'SZ' => 'Sumvision', + '0H' => 'Sunstech', + 'S3' => 'SunVan', + '5S' => 'Sunvell', + '5Y' => 'Sunny', + 'W8' => 'SUNWIND', + 'SBX' => 'SuperBOX', + 'SBM' => 'Supermax', + 'SBR' => 'Sber', + 'SGE' => 'Super General', + 'SU' => 'SuperSonic', + '79' => 'SuperTab', + 'STV' => 'SuperTV', + 'S5' => 'Supra', + 'SUP' => 'Supraim', + 'ZS' => 'Suzuki', + '2J' => 'Sunmi', + 'SUN' => 'Sunmax', + '0W' => 'Swipe', + 'SWI' => 'Switel', + 'SS' => 'SWISSMOBILITY', + '1W' => 'Swisstone', + 'SWO' => 'SWOFY', + 'SSK' => 'SSKY', + 'SYC' => 'Syco', + 'SM' => 'Symphony', + '4S' => 'Syrox', + 'SYS' => 'System76', + 'TM' => 'T-Mobile', + 'T96' => 'T96', + 'TAD' => 'TADAAM', + 'TK' => 'Takara', + '73' => 'Tambo', + '9N' => 'Tanix', + 'U5' => 'Taiga System', + 'TAL' => 'Talius', + '7G' => 'TAG Tech', + 'TLB' => 'TALBERG', + 'TAU' => 'TAUBE', + 'T5' => 'TB Touch', + 'TC' => 'TCL', + 'T0' => 'TD Systems', + 'YY' => 'TD Tech', + 'H4' => 'Technicolor', + 'TEA' => 'TeachTouch', + 'Z5' => 'Technika', + 'TE1' => 'TechSmart', + 'TX' => 'TechniSat', + 'TT' => 'TechnoTrend', + 'TTS' => 'TECHNOSAT', + 'TM1' => 'Temigereev', + 'TP' => 'TechPad', + 'TPS' => 'TPS', + '9E' => 'Techwood', + '7F' => 'Technopc', + 'TCH' => 'Techstorm', + 'T7' => 'Teclast', + 'TB' => 'Tecno Mobile', + 'TEC' => 'TecToy', + '91' => 'TEENO', + 'TLK' => 'Telkom', + '2L' => 'Tele2', + 'TL' => 'Telefunken', + 'TG' => 'Telego', + 'T2' => 'Telenor', + 'TE' => 'Telit', + '65' => 'Telia', + 'TLY' => 'Telly', + 'TEL' => 'Telma', + 'PW' => 'Telpo', + 'TLS' => 'TeloSystems', + 'TER' => 'Teracube', + 'TD' => 'Tesco', + 'TA' => 'Tesla', + '9T' => 'Tetratab', + 'TET' => 'TETC', + 'TZ' => 'teXet', + '29' => 'Teknosa', + 'JZ' => 'TJC', + 'TJD' => 'TJD', + 'JC' => 'TENPLUS', + 'T4' => 'ThL', + 'TN' => 'Thomson', + 'O0' => 'Thuraya', + 'TI' => 'TIANYU', + 'JY' => 'Tigers', + '8T' => 'Time2', + 'TQ' => 'Timovi', + 'TIM' => 'TIMvision', + '2T' => 'Tinai', + 'TF' => 'Tinmo', + 'TH' => 'TiPhone', + 'YV' => 'TiVo', + 'TIV' => 'Tivax', + 'TIB' => 'Tibuta', + 'Y3' => 'TOKYO', + 'TOX' => 'TOX', + 'T1' => 'Tolino', + '0T' => 'Tone', + 'TY' => 'Tooky', + 'TYD' => 'TYD', + 'TOO' => 'TOOGO', + 'TPT' => 'Top-Tech', + 'T9' => 'Top House', + 'DK' => 'Topelotek', + '42' => 'Topway', + 'TO' => 'Toplux', + 'TOD' => 'TOPDON', + 'TOP' => 'TopDevice', + 'TO2' => 'TOPSHOWS', + '7T' => 'Torex', + 'TOR' => 'Torque', + 'TRN' => 'TORNADO', + '6O' => 'TOSCIDO', + 'TO1' => 'Topsion', + 'TS' => 'Toshiba', + 'T8' => 'Touchmate', + 'TOU' => 'Touch Plus', + '5R' => 'Transpeed', + 'T6' => 'TrekStor', + 'TRP' => 'Trecfone', + 'T3' => 'Trevi', + 'TRI' => 'TriaPlay', + 'TJ' => 'Trifone', + 'Q5' => 'Trident', + 'TRB' => 'Trimble', + '4T' => 'Tronsmart', + '11' => 'True', + 'JT' => 'True Slim', + 'J1' => 'Trio', + 'TRC' => 'Tricolor', + 'THT' => 'Tsinghua Tongfang', + '5C' => 'TTEC', + 'TTF' => 'TTfone', + 'TTK' => 'TTK-TV', + 'TU' => 'Tunisie Telecom', + '1T' => 'Turbo', + 'TR' => 'Turbo-X', + '5X' => 'TurboPad', + '5T' => 'TurboKids', + 'UR' => 'Turkcell', + '4U' => 'TuCEL', + 'TUV' => 'Tuvio', + 'TUC' => 'TUCSON', + '2U' => 'Türk Telekom', + 'TV' => 'TVC', + 'TVP' => 'TV+', + 'TW' => 'TWM', + 'Z1' => 'TWZ', + '6T' => 'Twoe', + 'TWN' => 'TwinMOS', + '15' => 'Tymes', + 'UC' => 'U.S. Cellular', + 'UD1' => 'UD', + 'UGI' => 'UGINE', + 'UG' => 'Ugoos', + 'U1' => 'Uhans', + 'UH' => 'Uhappy', + 'UL' => 'Ulefone', + 'UA' => 'Umax', + 'UM' => 'UMIDIGI', + 'UM2' => 'Umiio', + 'UNT' => 'Unitech', + 'UZ' => 'Unihertz', + '3Z' => 'UZ Mobile', + 'UX' => 'Unimax', + 'UNQ' => 'Uniqcell', + 'US' => 'Uniscope', + 'UNI' => 'Unistrong', + 'U2' => 'UNIWA', + 'UND' => 'Uniden', + 'UNE' => 'UNITED', + 'UGR' => 'United Group', + 'UO' => 'Unnecto', + 'UNN' => 'Unnion Technologies', + 'UNP' => 'UnoPhone', + 'UU' => 'Unonu', + 'UN' => 'Unowhy', + 'UY' => 'UNNO', + 'UOO' => 'UOOGOU', + 'UNB' => 'Unblock Tech', + 'UK' => 'UTOK', + '3U' => 'IUNI', + 'UT' => 'UTStarcom', + '6U' => 'UTime', + '9U' => 'Urovo', + 'UW' => 'U-Magic', + '5V' => 'VAIO', + 'WV' => 'VAVA', + 'VA' => 'Vastking', + 'VAS' => 'VASOUN', + 'VP' => 'Vargo', + 'VC' => 'Vankyo', + 'VAL' => 'VALEM', + 'VA2' => 'VALE', + 'VAT' => 'VALTECH', + 'VAN' => 'VANGUARD', + 'VAW' => 'VANWIN', + 'VB' => 'VC', + 'VEI' => 'Veidoo', + 'VN' => 'Venso', + 'VNP' => 'VNPT Technology', + 'VEN' => 'Venstar', + 'UV' => 'Venturer', + 'VQ' => 'Vega', + 'WC' => 'VEON', + '4V' => 'Verico', + 'V4' => 'Verizon', + 'VR' => 'Vernee', + 'VX' => 'Vertex', + 'VE' => 'Vertu', + 'VET' => 'VETAS', + 'VL' => 'Verykool', + 'QV' => 'Verssed', + 'VER' => 'Versus', + 'V8' => 'Vesta', + 'VEK' => 'Vekta', + 'VT' => 'Vestel', + '48' => 'Vexia', + 'V6' => 'VGO TEL', + 'QJ' => 'VDVD', + 'VIC' => 'Victurio', + 'VD' => 'Videocon', + 'VW' => 'Videoweb', + 'VS' => 'ViewSonic', + 'VIE' => 'Viendo', + 'VIK' => 'VIKUSHA', + 'V7' => 'Vinga', + 'V7T' => 'V7', + 'V3' => 'Vinsoc', + 'XD' => 'Vinabox', + 'FV' => 'Vios', + '0V' => 'Vipro', + 'ZV' => 'Virzo', + 'VIP' => 'Viper', + 'VI' => 'Vitelcom', + 'VIB' => 'ViBox', + '8V' => 'Viumee', + 'V5' => 'Vivax', + 'VIV' => 'VIVIMAGE', + 'VI2' => 'VIVIBright', + 'VV' => 'Vivo', + '6V' => 'VIWA', + 'VII' => 'VIIPOO', + 'VID' => 'VIDA', + 'VZ' => 'Vizio', + 'VIZ' => 'Vizmo', + 'VIT' => 'Vityaz', + 'VTM' => 'Vitumi', + '9V' => 'Vision Touch', + 'VIS' => 'Vision Technology', + 'VST' => 'Visitech', + 'VIL' => 'Visual Land', + 'VI1' => 'VILLAON', + 'VIM' => 'VIMOQ', + 'VK' => 'VK Mobile', + 'JM' => 'v-mobile', + 'VHO' => 'V-HOPE', + 'VHM' => 'V-HOME', + 'VGE' => 'V-Gen', + 'V0' => 'VKworld', + 'VM' => 'Vodacom', + 'VOC' => 'VOCAL', + 'VF' => 'Vodafone', + '7W' => 'VOGA', + 'V2' => 'Vonino', + '1V' => 'Vontar', + 'VG' => 'Vorago', + '2V' => 'Vorke', + '8U' => 'Vorcom', + 'JW' => 'Vortex', + 'VRX' => 'VORTEX (RO)', + 'VOR' => 'Vormor', + 'V1' => 'Voto', + 'Z7' => 'VOX', + 'VO' => 'Voxtel', + 'VY' => 'Voyo', + 'VOL' => 'Völfen', + 'VO1' => 'Volt', + 'VOP' => 'Volla', + 'V02' => 'VOLIA', + 'VH' => 'Vsmart', + 'V9' => 'Vsun', + 'VU' => 'Vulcan', + '3V' => 'VVETIME', + 'ZC' => 'VUCATIMES', + 'VO2' => 'VOLKANO', + 'VUE' => 'Vue Micro', + 'WAK' => 'Walker', + 'WA' => 'Walton', + 'WAF' => 'WAF', + 'WAO' => 'W&O', + 'WLT' => 'Waltham', + 'WAL' => 'Waltter', + 'WAI' => 'Wainyok', + 'WAT' => 'waipu.tv', + 'WHI' => 'White Mobile', + 'WHO' => 'Whoop', + 'WBL' => 'We. by Loewe.', + 'WCP' => 'WeChip', + 'WM' => 'Weimei', + 'WM1' => 'Weiimi', + 'WE' => 'WellcoM', + 'W6' => 'WELLINGTON', + 'WD' => 'Western Digital', + 'WST' => 'Weston', + 'WT' => 'Westpoint', + 'WAN' => 'Wanmukang', + 'WA1' => 'WANSA', + 'WY' => 'Wexler', + '3W' => 'WE', + 'WEC' => 'Wecool', + 'WEE' => 'Weelikeit', + 'WP' => 'Wieppo', + 'W2' => 'Wigor', + 'WI' => 'Wiko', + 'WF' => 'Wileyfox', + 'WLR' => 'WildRed', + 'WS' => 'Winds', + 'WN' => 'Wink', + '9W' => 'Winmax', + 'W5' => 'Winnovo', + 'WU' => 'Wintouch', + 'WIS' => 'Winstar', + 'W0' => 'Wiseasy', + '2W' => 'Wizz', + 'W4' => 'WIWA', + 'WIZ' => 'WizarPos', + 'WL' => 'Wolder', + 'WG' => 'Wolfgang', + 'WQ' => 'Wolki', + 'WON' => 'WONDER', + 'WO' => 'Wonu', + 'W1' => 'Woo', + 'WR' => 'Wortmann', + 'WX' => 'Woxter', + 'WOZ' => 'WOZIFAN', + 'XQ' => 'X-AGE', + 'XEL' => 'XElectron', + 'X3' => 'X-BO', + 'XBI' => 'XB', + 'XMO' => 'X-Mobile', + 'XT' => 'X-TIGI', + 'XV' => 'X-View', + 'X4' => 'X.Vision', + 'X88' => 'X88', + 'X96' => 'X96', + '96Q' => 'X96Q', + 'A95' => 'A95X', + 'XG' => 'Xgody', + 'XGE' => 'XGEM', + 'QX' => 'XGIMI', + 'XL' => 'Xiaolajiao', + 'XI' => 'Xiaomi', + 'XW' => 'Xiaodu', + 'XN' => 'Xion', + 'XO' => 'Xolo', + 'XR' => 'Xoro', + 'XPP' => 'XPPen', + 'XRL' => 'XREAL', + 'XS' => 'Xshitou', + 'XSM' => 'Xsmart', + '4X' => 'Xtouch', + 'X8' => 'Xtratech', + 'XCR' => 'Xcruiser', + 'XCO' => 'XCOM', + 'XCL' => 'Xcell', + 'XWA' => 'Xwave', + 'YD' => 'Yandex', + 'YA' => 'Yarvik', + 'Y2' => 'Yes', + 'YES' => 'Yestel', + 'YE' => 'Yezz', + 'YG' => 'YEPEN', + 'YEL' => 'YELLYOUTH', + 'YK' => 'Yoka TV', + 'YO' => 'Yota', + 'YOU' => 'Youin', + 'YO1' => 'Youwei', + 'YOO' => 'Yooz', + 'YT' => 'Ytone', + 'Y9' => 'YOTOPT', + 'Y1' => 'Yu', + 'YF' => 'YU Fly', + 'Y0' => 'YUHO', + 'YN' => 'Yuno', + 'YUN' => 'YUNDOO', + 'YUS' => 'YunSong', + 'YUM' => 'YUMKEM', + 'YIK' => 'YIKEMI', + 'YU' => 'Yuandao', + 'YS' => 'Yusun', + 'YJ' => 'YASIN', + 'YX' => 'Yxtel', + '0Z' => 'Zatec', + '2Z' => 'Zaith', + 'ZAM' => 'Zamolxe', + 'ZAL' => 'ZALA', + 'ZZB' => 'ZZB', + 'ZEA' => 'Zealot', + 'PZ' => 'Zebra', + 'ZE1' => 'Zeblaze', + 'ZE' => 'Zeemi', + 'WZ' => 'Zeeker', + 'ZN' => 'Zen', + 'ZK' => 'Zenek', + 'ZL' => 'Zentality', + 'ZF' => 'Zfiner', + 'ZI' => 'Zidoo', + 'FZ' => 'ZIFRO', + 'ZIF' => 'ZIFFLER', + 'ZIO' => 'ZIOVO', + 'ZX' => 'Ziox', + 'ZIK' => 'ZIK', + 'ZKI' => 'Z-Kai', + 'ZIG' => 'Zigo', + 'ZIN' => 'Zinox', + 'ZO' => 'Zonda', + 'ZW' => 'Zonko', + 'ZP' => 'Zopo', + 'ZOO' => 'ZoomSmart', + 'ZO1' => 'Zoom', + 'ZT' => 'ZTE', + 'ZU' => 'Zuum', + 'ZY' => 'Zync', + 'ZR' => 'Zyrex', + 'ZQ' => 'ZYQ', + 'Z4' => 'ZH&K', + 'OW' => 'öwn', + 'WBF' => 'Webfleet', + 'WSS' => 'WS', + // legacy brands, might be removed in future versions + 'WB' => 'Web TV', + 'XX' => 'Unknown', + ]; + + /** + * Mapping formFactor types to parser device types + * (not sort array the array priority result device type) + * @var array + */ + protected static $clientHintFormFactorsMapping = [ + 'automotive' => self::DEVICE_TYPE_CAR_BROWSER, + 'xr' => self::DEVICE_TYPE_WEARABLE, + 'watch' => self::DEVICE_TYPE_WEARABLE, + 'mobile' => self::DEVICE_TYPE_SMARTPHONE, + 'tablet' => self::DEVICE_TYPE_TABLET, + 'desktop' => self::DEVICE_TYPE_DESKTOP, + 'eink' => self::DEVICE_TYPE_TABLET, + ]; + + /** + * Returns the device type represented by one of the DEVICE_TYPE_* constants + * + * @return int|null + */ + public function getDeviceType(): ?int + { + return $this->deviceType; + } + + /** + * Returns available device types + * + * @see $deviceTypes + * + * @return array + */ + public static function getAvailableDeviceTypes(): array + { + return self::$deviceTypes; + } + + /** + * Returns names of all available device types + * + * @return array + */ + public static function getAvailableDeviceTypeNames(): array + { + return \array_keys(self::$deviceTypes); + } + + /** + * Returns the name of the given device type + * + * @param int $deviceType one of the DEVICE_TYPE_* constants + * + * @return string + */ + public static function getDeviceName(int $deviceType): string + { + $deviceName = \array_search($deviceType, self::$deviceTypes); + + if (\is_string($deviceName)) { + return $deviceName; + } + + return ''; + } + + /** + * Returns the detected device model + * + * @return string + */ + public function getModel(): string + { + return $this->model; + } + + /** + * Returns the detected device brand + * + * @return string + */ + public function getBrand(): string + { + return $this->brand; + } + + /** + * Returns the full brand name for the given short name + * + * @param string $brandId short brand name + * + * @return string + */ + public static function getFullName(string $brandId): string + { + if (\array_key_exists($brandId, self::$deviceBrands)) { + return self::$deviceBrands[$brandId]; + } + + return ''; + } + + /** + * Returns the brand short code for the given name + * + * @param string $brand brand name + * + * @return string + * + * @deprecated since 4.0 - short codes might be removed in next major release + */ + public static function getShortCode(string $brand): string + { + return (string) \array_search($brand, self::$deviceBrands) ?: ''; + } + + /** + * Sets the useragent to be parsed + * + * @param string $userAgent + */ + public function setUserAgent(string $userAgent): void + { + $this->reset(); + parent::setUserAgent($userAgent); + } + + /** + * @inheritdoc + */ + public function parse(): ?array + { + $resultClientHint = $this->parseClientHints(); + $deviceModel = $resultClientHint['model'] ?? ''; + + $this->restoreUserAgentFromClientHints(); + + if ('' === $deviceModel && $this->hasUserAgentClientHintsFragment()) { + return $this->getResult(); + } + + if ('' === $deviceModel && $this->hasDesktopFragment()) { + return $this->getResult(); + } + + $brand = ''; + $regexes = $this->getRegexes(); + + foreach ($regexes as $brand => $regex) { + $matches = $this->matchUserAgent($regex['regex']); + + if ($matches) { + break; + } + } + + if (empty($matches)) { + $this->deviceType = $resultClientHint['deviceType'] ?? null; + + return $resultClientHint; + } + + if ('Unknown' !== $brand) { + if (!\in_array($brand, self::$deviceBrands)) { + // This Exception should never be thrown. If so a defined brand name is missing in $deviceBrands + throw new \Exception(\sprintf( + "The brand with name '%s' should be listed in deviceBrands array. Tried to parse user agent: %s", + $brand, + $this->userAgent + )); // @codeCoverageIgnore + } + + $this->brand = (string) $brand; + } + + if (isset($regex['device']) && \array_key_exists($regex['device'], self::$deviceTypes)) { + $this->deviceType = self::$deviceTypes[$regex['device']]; + } + + $this->model = ''; + + if (isset($regex['model'])) { + $this->model = $this->buildModel($regex['model'], $matches); + } + + if (isset($regex['models'])) { + $modelRegex = ''; + + foreach ($regex['models'] as $modelRegex) { + $modelMatches = $this->matchUserAgent($modelRegex['regex']); + + if ($modelMatches) { + break; + } + } + + if (empty($modelMatches)) { + return $this->getResult(); + } + + $this->model = $this->buildModel($modelRegex['model'], $modelMatches); + + if (isset($modelRegex['brand']) && \in_array($modelRegex['brand'], self::$deviceBrands)) { + $this->brand = (string) $modelRegex['brand']; + } + + if (isset($modelRegex['device']) && \array_key_exists($modelRegex['device'], self::$deviceTypes)) { + $this->deviceType = self::$deviceTypes[$modelRegex['device']]; + } + } + + return $this->getResult(); + } + + /** + * @param string $model + * @param array $matches + * + * @return string + */ + protected function buildModel(string $model, array $matches): string + { + $model = $this->buildByMatch($model, $matches); + + $model = \str_replace('_', ' ', $model); + + $model = \preg_replace('/ TD$/i', '', $model); + + if ('Build' === $model || empty($model)) { + return ''; + } + + return \trim($model); + } + + /** + * @return array|null + */ + protected function parseClientHints(): ?array + { + if ($this->clientHints && $this->clientHints->getModel()) { + $detectedDeviceType = null; + $formFactors = $this->clientHints->getFormFactors(); + + foreach (self::$clientHintFormFactorsMapping as $formFactor => $deviceType) { + if (\in_array($formFactor, $formFactors)) { + $detectedDeviceType = $deviceType; + + break; + } + } + + return [ + 'deviceType' => $detectedDeviceType, + 'model' => $this->clientHints->getModel(), + 'brand' => '', + ]; + } + + return null; + } + + /** + * Resets the stored values + */ + protected function reset(): void + { + $this->deviceType = null; + $this->model = ''; + $this->brand = ''; + } + + /** + * @return array + */ + protected function getResult(): array + { + return [ + 'deviceType' => $this->deviceType, + 'model' => $this->model, + 'brand' => $this->brand, + ]; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/Camera.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/Camera.php new file mode 100644 index 0000000..ce7a1b2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/Camera.php @@ -0,0 +1,43 @@ +preMatchOverall()) { + return null; + } + + return parent::parse(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/CarBrowser.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/CarBrowser.php new file mode 100644 index 0000000..59edcd3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/CarBrowser.php @@ -0,0 +1,43 @@ +preMatchOverall()) { + return null; + } + + return parent::parse(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/Console.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/Console.php new file mode 100644 index 0000000..f3d0599 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/Console.php @@ -0,0 +1,43 @@ +preMatchOverall()) { + return null; + } + + return parent::parse(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/HbbTv.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/HbbTv.php new file mode 100644 index 0000000..8a7e15e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/HbbTv.php @@ -0,0 +1,68 @@ +isHbbTv()) { + return null; + } + + parent::parse(); + + // always set device type to tv, even if no model/brand could be found + if (null === $this->deviceType) { + $this->deviceType = self::DEVICE_TYPE_TV; + } + + return $this->getResult(); + } + + /** + * Returns if the parsed UA was identified as a HbbTV device + * + * @return string|null + */ + public function isHbbTv(): ?string + { + $regex = '(?:HbbTV|SmartTvA)/([1-9]{1}(?:\.[0-9]{1}){1,2})'; + $match = $this->matchUserAgent($regex); + + return $match[1] ?? null; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/Mobile.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/Mobile.php new file mode 100644 index 0000000..d04856b --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/Mobile.php @@ -0,0 +1,31 @@ +matchUserAgent('FBMD/')) { + return null; + } + + return parent::parse(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/PortableMediaPlayer.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/PortableMediaPlayer.php new file mode 100644 index 0000000..7a60f6e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/PortableMediaPlayer.php @@ -0,0 +1,43 @@ +preMatchOverall()) { + return null; + } + + return parent::parse(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/ShellTv.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/ShellTv.php new file mode 100644 index 0000000..9246a81 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/Device/ShellTv.php @@ -0,0 +1,66 @@ +matchUserAgent($regex); + + return null !== $match; + } + + /** + * Parses the current UA and checks whether it contains ShellTv information + * + * @see shell_tv.yml for list of detected televisions + * + * @return array|null + */ + public function parse(): ?array + { + // only parse user agents containing fragments: {brand} shell + if (false === $this->isShellTv()) { + return null; + } + + parent::parse(); + + // always set device type to tv, even if no model/brand could be found + $this->deviceType = self::DEVICE_TYPE_TV; + + return $this->getResult(); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/OperatingSystem.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/OperatingSystem.php new file mode 100644 index 0000000..2c917ac --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/OperatingSystem.php @@ -0,0 +1,756 @@ + 'AIX', + 'AND' => 'Android', + 'ADR' => 'Android TV', + 'ALP' => 'Alpine Linux', + 'AMZ' => 'Amazon Linux', + 'AMG' => 'AmigaOS', + 'ARM' => 'Armadillo OS', + 'ARO' => 'AROS', + 'ATV' => 'tvOS', + 'ARL' => 'Arch Linux', + 'AOS' => 'AOSC OS', + 'ASP' => 'ASPLinux', + 'AZU' => 'Azure Linux', + 'BTR' => 'BackTrack', + 'SBA' => 'Bada', + 'BYI' => 'Baidu Yi', + 'BEO' => 'BeOS', + 'BLB' => 'BlackBerry OS', + 'QNX' => 'BlackBerry Tablet OS', + 'PAN' => 'blackPanther OS', + 'BOS' => 'Bliss OS', + 'BMP' => 'Brew', + 'BSN' => 'BrightSignOS', + 'CAI' => 'Caixa Mágica', + 'CES' => 'CentOS', + 'CST' => 'CentOS Stream', + 'CLO' => 'Clear Linux OS', + 'CLR' => 'ClearOS Mobile', + 'COS' => 'Chrome OS', + 'CRS' => 'Chromium OS', + 'CHN' => 'China OS', + 'COL' => 'Coolita OS', + 'CYN' => 'CyanogenMod', + 'DEB' => 'Debian', + 'DEE' => 'Deepin', + 'DFB' => 'DragonFly', + 'DVK' => 'DVKBuntu', + 'ELE' => 'ElectroBSD', + 'EUL' => 'EulerOS', + 'FED' => 'Fedora', + 'FEN' => 'Fenix', + 'FOS' => 'Firefox OS', + 'FIR' => 'Fire OS', + 'FOR' => 'Foresight Linux', + 'FRE' => 'Freebox', + 'BSD' => 'FreeBSD', + 'FRI' => 'FRITZ!OS', + 'FYD' => 'FydeOS', + 'FUC' => 'Fuchsia', + 'GNT' => 'Gentoo', + 'GNX' => 'GENIX', + 'GEO' => 'GEOS', + 'GNS' => 'gNewSense', + 'GRI' => 'GridOS', + 'GTV' => 'Google TV', + 'HPX' => 'HP-UX', + 'HAI' => 'Haiku OS', + 'IPA' => 'iPadOS', + 'HAR' => 'HarmonyOS', + 'HAS' => 'HasCodingOS', + 'HEL' => 'HELIX OS', + 'IRI' => 'IRIX', + 'INF' => 'Inferno', + 'JME' => 'Java ME', + 'JOL' => 'Joli OS', + 'KOS' => 'KaiOS', + 'KAL' => 'Kali', + 'KAN' => 'Kanotix', + 'KIN' => 'KIN OS', + 'KOL' => 'KolibriOS', + 'KNO' => 'Knoppix', + 'KTV' => 'KreaTV', + 'KBT' => 'Kubuntu', + 'LIN' => 'GNU/Linux', + 'LEA' => 'LeafOS', + 'LND' => 'LindowsOS', + 'LNS' => 'Linspire', + 'LEN' => 'Lineage OS', + 'LIR' => 'Liri OS', + 'LOO' => 'Loongnix', + 'LBT' => 'Lubuntu', + 'LOS' => 'Lumin OS', + 'LUN' => 'LuneOS', + 'VLN' => 'VectorLinux', + 'MAC' => 'Mac', + 'MAE' => 'Maemo', + 'MAG' => 'Mageia', + 'MDR' => 'Mandriva', + 'SMG' => 'MeeGo', + 'MET' => 'Meta Horizon', + 'MCD' => 'MocorDroid', + 'MON' => 'moonOS', + 'EZX' => 'Motorola EZX', + 'MIN' => 'Mint', + 'MLD' => 'MildWild', + 'MOR' => 'MorphOS', + 'NBS' => 'NetBSD', + 'MTK' => 'MTK / Nucleus', + 'MRE' => 'MRE', + 'NXT' => 'NeXTSTEP', + 'NWS' => 'NEWS-OS', + 'WII' => 'Nintendo', + 'NDS' => 'Nintendo Mobile', + 'NOV' => 'Nova', + 'OS2' => 'OS/2', + 'T64' => 'OSF1', + 'OBS' => 'OpenBSD', + 'OVS' => 'OpenVMS', + 'OVZ' => 'OpenVZ', + 'OWR' => 'OpenWrt', + 'OTV' => 'Opera TV', + 'ORA' => 'Oracle Linux', + 'ORD' => 'Ordissimo', + 'PAR' => 'Pardus', + 'PCL' => 'PCLinuxOS', + 'PIC' => 'PICO OS', + 'PLA' => 'Plasma Mobile', + 'PSP' => 'PlayStation Portable', + 'PS3' => 'PlayStation', + 'PVE' => 'Proxmox VE', + 'PUF' => 'Puffin OS', + 'PUR' => 'PureOS', + 'QTP' => 'Qtopia', + 'PIO' => 'Raspberry Pi OS', + 'RAS' => 'Raspbian', + 'RHT' => 'Red Hat', + 'RST' => 'Red Star', + 'RED' => 'RedOS', + 'REV' => 'Revenge OS', + 'RIS' => 'risingOS', + 'ROS' => 'RISC OS', + 'ROC' => 'Rocky Linux', + 'ROK' => 'Roku OS', + 'RSO' => 'Rosa', + 'ROU' => 'RouterOS', + 'REM' => 'Remix OS', + 'RRS' => 'Resurrection Remix OS', + 'REX' => 'REX', + 'RZD' => 'RazoDroiD', + 'RXT' => 'RTOS & Next', + 'SAB' => 'Sabayon', + 'SSE' => 'SUSE', + 'SAF' => 'Sailfish OS', + 'SCI' => 'Scientific Linux', + 'SEE' => 'SeewoOS', + 'SER' => 'SerenityOS', + 'SIR' => 'Sirin OS', + 'SLW' => 'Slackware', + 'SOS' => 'Solaris', + 'SBL' => 'Star-Blade OS', + 'SYL' => 'Syllable', + 'SYM' => 'Symbian', + 'SYS' => 'Symbian OS', + 'S40' => 'Symbian OS Series 40', + 'S60' => 'Symbian OS Series 60', + 'SY3' => 'Symbian^3', + 'TEN' => 'TencentOS', + 'TDX' => 'ThreadX', + 'TIZ' => 'Tizen', + 'TIV' => 'TiVo OS', + 'TOS' => 'TmaxOS', + 'TUR' => 'Turbolinux', + 'UBT' => 'Ubuntu', + 'ULT' => 'ULTRIX', + 'UOS' => 'UOS', + 'VID' => 'VIDAA', + 'VIZ' => 'ViziOS', + 'WAS' => 'watchOS', + 'WER' => 'Wear OS', + 'WTV' => 'WebTV', + 'WHS' => 'Whale OS', + 'WIN' => 'Windows', + 'WCE' => 'Windows CE', + 'WIO' => 'Windows IoT', + 'WMO' => 'Windows Mobile', + 'WPH' => 'Windows Phone', + 'WRT' => 'Windows RT', + 'WPO' => 'WoPhone', + 'XBX' => 'Xbox', + 'XBT' => 'Xubuntu', + 'YNS' => 'YunOS', + 'ZEN' => 'Zenwalk', + 'ZOR' => 'ZorinOS', + 'IOS' => 'iOS', + 'POS' => 'palmOS', + 'WEB' => 'Webian', + 'WOS' => 'webOS', + ]; + + /** + * Operating system families mapped to the short codes of the associated operating systems + * + * @var array + */ + protected static $osFamilies = [ + 'Android' => [ + 'AND', 'CYN', 'FIR', 'REM', 'RZD', 'MLD', 'MCD', 'YNS', 'GRI', 'HAR', + 'ADR', 'CLR', 'BOS', 'REV', 'LEN', 'SIR', 'RRS', 'WER', 'PIC', 'ARM', + 'HEL', 'BYI', 'RIS', 'PUF', 'LEA', 'MET', + ], + 'AmigaOS' => ['AMG', 'MOR', 'ARO'], + 'BlackBerry' => ['BLB', 'QNX'], + 'Brew' => ['BMP'], + 'BeOS' => ['BEO', 'HAI'], + 'Chrome OS' => ['COS', 'CRS', 'FYD', 'SEE'], + 'Firefox OS' => ['FOS', 'KOS'], + 'Gaming Console' => ['WII', 'PS3'], + 'Google TV' => ['GTV'], + 'IBM' => ['OS2'], + 'iOS' => ['IOS', 'ATV', 'WAS', 'IPA'], + 'RISC OS' => ['ROS'], + 'GNU/Linux' => [ + 'LIN', 'ARL', 'DEB', 'KNO', 'MIN', 'UBT', 'KBT', 'XBT', 'LBT', 'FED', + 'RHT', 'VLN', 'MDR', 'GNT', 'SAB', 'SLW', 'SSE', 'CES', 'BTR', 'SAF', + 'ORD', 'TOS', 'RSO', 'DEE', 'FRE', 'MAG', 'FEN', 'CAI', 'PCL', 'HAS', + 'LOS', 'DVK', 'ROK', 'OWR', 'OTV', 'KTV', 'PUR', 'PLA', 'FUC', 'PAR', + 'FOR', 'MON', 'KAN', 'ZEN', 'LND', 'LNS', 'CHN', 'AMZ', 'TEN', 'CST', + 'NOV', 'ROU', 'ZOR', 'RED', 'KAL', 'ORA', 'VID', 'TIV', 'BSN', 'RAS', + 'UOS', 'PIO', 'FRI', 'LIR', 'WEB', 'SER', 'ASP', 'AOS', 'LOO', 'EUL', + 'SCI', 'ALP', 'CLO', 'ROC', 'OVZ', 'PVE', 'RST', 'EZX', 'GNS', 'JOL', + 'TUR', 'QTP', 'WPO', 'PAN', 'VIZ', 'AZU', 'COL', + ], + 'Mac' => ['MAC'], + 'Mobile Gaming Console' => ['PSP', 'NDS', 'XBX'], + 'OpenVMS' => ['OVS'], + 'Real-time OS' => ['MTK', 'TDX', 'MRE', 'JME', 'REX', 'RXT', 'KOL'], + 'Other Mobile' => ['WOS', 'POS', 'SBA', 'TIZ', 'SMG', 'MAE', 'LUN', 'GEO'], + 'Symbian' => ['SYM', 'SYS', 'SY3', 'S60', 'S40'], + 'Unix' => [ + 'SOS', 'AIX', 'HPX', 'BSD', 'NBS', 'OBS', 'DFB', 'SYL', 'IRI', 'T64', + 'INF', 'ELE', 'GNX', 'ULT', 'NWS', 'NXT', 'SBL', + ], + 'WebTV' => ['WTV'], + 'Windows' => ['WIN'], + 'Windows Mobile' => ['WPH', 'WMO', 'WCE', 'WRT', 'WIO', 'KIN'], + 'Other Smart TV' => ['WHS'], + ]; + + /** + * Contains a list of mappings from OS names we use to known client hint values + * + * @var array> + */ + protected static $clientHintMapping = [ + 'GNU/Linux' => ['Linux'], + 'Mac' => ['MacOS'], + ]; + + /** + * Operating system families that are known as desktop only + * + * @var array + */ + protected static $desktopOsArray = [ + 'AmigaOS', 'IBM', 'GNU/Linux', 'Mac', 'Unix', 'Windows', 'BeOS', 'Chrome OS', + ]; + + /** + * Fire OS version mapping + * + * @var array + */ + private $fireOsVersionMapping = [ + '11' => '8', + '10' => '8', + '9' => '7', + '7' => '6', + '5' => '5', + '4.4.3' => '4.5.1', + '4.4.2' => '4', + '4.2.2' => '3', + '4.0.3' => '3', + '4.0.2' => '3', + '4' => '2', + '2' => '1', + ]; + + /** + * Lineage OS version mapping + * + * @var array + */ + private $lineageOsVersionMapping = [ + '16' => '23', + '15' => '22', + '14' => '21', + '13' => '20.0', + '12.1' => '19.1', + '12' => '19.0', + '11' => '18.0', + '10' => '17.0', + '9' => '16.0', + '8.1.0' => '15.1', + '8.0.0' => '15.0', + '7.1.2' => '14.1', + '7.1.1' => '14.1', + '7.0' => '14.0', + '6.0.1' => '13.0', + '6.0' => '13.0', + '5.1.1' => '12.1', + '5.0.2' => '12.0', + '5.0' => '12.0', + '4.4.4' => '11.0', + '4.3' => '10.2', + '4.2.2' => '10.1', + '4.0.4' => '9.1.0', + ]; + + /** + * Returns all available operating systems + * + * @return array + */ + public static function getAvailableOperatingSystems(): array + { + return self::$operatingSystems; + } + + /** + * Returns all available operating system families + * + * @return array + */ + public static function getAvailableOperatingSystemFamilies(): array + { + return self::$osFamilies; + } + + /** + * Returns the os name and shot name + * + * @param string $name + * + * @return array + */ + public static function getShortOsData(string $name): array + { + $short = 'UNK'; + + foreach (self::$operatingSystems as $osShort => $osName) { + if (\strtolower($name) !== \strtolower($osName)) { + continue; + } + + $name = $osName; + $short = $osShort; + + break; + } + + return \compact('short', 'name'); + } + + /** + * @inheritdoc + */ + public function parse(): ?array + { + $this->restoreUserAgentFromClientHints(); + + $osFromClientHints = $this->parseOsFromClientHints(); + $osFromUserAgent = $this->parseOsFromUserAgent(); + + if (!empty($osFromClientHints['name'])) { + $name = $osFromClientHints['name']; + $version = $osFromClientHints['version']; + + // use version from user agent if non was provided in client hints, but os family from useragent matches + if (empty($version) + && self::getOsFamily($name) === self::getOsFamily($osFromUserAgent['name']) + ) { + $version = $osFromUserAgent['version']; + } + + // On Windows, version 0.0.0 can be either 7, 8 or 8.1 + if ('Windows' === $name && '0.0.0' === $version) { + $version = ('10' === $osFromUserAgent['version']) ? '' : $osFromUserAgent['version']; + } + + // If the OS name detected from client hints matches the OS family from user agent + // but the os name is another, we use the one from user agent, as it might be more detailed + if (self::getOsFamily($osFromUserAgent['name']) === $name && $osFromUserAgent['name'] !== $name) { + $name = $osFromUserAgent['name']; + + if ('LeafOS' === $name || 'HarmonyOS' === $name) { + $version = ''; + } + + if ('PICO OS' === $name) { + $version = $osFromUserAgent['version']; + } + + if ('Fire OS' === $name && !empty($osFromClientHints['version'])) { + $majorVersion = (int) (\explode('.', $version, 1)[0] ?? '0'); + + $version = $this->fireOsVersionMapping[$version] + ?? $this->fireOsVersionMapping[$majorVersion] ?? ''; + } + } + + $short = $osFromClientHints['short_name']; + + // Chrome OS is in some cases reported as Linux in client hints, we fix this only if the version matches + if ('GNU/Linux' === $name + && 'Chrome OS' === $osFromUserAgent['name'] + && $osFromClientHints['version'] === $osFromUserAgent['version'] + ) { + $name = $osFromUserAgent['name']; + $short = $osFromUserAgent['short_name']; + } + + // Chrome OS is in some cases reported as Android in client hints + if ('Android' === $name && 'Chrome OS' === $osFromUserAgent['name']) { + $name = $osFromUserAgent['name']; + $version = ''; + $short = $osFromUserAgent['short_name']; + } + + // Meta Horizon is reported as Linux in client hints + if ('GNU/Linux' === $name && 'Meta Horizon' === $osFromUserAgent['name']) { + $name = $osFromUserAgent['name']; + $short = $osFromUserAgent['short_name']; + } + } elseif (!empty($osFromUserAgent['name'])) { + $name = $osFromUserAgent['name']; + $version = $osFromUserAgent['version']; + $short = $osFromUserAgent['short_name']; + } else { + return []; + } + + $platform = $this->parsePlatform(); + $family = self::getOsFamily($short); + $androidApps = [ + 'com.hisense.odinbrowser', 'com.seraphic.openinet.pre', 'com.appssppa.idesktoppcbrowser', + 'every.browser.inc', + ]; + + if (null !== $this->clientHints) { + if (\in_array($this->clientHints->getApp(), $androidApps) && 'Android' !== $name) { + $name = 'Android'; + $family = 'Android'; + $short = 'ADR'; + $version = ''; + } + + if ('org.lineageos.jelly' === $this->clientHints->getApp() && 'Lineage OS' !== $name) { + $majorVersion = (int) (\explode('.', $version, 1)[0] ?? '0'); + + $name = 'Lineage OS'; + $family = 'Android'; + $short = 'LEN'; + $version = $this->lineageOsVersionMapping[$version] + ?? $this->lineageOsVersionMapping[$majorVersion] ?? ''; + } + + if ('org.mozilla.tv.firefox' === $this->clientHints->getApp() && 'Fire OS' !== $name) { + $majorVersion = (int) (\explode('.', $version, 1)[0] ?? '0'); + + $name = 'Fire OS'; + $family = 'Android'; + $short = 'FIR'; + $version = $this->fireOsVersionMapping[$version] ?? $this->fireOsVersionMapping[$majorVersion] ?? ''; + } + } + + $return = [ + 'name' => $name, + 'short_name' => $short, + 'version' => $version, + 'platform' => $platform, + 'family' => $family, + ]; + + if (\in_array($return['name'], self::$operatingSystems)) { + $return['short_name'] = \array_search($return['name'], self::$operatingSystems); + } + + return $return; + } + + /** + * Returns the operating system family for the given operating system + * + * @param string $osLabel name or short name + * + * @return string|null If null, "Unknown" + */ + public static function getOsFamily(string $osLabel): ?string + { + if (\in_array($osLabel, self::$operatingSystems)) { + $osLabel = \array_search($osLabel, self::$operatingSystems); + } + + foreach (self::$osFamilies as $family => $labels) { + if (\in_array($osLabel, $labels)) { + return (string) $family; + } + } + + return null; + } + + /** + * Returns true if OS is desktop + * + * @param string $osName OS short name + * + * @return bool + */ + public static function isDesktopOs(string $osName): bool + { + $osFamily = self::getOsFamily($osName); + + return \in_array($osFamily, self::$desktopOsArray); + } + + /** + * Returns the full name for the given short name + * + * @param string $os + * @param string|null $ver + * + * @return ?string + */ + public static function getNameFromId(string $os, ?string $ver = null): ?string + { + if (\array_key_exists($os, self::$operatingSystems)) { + $osFullName = self::$operatingSystems[$os]; + + return \trim($osFullName . ' ' . $ver); + } + + return null; + } + + /** + * Returns the OS that can be safely detected from client hints + * + * @return array + */ + protected function parseOsFromClientHints(): array + { + $name = $version = $short = ''; + + if ($this->clientHints instanceof ClientHints && $this->clientHints->getOperatingSystem()) { + $hintName = $this->applyClientHintMapping($this->clientHints->getOperatingSystem()); + + foreach (self::$operatingSystems as $osShort => $osName) { + if ($this->fuzzyCompare($hintName, $osName)) { + $name = $osName; + $short = $osShort; + + break; + } + } + + $version = $this->clientHints->getOperatingSystemVersion(); + + if ('Windows' === $name) { + $majorVersion = (int) (\explode('.', $version, 1)[0] ?? '0'); + $minorVersion = (int) (\explode('.', $version, 2)[1] ?? '0'); + + if (0 === $majorVersion) { + $minorVersionMapping = [1 => '7', 2 => '8', 3 => '8.1']; + $version = $minorVersionMapping[$minorVersion] ?? $version; + } elseif ($majorVersion > 0 && $majorVersion < 11) { + $version = '10'; + } elseif ($majorVersion > 10) { + $version = '11'; + } + } + + // On Windows, version 0.0.0 can be either 7, 8 or 8.1, so we return 0.0.0 + if ('Windows' !== $name && '0.0.0' !== $version && 0 === (int) $version) { + $version = ''; + } + } + + return [ + 'name' => $name, + 'short_name' => $short, + 'version' => $this->buildVersion($version, []), + ]; + } + + /** + * Returns the OS that can be detected from useragent + * + * @return array + * + * @throws \Exception + */ + protected function parseOsFromUserAgent(): array + { + $osRegex = $matches = []; + $name = $version = $short = ''; + + foreach ($this->getRegexes() as $osRegex) { + $matches = $this->matchUserAgent($osRegex['regex']); + + if ($matches) { + break; + } + } + + if (!empty($matches)) { + $name = $this->buildByMatch($osRegex['name'], $matches); + ['name' => $name, 'short' => $short] = self::getShortOsData($name); + + $version = \array_key_exists('version', $osRegex) + ? $this->buildVersion((string) $osRegex['version'], $matches) + : ''; + + foreach ($osRegex['versions'] ?? [] as $regex) { + $matches = $this->matchUserAgent($regex['regex']); + + if (!$matches) { + continue; + } + + if (\array_key_exists('name', $regex)) { + $name = $this->buildByMatch($regex['name'], $matches); + ['name' => $name, 'short' => $short] = self::getShortOsData($name); + } + + if (\array_key_exists('version', $regex)) { + $version = $this->buildVersion((string) $regex['version'], $matches); + } + + break; + } + } + + return [ + 'name' => $name, + 'short_name' => $short, + 'version' => $version, + ]; + } + + /** + * Parse current UserAgent string for the operating system platform + * + * @return string + */ + protected function parsePlatform(): string + { + // Use architecture from client hints if available + if ($this->clientHints instanceof ClientHints && $this->clientHints->getArchitecture()) { + $arch = \strtolower($this->clientHints->getArchitecture()); + + if (false !== \strpos($arch, 'arm')) { + return 'ARM'; + } + + if (false !== \strpos($arch, 'loongarch64')) { + return 'LoongArch64'; + } + + if (false !== \strpos($arch, 'mips')) { + return 'MIPS'; + } + + if (false !== \strpos($arch, 'sh4')) { + return 'SuperH'; + } + + if (false !== \strpos($arch, 'sparc64')) { + return 'SPARC64'; + } + + if (false !== \strpos($arch, 'x64') + || (false !== \strpos($arch, 'x86') && '64' === $this->clientHints->getBitness()) + ) { + return 'x64'; + } + + if (false !== \strpos($arch, 'x86')) { + return 'x86'; + } + } + + if ($this->matchUserAgent('arm[ _;)ev]|.*arm$|.*arm64|aarch64|Apple ?TV|Watch ?OS|Watch1,[12]')) { + return 'ARM'; + } + + if ($this->matchUserAgent('loongarch64')) { + return 'LoongArch64'; + } + + if ($this->matchUserAgent('mips')) { + return 'MIPS'; + } + + if ($this->matchUserAgent('sh4')) { + return 'SuperH'; + } + + if ($this->matchUserAgent('sparc64')) { + return 'SPARC64'; + } + + if ($this->matchUserAgent('64-?bit|WOW64|(?:Intel)?x64|WINDOWS_64|win64|.*amd64|.*x86_?64')) { + return 'x64'; + } + + if ($this->matchUserAgent('.*32bit|.*win32|(?:i[0-9]|x)86|i86pc')) { + return 'x86'; + } + + return ''; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/VendorFragment.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/VendorFragment.php new file mode 100644 index 0000000..faf0d7a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Parser/VendorFragment.php @@ -0,0 +1,62 @@ +getRegexes() as $brand => $regexes) { + foreach ($regexes as $regex) { + if ($this->matchUserAgent($regex . '[^a-z0-9]+')) { + $this->matchedRegex = $regex; + + return ['brand' => $brand]; + } + } + } + + return null; + } + + /** + * @return string|null + */ + public function getMatchedRegex(): ?string + { + return $this->matchedRegex; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/README.md b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/README.md new file mode 100644 index 0000000..f5ae1e8 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/README.md @@ -0,0 +1,320 @@ +DeviceDetector +============== + +[![Latest Stable Version](https://poser.pugx.org/matomo/device-detector/v/stable)](https://packagist.org/packages/matomo/device-detector) +[![Total Downloads](https://poser.pugx.org/matomo/device-detector/downloads)](https://packagist.org/packages/matomo/device-detector) +[![License](https://poser.pugx.org/matomo/device-detector/license)](https://packagist.org/packages/matomo/device-detector) + +## Code Status + +[![PHPUnit](https://github.com/matomo-org/device-detector/actions/workflows/phpunit.yml/badge.svg?branch=master)](https://github.com/matomo-org/device-detector/actions/workflows/phpunit.yml?branch=master "PHPUnit") +[![PHPStan](https://github.com/matomo-org/device-detector/actions/workflows/phpstan.yml/badge.svg?branch=master)](https://github.com/matomo-org/device-detector/actions/workflows/phpstan.yml?branch=master "PHPStan") +[![PHPCS](https://github.com/matomo-org/device-detector/actions/workflows/phpcs.yml/badge.svg?branch=master)](https://github.com/matomo-org/device-detector/actions/workflows/phpcs.yml?branch=master "PHPCS") +[![YAML Lint](https://github.com/matomo-org/device-detector/actions/workflows/yamllint.yml/badge.svg?branch=master)](https://github.com/matomo-org/device-detector/actions/workflows/yamllint.yml?branch=master "YAML Lint") +[![Validate regular Expressions](https://github.com/matomo-org/device-detector/actions/workflows/regular_expressions.yml/badge.svg?branch=master)](https://github.com/matomo-org/device-detector/actions/workflows/regular_expressions.yml?branch=master "Validate regular Expressions") + +[![Average time to resolve an issue](https://www.isitmaintained.com/badge/resolution/matomo-org/device-detector.svg)](https://www.isitmaintained.com/project/matomo-org/device-detector "Average time to resolve an issue") +[![Percentage of issues still open](https://www.isitmaintained.com/badge/open/matomo-org/device-detector.svg)](https://www.isitmaintained.com/project/matomo-org/device-detector "Percentage of issues still open") + +## Description + +The Universal Device Detection library that parses User Agents and Browser Client Hints to detect devices (desktop, tablet, mobile, tv, cars, console, etc.), clients (browsers, feed readers, media players, PIMs, ...), operating systems, brands and models. + +## Usage + +Using DeviceDetector with composer is quite easy. Just add `matomo/device-detector` to your projects requirements. + +``` +composer require matomo/device-detector +``` + +And use some code like this one: + + +```php +require_once 'vendor/autoload.php'; + +use DeviceDetector\ClientHints; +use DeviceDetector\DeviceDetector; +use DeviceDetector\Parser\Device\AbstractDeviceParser; + +// OPTIONAL: Set version truncation to none, so full versions will be returned +// By default only minor versions will be returned (e.g. X.Y) +// for other options see VERSION_TRUNCATION_* constants in DeviceParserAbstract class +AbstractDeviceParser::setVersionTruncation(AbstractDeviceParser::VERSION_TRUNCATION_NONE); + +$userAgent = $_SERVER['HTTP_USER_AGENT']; // change this to the useragent you want to parse + +// Client Hints are optional +// If you want to use them your server must announce that it supports client hints, using the Accept-CH header to specify the hints that it is interested in receiving. +// See e.g. https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints +$clientHints = ClientHints::factory($_SERVER); + +$dd = new DeviceDetector($userAgent, $clientHints); + +// OPTIONAL: Set caching method +// By default static cache is used, which works best within one php process (memory array caching) +// To cache across requests use caching in files or memcache +// $dd->setCache(new Doctrine\Common\Cache\PhpFileCache('./tmp/')); + +// OPTIONAL: Set custom yaml parser +// By default Spyc will be used for parsing yaml files. You can also use another yaml parser. +// You may need to implement the Yaml Parser facade if you want to use another parser than Spyc or [Symfony](https://github.com/symfony/yaml) +// $dd->setYamlParser(new DeviceDetector\Yaml\Symfony()); + +// OPTIONAL: If called, getBot() will only return true if a bot was detected (speeds up detection a bit) +// $dd->discardBotInformation(); + +// OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) +// $dd->skipBotDetection(); + +$dd->parse(); + +if ($dd->isBot()) { + // handle bots,spiders,crawlers,... + $botInfo = $dd->getBot(); +} else { + $clientInfo = $dd->getClient(); // holds information about browser, feed reader, media player, ... + $osInfo = $dd->getOs(); + $device = $dd->getDeviceName(); + $brand = $dd->getBrandName(); + $model = $dd->getModel(); +} +``` +Methods check device type: +```php +$dd->isSmartphone(); +$dd->isFeaturePhone(); +$dd->isTablet(); +$dd->isPhablet(); +$dd->isConsole(); +$dd->isPortableMediaPlayer(); +$dd->isCarBrowser(); +$dd->isTV(); +$dd->isSmartDisplay(); +$dd->isSmartSpeaker(); +$dd->isCamera(); +$dd->isWearable(); +$dd->isPeripheral(); +``` +Methods check client type: +```php +$dd->isBrowser(); +$dd->isFeedReader(); +$dd->isMobileApp(); +$dd->isPIM(); +$dd->isLibrary(); +$dd->isMediaPlayer(); +``` +Get OS family: +```php +use DeviceDetector\Parser\OperatingSystem; + +$osFamily = OperatingSystem::getOsFamily($dd->getOs('name')); +``` +Get browser family: +```php +use DeviceDetector\Parser\Client\Browser; + +$browserFamily = Browser::getBrowserFamily($dd->getClient('name')); +``` + +Instead of using the full power of DeviceDetector it might in some cases be better to use only specific parsers. +If you aim to check if a given useragent is a bot and don't require any of the other information, you can directly use the bot parser. + +```php +require_once 'vendor/autoload.php'; + +use DeviceDetector\Parser\Bot AS BotParser; + +$botParser = new BotParser(); +$botParser->setUserAgent($userAgent); + +// OPTIONAL: discard bot information. parse() will then return true instead of information +$botParser->discardDetails(); + +$result = $botParser->parse(); + +if (!is_null($result)) { + // do not do anything if a bot is detected + return; +} + +// handle non-bot requests + +``` + +## Using without composer + +Alternatively to using composer you can also use the included `autoload.php`. +This script will register an autoloader to dynamically load all classes in `DeviceDetector` namespace. + +Device Detector requires a YAML parser. By default `Spyc` parser is used. +As this library is not included you need to include it manually or use another YAML parser. + +```php +setCache( + new \DeviceDetector\Cache\PSR6Bridge($cache) +); + +// Example with PSR-16 and ScrapBook +$cache = new \MatthiasMullie\Scrapbook\Psr16\SimpleCache( + new \MatthiasMullie\Scrapbook\Adapters\Apc() +); +$dd->setCache( + new \DeviceDetector\Cache\PSR16Bridge($cache) +); + +// Example with Doctrine +$cache = new \Doctrine\Common\Cache\ApcuCache(); +$dd->setCache( + new \DeviceDetector\Cache\DoctrineBridge($cache) +); + +// Example with Laravel +$dd->setCache( + new \DeviceDetector\Cache\LaravelCache() +); +``` + +## Contributing + +### Hacking the library + +This is a free/libre library under license LGPL v3 or later. + +Your pull requests and/or feedback is very welcome! + +### Listing all user agents from your logs +Sometimes it may be useful to generate the list of most used user agents on your website, +extracting this list from your access logs using the following command: + +``` +zcat ~/path/to/access/logs* | awk -F'"' '{print $6}' | sort | uniq -c | sort -rn | head -n20000 > /home/matomo/top-user-agents.txt +``` + +### Contributors +Created by the [Matomo team](https://matomo.org/team/), Stefan Giehl, Matthieu Aubry, Michał Gaździk, +Tomasz Majczak, Grzegorz Kaszuba, Piotr Banaszczyk and contributors. + +Together we can build the best Device Detection library. + +We are looking forward to your contributions and pull requests! + +## Tests + +See also: [QA at Matomo](https://developer.matomo.org/guides/tests) + +### Running tests + +``` +cd /path/to/device-detector +curl -sS https://getcomposer.org/installer | php +php composer.phar install +./vendor/bin/phpunit +``` + +## Device Detector for other languages + +There are already a few ports of this tool to other languages: + +- **.NET** https://github.com/totpero/DeviceDetector.NET +- **Ruby** https://github.com/podigee/device_detector +- **JavaScript/TypeScript/NodeJS** https://github.com/etienne-martin/device-detector-js +- **NodeJS** https://github.com/sanchezzzhak/node-device-detector +- **Python 3** https://github.com/thinkwelltwd/device_detector +- **Crystal** https://github.com/creadone/device_detector +- **Elixir** https://github.com/elixir-inspector/ua_inspector +- **Java** https://github.com/deevvicom/device-detector +- **Java** https://github.com/PaniniGelato/java-device-detector +- **Rust** https://github.com/simplecastapps/rust-device-detector +- **Rust** https://github.com/stry-rs/device-detector +- **Go** https://github.com/gamebtc/devicedetector +- **Go** https://github.com/umutbasal/device-detector-go +- **Go** https://github.com/robicode/device-detector + +## Icon packs + +If you are looking for icons to use alongside Device Detector, these repositories can be of use: +- Official [Matomo](https://github.com/matomo-org/matomo-icons/) pack +- Unofficial [Simbiat](https://github.com/Simbiat/DeviceDetectorIcons) pack + +## What Device Detector is able to detect + +The lists below are auto generated and updated from time to time. Some of them might not be complete. + +*Last update: 2025/08/17* + +### List of detected operating systems: + +AIX, Android, Android TV, Alpine Linux, Amazon Linux, AmigaOS, Armadillo OS, AROS, tvOS, Arch Linux, AOSC OS, ASPLinux, Azure Linux, BackTrack, Bada, Baidu Yi, BeOS, BlackBerry OS, BlackBerry Tablet OS, blackPanther OS, Bliss OS, Brew, BrightSignOS, Caixa Mágica, CentOS, CentOS Stream, Clear Linux OS, ClearOS Mobile, Chrome OS, Chromium OS, China OS, Coolita OS, CyanogenMod, Debian, Deepin, DragonFly, DVKBuntu, ElectroBSD, EulerOS, Fedora, Fenix, Firefox OS, Fire OS, Foresight Linux, Freebox, FreeBSD, FRITZ!OS, FydeOS, Fuchsia, Gentoo, GENIX, GEOS, gNewSense, GridOS, Google TV, HP-UX, Haiku OS, iPadOS, HarmonyOS, HasCodingOS, HELIX OS, IRIX, Inferno, Java ME, Joli OS, KaiOS, Kali, Kanotix, KIN OS, KolibriOS, Knoppix, KreaTV, Kubuntu, GNU/Linux, LeafOS, LindowsOS, Linspire, Lineage OS, Liri OS, Loongnix, Lubuntu, Lumin OS, LuneOS, VectorLinux, Mac, Maemo, Mageia, Mandriva, MeeGo, Meta Horizon, MocorDroid, moonOS, Motorola EZX, Mint, MildWild, MorphOS, NetBSD, MTK / Nucleus, MRE, NeXTSTEP, NEWS-OS, Nintendo, Nintendo Mobile, Nova, OS/2, OSF1, OpenBSD, OpenVMS, OpenVZ, OpenWrt, Opera TV, Oracle Linux, Ordissimo, Pardus, PCLinuxOS, PICO OS, Plasma Mobile, PlayStation Portable, PlayStation, Proxmox VE, Puffin OS, PureOS, Qtopia, Raspberry Pi OS, Raspbian, Red Hat, Red Star, RedOS, Revenge OS, risingOS, RISC OS, Rocky Linux, Roku OS, Rosa, RouterOS, Remix OS, Resurrection Remix OS, REX, RazoDroiD, RTOS & Next, Sabayon, SUSE, Sailfish OS, Scientific Linux, SeewoOS, SerenityOS, Sirin OS, Slackware, Solaris, Star-Blade OS, Syllable, Symbian, Symbian OS, Symbian OS Series 40, Symbian OS Series 60, Symbian^3, TencentOS, ThreadX, Tizen, TiVo OS, TmaxOS, Turbolinux, Ubuntu, ULTRIX, UOS, VIDAA, ViziOS, watchOS, Wear OS, WebTV, Whale OS, Windows, Windows CE, Windows IoT, Windows Mobile, Windows Phone, Windows RT, WoPhone, Xbox, Xubuntu, YunOS, Zenwalk, ZorinOS, iOS, palmOS, Webian, webOS + +### List of detected browsers: + +Via, Pure Mini Browser, Pure Lite Browser, Raise Fast Browser, Rabbit Private Browser, Fast Browser UC Lite, Fast Explorer, Lightning Browser, Cake Browser, IE Browser Fast, Vegas Browser, OH Browser, OH Private Browser, XBrowser Mini, Sharkee Browser, Lark Browser, Pluma, Anka Browser, Azka Browser, Dragon Browser, Easy Browser, Dark Web Browser, Dark Browser, 18+ Privacy Browser, 115 Browser, 1DM Browser, 1DM+ Browser, 2345 Browser, 360 Secure Browser, 360 Phone Browser, 7654 Browser, Avant Browser, ABrowse, Acoo Browser, AdBlock Browser, Adult Browser, Ai Browser, Airfind Secure Browser, ANT Fresco, ANTGalio, Aloha Browser, Aloha Browser Lite, ALVA, AltiBrowser, Amaya, Amaze Browser, Amerigo, Amigo, Android Browser, AOL Explorer, AOL Desktop, AOL Shield, AOL Shield Pro, Aplix, AppBrowzer, AppTec Secure Browser, APUS Browser, Arora, Arctic Fox, Amiga Voyager, Amiga Aweb, APN Browser, Arachne, Arc Search, Armorfly Browser, Arvin, Ask.com, Asus Browser, Atom, Atomic Web Browser, Atlas, Avast Secure Browser, AVG Secure Browser, Avira Secure Browser, AwoX, Awesomium, Basic Web Browser, Beaker Browser, Beamrise, BF Browser, BlackBerry Browser, Bluefy, BrowseHere, Browser Hup Pro, Baidu Browser, Baidu Spark, Bang, Bangla Browser, Basilisk, Belva Browser, Beyond Private Browser, Beonex, Berry Browser, Bitchute Browser, BizBrowser, BlackHawk, Bloket, Bunjalloo, B-Line, Black Lion Browser, Blue Browser, Bonsai, Borealis Navigator, Brave, BriskBard, BroKeep Browser, Browspeed Browser, BrowseX, Browzar, Browlser, Browser Mini, BrowsBit, Biyubi, Byffox, BXE Browser, Camino, Catalyst, Catsxp, Cave Browser, CCleaner, CG Browser, ChanjetCloud, Chedot, Cherry Browser, Centaury, Cliqz, Coc Coc, CoolBrowser, Colibri, Columbus Browser, Comodo Dragon, Coast, Charon, CM Browser, CM Mini, Chrome Frame, Headless Chrome, Chrome, Chrome Mobile iOS, Conkeror, Chrome Mobile, Chowbo, Classilla, CoolNovo, Colom Browser, CometBird, Comfort Browser, COS Browser, Cornowser, Chim Lac, ChromePlus, Chromium, Chromium GOST, Cyberfox, Cheshire, Cromite, Crow Browser, Crusta, Craving Explorer, Crazy Browser, Cunaguaro, Chrome Webview, CyBrowser, dbrowser, Peeps dBrowser, Dark Web, Dark Web Private, Debuggable Browser, Decentr, Deepnet Explorer, deg-degan, Deledao, Delta Browser, Desi Browser, DeskBrowse, Dezor, Diigo Browser, DoCoMo, Dolphin, Dolphin Zero, Dorado, Dot Browser, Dooble, Dillo, DUC Browser, DuckDuckGo Privacy Browser, East Browser, Ecosia, Edge WebView, Every Browser, Epic, Elinks, EinkBro, Element Browser, Elements Browser, Eolie, Explore Browser, eZ Browser, EudoraWeb, EUI Browser, GNOME Web, G Browser, Espial TV Browser, fGet, Falkon, Faux Browser, Fire Browser, Fiery Browser, Firefox Mobile iOS, Firebird, Fluid, Fennec, Firefox, Firefox Focus, Firefox Reality, Firefox Rocket, Firefox Klar, Float Browser, Flock, Floorp, Flow, Flow Browser, Firefox Mobile, Fireweb, Fireweb Navigator, Flash Browser, Flast, Flyperlink, FOSS Browser, FreeU, Freedom Browser, Frost, Frost+, Fulldive, Galeon, Gener8, Ghostery Privacy Browser, GinxDroid Browser, Glass Browser, Godzilla Browser, Good Browser, Google Earth, Google Earth Pro, GOG Galaxy, GoBrowser, GoKu, GO Browser, GreenBrowser, Habit Browser, Halo Browser, Harman Browser, HasBrowser, Hawk Turbo Browser, Hawk Quick Browser, Helio, Herond Browser, Hexa Web Browser, Hi Browser, hola! Browser, Holla Web Browser, HotBrowser, HotJava, HONOR Browser, HTC Browser, Huawei Browser Mobile, Huawei Browser, HUB Browser, iBrowser, iBrowser Mini, IBrowse, iDesktop PC Browser, iCab, iCab Mobile, iNet Browser, Iridium, Iron Mobile, IceCat, IceDragon, Isivioo, IVVI Browser, Iceweasel, Impervious Browser, Incognito Browser, Inspect Browser, Insta Browser, Internet Explorer, Internet Browser Secure, Internet Webbrowser, Intune Managed Browser, Indian UC Mini Browser, InBrowser, Involta Go, IE Mobile, Iron, Japan Browser, Jasmine, JavaFX, Jelly, Jig Browser, Jig Browser Plus, JioSphere, JUZI Browser, K.Browser, Keepsafe Browser, KeepSolid Browser, Kids Safe Browser, Kindle Browser, K-meleon, K-Ninja, Konqueror, Kapiko, Keyboard Browser, Kinza, Kitt, Kiwi, Kode Browser, KUN, KUTO Mini Browser, Kylo, Kazehakase, Cheetah Browser, Ladybird, Lagatos Browser, Legan Browser, Lexi Browser, Lenovo Browser, LieBaoFast, LG Browser, Light, Lightning Browser Plus, Lilo, Links, Liri Browser, LogicUI TV Browser, Lolifox, Lotus, Lovense Browser, LT Browser, LuaKit, LUJO TV Browser, Lulumi, Lunascape, Lunascape Lite, Lynx, Lynket Browser, Mandarin, Maple, MarsLab Web Browser, MaxBrowser, mCent, MicroB, NCSA Mosaic, Meizu Browser, Mercury, Me Browser, Mobile Safari, Midori, Midori Lite, MixerBox AI, Mobicip, Mi Browser, Mobile Silk, Mogok Browser, Motorola Internet Browser, Minimo, Mint Browser, Maxthon, MaxTube Browser, Maelstrom, Mises, Mmx Browser, MxNitro, Mypal, Monument Browser, MAUI WAP Browser, Naenara Browser, Navigateur Web, Naked Browser, Naked Browser Pro, NFS Browser, Ninetails, Nokia Browser, Nokia OSS Browser, Nokia Ovi Browser, Norton Private Browser, Nox Browser, NOMone VR Browser, NOOK Browser, NetSurf, NetFront, NetFront Life, NetPositive, Netscape, NextWord Browser, Ninesky, NTENT Browser, Nuanti Meta, Nuviu, Ocean Browser, Oculus Browser, Odd Browser, Opera Mini iOS, Obigo, Odin, Odin Browser, OceanHero, Odyssey Web Browser, Off By One, Office Browser, OhHai Browser, OnBrowser Lite, ONE Browser, Onion Browser, ONIONBrowser, Opera Crypto, Opera GX, Opera Neon, Opera Devices, Opera Mini, Opera Mobile, Opera, Opera Next, Opera Touch, Orbitum, Orca, Ordissimo, Oregano, Origin In-Game Overlay, Origyn Web Browser, OrNET Browser, Openwave Mobile Browser, OpenFin, Open Browser, Open Browser 4U, Open Browser fast 5G, Open Browser Lite, Open TV Browser, OmniWeb, Otter Browser, Owl Browser, OJR Browser, Palm Blazer, Pale Moon, Polypane, Prism, Oppo Browser, Opus Browser, Palm Pre, Pocket Internet Explorer, Puffin Cloud Browser, Puffin Incognito Browser, Puffin Secure Browser, Puffin Web Browser, Palm WebPro, Palmscape, Pawxy, Peach Browser, Perfect Browser, Perk, Phantom.me, Phantom Browser, Phoenix, Phoenix Browser, Photon, Pintar Browser, PirateBrowser, PICO Browser, PlayFree Browser, PocketBook Browser, Polaris, Polarity, PolyBrowser, Presearch, Privacy Browser, PrivacyWall, Privacy Explorer Fast Safe, Privacy Pioneer Browser, Private Internet Browser, Proxy Browser, Proxyium, Proxynet, ProxyFox, ProxyMax, Pi Browser, PronHub Browser, PSI Secure Browser, Reqwireless WebViewer, Roccat, Microsoft Edge, Qazweb, Qiyu, QJY TV Browser, Qmamu, Quick Search TV, QQ Browser Lite, QQ Browser Mini, QQ Browser, Quick Browser, Qutebrowser, Quark, QuarkPC, Quetta, QupZilla, Qwant Mobile, QtWeb, QtWebEngine, Rakuten Browser, Rakuten Web Search, Raspbian Chromium, RCA Tor Explorer, Realme Browser, Rekonq, RockMelt, Roku Browser, Samsung Browser, Samsung Browser Lite, Sailfish Browser, SberBrowser, Seewo Browser, SEMC-Browser, Sogou Explorer, Sogou Mobile Browser, SOTI Surf, Soul Browser, Soundy Browser, Safari, Safari Technology Preview, Safe Exam Browser, SalamWeb, Savannah Browser, SavySoda, Secure Browser, SFive, Shiira, Sidekick, SimpleBrowser, SilverMob US, Singlebox, Sizzy, Skye, Skyfire, SkyLeap, Seraphic Sraf, SiteKiosk, Sleipnir, SlimBoat, Slimjet, SP Browser, Sony Small Browser, Secure Private Browser, SecureX, Stampy Browser, 7Star, Smart Browser, Smart Search & Web Browser, Smart Lenovo Browser, Smooz, Snowshoe, Spark, Spectre Browser, Splash, Sputnik Browser, Sunrise, Sunflower Browser, SuperBird, Super Fast Browser, SuperFast Browser, Sushi Browser, surf, Surf Browser, Surfy Browser, Stargon, START Internet Browser, Stealth Browser, Steam In-Game Overlay, Streamy, Swiftfox, Swiftweasel, Seznam Browser, Sweet Browser, SX Browser, T+Browser, T-Browser, t-online.de Browser, TalkTo, Tao Browser, tararia, Thor, Tor Browser, TenFourFox, Tenta Browser, Tesla Browser, Tizen Browser, Tint Browser, TrueLocation Browser, TUC Mini Browser, TUSK, Tungsten, ToGate, Total Browser, TQ Browser, TweakStyle, TV Bro, TV-Browser Internet, U Browser, UBrowser, UC Browser, UC Browser HD, UC Browser Mini, UC Browser Turbo, Ui Browser Mini, UPhone Browser, UR Browser, Uzbl, Ume Browser, vBrowser, Vast Browser, VD Browser, Veera, Venus Browser, Vewd Browser, VibeMate, Nova Video Downloader Pro, Viasat Browser, Vivaldi, vivo Browser, Vivid Browser Mini, Vision Mobile Browser, Vertex Surf, VMware AirWatch, VMS Mosaic, Vonkeror, Vuhuv, Wear Internet Browser, Web Explorer, Web Browser & Explorer, Webian Shell, WebDiscover, WebPositive, Weltweitimnetz Browser, Wexond, Waterfox, Wave Browser, Wavebox, Whale Browser, Whale TV Browser, wOSBrowser, w3m, WeTab Browser, World Browser, Wolvic, Wukong Browser, Wyzo, YAGI, Yahoo! Japan Browser, Yandex Browser, Yandex Browser Corp, Yandex Browser Lite, Yaani Browser, Yo Browser, Yolo Browser, YouCare, YouBrowser, Yuzu Browser, xBrowser, MMBOX XBrowser, X Browser Lite, X-VPN, xBrowser Pro Super Fast, XNX Browser, XtremeCast, xStand, Xiino, XnBrowse, Xooloo Internet, Xvast, Zetakey, Zvu, Zirco Browser, Zordo Browser, ZTE Browser + +### List of detected browser engines: + +WebKit, Blink, Trident, Text-based, Dillo, iCab, Elektra, Presto, Clecko, Gecko, KHTML, NetFront, Edge, NetSurf, Servo, Goanna, EkiohFlow, Arachne, LibWeb, Maple + +### List of detected libraries: + +aiohttp, Akka HTTP, Android License Verification Library, AnyEvent HTTP, Apache HTTP Client, Apidog, Aria2, Artifactory, Axios, Azure Blob Storage, Azure Data Factory, Babashka HTTP Client, Boto3, Buildah, BuildKit, Bun, C++ REST SDK, CakePHP, CarrierWave, Containerd, containers, cPanel HTTP Client, cpp-httplib, cri-o, curl, Cygwin, Cypress, Dart, Deno, docker, Down, Electron Fetch, Emacs, Embarcadero URI Client, ESP32 HTTP Client, Faraday, fasthttp, ffmpeg, FFUF, FileDownloader, Free Download Manager, GeoIP Update, git-annex, go-container registry, Go-http-client, go-network, Google HTTP Java Client, got, GRequests, gRPC-Java, GStreamer, Guzzle (PHP HTTP Client), gvfs, hackney, Harbor registry client, Helm, HTML Parser, http.rb, HTTP:Tiny, HTTPie, httplib2, httprs, HTTPX, HTTP_Request2, ICAP Client, Insomnia REST Client, iOS Application, IPinfo, Jakarta Commons HttpClient, Jaunt, Java, Java HTTP Client, jsdom, KaiOS Downloader, Kiwi TCMS, Kiwi TCMS API, libdnf, LibHTTP, libpod, libsoup, Libsyn, LUA OpenResty NGINX, Mandrill PHP, MatomoTracker, Mechanize, Mikrotik Fetch, Msray-Plus, Node Fetch, OKDownload Library, OkHttp, Open Build Service, Pa11y, Perl, Perl REST::Client, PhantomJS, PHP, PHP cURL Class, Podgrab, Postman Desktop, PRDownloader, Python Requests, Python urllib, QbHttp, quic-go, r-curl, Radio Downloader, ReactorNetty, req, request, Requests, reqwest, REST Client for Ruby, RestSharp, Resty, resty-requests, ruby, Safari View Service, ScalaJ HTTP, Skopeo, SlimerJS, Slim Framework, sqlmap, Stealer, superagent, Symfony, trafilatura, Typhoeus, uclient-fetch, Ultimate Sitemap Parser, undici, Unirest for Java, urlgrabber (yum), uTorrent, vimeo.php, webchk, Wget, Windows HTTP, WinHttp WinHttpRequest, WWW-Mechanize, XML-RPC + +### List of detected media players: + +Alexa, Amarok, Audacious, Banshee, Boxee, Clementine, Deezer, DIGA, Downcast, FlyCast, Foobar2000, Google Podcasts, HTC Streaming Player, Hubhopper, iTunes, JHelioviewer, JRiver Media Center, Juice, Just Audio, Kasts, Kodi, MediaMonkey, Miro, MixerBox, MPlayer, mpv, MusicBee, Music Player Daemon, NexPlayer, Nightingale, QuickTime, Songbird, SONOS, Sony Media Go, Stagefright, StudioDisplay, SubStream, VLC, Winamp, Windows Media Player, XBMC, Xtream Player, YouView + +### List of detected mobile apps: + +'sodes, +Simple, 1Password, 2tch, ActionExtension, Adobe Acrobat Reader, Adobe Creative Cloud, Adobe IPM, Adobe NGL, Adobe Synchronizer, Adori, Agora, Aha Radio 2, AIDA64, Airr, Airsonic, AliExpress, Alipay, AllHitMusicRadio, All You Can Books, Amazon Fire, Amazon Music, Amazon Shopping, Ameba, Anchor, AnchorFM, AndroidDownloadManager, Anghami, AntennaPod, AntiBrowserSpy, AN WhatsApp, Anybox, Anytime Podcast Player, Apache, APK Downloader, Apollo, appdb, Apple iMessage, Apple News, Apple Podcasts, Apple Reminders, Apple TV, Arvocast, ASUS Updater, Audacy, Audials, Audible, Audio, Audiobooks, Audio Now, Autoplius.lt, Avid Link, Awasu, Background Intelligent Transfer Service, Baidu Box App, Baidu Input, Ballz, Bank Millenium, Battle.net, BB2C, BBC News, Bear, Be Focused, BetBull, BeyondPod, Bible, Bible KJV, Binance, Bitcoin Core, Bitsboard, Bitwarden, Blackboard, Blitz, Blue Proxy, BlueStacks, Bolt, BonPrix, Bookmobile, Bookshelf, Boom, Boom360, Boomplay, Bose Music, Bose SoundTouch, bPod, Breez, Bridge, Broadcast, Broadway Podcast Network, Browser-Anonymizer, Browser app, BrowserPlus, Bullhorn, BuzzVideo, CamScanner, Capital, capsule.fm, Castamatic, Castaway, CastBox, Castro, Castro 2, CCleaner, CGN, ChMate, Chrome Update, Ciisaa, Citrix Workspace, Classic FM, Client, Clipbox+, Clovia, COAF SMART Citizen, Coinbase, Cooler, Copied, Cortana, Cosmicast, Covenant Eyes, CPod, CPU-Z, CrosswalkApp, Crypto.com DeFi Wallet, CSDN, Damus, Daum, DeepL, De Standaard, De Telegraaf, DevCasts, DeviantArt, DingTalk, DIRECTV, Discord, DManager, Dogecoin Core, DoggCatcher, Don't Waste My Time!, douban App, DoubleTwist CloudPlayer, Doughnut, Douyin, Downcast, Downie, Downloader, Dr. Watson, DStream Air, Edge Update, Edmodo, EMAudioPlayer, Emby Theater, Epic Games Launcher, ESET Remote Administrator, eToro, Evolve Podcast, Expedia, Expo, F-Secure SAFE, Facebook, Facebook Audience Network, Facebook Groups, Facebook Lite, Facebook Messenger, Facebook Messenger Lite, faidr, Fathom, FeedR, FeedStation, Fiddler Classic, Files, Flipboard App, Flipp, FM WhatsApp, Focus Keeper, Focus Matrix, Fountain, Freespoke, Gaana, Garmin fenix 5X, Garmin Forerunner, GBWhatsApp, GetPodcast, Git, GitHub Desktop, GlobalProtect, GoEuro, Gold, GoldenPod, GoLoud, GoNative, Goodpods, GoodReader, Google Assistant, Google Drive, Google Fiber TV, Google Go, Google Lens, Google Nest Hub, Google Photos, Google Play Newsstand, Google Plus, Google Podcasts, Google Search App, Google Tag Manager, GroupMe, Guacamole, Hago, Hammel, HandBrake, HardCast, Hark Audio, Heart, HeartFocus, HeartFocus Education, HermesPod, HeyTapBrowser, HiCast, HideX, Hik-Connect, Himalaya, HiSearch, HisThumbnail, HistoryHound, Hotels.com, HP Smart, HTTP request maker, Huawei Mobile Services, HyperCatcher, iCatcher, iHeartRadio, IMO HD Video Calls & Chat, IMO International Calls & Chat, Instabridge, Instacast, Instagram, Instapaper, iPlayTV, IPTV, IPTV Pro, iVoox, Jam, JaneStyle, JioSaavn, Jitsi Meet, JJ2GO, Jungle Disk, Just Listen, Kajabi, KakaoTalk, Keeper Password Manager, Kids Listen, KidsPod, Kik, KKBOX, Klara, Klarna, KPN Veilig, Kwai, Kwai Pro, L.A. Times, Landis+Gyr AIM Browser, Lark, Laughable, Lazada, LBC, LG Player, Line, LinkedIn, Listen, LiSTNR, Liulo, Logi Options+, LoseIt!, Luminary, Macrium Reflect, MBolsa, Megaphone, MEmpresas, Menucast, Mercantile Bank of Michigan, MessengerX, Meta Business Suite, Metacast, MetaMask, MetaTrader, Microsoft Bing, Microsoft Copilot, Microsoft Lync, Microsoft Office, Microsoft Office Access, Microsoft Office Excel, Microsoft Office Mobile, Microsoft Office OneDrive for Business, Microsoft Office OneNote, Microsoft Office PowerPoint, Microsoft Office Project, Microsoft Office Publisher, Microsoft Office Visio, Microsoft Office Word, Microsoft OneDrive, Microsoft Power Query, Microsoft Start, Microsoft Store, Mimir, mobile.de, MobileSMS, Mojeek, MOMO, MoonFM, mowPod, Moya, MX Player, My Bentley, MyTuner, My Watch Party, My World, nate, Naver, NAVER Dictionary, NET.mede, Netflix, NewsArticle App, Newsly, Nextcloud, NPR, NRC, NRC Audio, NTV Mobil, NuMuKi Browser, Obsidian, OBS Studio, Odnoklassniki, OfferUp, Opal Travel, OpenVAS, Opera News, Opera Updater, Orange Radio, Outcast, Overcast, Overhaul FM, Paint by Number, Palco MP3, Pandora, Papers, PeaCast, Perplexity, Petal Search, Pic Collage, Pinterest, Player FM, PLAYit, Plex Media Server, Pocket Casts, Podbay, Podbean, Podcast & Radio Addict, Podcast App, Podcast Guru, Podcastly, Podcast Player, Podcast Republic, Podcat, Podcatcher Deluxe, Podchaser, Podclipper, PodCruncher, Podeo, Podfriend, Podhero, Podimo, PodKast, Podkicker, Podkicker Classic, Podkicker Pro, PodLP, PodMe, PodMN, PodNL, Podopolo, Podplay, Pods, PodTrapper, podU, Podurama, Podverse, Podvine, Podyssey, PowerShell, Procast, PugPig Bolt, Q-municate, qBittorrent, QQ, QQMusic, QuickCast, Quicksilver, Quora, R, radio.at, radio.de, radio.dk, radio.es, radio.fr, radio.it, radio.net, radio.pl, radio.pt, radio.se, RadioApp, Radio Italiane, Radioline, Radio Next, RadioPublic, Rave Social, Razer Synapse, RDDocuments, Reddit, Redditor, rekordbox, Repod, Reuters News, Rhythmbox, RNPS Action Cards, Roblox, RoboForm, Rocket Chat, RSSDemon, RSSRadio, Rutube, SachNoi, Safari Search Helper, SafeIP, Samsung Magician, Samsung Podcasts, SearchCraft, Seekr, ServeStream, Shadow, Shadowrocket, Shopee, ShowMe, Signal, Sina Weibo, Siri, SiriusXM, Skyeng, Skyeng Teachers, Skype, Skype for Business, Slack, smzdm, Snapchat, Snipd, SoFi, SogouSearch App, SohuNews, Soldier, Sonnet, Sony PlayStation 5, SOOP, SoundOn, SoundWaves, SPORT1, Spotify, Spreaker, Startsiden, Stitcher, StoryShots, Streamlabs OBS, Stream Master, Strimio, Surfshark, Swinsian, Swoot, Taobao, Teams, Telegram, Tencent Docs, The Crossword, The Epoch Times, The New York Times, The Wall Street Journal, Theyub, Threads, Thunder, tieba, TikTok, TikTok Lite, TIM, TiviMate, TopBuzz, TopSecret Chat, TownNews Now, TracePal, Trade Me, TradingView, Treble.fm, TRP Retail Locator, TuneIn Radio, TuneIn Radio Pro, Turtlecast, Tuya Smart Life, TVirl, twinkle, Twitch Studio, Twitter, Twitterrific, U-Cursos, Ubook Player, UCast, Uconnect LIVE, Uforia, Unibox, UnityPlayer, Viber, Victor Reader Stream 3, Victor Reader Stream New Generation, Victor Reader Stream Trek, Virgin Radio, Visha, Visual Studio Code, Vodacast, Vuhuv, Vuze, waipu.tv, Walla News, WatchFree+, Wattpad, Wayback Machine, WebDAV, Webex Teams, WeChat, WeChat Share Extension, WeCom, Whatplay, WhatsApp, WhatsApp+2, WhatsApp Business, Whisper, WH Questions, Windows Antivirus, Windows CryptoAPI, Windows Delivery Optimization, Windows Push Notification Services, Windows Update Agent, Wireshark, Wirtschafts Woche, WNYC, Word Cookies!, WPS Office, Wynk Music, Xiao Yu Zhou, XING, XShare, XSplit Broadcaster, Y8 Browser, Yahoo! Japan, Yahoo OneSearch, YakYak, Yandex, Yandex Music, Yapa, Yelp Mobile, YouTube, Youtube Music, Yo WhatsApp, Zalo, ZEIT ONLINE, Zen, ZEPETO, Zite, Zoho Chat, Zune and *mobile apps using [AFNetworking](https://github.com/AFNetworking/AFNetworking) or [Electron](https://github.com/electron/electron)* + +### List of detected PIMs (personal information manager): + +Airmail, Apple Mail, Barca, Basecamp, BathyScaphe, BlueMail, DAVdroid, eM Client, Evernote, Foxmail, Franz, Gmail, JaneView, Live5ch, Lotus Notes, mailapp, MailBar, Mailbird, Mail Master, Mailspring, Microsoft Outlook, NAVER Mail, Notion, Outlook Express, Postbox, Raindrop.io, Rambox Pro, SeaMonkey, Spicebird, The Bat!, Thunderbird, Windows Mail, Yahoo! Mail, Yahoo Mail + +### List of detected feed readers: + +Akregator, Apple PubSub, BashPodder, Breaker, castero, castget, FeedDemon, Feeddler RSS Reader, gPodder, JetBrains Omea Reader, Liferea, NetNewsWire, Newsbeuter, NewsBlur, NewsBlur Mobile App, Newsboat, Playapod, PodPuppy, PritTorrent, Pulp, QuiteRSS, ReadKit, Reeder, RSS Bandit, RSS Junkie, RSSOwl, Stringer + +### List of brands with detected devices: + +2E, 3GNET, 3GO, 3Q, 4Good, 4ife, 5IVE, 7 Mobile, 10moons, 360, 8848, A&K, A1, A95X, AAUW, Accent, Accesstyle, ACD, Ace, Aceline, Acepad, Acer, Acteck, actiMirror, Adreamer, Adronix, Advan, Advance, Advantage Air, AEEZO, AFFIX, AfriOne, AGM, AG Mobile, AIDATA, AileTV, Ainol, Airis, Airness, AIRON, Airpha, Airtel, Airties, AirTouch, AIS, Aiuto, Aiwa, Ajib, Akai, AKIRA, Alba, Alcatel, Alcor, ALDI NORD, ALDI SÜD, Alfawise, Alienware, Aligator, AllCall, AllDocube, allente, ALLINmobile, All Star, Allview, Allwinner, Alps, alpsmart, Altech UEC, Altibox, Altice, Altimo, altron, Altus, AMA, Amazon, Amazon Basics, AMCV, AMGOO, Amigoo, Amino, Amoi, ANBERNIC, ANCEL, andersson, Andowl, Angelcare, AngelTech, Anker, Anry, ANS, ANXONIT, AOC, Aocos, Aocwei, AOpen, Aoro, Aoson, AOYODKG, ApoloSign, Apple, Aquarius, Archos, Arian Space, Arival, Ark, ArmPhone, Arnova, ARRIS, Artel, Artizlee, ArtLine, Arçelik, Asano, Asanzo, Ask, Aspera, ASSE, Assistant, astro (MY), Astro (UA), Asus, AT&T, Athesi, Atlantic Electrics, Atmaca Elektronik, ATMAN, ATMPC, ATOL, Atom, Atouch, Atozee, Attila, Atvio, Audiovox, AUPO, AURIS, Autan, AUX, Avaya, Avenzo, AVH, Avvio, Awow, AWOX, AXEN, Axioo, AXXA, Axxion, AYA, AYYA, Azeyou, AZOM, Azumi Mobile, Azupik, b2m, Backcell, BAFF, BangOlufsen, Barnes & Noble, BARTEC, BASE, BAUHN, BBK, BB Mobile, BDF, BDQ, BDsharing, Beafon, Becker, Beeline, Beelink, Beetel, Beista, Beko, Bell, Bellphone, Benco, Benesse, BenQ, BenQ-Siemens, BenWee, Benzo, Beyond, Bezkam, BGH, Biegedy, Bigben, BIHEE, BilimLand, Billion, Billow, BioRugged, Bird, Bitel, Bitmore, Bittium, Bkav, Black Bear, Black Box, Black Fox, Blackpcs, Blackphone, Blackton, Blackview, Blaupunkt, Bleck, BLISS, Blloc, Blow, Blu, Bluboo, Bluebird, Bluedot, Bluegood, BlueSky, Bluewave, BluSlate, BMAX, Bmobile, BMW, BMXC, Bobarry, bogo, Bolva, Bookeen, Boost, Botech, Boway, bq, Bqeel, BrandCode, Brandt, BRAVE, Bravis, BrightSign, Brigmton, Brondi, BROR, BS Mobile, Bubblegum, Bundy, Bush, BuzzTV, BYD, BYJU'S, BYYBUO, C5 Mobile, CADENA, CAGI, Caixun, CALME, Camfone, Canaima, Canal+, Canal Digital, Canguro, Capitel, Captiva, Carbon Mobile, Carrefour, Casio, Casper, Cat, Cavion, CCIT, Cecotec, Ceibal, Celcus, Celkon, Cell-C, Cellacom, CellAllure, Cellution, CENTEK, Centric, CEPTER, CG Mobile, CGV, Chainway, Changhong, CHCNAV, Cherry Mobile, Chico Mobile, ChiliGreen, China Mobile, China Telecom, Chuwi, C Idea, CipherLab, Citycall, CKK Mobile, Claresta, Clarmin, CLAYTON, ClearPHONE, Clementoni, Cloud, Cloudfone, Cloudpad, Clout, Clovertek, CMF, CnM, Cobalt, Coby Kyros, Cogeco, COLORROOM, Colors, Comio, CommScope, Compal, Compaq, COMPUMAX, ComTrade Tesla, Conceptum, Concord, ConCorde, Condor, Connectce, Connex, Conquest, CONSUNG, Continental Edison, Contixo, coocaa, COOD-E, Coolpad, Coopers, CORN, Cosmote, Covia, Cowon, COYOTE, CPDEVICE, CreNova, Crescent, Crestron, Cricket, Crius Mea, Crony, Crosscall, Crown, Ctroniq, Cube, CUBOT, CUD, Cuiud, Cultraview, CVTE, Cwowdefu, CX, Cyrus, D-Link, D-Tech, Daewoo, Danew, DangcapHD, Dany, Daria, DASS, Datalogic, Datamini, Datang, Datawind, Datsun, Dawlance, Dazen, DbPhone, Dbtel, Dcode, DEALDIG, Dell, Denali, Denka, Denver, Desay, DeWalt, DEXP, DEYI, DF, DGTEC, DIALN, Dialog, Dicam, Digi, Digicel, DIGICOM, Digidragon, DIGIFORS, Digihome, Digiland, Digit4G, Digma, DIJITSU, DIKOM, DIMO, Dinalink, Dinax, DING DING, Diofox, DIORA, DISH, Disney, Ditecma, Diva, DiverMax, Divisat, DIXON, DL, DMM, DMOAO, DNS, DoCoMo, Doffler, Dolamee, Dom.ru, Doogee, Doopro, Doov, Dopod, Doppio, Dora, DORLAND, Doro, DPA, DRAGON, Dragon Touch, Dreamgate, DreamStar, DreamTab, Droidlogic, Droxio, DSDevices, DSIC, Dtac, DUDU AUTO, Dune HD, DUNNS Mobile, DuoTV, Durabook, Duubee, Dykemann, Dyon, E-Boda, E-Ceros, E-TACHI, E-tel, Eagle, EagleSoar, EAS Electric, Easypix, EBEN, EBEST, Echo Mobiles, ecom, ECON, ECOO, EcoStar, ECS, Edenwood, EE, EFT, EGL, EGOTEK, Ehlel, Einstein, EKINOX, EKO, Eks Mobility, EKT, ELARI, ELE-GATE, Elecson, Electroneum, ELECTRONIA, Elekta, Elektroland, Element, Elenberg, Elephone, Elevate, Elista, elit, Elong Mobile, Eltex, Ematic, Emporia, ENACOM, ENDURO, Energizer, Energy Sistem, Engel, ENIE, Enot, eNOVA, Entity, Envizen, Ephone, Epic, Epik One, Epson, Equator, Ergo, Ericsson, Ericy, Erisson, Essential, Essentielb, eSTAR, ETOE, Eton, eTouch, Etuline, Eudora, Eurocase, EUROLUX, Eurostar, Evercoss, Everest, Everex, Everfine, Everis, Evertek, Evolio, Evolveo, Evoo, EVPAD, EvroMedia, evvoli, EWIS, EXCEED, Exmart, ExMobile, EXO, Explay, Express LUCK, ExtraLink, Extrem, Eyemoo, EYU, Ezio, Ezze, F&U, F+, F2 Mobile, F150, Facebook, Facetel, Facime, Fairphone, Famoco, Famous, Fantec, Fanvace, FaRao Pro, Farassoo, FarEasTone, Fengxiang, Fenoti, FEONAL, Fero, FFF SmartLife, Figgers, FiGi, FiGO, FiiO, Filimo, FILIX, FinePower, FINIX, Finlux, FireFly Mobile, FISE, Fision, FITCO, Fluo, Fly, FLYCAT, FLYCOAY, FMT, FNB, FNF, Fobem, Fondi, Fonos, FONTEL, FOODO, FORME, Formovie, Formuler, Forstar, Fortis, FortuneShip, FOSSiBOT, Fourel, Four Mobile, Foxconn, FoxxD, FPT, free, Freetel, FreeYond, FRESH, Frunsi, Fuego, FUJICOM, Fujitsu, Funai, Fusion5, Future Mobile Technology, Fxtec, G-Guard, G-PLUS, G-TiDE, G-Touch, G-Vill, Galactic, Galatec, Galaxy Innovations, Gamma, Garmin-Asus, Gateway, Gazal, Gazer, GDL, Geanee, Geant, Gear Mobile, Gemini, General Mobile, Genesis, GEOFOX, Geo Phone, Geotel, Geotex, GEOZON, Getnord, GFive, Gfone, Ghia, Ghong, Ghost, Gigabyte, Gigaset, Gini, Ginzzu, Gionee, GIRASOLE, GlobalSec, Globex, Globmall, GlocalMe, Glofiish, GLONYX, Glory Star, GLX, GN Electronics, GOCLEVER, Gocomma, GoGEN, GOLDBERG, GoldMaster, GoldStar, Gol Mobile, Goly, Gome, GoMobile, GOODTEL, Google, Goophone, Gooweel, GOtv, Gplus, Gradiente, Graetz, Grape, Great Asia, Gree, Green Lion, Green Orange, Greentel, Gresso, Gretel, GroBerwert, Grundig, Grünberg, Gtel, GTMEDIA, GTX, Guophone, GVC Pro, H96, H133, Hafury, Haier, Haipai, Haixu, Hamlet, Hammer, Handheld, HannSpree, Hanseatic, Hanson, HAOQIN, HAOVM, Hardkernel, Harper, Hartens, Hasee, Hathway, HAVIT, HDC, HeadWolf, HEC, Heimat, Helio, Hemilton, HERO, HexaByte, Hezire, Hi, Hi-Level, Hiberg, HiBy, HIGH1ONE, High Q, Highscreen, HiGrace, HiHi, HiKing, HiMax, Hi Nova, HIPER, Hipstreet, Hiremco, Hisense, Hitachi, Hitech, HKC, HKPro, HLLO, HMD, hoco, HOFER, Hoffmann, HOLLEBERG, Homatics, Hometech, HOMII, Homtom, Honeywell, HongTop, HONKUAHG, Hoozo, Hopeland, Horion, Horizon, Horizont, Hosin, HOTACK, Hotel, Hot Pepper, HOTREALS, Hotwav, How, HP, HTC, Huadoo, Huagan, Huavi, Huawei, Hugerock, Humanware, Humax, HUMElab, Hurricane, Huskee, Hyatta, Hykker, Hyrican, Hytera, Hyundai, Hyve, i-Cherry, I-INN, i-Joy, i-mate, i-mobile, I-Plus, iBall, iBerry, ibowin, iBrit, IconBIT, Icone Gold, iData, IDC, iDino, iDroid, iFIT, iGet, iHome Life, iHunt, I KALL, Ikea, IKI Mobile, iKoMo, iKon, iKonia, IKU Mobile, iLA, iLepo, iLife, iMan, Imaq, iMars, iMI, IMO Mobile, Imose, Impression, iMuz, iNavi, INCAR, Inch, Inco, Indurama, iNew, Infiniton, InfinityPro, Infinix, InFocus, InfoKit, Infomir, InFone, Inhon, Inka, Inkti, InnJoo, Innos, Innostream, Inoi, iNo Mobile, iNOVA, inovo, INQ, Insignia, INSYS, Intek, Intel, Intex, Invens, Inverto, Invin, iOcean, IOTWE, iOutdoor, iPEGTOP, iPro, iQ&T, IQM, IRA, Irbis, iReplace, Iris, iRobot, iRola, iRulu, iSafe Mobile, iStar, iSWAG, IT, iTel, iTruck, IUNI, iVA, iView, iVooMi, ivvi, iWaylink, iXTech, iYou, iZotron, Jambo, JAY-Tech, Jedi, Jeep, Jeka, Jesy, JFone, Jiake, Jiayu, Jinga, Jin Tu, Jio, Jivi, JKL, Jolla, Joy, JoySurf, JPay, JREN, Jumper, Juniper Systems, Just5, JUSYEA, JVC, JXD, K-Lite, K-Touch, Kaan, Kaiomy, Kalley, Kanji, Kapsys, Karbonn, Kata, KATV1, Kazam, Kazuna, KDDI, Kempler & Strauss, Kenbo, Kendo, Keneksi, KENSHI, KENWOOD, Kenxinda, KGTEL, Khadas, Kiano, kidiby, Kingbox, Kingstar, Kingsun, KINGZONE, Kinstone, Kiowa, Kivi, Klipad, KMC, KN Mobile, Kocaso, Kodak, Kogan, Komu, Konka, Konrow, Koobee, Koolnee, Kooper, KOPO, Korax, Koridy, Koslam, Kraft, KREZ, KRIP, KRONO, Krüger&Matz, KT-Tech, KTC, KUBO, KuGou, Kuliao, Kult, Kumai, Kurio, KVADRA, Kvant, Kydos, Kyocera, Kyowon, Kzen, KZG, L-Max, LAIQ, Land Rover, Landvo, Lanin, Lanix, Lark, Laser, Laurus, Lava, LCT, Leader Phone, Leagoo, Leben, LeBest, Lectrus, Ledstar, LeEco, Leelbox, Leff, Legend, Leke, Lemco, LEMFO, Lemhoov, Lenco, Lenovo, Leotec, Le Pan, Lephone, Lesia, Lexand, Lexibook, LG, Liberton, Lifemaxx, Lime, Lingbo, Lingwin, Linnex, Linsar, Linsay, Listo, LNMBBS, Loewe, LOGAN, Logic, Logic Instrument, Logicom, Logik, Logitech, LOKMAT, LongTV, Loview, Lovme, LPX-G, LT Mobile, Lumigon, Lumitel, Lumus, Luna, LUNNEN, LUO, Luxor, Lville, LW, LYF, LYOTECH LABS, M-Horse, M-KOPA, M-Tech, M.T.T., M3 Mobile, M4tel, MAC AUDIO, Macoox, Mafe, MAG, MAGCH, Magenta, Magicsee, Magnus, Majestic, Malata, Mango, Manhattan, Mann, Manta Multimedia, Mantra, Mara, Marshal, Mascom, Massgo, Masstel, Master-G, Mastertech, Matco Tools, Matrix, Maunfeld, Maxcom, Maxfone, Maximus, Maxtron, MAXVI, Maxwell, Maxwest, MAXX, Maze, Maze Speed, MBI, MBK, MBOX, McLaut, MDC Store, MDTV, meanIT, Mecer, MECHEN, Mecool, Mediacom, Medion, MEEG, Megacable, MegaFon, MEGAMAX, MEGA VISION, Meitu, Meizu, Melrose, MeMobile, Memup, MEO, MESWAO, Meta, Metz, MEU, MicroMax, Microsoft, Microtech, Mightier, Minix, Mint, Mintt, Mio, Mione, mipo, Miray, Mitchell & Brown, Mito, Mitsubishi, Mitsui, MIVO, MIWANG, MIXC, MiXzo, MLAB, MLLED, MLS, MMI, Mobell, Mobicel, MobiIoT, Mobiistar, Mobile Kingdom, Mobiola, Mobistel, MobiWire, Mobo, Mobvoi, Modecom, Mode Mobile, Mofut, Moondrop, MORTAL, Mosimosi, Motiv, Motorola, Motorola Solutions, Movic, MOVISUN, Movitel, Moxee, mPhone, Mpman, MSI, MStar, MTC, MTN, multibox, Multilaser, MultiPOS, MULTYNET, MwalimuPlus, MYFON, MyGica, MygPad, Mymaga, MyMobile, MyPhone (PH), myPhone (PL), Myria, Myros, Mystery, MyTab, MyWigo, N-one, Nabi, NABO, Nanho, Naomi Phone, NASCO, National, Navcity, Navitech, Navitel, Navon, NavRoad, NEC, Necnot, Nedaphone, Neffos, NEKO, Neo, neoCore, Neolix, Neomi, Neon IQ, Neoregent, Nesons, NetBox, Netgear, Netmak, NETWIT, NeuImage, NeuTab, NEVIR, Newal, New Balance, New Bridge, Newgen, Newland, Newman, Newsday, NewsMy, Nexa, Nexar, NEXBOX, Nexian, NEXON, NEXT, Next & NextStar, Nextbit, NextBook, NextTab, NGM, NG Optics, NGpon, Nikon, NILAIT, NINETEC, NINETOLOGY, Nintendo, nJoy, NOA, Noain, Nobby, Noblex, NOBUX, noDROPOUT, NOGA, Nokia, Nomi, Nomu, Noontec, Nordfrost, Nordmende, NORMANDE, NorthTech, Nos, Nothing, Nous, Novacom, Novex, Novey, NOVIS, NoviSea, NOVO, NTT West, NuAns, Nubia, NUU Mobile, NuVision, Nuvo, Nvidia, NYX Mobile, O+, O2, Oale, Oangcc, OASYS, Obabox, Ober, Obi, OCEANIC, Odotpad, Odys, Oilsky, OINOM, ok., Okapi, Okapia, Oking, OKSI, OKWU, Olax, Olkya, Ollee, OLTO, Olympia, OMIX, Onda, OneClick, OneLern, OnePlus, Onida, Onix, Onkyo, ONN, ONVO, ONYX BOOX, Ookee, Ooredoo, OpelMobile, Openbox, Ophone, OPPO, Opsson, Optoma, Orange, Orange Pi, Orava, Orbic, Orbita, Orbsmart, Ordissimo, Orion, OSCAL, OTTO, OUJIA, Ouki, Oukitel, OUYA, Overmax, Ovvi, Owwo, OX TAB, OYSIN, Oysters, Oyyu, OzoneHD, P-UP, Pacific Research Alliance, Packard Bell, Padpro, PAGRAER, Paladin, Palm, Panacom, Panasonic, Panavox, Pano, Panodic, Panoramic, Pantech, PAPYRE, Parrot Mobile, Partner Mobile, PCBOX, PCD, PCD Argentina, PC Smart, PEAQ, Pelitt, Pendoo, Penta, Pentagram, Perfeo, Phicomm, Philco, Philips, Phonemax, phoneOne, Pico, PINE, Pioneer, Pioneer Computers, PiPO, PIRANHA, Pixela, Pixelphone, PIXPRO, Pixus, Planet Computers, Platoon, Play Now, PLDT, Ployer, Plum, PlusStyle, Pluzz, PocketBook, POCO, Point Mobile, Point of View, Polar, PolarLine, Polaroid, Polestar, PolyPad, Polytron, Pomp, Poppox, POPTEL, Porsche, Portfolio, Positivo, Positivo BGH, PPDS, PPTV, Premier, Premio, Prestigio, PRIME, Primepad, Primux, PRISM+, Pritom, Prixton, PROFiLO, Proline, Prology, ProScan, PROSONIC, Protruly, ProVision, PULID, Punos, Purism, PVBox, Q-Box, Q-Touch, Q.Bell, QFX, Qilive, QIN, Qiuwoky, QLink, QMobile, Qnet Mobile, QTECH, Qtek, Quantum, Quatro, Qubo, Quechua, Quest, Quipus, Qumo, Qupi, Qware, QWATT, R-TV, R3Di, Rakuten, Ramos, Raspberry, Ravoz, Raylandz, Razer, RAZZ, RCA Tablets, RCT, Reach, Readboy, Realix, Realme, RED, RED-X, Redbean, Redfox, RedLine, Redway, Reeder, REGAL, RelNAT, Relndoo, Remdun, Renova, RENSO, rephone, Retroid Pocket, Revo, Revomovil, Rhino, Ricoh, Rikomagic, RIM, Ringing Bells, Rinno, Ritmix, Ritzviva, Riviera, Rivo, Rizzen, ROADMAX, Roadrover, Roam Cat, ROCH, Rocket, ROiK, Rokit, Roku, Rombica, Romsat, Ross&Moor, Rover, RoverPad, Royole, RoyQueen, RTK, RT Project, RugGear, RuggeTech, Ruggex, Ruio, Runbo, RunGee, Rupa, Ryte, S-Color, S-TELL, S2Tel, Saba, Safaricom, Sagem, Sagemcom, Saiet, SAILF, Salora, Sambox, Samsung, Samtech, Samtron, Sanei, Sankey, Sansui, Santin, SANY, Sanyo, Savio, Sber, SCHAUB LORENZ, Schneider, Schok, SCHONTECH, Scoole, Scosmos, Seatel, SEBBE, Seeken, SEEWO, SEG, Sega, SEHMAX, Selecline, Selenga, Selevision, Selfix, SEMP TCL, Sencor, Sencrom, Sendo, Senkatel, SENNA, Senseit, Senwa, SERVO, Seuic, Sewoo, SFR, SGIN, Shanling, Sharp, Shift Phones, Shivaki, Shtrikh-M, Shuttle, Sico, Siemens, Sigma, Silelis, Silent Circle, Silva Schneider, Simbans, simfer, Simply, SINGER, Singtech, Siragon, Sirin Labs, Siswoo, SK Broadband, SKG, SKK Mobile, Sky, Skyline, SkyStream, Skytech, Skyworth, Smadl, Smailo, Smart, Smartab, SmartBook, SMARTEC, Smart Electronic, Smartex, Smartfren, Smartisan, Smart Kassel, Smart Tech, Smarty, Smooth Mobile, Smotreshka, SMT Telecom, SMUX, SNAMI, SobieTech, Soda, Softbank, Soho Style, Solas, SOLE, SOLO, Solone, Sonim, SONOS, Sony, Sony Ericsson, SOSH, SoulLink, Soundmax, SOWLY, Soyes, Spark, Sparx, SPC, Spectralink, Spectrum, Spice, Spider, Sprint, SPURT, SQOOL, SSKY, Star, Star-Light, Starlight, Starmobile, Starway, Starwind, STF Mobile, STG Telecom, Stilevs, STK, Stonex, Storex, StrawBerry, Stream, STRONG, Stylo, Subor, Sugar, SULPICE TV, Sumvision, Sunmax, Sunmi, Sunny, Sunstech, SunVan, Sunvell, SUNWIND, SuperBOX, Super General, Supermax, SuperSonic, SuperTab, SuperTV, Supra, Supraim, Surfans, Surge, Suzuki, Sveon, Swipe, SWISSMOBILITY, Swisstone, Switel, SWOFY, Syco, SYH, Sylvania, Symphony, Syrox, System76, T-Mobile, T96, TADAAM, TAG Tech, Taiga System, Takara, TALBERG, Talius, Tambo, Tanix, TAUBE, TB Touch, TCL, TCL SCBC, TD Systems, TD Tech, TeachTouch, Technicolor, Technika, TechniSat, Technopc, TECHNOSAT, TechnoTrend, TechPad, TechSmart, Techstorm, Techwood, Teclast, Tecno Mobile, TecToy, TEENO, Teknosa, Tele2, Telefunken, Telego, Telenor, Telia, Telit, Telkom, Telly, Telma, TeloSystems, Telpo, Temigereev, TENPLUS, Teracube, Tesco, Tesla, TETC, Tetratab, teXet, ThL, Thomson, Thuraya, TIANYU, Tibuta, Tigers, Time2, Timovi, TIMvision, Tinai, Tinmo, TiPhone, Tivax, TiVo, TJC, TJD, TOKYO, Tolino, Tone, TOOGO, Tooky, Top-Tech, TopDevice, TOPDON, Topelotek, Top House, Toplux, TOPSHOWS, Topsion, Topway, Torex, TORNADO, Torque, TOSCIDO, Toshiba, Touchmate, Touch Plus, TOX, TPS, Transpeed, Trecfone, TrekStor, Trevi, TriaPlay, Tricolor, Trident, Trifone, Trimble, Trio, Tronsmart, True, True Slim, Tsinghua Tongfang, TTEC, TTfone, TTK-TV, TuCEL, TUCSON, Tunisie Telecom, Turbo, Turbo-X, TurboKids, TurboPad, Turkcell, Tuvio, TV+, TVC, TwinMOS, TWM, Twoe, TWZ, TYD, Tymes, Türk Telekom, U-Magic, U.S. Cellular, UD, UE, UGINE, Ugoos, Uhans, Uhappy, Ulefone, Umax, UMIDIGI, Umiio, Unblock Tech, Uniden, Unihertz, Unimax, Uniqcell, Uniscope, Unistrong, Unitech, UNITED, United Group, UNIWA, Unknown, Unnecto, Unnion Technologies, UNNO, Unonu, UnoPhone, Unowhy, UOOGOU, Urovo, UTime, UTOK, UTStarcom, UZ Mobile, V-Gen, V-HOME, V-HOPE, v-mobile, V7, VAIO, VALE, VALEM, VALTECH, VANGUARD, Vankyo, VANWIN, Vargo, VASOUN, Vastking, VAVA, VC, VDVD, Vega, Veidoo, Vekta, Venso, Venstar, Venturer, VEON, Verico, Verizon, Vernee, Verssed, Versus, Vertex, Vertu, Verykool, Vesta, Vestel, VETAS, Vexia, VGO TEL, ViBox, Victurio, VIDA, Videocon, Videoweb, Viendo, ViewSonic, VIIPOO, VIKUSHA, VILLAON, VIMOQ, Vinabox, Vinga, Vinsoc, Vios, Viper, Vipro, Virzo, Vision Technology, Vision Touch, Visitech, Visual Land, Vitelcom, Vitumi, Vityaz, Viumee, Vivax, VIVIBright, VIVIMAGE, Vivo, VIWA, Vizio, Vizmo, VK Mobile, VKworld, VNPT Technology, VOCAL, Vodacom, Vodafone, VOGA, VOLIA, VOLKANO, Volla, Volt, Vonino, Vontar, Vorago, Vorcom, Vorke, Vormor, Vortex, VORTEX (RO), Voto, VOX, Voxtel, Voyo, Vsmart, Vsun, VUCATIMES, Vue Micro, Vulcan, VVETIME, Völfen, W&O, WAF, Wainyok, Walker, Waltham, Walton, Waltter, Wanmukang, WANSA, WE, We. by Loewe., Webfleet, Web TV, WeChip, Wecool, Weelikeit, Weiimi, Weimei, WellcoM, WELLINGTON, Western Digital, Weston, Westpoint, Wexler, White Mobile, Whoop, Wieppo, Wigor, Wiko, WildRed, Wileyfox, Winds, Wink, Winmax, Winnovo, Winstar, Wintouch, Wiseasy, WIWA, WizarPos, Wizz, Wolder, Wolfgang, Wolki, WONDER, Wonu, Woo, Wortmann, Woxter, WOZIFAN, WS, X-AGE, X-BO, X-Mobile, X-TIGI, X-View, X.Vision, X88, X96, X96Q, XB, Xcell, XCOM, Xcruiser, XElectron, XGEM, XGIMI, Xgody, Xiaodu, Xiaolajiao, Xiaomi, Xion, Xolo, Xoro, XPPen, XREAL, Xshitou, Xsmart, Xtouch, Xtratech, Xwave, XY Auto, Yandex, Yarvik, YASIN, YELLYOUTH, YEPEN, Yes, Yestel, Yezz, YIKEMI, Yoka TV, Yooz, Yota, YOTOPT, Youin, Youwei, Ytone, Yu, Yuandao, YU Fly, YUHO, YUMKEM, YUNDOO, Yuno, YunSong, Yusun, Yxtel, Z-Kai, Zaith, Zamolxe, Zatec, Zealot, Zeblaze, Zebra, Zeeker, Zeemi, Zen, Zenek, Zentality, Zfiner, ZH&K, Zidoo, ZIFFLER, ZIFRO, Zigo, ZIK, Zinox, ZIOVO, Ziox, Zonda, Zonko, Zoom, ZoomSmart, Zopo, ZTE, Zuum, Zync, ZYQ, Zyrex, ZZB, öwn + +### List of detected bots: + +2GDPR, 2ip, 360 Monitoring, 360JK, 360Spider, 1001FirmsBot, Abonti, Aboundexbot, AccompanyBot, Acoon, AdAuth, Adbeat, AddThis.com, ADMantX, ADmantX Service Fetcher, Adsbot, Adscanner, AdsTxtCrawler, AdsTxtCrawlerTP, adstxtlab.com, Aegis, aHrefs Bot, AhrefsSiteAudit, aiHitBot, Alexa Crawler, Alexa Site Audit, Allloadin Favicon Bot, AlltheWeb, AlphaXCrawl, Amazon AdBot, Amazon Bot, Amazon ELB, Amazon Route53 Health Check, Amorank Spider, Analytics SEO Crawler, Ant, Anthropic AI, ApacheBench, Applebot, AppSignalBot, Arachni, archive.org bot, ArchiveBot, ArchiveBox, Arquivo.pt, ARSNova Filter System, Asana, Ask Jeeves, AspiegelBot, Automattic Analytics, Awario, Backlink-Check.de, BacklinkCrawler, BacklinksExtendedBot, BackupLand, Baidu Spider, Barkrowler, Barracuda Sentinel, BazQux Reader, BBC Forge URL Monitor, BBC Page Monitor, BDCbot, BDFetch, Better Uptime Bot, BingBot, Birdcrawlerbot, BitlyBot, BitSight, Blackbox Exporter, Blekkobot, BLEXBot Crawler, Bloglines, Bloglovin, Blogtrottr, Bluesky, BoardReader, BoardReader Blog Indexer, Botify, Bountii Bot, BrandVerity, Bravebot, BrightBot, BrightEdge, Browsershots, BUbiNG, Buck, BuiltWith, Butterfly Robot, Bytespider, CareerBot, Castopod, Castro 2, Catchpoint, CATExplorador, ccBot crawler, CensysInspect, Charlotte, Chartable, ChatGPT-User, Chatwork LinkPreview, CheckHost, CheckMark Network, Choosito, Chrome Privacy Preserving Prefetch Proxy, Cincraw, CISPA Web Analyzer, CLASSLA-web, ClaudeBot, Clickagy, Cliqzbot, CloudFlare Always Online, CloudFlare AMP Fetcher, Cloudflare Custom Hostname Verification, Cloudflare Diagnostics, Cloudflare Health Checks, Cloudflare Observatory, Cloudflare Security Insights, Cloudflare Smart Transit, Cloudflare SSL Detector, Cloudflare Traffic Manager, CloudServerMarketSpider, CMS Experiment, Cocolyzebot, Cohere AI, Collectd, colly, CommaFeed, COMODO DCV, Comscore, ContentKing, Convertify, Cookiebot, Cotoyogi, Crawldad, Crawlson, Crawly Project, CriteoBot, CrowdTangle, CrystalSemanticsBot, CSS Certificate Spider, CSSCheck, CyberFind Crawler, Cyberscan, Cốc Cốc Bot, DaspeedBot, Datadog Agent, DataForSeoBot, datagnionbot, Datanyze, Dataprovider, DataXu, Daum, Dazoobot, Deepfield Genome, deepnoc, Deep SEARCH 9, DepSpid, Detectify, Diffbot, Discobot, Discord Bot, Disqus, DNSResearchBot, DomainAppender, Domain Codex, DomainCrawler, Domain Re-Animator Bot, Domain Research Project, Domains Project, DomainStatsBot, DomCop Bot, DotBot, Dotcom Monitor, Dubbotbot, DuckAssistBot, DuckDuckBot, ducks.party, DuplexWeb-Google, DynatraceSynthetic, Easou Spider, eCairn-Grabber, EFF Do Not Track Verifier, Elastic Synthetics, EMail Exractor, EmailWolf, Embedly, Entfer, evc-batch, Everyfeed, ExaBot, ExactSeek Crawler, Example3, Exchange check, Expanse, EyeMonit, Ezgif, Ezooms, eZ Publish Link Validator, FacebookBot, Facebook Crawler, Faveeo, FediList, Feedbin, FeedBurner, Feedly, Feedspot, Feed Wrangler, Femtosearch, Fever, Findxbot, Flipboard, FontRadar, fragFINN, FreeWebMonitoring, FreshRSS, Functionize, Gaisbot, GDNP, GeedoBot, GeedoProductSearch, Generic Bot, Genieo Web filter, Ghost Inspector, Gigablast, Gigabot, GitCrawlerBot, Github Camo, GitHubCopilotChat, Gluten Free Crawler, Gmail Image Proxy, Gobuster, Golfe, Goo, Google-CloudVertexBot, Google-Document-Conversion, Google-Safety, Google Apps Script, Google Area 120 Privacy Policy Fetcher, Googlebot, Googlebot News, Google Cloud Scheduler, Google Docs, Google Favicon, Google PageSpeed Insights, Google Partner Monitoring, Google Search Console, Google Sheets, Google Slides, Google Stackdriver Monitoring, Google StoreBot, Google Structured Data Testing Tool, Google Transparency Report, Gowikibot, Gozle, GPTBot, Grafana, Grammarly, Grapeshot, Gregarius, GTmetrix, GumGum Verity, hackermention, Hatena Bookmark, Hatena Favicon, Headline, Heart Rails Capture, Heritrix, Heureka Feed, htmlyse, HTTPMon, httpx, HuaweiWebCatBot, HubPages, HubSpot, ICC-Crawler, ichiro, IDG, Iframely, IIS Site Analysis, ImageSift, Inetdex Bot, Infegy, InfoTigerBot, Inktomi Slurp, inoreader, Inspici, InsytfulBot, Intelligence X, Interactsh, InternetMeasurement, IONOS Crawler, IP-Guide Crawler, IPIP, IPS Agent, IsItWP, iTMS, JobboerseBot, JungleKeyThumbnail, K6, KadoBot, Kaspersky, KeyCDN Tools, Keys.so, Kiwi TCMS GitOps, KlarnaBot, KomodiaBot, Konturbot, Kouio, Kozmonavt, KStandBot, l9explore, l9tcpid, LAC IA Harvester, Larbin web crawler, LastMod Bot, LCC, leak.info, LeakIX, Let's Encrypt Validation, LetSearch, Lighthouse, LightspeedSystemsCrawler, Linespider, Linkdex Bot, LinkedIn Bot, LinkpadBot, LinkPreview, LinkWalker, LiveJournal, LTX71, Lumar, LumtelBot, Lycos, MaCoCu, MADBbot, Magpie-Crawler, MagpieRSS, Mail.Ru Bot, MakeMerryBot, Marginalia, MariaDB/MySQL Knowledge Base, masscan, masscan-ng, Mastodon Bot, Matomo, Meanpath Bot, Mediatoolkit Bot, MegaIndex, MeltwaterNews, Meta-ExternalAgent, Meta-ExternalFetcher, MetaInspector, MetaJobBot, MicroAdBot, Microsoft Power Automate, Microsoft Preview, Miniature.io, Mixnode, Mixrank Bot, MJ12 Bot, Mnogosearch, ModatScanner, MojeekBot, Monitor.Us, Monitor Backlinks, Monsidobot, Montastic Monitor, MoodleBot Linkchecker, Morningscore Bot, MTRobot, MuckRack, Munin, MuscatFerret, Nagios check_http, najdu.s.holubem.eu, NalezenCzBot, NameProtectBot, nbertaupete95, Neevabot, Netcraft Survey Bot, netEstate, NetLyzer FastProbe, Netpeak Checker, NetResearchServer, NetSystemsResearch, NetTrack, Netvibes, NETZZAPPEN, NewsBlur, NewsGator, Newslitbot, NiceCrawler, Nimbostratus Bot, NLCrawler, Nmap, NodePing, Notify Ninja, Nutch-based Bot, Nuzzel, OAI-SearchBot, oBot, Octopus, Odin, Odnoklassniki Bot, Oh Dear, Omgili bot, OmtrBot, Onalytica, OnlineOrNot Bot, OpenGraph.io, Openindex Spider, OpenLinkProfiler, OpenWebSpider, Orange Bot, OSZKbot, Outbrain, Overcast Podcast Sync, OWLer, Pageburst, Page Modified Pinger, PagePeeker, PageThing, Pandalytics, Panscient, PaperLiBot, Paqlebot, parse.ly, PATHspider, PayPal IPN, PDR Labs, Peer39, PerplexityBot, Petal Bot, Phantomas, phpMyAdmin, PHP Server Monitor, Picsearch bot, Pigafetta, PingAdmin.Ru, Pingdom Bot, Pinterest, PiplBot, Plesk Screenshot Service, Plukkie, Pocket, Podroll Analyzer, PodUptime, Pompos, Prerender, PritTorrent, Probely, Project Patchwatch, Project Resonance, Prometheus, PRTG Network Monitor, Punk Map, Quantcast, QuerySeekerSpider, Quora Bot, Quora Link Preview, Qwantbot, Rainmeter, RamblerMail Image Proxy, RavenCrawler, Reddit Bot, RedekenBot, RenovateBot, Repo Lookout, ReqBin, researchcyber.net, Research JLU, Research Scan, Riddler, Robozilla, RocketMonitorBot, Rogerbot, ROI Hunter, RSSRadio Bot, RuxitSynthetic, Ryowl, SabsimBot, SafeDNSBot, Sandoba//Crawler, SBIder, Scamadviser External Hit, Scooter, ScoutJet, Scraping Robot, Scrapy, Screaming Frog SEO Spider, ScreenerBot, Sectigo DCV, security.txt scanserver, Seekport, Sellers.Guide, semaltbot, Semantic Scholar Bot, SemrushBot, Semrush Reputation Management, Sensika Bot, Sentry Bot, Senuto, Seobility, SEOENGBot, SEOkicks, SeolytBot, Seoscanners.net, Serendeputy Bot, Serenety, serpstatbot, Server Density, Seznam Bot, Seznam Email Proxy, Seznam Zbozi.cz, sfFeedReader, ShopAlike, Shopify Partner, ShopWiki, SilverReader, SimplePie, Sirdata, SISTRIX Crawler, SISTRIX Optimizer, Site24x7 Defacement Monitor, Site24x7 Website Monitoring, SiteAuditBot, Sitebulb, SiteCheckerBotCrawler, Siteimprove, SitemapParser-VIPnytt, SiteOne Crawler, SiteScore, SiteSucker, Sixy.ch, Skype URI Preview, Slackbot, SMTBot, Snapchat Ads, Snapchat Proxy, Snap URL Preview Service, SnoopSecInspect, Sogou Spider, Soso Spider, Sparkler, Spawning AI, Speedy, SpiderLing, Spinn3r, SplitSignalBot, Spotify, Sprinklr, Sputnik Bot, Sputnik Favicon Bot, Sputnik Image Bot, SSL Labs, start.me, Startpagina Linkchecker, Statista, StatOnline.ru, StatusCake, Steam Chat URL Lookup, Steve Bot, Stract, Sublinq, Substack Content Fetch, SuggestBot, Superfeedr Bot, SurdotlyBot, Survey Bot, Swiftbot, Swisscows Favicons, Synapse, t3versions, Taboolabot, TactiScout, Tag Inspector, Tarmot Gezgin, tchelebi, TelegramBot, Tenable.asm, TestCrawler, The British Library Legal Deposit Bot, The Knowledge AI, theoldreader, The Trade Desk Content, ThinkChaos, ThousandEyes, TigerBot, Timpibot, TinEye, TinEye Crawler, Tiny Tiny RSS, TLSProbe, TraceMyFile, Trendiction Bot, Trendsmap, Turnitin, TurnitinBot, TweetedTimes Bot, Tweetmeme Bot, Twingly Recon, Twitterbot, Twurly, UCSB Network Measurement, UkrNet Mail Proxy, uMBot, UniversalFeedParser, Uptime-Kuma, Uptimebot, UptimeRobot, Uptimia, URLAppendBot, URLinspector, URLSuMaBot, Vagabondo, ValidBot, Velen Public Web Crawler, Vercel Bot, VeryHip, Viber Url Downloader, VirusTotal Cloud, Visual Site Mapper Crawler, VK Robot, VK Share Button, VORTEX, vuhuvBot, VU Server Health Scanner, W3C CSS Validator, W3C I18N Checker, W3C Link Checker, W3C Markup Validation Service, W3C MobileOK Checker, W3C P3P Validator, W3C Unified Validator, Wappalyzer, WDG HTML Validator, WebbCrawler, WebCEO, WebDataStats, WebMon, Weborama, WebPageTest, WebPros, Website-info, WebSitePulse, WebThumbnail, webtru, Webwiki, WellKnownBot, WeSEE:Search, WeViKaBot, WhatCMS, WhatsMyIP.org, WhereGoes, Who.is Bot, Wibybot, WikiDo, Willow Internet Crawler, WireReaderBot, WooRank, WordPress, WordPress.com mShots, Workona, Wotbox, wp.com feedbot, WPMU DEV, XenForo, XoviBot, xtate, YaCy, Yahoo! Cache System, Yahoo! Japan, Yahoo! Japan ASR, Yahoo! Japan BRW, Yahoo! Japan WSC, Yahoo! Link Preview, Yahoo! Mail Proxy, Yahoo! Slurp, Yahoo Gemini, YaK, Yandex Bot, Yeti/Naverbot, Yottaa Site Monitor, YouBot, Youdao Bot, Yourls, Yunyun Bot, Zaldamo, Zao, Ze List, Zeno, zgrab, Zookabot, ZoomBot, ZoominfoBot, Zotero Translation Server, ZumBot diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Yaml/ParserInterface.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Yaml/ParserInterface.php new file mode 100644 index 0000000..e3aa2f0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/Yaml/ParserInterface.php @@ -0,0 +1,25 @@ +class. + * + * @param string $class + * the name of the class to load + * + * @return void + */ +function dd_autoload(string $class): void +{ + if (false === strpos($class, 'DeviceDetector\\')) { + return; + } + + $namespaceMap = ['DeviceDetector\\' => __DIR__ . '/']; + + foreach ($namespaceMap as $prefix => $dir) { + /* First swap out the namespace prefix with a directory... */ + $path = str_replace($prefix, $dir, $class); + /* replace the namespace separator with a directory separator... */ + $path = str_replace('\\', '/', $path); + /* and finally, add the PHP file extension to the result. */ + $path .= '.php'; + /* $path should now contain the path to a PHP file defining $class */ + require $path; + } +} + +spl_autoload_register('dd_autoload'); diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/composer.json b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/composer.json new file mode 100644 index 0000000..d464332 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/composer.json @@ -0,0 +1,57 @@ +{ + "name": "matomo/device-detector", + "type": "library", + "description": "The Universal Device Detection library, that parses User Agents and detects devices (desktop, tablet, mobile, tv, cars, console, etc.), clients (browsers, media players, mobile apps, feed readers, libraries, etc), operating systems, devices, brands and models.", + "keywords": ["useragent","parser","devicedetection"], + "homepage": "https://matomo.org", + "license": "LGPL-3.0-or-later", + "authors": [ + { + "name": "The Matomo Team", + "email": "hello@matomo.org", + "homepage": "https://matomo.org/team/" + } + ], + "support": { + "forum": "https://forum.matomo.org/", + "issues": "https://github.com/matomo-org/device-detector/issues", + "wiki": "https://dev.matomo.org/", + "source": "https://github.com/matomo-org/matomo" + }, + "autoload": { + "psr-4": { "DeviceDetector\\": "" }, + "exclude-from-classmap": ["Tests/"] + }, + "require": { + "php": "^7.2|^8.0", + "mustangostang/spyc": "*" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.8", + "psr/cache": "^1.0.1", + "psr/simple-cache": "^1.0.1", + "matthiasmullie/scrapbook": "^1.4.7", + "phpstan/phpstan": "^1.10.44", + "mayflower/mo4-coding-standard": "^v9.0.0", + "slevomat/coding-standard": "<8.16.0", + "symfony/yaml": "^5.1.7" + }, + "suggest": { + "doctrine/cache": "Can directly be used for caching purpose", + "ext-yaml": "Necessary for using the Pecl YAML parser" + }, + "scripts": { + "php-cs-fixed": "php vendor/bin/phpcbf" + }, + "archive": { + "exclude": ["/autoload.php"] + }, + "replace": { + "piwik/device-detector":"self.version" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/phpcs.xml b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/phpcs.xml new file mode 100644 index 0000000..1e17a22 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/phpcs.xml @@ -0,0 +1,111 @@ + + + Device Detector Coding Standard + + . + + vendor/ + + + + + + + + + + + + + + + + + + misc/ + Tests/ + + + + misc/ + autoload.php + + + + misc/ + autoload.php + + + + + + + + + misc/ + Tests/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/regexes/bots.yml b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/regexes/bots.yml new file mode 100644 index 0000000..c89e86f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/matomo/device-detector/regexes/bots.yml @@ -0,0 +1,5050 @@ +############### +# Device Detector - The Universal Device Detection library for parsing User Agents +# +# @link https://matomo.org +# @license http://www.gnu.org/licenses/lgpl.html LGPL v3 or later +############### + +- regex: 'WireReaderBot' + name: 'WireReaderBot' + category: 'Feed Fetcher' + url: 'https://wirereader.app/' + +- regex: 'monitoring360bot' + name: '360 Monitoring' + category: 'Site Monitor' + url: 'https://www.360monitoring.io' + producer: + name: 'Plesk International GmbH' + url: 'https://www.plesk.com' + +- regex: 'Cloudflare-Healthchecks' + name: 'Cloudflare Health Checks' + category: 'Service Agent' + url: 'https://developers.cloudflare.com/health-checks/' + producer: + name: 'CloudFlare' + url: 'https://www.cloudflare.com/' + +- regex: '360Spider' + name: '360Spider' + category: 'Search bot' + url: 'https://www.so.com/help/help_3_2.html' + producer: + name: 'Online Media Group, Inc.' + url: '' + +- regex: 'Aboundex' + name: 'Aboundexbot' + category: 'Search bot' + url: 'http://www.aboundex.com/crawler/' + producer: + name: 'Aboundex.com' + url: 'http://www.aboundex.com' + +- regex: 'AcoonBot' + name: 'Acoon' + category: 'Search bot' + url: 'http://www.acoon.de/robot.asp' + producer: + name: 'Acoon GmbH' + url: 'http://www.acoon.de' + +- regex: 'AddThis\.com' + name: 'AddThis.com' + category: 'Social Media Agent' + url: '' + producer: + name: 'Clearspring Technologies, Inc.' + url: 'http://www.clearspring.com' + +- regex: 'AhrefsBot' + name: 'aHrefs Bot' + category: 'Crawler' + url: 'https://ahrefs.com/robot' + producer: + name: 'Ahrefs Pte Ltd' + url: 'https://ahrefs.com/robot' + +- regex: 'AhrefsSiteAudit' + name: 'AhrefsSiteAudit' + category: 'Site Monitor' + url: 'https://ahrefs.com/robot/site-audit' + producer: + name: 'Ahrefs Pte Ltd' + url: 'https://ahrefs.com/' + +- regex: 'ia_archiver|alexabot|verifybot' + name: 'Alexa Crawler' + category: 'Search bot' + url: 'https://support.alexa.com/hc/en-us/sections/200100794-Crawlers' + producer: + name: 'Alexa Internet' + url: 'https://www.alexa.com' + +- regex: 'alexa site audit' + name: 'Alexa Site Audit' + category: 'Site Monitor' + url: 'https://support.alexa.com/hc/en-us/articles/200450194' + producer: + name: 'Alexa Internet' + url: 'https://www.alexa.com' + +- regex: 'Amazonbot' + name: 'Amazon Bot' + category: 'Crawler' + url: 'https://developer.amazon.com/support/amazonbot' + producer: + name: 'Amazon.com, Inc.' + url: 'https://www.amazon.com/' + +- regex: 'AmazonAdBot' + name: 'Amazon AdBot' + category: 'Crawler' + url: 'https://adbot.amazon.com/' + producer: + name: 'Amazon.com, Inc.' + url: 'https://www.amazon.com/' + +- regex: 'Amazon[ -]Route ?53[ -]Health[ -]Check[ -]Service' + name: 'Amazon Route53 Health Check' + category: 'Service Agent' + producer: + name: 'Amazon Web Services' + url: 'https://aws.amazon.com/' + +- regex: 'AmorankSpider' + name: 'Amorank Spider' + category: 'Crawler' + url: 'http://amorank.com/webcrawler.html' + producer: + name: 'Amorank' + url: 'http://www.amorank.com' + +- regex: 'ApacheBench' + name: 'ApacheBench' + category: 'Benchmark' + url: 'https://httpd.apache.org/docs/2.4/programs/ab.html' + producer: + name: 'The Apache Software Foundation' + url: 'https://www.apache.org/foundation/' + +- regex: 'Applebot' + name: 'Applebot' + category: 'Crawler' + url: 'https://support.apple.com/en-us/119829' + producer: + name: 'Apple Inc' + url: 'https://www.apple.com/' + +- regex: 'iTMS' + name: 'iTMS' + category: 'Crawler' + url: 'https://support.apple.com/en-us/119829' + producer: + name: 'Apple Inc' + url: 'https://www.apple.com/' + +- regex: 'AppSignalBot' + name: 'AppSignalBot' + category: 'Site Monitor' + url: 'https://docs.appsignal.com/uptime-monitoring/' + producer: + name: 'AppSignal' + url: 'https://appsignal.com/' + +- regex: 'Arachni' + name: 'Arachni' + category: 'Security Checker' + url: 'https://www.arachni-scanner.com/' + producer: + name: 'Sarosys LLC' + url: 'https://www.sarosys.com/' + +- regex: 'AspiegelBot' + name: 'AspiegelBot' + category: 'Crawler' + url: 'https://aspiegel.com/' + producer: + name: 'Huawei' + url: 'https://www.huawei.com/' + +- regex: 'Castro 2, Episode Duration Lookup' + name: 'Castro 2' + category: 'Service Agent' + url: 'http://supertop.co/castro/' + producer: + name: 'Supertop' + url: 'http://supertop.co' + +- regex: 'Curious George' + name: 'Analytics SEO Crawler' + category: 'Crawler' + url: 'http://www.analyticsseo.com/crawler' + producer: + name: 'Analytics SEO' + url: 'http://www.analyticsseo.com' + +- regex: 'archive\.org_bot|special_archiver' + name: 'archive.org bot' + category: 'Crawler' + url: 'https://archive.org/details/archive.org_bot' + producer: + name: 'The Internet Archive' + url: 'https://archive.org' + +- regex: 'Ask Jeeves/Teoma' + name: 'Ask Jeeves' + category: 'Search bot' + url: '' + producer: + name: 'Ask Jeeves Inc.' + url: 'http://www.ask.com' + +- regex: 'Backlink-Check\.de' + name: 'Backlink-Check.de' + category: 'Crawler' + url: 'http://www.backlink-check.de/bot.html' + producer: + name: 'Mediagreen Medienservice' + url: 'http://www.backlink-check.de' + +- regex: 'BacklinkCrawler' + name: 'BacklinkCrawler' + category: 'Crawler' + url: 'http://www.backlinktest.com/crawler.html' + producer: + name: '2.0Promotion GbR' + url: 'http://www.backlinktest.com' + +- regex: 'Baidu.*spider|baidu Transcoder' + name: 'Baidu Spider' + category: 'Search bot' + url: 'http://www.baidu.com/search/spider.htm' + producer: + name: 'Baidu' + url: 'http://www.baidu.com' + +- regex: 'BazQux' + name: 'BazQux Reader' + url: 'https://bazqux.com/fetcher' + category: 'Feed Fetcher' + producer: + name: '' + url: '' + +- regex: 'Better Uptime Bot' + name: 'Better Uptime Bot' + category: 'Site Monitor' + url: 'https://betteruptime.com/faq' + producer: + name: 'Better Uptime' + url: 'https://betteruptime.com/' + +- regex: 'MSNBot|msrbot|bingbot|bingadsbot|BingPreview|msnbot-(UDiscovery|NewsBlogs)|adidxbot' + name: 'BingBot' + category: 'Search bot' + url: 'http://search.msn.com/msnbot.htmn' + producer: + name: 'Microsoft Corporation' + url: 'http://www.microsoft.com' + +- regex: 'Blackbox Exporter' + name: 'Blackbox Exporter' + category: 'Site Monitor' + url: 'https://github.com/prometheus/blackbox_exporter' + producer: + name: 'Prometheus' + url: 'https://prometheus.io/' + +- regex: 'Blekkobot' + name: 'Blekkobot' + category: 'Search bot' + url: 'http://blekko.com/about/blekkobot' + producer: + name: 'Blekko' + url: 'http://blekko.com' + +- regex: 'BLEXBot' + name: 'BLEXBot Crawler' + category: 'Crawler' + url: 'http://webmeup-crawler.com' + producer: + name: 'WebMeUp' + url: 'http://webmeup.com' + +- regex: 'Bloglovin' + name: 'Bloglovin' + url: 'http://www.bloglovin.com' + category: 'Feed Fetcher' + producer: + name: '' + url: '' + +- regex: 'Blogtrottr' + name: 'Blogtrottr' + url: '' + category: 'Feed Fetcher' + producer: + name: 'Blogtrottr Ltd' + url: 'https://blogtrottr.com/' + +- regex: 'BoardReader Blog Indexer' + name: 'BoardReader Blog Indexer' + category: 'Crawler' + producer: + name: 'BoardReader' + url: 'https://boardreader.com/' + +- regex: 'BountiiBot' + name: 'Bountii Bot' + category: 'Search bot' + url: 'http://bountii.com/contact.php' + producer: + name: 'Bountii Inc.' + url: 'http://bountii.com' + +- regex: 'Browsershots' + name: 'Browsershots' + category: 'Service Agent' + url: 'http://browsershots.org/faq' + producer: + name: 'Browsershots.org' + url: 'http://browsershots.org' + +- regex: 'BUbiNG' + name: 'BUbiNG' + category: 'Crawler' + url: 'http://law.di.unimi.it/BUbiNG.html' + producer: + name: 'The Laboratory for Web Algorithmics (LAW)' + url: 'http://law.di.unimi.it/software.php#buging' + +- regex: '(? + * @author Chris Wanstrath + * @link https://github.com/mustangostang/spyc/ + * @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2011 Vlad Andersen + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @package Spyc + */ + +if (!function_exists('spyc_load')) { + /** + * Parses YAML to array. + * @param string $string YAML string. + * @return array + */ + function spyc_load ($string) { + return Spyc::YAMLLoadString($string); + } +} + +if (!function_exists('spyc_load_file')) { + /** + * Parses YAML to array. + * @param string $file Path to YAML file. + * @return array + */ + function spyc_load_file ($file) { + return Spyc::YAMLLoad($file); + } +} + +if (!function_exists('spyc_dump')) { + /** + * Dumps array to YAML. + * @param array $data Array. + * @return string + */ + function spyc_dump ($data) { + return Spyc::YAMLDump($data, false, false, true); + } +} + +if (!class_exists('Spyc')) { + +/** + * The Simple PHP YAML Class. + * + * This class can be used to read a YAML file and convert its contents + * into a PHP array. It currently supports a very limited subsection of + * the YAML spec. + * + * Usage: + * + * $Spyc = new Spyc; + * $array = $Spyc->load($file); + * + * or: + * + * $array = Spyc::YAMLLoad($file); + * + * or: + * + * $array = spyc_load_file($file); + * + * @package Spyc + */ +class Spyc { + + // SETTINGS + + const REMPTY = "\0\0\0\0\0"; + + /** + * Setting this to true will force YAMLDump to enclose any string value in + * quotes. False by default. + * + * @var bool + */ + public $setting_dump_force_quotes = false; + + /** + * Setting this to true will forse YAMLLoad to use syck_load function when + * possible. False by default. + * @var bool + */ + public $setting_use_syck_is_possible = false; + + /** + * Setting this to true will forse YAMLLoad to use syck_load function when + * possible. False by default. + * @var bool + */ + public $setting_empty_hash_as_object = false; + + + /**#@+ + * @access private + * @var mixed + */ + private $_dumpIndent; + private $_dumpWordWrap; + private $_containsGroupAnchor = false; + private $_containsGroupAlias = false; + private $path; + private $result; + private $LiteralPlaceHolder = '___YAML_Literal_Block___'; + private $SavedGroups = array(); + private $indent; + /** + * Path modifier that should be applied after adding current element. + * @var array + */ + private $delayedPath = array(); + + /**#@+ + * @access public + * @var mixed + */ + public $_nodeId; + +/** + * Load a valid YAML string to Spyc. + * @param string $input + * @return array + */ + public function load ($input) { + return $this->_loadString($input); + } + + /** + * Load a valid YAML file to Spyc. + * @param string $file + * @return array + */ + public function loadFile ($file) { + return $this->_load($file); + } + + /** + * Load YAML into a PHP array statically + * + * The load method, when supplied with a YAML stream (string or file), + * will do its best to convert YAML in a file into a PHP array. Pretty + * simple. + * Usage: + * + * $array = Spyc::YAMLLoad('lucky.yaml'); + * print_r($array); + * + * @access public + * @return array + * @param string $input Path of YAML file or string containing YAML + * @param array set options + */ + public static function YAMLLoad($input, $options = []) { + $Spyc = new Spyc; + foreach ($options as $key => $value) { + if (property_exists($Spyc, $key)) { + $Spyc->$key = $value; + } + } + return $Spyc->_load($input); + } + + /** + * Load a string of YAML into a PHP array statically + * + * The load method, when supplied with a YAML string, will do its best + * to convert YAML in a string into a PHP array. Pretty simple. + * + * Note: use this function if you don't want files from the file system + * loaded and processed as YAML. This is of interest to people concerned + * about security whose input is from a string. + * + * Usage: + * + * $array = Spyc::YAMLLoadString("---\n0: hello world\n"); + * print_r($array); + * + * @access public + * @return array + * @param string $input String containing YAML + * @param array set options + */ + public static function YAMLLoadString($input, $options = []) { + $Spyc = new Spyc; + foreach ($options as $key => $value) { + if (property_exists($Spyc, $key)) { + $Spyc->$key = $value; + } + } + return $Spyc->_loadString($input); + } + + /** + * Dump YAML from PHP array statically + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as nothing.yaml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @access public + * @return string + * @param array|\stdClass $array PHP array + * @param int $indent Pass in false to use the default, which is 2 + * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) + * @param bool $no_opening_dashes Do not start YAML file with "---\n" + */ + public static function YAMLDump($array, $indent = false, $wordwrap = false, $no_opening_dashes = false) { + $spyc = new Spyc; + return $spyc->dump($array, $indent, $wordwrap, $no_opening_dashes); + } + + + /** + * Dump PHP array to YAML + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as tasteful.yaml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @access public + * @return string + * @param array $array PHP array + * @param int $indent Pass in false to use the default, which is 2 + * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) + */ + public function dump($array,$indent = false,$wordwrap = false, $no_opening_dashes = false) { + // Dumps to some very clean YAML. We'll have to add some more features + // and options soon. And better support for folding. + + // New features and options. + if ($indent === false or !is_numeric($indent)) { + $this->_dumpIndent = 2; + } else { + $this->_dumpIndent = $indent; + } + + if ($wordwrap === false or !is_numeric($wordwrap)) { + $this->_dumpWordWrap = 40; + } else { + $this->_dumpWordWrap = $wordwrap; + } + + // New YAML document + $string = ""; + if (!$no_opening_dashes) $string = "---\n"; + + // Start at the base of the array and move through it. + if ($array) { + $array = (array)$array; + $previous_key = -1; + foreach ($array as $key => $value) { + if (!isset($first_key)) $first_key = $key; + $string .= $this->_yamlize($key,$value,0,$previous_key, $first_key, $array); + $previous_key = $key; + } + } + return $string; + } + + /** + * Attempts to convert a key / value array item to YAML + * @access private + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + private function _yamlize($key,$value,$indent, $previous_key = -1, $first_key = 0, $source_array = null) { + if(is_object($value)) $value = (array)$value; + if (is_array($value)) { + if (empty ($value)) + return $this->_dumpNode($key, array(), $indent, $previous_key, $first_key, $source_array); + // It has children. What to do? + // Make it the right kind of item + $string = $this->_dumpNode($key, self::REMPTY, $indent, $previous_key, $first_key, $source_array); + // Add the indent + $indent += $this->_dumpIndent; + // Yamlize the array + $string .= $this->_yamlizeArray($value,$indent); + } elseif (!is_array($value)) { + // It doesn't have children. Yip. + $string = $this->_dumpNode($key, $value, $indent, $previous_key, $first_key, $source_array); + } + return $string; + } + + /** + * Attempts to convert an array to YAML + * @access private + * @return string + * @param $array The array you want to convert + * @param $indent The indent of the current level + */ + private function _yamlizeArray($array,$indent) { + if (is_array($array)) { + $string = ''; + $previous_key = -1; + foreach ($array as $key => $value) { + if (!isset($first_key)) $first_key = $key; + $string .= $this->_yamlize($key, $value, $indent, $previous_key, $first_key, $array); + $previous_key = $key; + } + return $string; + } else { + return false; + } + } + + /** + * Returns YAML from a key and a value + * @access private + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + private function _dumpNode($key, $value, $indent, $previous_key = -1, $first_key = 0, $source_array = null) { + // do some folding here, for blocks + if (is_string ($value) && ((strpos($value,"\n") !== false || strpos($value,": ") !== false || strpos($value,"- ") !== false || + strpos($value,"*") !== false || strpos($value,"#") !== false || strpos($value,"<") !== false || strpos($value,">") !== false || strpos ($value, '%') !== false || strpos ($value, ' ') !== false || + strpos($value,"[") !== false || strpos($value,"]") !== false || strpos($value,"{") !== false || strpos($value,"}") !== false) || strpos($value,"&") !== false || strpos($value, "'") !== false || strpos($value, "!") === 0 || + substr ($value, -1, 1) == ':') + ) { + $value = $this->_doLiteralBlock($value,$indent); + } else { + $value = $this->_doFolding($value,$indent); + } + + if ($value === array()) $value = '[ ]'; + if ($value === "") $value = '""'; + if (self::isTranslationWord($value)) { + $value = $this->_doLiteralBlock($value, $indent); + } + if (trim ($value) != $value) + $value = $this->_doLiteralBlock($value,$indent); + + if (is_bool($value)) { + $value = $value ? "true" : "false"; + } + + if ($value === null) $value = 'null'; + if ($value === "'" . self::REMPTY . "'") $value = null; + + $spaces = str_repeat(' ',$indent); + + //if (is_int($key) && $key - 1 == $previous_key && $first_key===0) { + if (is_array ($source_array) && array_keys($source_array) === range(0, count($source_array) - 1)) { + // It's a sequence + $string = $spaces.'- '.$value."\n"; + } else { + // if ($first_key===0) throw new Exception('Keys are all screwy. The first one was zero, now it\'s "'. $key .'"'); + // It's mapped + if (strpos($key, ":") !== false || strpos($key, "#") !== false) { $key = '"' . $key . '"'; } + $string = rtrim ($spaces.$key.': '.$value)."\n"; + } + return $string; + } + + /** + * Creates a literal block for dumping + * @access private + * @return string + * @param $value + * @param $indent int The value of the indent + */ + private function _doLiteralBlock($value,$indent) { + if ($value === "\n") return '\n'; + if (strpos($value, "\n") === false && strpos($value, "'") === false) { + return sprintf ("'%s'", $value); + } + if (strpos($value, "\n") === false && strpos($value, '"') === false) { + return sprintf ('"%s"', $value); + } + $exploded = explode("\n",$value); + $newValue = '|'; + if (isset($exploded[0]) && ($exploded[0] == "|" || $exploded[0] == "|-" || $exploded[0] == ">")) { + $newValue = $exploded[0]; + unset($exploded[0]); + } + $indent += $this->_dumpIndent; + $spaces = str_repeat(' ',$indent); + foreach ($exploded as $line) { + $line = trim($line); + if (strpos($line, '"') === 0 && strrpos($line, '"') == (strlen($line)-1) || strpos($line, "'") === 0 && strrpos($line, "'") == (strlen($line)-1)) { + $line = substr($line, 1, -1); + } + $newValue .= "\n" . $spaces . ($line); + } + return $newValue; + } + + /** + * Folds a string of text, if necessary + * @access private + * @return string + * @param $value The string you wish to fold + */ + private function _doFolding($value,$indent) { + // Don't do anything if wordwrap is set to 0 + + if ($this->_dumpWordWrap !== 0 && is_string ($value) && strlen($value) > $this->_dumpWordWrap) { + $indent += $this->_dumpIndent; + $indent = str_repeat(' ',$indent); + $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); + $value = ">\n".$indent.$wrapped; + } else { + if ($this->setting_dump_force_quotes && is_string ($value) && $value !== self::REMPTY) + $value = '"' . $value . '"'; + if (is_numeric($value) && is_string($value)) + $value = '"' . $value . '"'; + } + + + return $value; + } + + private function isTrueWord($value) { + $words = self::getTranslations(array('true', 'on', 'yes', 'y')); + return in_array($value, $words, true); + } + + private function isFalseWord($value) { + $words = self::getTranslations(array('false', 'off', 'no', 'n')); + return in_array($value, $words, true); + } + + private function isNullWord($value) { + $words = self::getTranslations(array('null', '~')); + return in_array($value, $words, true); + } + + private function isTranslationWord($value) { + return ( + self::isTrueWord($value) || + self::isFalseWord($value) || + self::isNullWord($value) + ); + } + + /** + * Coerce a string into a native type + * Reference: http://yaml.org/type/bool.html + * TODO: Use only words from the YAML spec. + * @access private + * @param $value The value to coerce + */ + private function coerceValue(&$value) { + if (self::isTrueWord($value)) { + $value = true; + } else if (self::isFalseWord($value)) { + $value = false; + } else if (self::isNullWord($value)) { + $value = null; + } + } + + /** + * Given a set of words, perform the appropriate translations on them to + * match the YAML 1.1 specification for type coercing. + * @param $words The words to translate + * @access private + */ + private static function getTranslations(array $words) { + $result = array(); + foreach ($words as $i) { + $result = array_merge($result, array(ucfirst($i), strtoupper($i), strtolower($i))); + } + return $result; + } + +// LOADING FUNCTIONS + + private function _load($input) { + $Source = $this->loadFromSource($input); + return $this->loadWithSource($Source); + } + + private function _loadString($input) { + $Source = $this->loadFromString($input); + return $this->loadWithSource($Source); + } + + private function loadWithSource($Source) { + if (empty ($Source)) return array(); + if ($this->setting_use_syck_is_possible && function_exists ('syck_load')) { + $array = syck_load (implode ("\n", $Source)); + return is_array($array) ? $array : array(); + } + + $this->path = array(); + $this->result = array(); + + $cnt = count($Source); + for ($i = 0; $i < $cnt; $i++) { + $line = $Source[$i]; + + $this->indent = strlen($line) - strlen(ltrim($line)); + $tempPath = $this->getParentPathByIndent($this->indent); + $line = self::stripIndent($line, $this->indent); + if (self::isComment($line)) continue; + if (self::isEmpty($line)) continue; + $this->path = $tempPath; + + $literalBlockStyle = self::startsLiteralBlock($line); + if ($literalBlockStyle) { + $line = rtrim ($line, $literalBlockStyle . " \n"); + $literalBlock = ''; + $line .= ' '.$this->LiteralPlaceHolder; + $literal_block_indent = strlen($Source[$i+1]) - strlen(ltrim($Source[$i+1])); + while (++$i < $cnt && $this->literalBlockContinues($Source[$i], $this->indent)) { + $literalBlock = $this->addLiteralLine($literalBlock, $Source[$i], $literalBlockStyle, $literal_block_indent); + } + $i--; + } + + // Strip out comments + if (strpos ($line, '#')) { + $line = preg_replace('/\s*#([^"\']+)$/','',$line); + } + + while (++$i < $cnt && self::greedilyNeedNextLine($line)) { + $line = rtrim ($line, " \n\t\r") . ' ' . ltrim ($Source[$i], " \t"); + } + $i--; + + $lineArray = $this->_parseLine($line); + + if ($literalBlockStyle) + $lineArray = $this->revertLiteralPlaceHolder ($lineArray, $literalBlock); + + $this->addArray($lineArray, $this->indent); + + foreach ($this->delayedPath as $indent => $delayedPath) + $this->path[$indent] = $delayedPath; + + $this->delayedPath = array(); + + } + return $this->result; + } + + private function loadFromSource ($input) { + if (!empty($input) && strpos($input, "\n") === false && file_exists($input)) + $input = file_get_contents($input); + + return $this->loadFromString($input); + } + + private function loadFromString ($input) { + $lines = explode("\n",$input); + foreach ($lines as $k => $_) { + $lines[$k] = rtrim ($_, "\r"); + } + return $lines; + } + + /** + * Parses YAML code and returns an array for a node + * @access private + * @return array + * @param string $line A line from the YAML file + */ + private function _parseLine($line) { + if (!$line) return array(); + $line = trim($line); + if (!$line) return array(); + + $array = array(); + + $group = $this->nodeContainsGroup($line); + if ($group) { + $this->addGroup($line, $group); + $line = $this->stripGroup ($line, $group); + } + + if ($this->startsMappedSequence($line)) { + return $this->returnMappedSequence($line); + } + + if ($this->startsMappedValue($line)) { + return $this->returnMappedValue($line); + } + + if ($this->isArrayElement($line)) + return $this->returnArrayElement($line); + + if ($this->isPlainArray($line)) + return $this->returnPlainArray($line); + + return $this->returnKeyValuePair($line); + + } + + /** + * Finds the type of the passed value, returns the value as the new type. + * @access private + * @param string $value + * @return mixed + */ + private function _toType($value) { + if ($value === '') return ""; + + if ($this->setting_empty_hash_as_object && $value === '{}') { + return new stdClass(); + } + + $first_character = $value[0]; + $last_character = substr($value, -1, 1); + + $is_quoted = false; + do { + if (!$value) break; + if ($first_character != '"' && $first_character != "'") break; + if ($last_character != '"' && $last_character != "'") break; + $is_quoted = true; + } while (0); + + if ($is_quoted) { + $value = str_replace('\n', "\n", $value); + if ($first_character == "'") + return strtr(substr ($value, 1, -1), array ('\'\'' => '\'', '\\\''=> '\'')); + return strtr(substr ($value, 1, -1), array ('\\"' => '"', '\\\''=> '\'')); + } + + if (strpos($value, ' #') !== false && !$is_quoted) + $value = preg_replace('/\s+#(.+)$/','',$value); + + if ($first_character == '[' && $last_character == ']') { + // Take out strings sequences and mappings + $innerValue = trim(substr ($value, 1, -1)); + if ($innerValue === '') return array(); + $explode = $this->_inlineEscape($innerValue); + // Propagate value array + $value = array(); + foreach ($explode as $v) { + $value[] = $this->_toType($v); + } + return $value; + } + + if (strpos($value,': ')!==false && $first_character != '{') { + $array = explode(': ',$value); + $key = trim($array[0]); + array_shift($array); + $value = trim(implode(': ',$array)); + $value = $this->_toType($value); + return array($key => $value); + } + + if ($first_character == '{' && $last_character == '}') { + $innerValue = trim(substr ($value, 1, -1)); + if ($innerValue === '') return array(); + // Inline Mapping + // Take out strings sequences and mappings + $explode = $this->_inlineEscape($innerValue); + // Propagate value array + $array = array(); + foreach ($explode as $v) { + $SubArr = $this->_toType($v); + if (empty($SubArr)) continue; + if (is_array ($SubArr)) { + $array[key($SubArr)] = $SubArr[key($SubArr)]; continue; + } + $array[] = $SubArr; + } + return $array; + } + + if ($value == 'null' || $value == 'NULL' || $value == 'Null' || $value == '' || $value == '~') { + return null; + } + + if ( is_numeric($value) && preg_match ('/^(-|)[1-9]+[0-9]*$/', $value) ){ + $intvalue = (int)$value; + if ($intvalue != PHP_INT_MAX && $intvalue != ~PHP_INT_MAX) + $value = $intvalue; + return $value; + } + + if ( is_string($value) && preg_match('/^0[xX][0-9a-fA-F]+$/', $value)) { + // Hexadecimal value. + return hexdec($value); + } + + $this->coerceValue($value); + + if (is_numeric($value)) { + if ($value === '0') return 0; + if (rtrim ($value, 0) === $value) + $value = (float)$value; + return $value; + } + + return $value; + } + + /** + * Used in inlines to check for more inlines or quoted strings + * @access private + * @return array + */ + private function _inlineEscape($inline) { + // There's gotta be a cleaner way to do this... + // While pure sequences seem to be nesting just fine, + // pure mappings and mappings with sequences inside can't go very + // deep. This needs to be fixed. + + $seqs = array(); + $maps = array(); + $saved_strings = array(); + $saved_empties = array(); + + // Check for empty strings + $regex = '/("")|(\'\')/'; + if (preg_match_all($regex,$inline,$strings)) { + $saved_empties = $strings[0]; + $inline = preg_replace($regex,'YAMLEmpty',$inline); + } + unset($regex); + + // Check for strings + $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; + if (preg_match_all($regex,$inline,$strings)) { + $saved_strings = $strings[0]; + $inline = preg_replace($regex,'YAMLString',$inline); + } + unset($regex); + + // echo $inline; + + $i = 0; + do { + + // Check for sequences + while (preg_match('/\[([^{}\[\]]+)\]/U',$inline,$matchseqs)) { + $seqs[] = $matchseqs[0]; + $inline = preg_replace('/\[([^{}\[\]]+)\]/U', ('YAMLSeq' . (count($seqs) - 1) . 's'), $inline, 1); + } + + // Check for mappings + while (preg_match('/{([^\[\]{}]+)}/U',$inline,$matchmaps)) { + $maps[] = $matchmaps[0]; + $inline = preg_replace('/{([^\[\]{}]+)}/U', ('YAMLMap' . (count($maps) - 1) . 's'), $inline, 1); + } + + if ($i++ >= 10) break; + + } while (strpos ($inline, '[') !== false || strpos ($inline, '{') !== false); + + $explode = explode(',',$inline); + $explode = array_map('trim', $explode); + $stringi = 0; $i = 0; + + while (1) { + + // Re-add the sequences + if (!empty($seqs)) { + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLSeq') !== false) { + foreach ($seqs as $seqk => $seq) { + $explode[$key] = str_replace(('YAMLSeq'.$seqk.'s'),$seq,$value); + $value = $explode[$key]; + } + } + } + } + + // Re-add the mappings + if (!empty($maps)) { + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLMap') !== false) { + foreach ($maps as $mapk => $map) { + $explode[$key] = str_replace(('YAMLMap'.$mapk.'s'), $map, $value); + $value = $explode[$key]; + } + } + } + } + + + // Re-add the strings + if (!empty($saved_strings)) { + foreach ($explode as $key => $value) { + while (strpos($value,'YAMLString') !== false) { + $explode[$key] = preg_replace('/YAMLString/',$saved_strings[$stringi],$value, 1); + unset($saved_strings[$stringi]); + ++$stringi; + $value = $explode[$key]; + } + } + } + + + // Re-add the empties + if (!empty($saved_empties)) { + foreach ($explode as $key => $value) { + while (strpos($value,'YAMLEmpty') !== false) { + $explode[$key] = preg_replace('/YAMLEmpty/', '', $value, 1); + $value = $explode[$key]; + } + } + } + + $finished = true; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLSeq') !== false) { + $finished = false; break; + } + if (strpos($value,'YAMLMap') !== false) { + $finished = false; break; + } + if (strpos($value,'YAMLString') !== false) { + $finished = false; break; + } + if (strpos($value,'YAMLEmpty') !== false) { + $finished = false; break; + } + } + if ($finished) break; + + $i++; + if ($i > 10) + break; // Prevent infinite loops. + } + + + return $explode; + } + + private function literalBlockContinues ($line, $lineIndent) { + if (!trim($line)) return true; + if (strlen($line) - strlen(ltrim($line)) > $lineIndent) return true; + return false; + } + + private function referenceContentsByAlias ($alias) { + do { + if (!isset($this->SavedGroups[$alias])) { echo "Bad group name: $alias."; break; } + $groupPath = $this->SavedGroups[$alias]; + $value = $this->result; + foreach ($groupPath as $k) { + $value = $value[$k]; + } + } while (false); + return $value; + } + + private function addArrayInline ($array, $indent) { + $CommonGroupPath = $this->path; + if (empty ($array)) return false; + + foreach ($array as $k => $_) { + $this->addArray(array($k => $_), $indent); + $this->path = $CommonGroupPath; + } + return true; + } + + private function addArray ($incoming_data, $incoming_indent) { + + // print_r ($incoming_data); + + if (count ($incoming_data) > 1) + return $this->addArrayInline ($incoming_data, $incoming_indent); + + $key = key ($incoming_data); + $value = isset($incoming_data[$key]) ? $incoming_data[$key] : null; + if ($key === '__!YAMLZero') $key = '0'; + + if ($incoming_indent == 0 && !$this->_containsGroupAlias && !$this->_containsGroupAnchor) { // Shortcut for root-level values. + if ($key || $key === '' || $key === '0') { + $this->result[$key] = $value; + } else { + $this->result[] = $value; end ($this->result); $key = key ($this->result); + } + $this->path[$incoming_indent] = $key; + return; + } + + + + $history = array(); + // Unfolding inner array tree. + $history[] = $_arr = $this->result; + foreach ($this->path as $k) { + $history[] = $_arr = $_arr[$k]; + } + + if ($this->_containsGroupAlias) { + $value = $this->referenceContentsByAlias($this->_containsGroupAlias); + $this->_containsGroupAlias = false; + } + + + // Adding string or numeric key to the innermost level or $this->arr. + if (is_string($key) && $key == '<<') { + if (!is_array ($_arr)) { $_arr = array (); } + + $_arr = array_merge ($_arr, $value); + } else if ($key || $key === '' || $key === '0') { + if (!is_array ($_arr)) + $_arr = array ($key=>$value); + else + $_arr[$key] = $value; + } else { + if (!is_array ($_arr)) { $_arr = array ($value); $key = 0; } + else { $_arr[] = $value; end ($_arr); $key = key ($_arr); } + } + + $reverse_path = array_reverse($this->path); + $reverse_history = array_reverse ($history); + $reverse_history[0] = $_arr; + $cnt = count($reverse_history) - 1; + for ($i = 0; $i < $cnt; $i++) { + $reverse_history[$i+1][$reverse_path[$i]] = $reverse_history[$i]; + } + $this->result = $reverse_history[$cnt]; + + $this->path[$incoming_indent] = $key; + + if ($this->_containsGroupAnchor) { + $this->SavedGroups[$this->_containsGroupAnchor] = $this->path; + if (is_array ($value)) { + $k = key ($value); + if (!is_int ($k)) { + $this->SavedGroups[$this->_containsGroupAnchor][$incoming_indent + 2] = $k; + } + } + $this->_containsGroupAnchor = false; + } + + } + + private static function startsLiteralBlock ($line) { + $lastChar = substr (trim($line), -1); + if ($lastChar != '>' && $lastChar != '|') return false; + if ($lastChar == '|') return $lastChar; + // HTML tags should not be counted as literal blocks. + if (preg_match ('#<.*?>$#', $line)) return false; + return $lastChar; + } + + private static function greedilyNeedNextLine($line) { + $line = trim ($line); + if (!strlen($line)) return false; + if (substr ($line, -1, 1) == ']') return false; + if ($line[0] == '[') return true; + if (preg_match ('#^[^:]+?:\s*\[#', $line)) return true; + return false; + } + + private function addLiteralLine ($literalBlock, $line, $literalBlockStyle, $indent = -1) { + $line = self::stripIndent($line, $indent); + if ($literalBlockStyle !== '|') { + $line = self::stripIndent($line); + } + $line = rtrim ($line, "\r\n\t ") . "\n"; + if ($literalBlockStyle == '|') { + return $literalBlock . $line; + } + if (strlen($line) == 0) + return rtrim($literalBlock, ' ') . "\n"; + if ($line == "\n" && $literalBlockStyle == '>') { + return rtrim ($literalBlock, " \t") . "\n"; + } + if ($line != "\n") + $line = trim ($line, "\r\n ") . " "; + return $literalBlock . $line; + } + + function revertLiteralPlaceHolder ($lineArray, $literalBlock) { + foreach ($lineArray as $k => $_) { + if (is_array($_)) + $lineArray[$k] = $this->revertLiteralPlaceHolder ($_, $literalBlock); + else if (substr($_, -1 * strlen ($this->LiteralPlaceHolder)) == $this->LiteralPlaceHolder) + $lineArray[$k] = rtrim ($literalBlock, " \r\n"); + } + return $lineArray; + } + + private static function stripIndent ($line, $indent = -1) { + if ($indent == -1) $indent = strlen($line) - strlen(ltrim($line)); + return substr ($line, $indent); + } + + private function getParentPathByIndent ($indent) { + if ($indent == 0) return array(); + $linePath = $this->path; + do { + end($linePath); $lastIndentInParentPath = key($linePath); + if ($indent <= $lastIndentInParentPath) array_pop ($linePath); + } while ($indent <= $lastIndentInParentPath); + return $linePath; + } + + + private function clearBiggerPathValues ($indent) { + + + if ($indent == 0) $this->path = array(); + if (empty ($this->path)) return true; + + foreach ($this->path as $k => $_) { + if ($k > $indent) unset ($this->path[$k]); + } + + return true; + } + + + private static function isComment ($line) { + if (!$line) return false; + if ($line[0] == '#') return true; + if (trim($line, " \r\n\t") == '---') return true; + return false; + } + + private static function isEmpty ($line) { + return (trim ($line) === ''); + } + + + private function isArrayElement ($line) { + if (!$line || !is_scalar($line)) return false; + if (substr($line, 0, 2) != '- ') return false; + if (strlen ($line) > 3) + if (substr($line,0,3) == '---') return false; + + return true; + } + + private function isHashElement ($line) { + return strpos($line, ':'); + } + + private function isLiteral ($line) { + if ($this->isArrayElement($line)) return false; + if ($this->isHashElement($line)) return false; + return true; + } + + + private static function unquote ($value) { + if (!$value) return $value; + if (!is_string($value)) return $value; + if ($value[0] == '\'') return trim ($value, '\''); + if ($value[0] == '"') return trim ($value, '"'); + return $value; + } + + private function startsMappedSequence ($line) { + return (substr($line, 0, 2) == '- ' && substr ($line, -1, 1) == ':'); + } + + private function returnMappedSequence ($line) { + $array = array(); + $key = self::unquote(trim(substr($line,1,-1))); + $array[$key] = array(); + $this->delayedPath = array(strpos ($line, $key) + $this->indent => $key); + return array($array); + } + + private function checkKeysInValue($value) { + if (strchr('[{"\'', $value[0]) === false) { + if (strchr($value, ': ') !== false) { + throw new Exception('Too many keys: '.$value); + } + } + } + + private function returnMappedValue ($line) { + $this->checkKeysInValue($line); + $array = array(); + $key = self::unquote (trim(substr($line,0,-1))); + $array[$key] = ''; + return $array; + } + + private function startsMappedValue ($line) { + return (substr ($line, -1, 1) == ':'); + } + + private function isPlainArray ($line) { + return ($line[0] == '[' && substr ($line, -1, 1) == ']'); + } + + private function returnPlainArray ($line) { + return $this->_toType($line); + } + + private function returnKeyValuePair ($line) { + $array = array(); + $key = ''; + if (strpos ($line, ': ')) { + // It's a key/value pair most likely + // If the key is in double quotes pull it out + if (($line[0] == '"' || $line[0] == "'") && preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { + $value = trim(str_replace($matches[1],'',$line)); + $key = $matches[2]; + } else { + // Do some guesswork as to the key and the value + $explode = explode(': ', $line); + $key = trim(array_shift($explode)); + $value = trim(implode(': ', $explode)); + $this->checkKeysInValue($value); + } + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + + if ($key === '0') $key = '__!YAMLZero'; + $array[$key] = $value; + } else { + $array = array ($line); + } + return $array; + + } + + + private function returnArrayElement ($line) { + if (strlen($line) <= 1) return array(array()); // Weird %) + $array = array(); + $value = trim(substr($line,1)); + $value = $this->_toType($value); + if ($this->isArrayElement($value)) { + $value = $this->returnArrayElement($value); + } + $array[] = $value; + return $array; + } + + + private function nodeContainsGroup ($line) { + $symbolsForReference = 'A-z0-9_\-'; + if (strpos($line, '&') === false && strpos($line, '*') === false) return false; // Please die fast ;-) + if ($line[0] == '&' && preg_match('/^(&['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; + if ($line[0] == '*' && preg_match('/^(\*['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; + if (preg_match('/(&['.$symbolsForReference.']+)$/', $line, $matches)) return $matches[1]; + if (preg_match('/(\*['.$symbolsForReference.']+$)/', $line, $matches)) return $matches[1]; + if (preg_match ('#^\s*<<\s*:\s*(\*[^\s]+).*$#', $line, $matches)) return $matches[1]; + return false; + + } + + private function addGroup ($line, $group) { + if ($group[0] == '&') $this->_containsGroupAnchor = substr ($group, 1); + if ($group[0] == '*') $this->_containsGroupAlias = substr ($group, 1); + //print_r ($this->path); + } + + private function stripGroup ($line, $group) { + $line = trim(str_replace($group, '', $line)); + return $line; + } +} +} + +// Enable use of Spyc from command line +// The syntax is the following: php Spyc.php spyc.yaml + +do { + if (PHP_SAPI != 'cli') break; + if (empty ($_SERVER['argc']) || $_SERVER['argc'] < 2) break; + if (empty ($_SERVER['PHP_SELF']) || FALSE === strpos ($_SERVER['PHP_SELF'], 'Spyc.php') ) break; + $file = $argv[1]; + echo json_encode (spyc_load_file ($file)); +} while (0); diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/composer.json b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/composer.json new file mode 100644 index 0000000..e5ab776 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/composer.json @@ -0,0 +1,30 @@ +{ + "name": "mustangostang/spyc", + "description": "A simple YAML loader/dumper class for PHP", + "type": "library", + "keywords": [ + "spyc", + "yaml", + "yml" + ], + "homepage": "https://github.com/mustangostang/spyc/", + "authors" : [{ + "name": "mustangostang", + "email": "vlad.andersen@gmail.com" + }], + "license": "MIT", + "require": { + "php": ">=5.3.1" + }, + "autoload": { + "files": [ "Spyc.php" ] + }, + "require-dev": { + "phpunit/phpunit": "4.3.*@dev" + }, + "extra": { + "branch-alias": { + "dev-master": "0.5.x-dev" + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/examples/yaml-dump.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/examples/yaml-dump.php new file mode 100644 index 0000000..9d2160a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/examples/yaml-dump.php @@ -0,0 +1,25 @@ + 'A sequence','second' => 'of mapped values'); +$array['Mapped'] = array('A sequence','which is mapped'); +$array['A Note'] = 'What if your text is too long?'; +$array['Another Note'] = 'If that is the case, the dumper will probably fold your text by using a block. Kinda like this.'; +$array['The trick?'] = 'The trick is that we overrode the default indent, 2, to 4 and the default wordwrap, 40, to 60.'; +$array['Old Dog'] = "And if you want\n to preserve line breaks, \ngo ahead!"; +$array['key:withcolon'] = "Should support this to"; + +$yaml = Spyc::YAMLDump($array,4,60); diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/examples/yaml-load.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/examples/yaml-load.php new file mode 100644 index 0000000..9e457e1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/examples/yaml-load.php @@ -0,0 +1,21 @@ +spyc.yaml loaded into PHP:
    '; +print_r($array); +echo ''; + + +echo '
    YAML Data dumped back:
    '; +echo Spyc::YAMLDump($array); +echo '
    '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/php4/5to4.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/php4/5to4.php new file mode 100644 index 0000000..5a48694 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/php4/5to4.php @@ -0,0 +1,17 @@ +', $code); + $f = fopen ($dest, 'w'); + fwrite($f, $code); + fclose ($f); + print "Written to $dest.\n"; +} \ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/php4/spyc.php4 b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/php4/spyc.php4 new file mode 100644 index 0000000..73f08cc --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/php4/spyc.php4 @@ -0,0 +1,1023 @@ + + * @author Chris Wanstrath + * @link http://code.google.com/p/spyc/ + * @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2009 Vlad Andersen + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @package Spyc + */ + +if (!function_exists('spyc_load')) { + /** + * Parses YAML to array. + * @param string $string YAML string. + * @return array + */ + function spyc_load ($string) { + return Spyc::YAMLLoadString($string); + } +} + +if (!function_exists('spyc_load_file')) { + /** + * Parses YAML to array. + * @param string $file Path to YAML file. + * @return array + */ + function spyc_load_file ($file) { + return Spyc::YAMLLoad($file); + } +} + +/** + * The Simple PHP YAML Class. + * + * This class can be used to read a YAML file and convert its contents + * into a PHP array. It currently supports a very limited subsection of + * the YAML spec. + * + * Usage: + * + * $Spyc = new Spyc; + * $array = $Spyc->load($file); + * + * or: + * + * $array = Spyc::YAMLLoad($file); + * + * or: + * + * $array = spyc_load_file($file); + * + * @package Spyc + */ +class Spyc { + + // SETTINGS + + /** + * Setting this to true will force YAMLDump to enclose any string value in + * quotes. False by default. + * + * @var bool + */ + var $setting_dump_force_quotes = false; + + /** + * Setting this to true will forse YAMLLoad to use syck_load function when + * possible. False by default. + * @var bool + */ + var $setting_use_syck_is_possible = false; + + + + /**#@+ + * @access private + * @var mixed + */ + var $_dumpIndent; + var $_dumpWordWrap; + var $_containsGroupAnchor = false; + var $_containsGroupAlias = false; + var $path; + var $result; + var $LiteralPlaceHolder = '___YAML_Literal_Block___'; + var $SavedGroups = array(); + var $indent; + /** + * Path modifier that should be applied after adding current element. + * @var array + */ + var $delayedPath = array(); + + /**#@+ + * @access public + * @var mixed + */ + var $_nodeId; + +/** + * Load a valid YAML string to Spyc. + * @param string $input + * @return array + */ + function load ($input) { + return $this->__loadString($input); + } + + /** + * Load a valid YAML file to Spyc. + * @param string $file + * @return array + */ + function loadFile ($file) { + return $this->__load($file); + } + + /** + * Load YAML into a PHP array statically + * + * The load method, when supplied with a YAML stream (string or file), + * will do its best to convert YAML in a file into a PHP array. Pretty + * simple. + * Usage: + * + * $array = Spyc::YAMLLoad('lucky.yaml'); + * print_r($array); + * + * @access public + * @return array + * @param string $input Path of YAML file or string containing YAML + */ + function YAMLLoad($input) { + $Spyc = new Spyc; + return $Spyc->__load($input); + } + + /** + * Load a string of YAML into a PHP array statically + * + * The load method, when supplied with a YAML string, will do its best + * to convert YAML in a string into a PHP array. Pretty simple. + * + * Note: use this function if you don't want files from the file system + * loaded and processed as YAML. This is of interest to people concerned + * about security whose input is from a string. + * + * Usage: + * + * $array = Spyc::YAMLLoadString("---\n0: hello world\n"); + * print_r($array); + * + * @access public + * @return array + * @param string $input String containing YAML + */ + function YAMLLoadString($input) { + $Spyc = new Spyc; + return $Spyc->__loadString($input); + } + + /** + * Dump YAML from PHP array statically + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as nothing.yaml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @access public + * @return string + * @param array $array PHP array + * @param int $indent Pass in false to use the default, which is 2 + * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) + */ + function YAMLDump($array,$indent = false,$wordwrap = false) { + $spyc = new Spyc; + return $spyc->dump($array,$indent,$wordwrap); + } + + + /** + * Dump PHP array to YAML + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as tasteful.yaml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @access public + * @return string + * @param array $array PHP array + * @param int $indent Pass in false to use the default, which is 2 + * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) + */ + function dump($array,$indent = false,$wordwrap = false) { + // Dumps to some very clean YAML. We'll have to add some more features + // and options soon. And better support for folding. + + // New features and options. + if ($indent === false or !is_numeric($indent)) { + $this->_dumpIndent = 2; + } else { + $this->_dumpIndent = $indent; + } + + if ($wordwrap === false or !is_numeric($wordwrap)) { + $this->_dumpWordWrap = 40; + } else { + $this->_dumpWordWrap = $wordwrap; + } + + // New YAML document + $string = "---\n"; + + // Start at the base of the array and move through it. + if ($array) { + $array = (array)$array; + $first_key = key($array); + + $previous_key = -1; + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key,$value,0,$previous_key, $first_key); + $previous_key = $key; + } + } + return $string; + } + + /** + * Attempts to convert a key / value array item to YAML + * @access private + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + function _yamlize($key,$value,$indent, $previous_key = -1, $first_key = 0) { + if (is_array($value)) { + if (empty ($value)) + return $this->_dumpNode($key, array(), $indent, $previous_key, $first_key); + // It has children. What to do? + // Make it the right kind of item + $string = $this->_dumpNode($key, NULL, $indent, $previous_key, $first_key); + // Add the indent + $indent += $this->_dumpIndent; + // Yamlize the array + $string .= $this->_yamlizeArray($value,$indent); + } elseif (!is_array($value)) { + // It doesn't have children. Yip. + $string = $this->_dumpNode($key, $value, $indent, $previous_key, $first_key); + } + return $string; + } + + /** + * Attempts to convert an array to YAML + * @access private + * @return string + * @param $array The array you want to convert + * @param $indent The indent of the current level + */ + function _yamlizeArray($array,$indent) { + if (is_array($array)) { + $string = ''; + $previous_key = -1; + $first_key = key($array); + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key, $value, $indent, $previous_key, $first_key); + $previous_key = $key; + } + return $string; + } else { + return false; + } + } + + /** + * Returns YAML from a key and a value + * @access private + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + function _dumpNode($key, $value, $indent, $previous_key = -1, $first_key = 0) { + // do some folding here, for blocks + if (is_string ($value) && ((strpos($value,"\n") !== false || strpos($value,": ") !== false || strpos($value,"- ") !== false || + strpos($value,"*") !== false || strpos($value,"#") !== false || strpos($value,"<") !== false || strpos($value,">") !== false || + strpos($value,"[") !== false || strpos($value,"]") !== false || strpos($value,"{") !== false || strpos($value,"}") !== false) || substr ($value, -1, 1) == ':')) { + $value = $this->_doLiteralBlock($value,$indent); + } else { + $value = $this->_doFolding($value,$indent); + if (is_bool($value)) { + $value = ($value) ? "true" : "false"; + } + } + + if ($value === array()) $value = '[ ]'; + + $spaces = str_repeat(' ',$indent); + + if (is_int($key) && $key - 1 == $previous_key && $first_key===0) { + // It's a sequence + $string = $spaces.'- '.$value."\n"; + } else { + if ($first_key===0) throw new Exception('Keys are all screwy. The first one was zero, now it\'s "'. $key .'"'); + // It's mapped + if (strpos($key, ":") !== false) { $key = '"' . $key . '"'; } + $string = $spaces.$key.': '.$value."\n"; + } + return $string; + } + + /** + * Creates a literal block for dumping + * @access private + * @return string + * @param $value + * @param $indent int The value of the indent + */ + function _doLiteralBlock($value,$indent) { + if (strpos($value, "\n") === false && strpos($value, "'") === false) { + return sprintf ("'%s'", $value); + } + if (strpos($value, "\n") === false && strpos($value, '"') === false) { + return sprintf ('"%s"', $value); + } + $exploded = explode("\n",$value); + $newValue = '|'; + $indent += $this->_dumpIndent; + $spaces = str_repeat(' ',$indent); + foreach ($exploded as $line) { + $newValue .= "\n" . $spaces . trim($line); + } + return $newValue; + } + + /** + * Folds a string of text, if necessary + * @access private + * @return string + * @param $value The string you wish to fold + */ + function _doFolding($value,$indent) { + // Don't do anything if wordwrap is set to 0 + + if ($this->_dumpWordWrap !== 0 && is_string ($value) && strlen($value) > $this->_dumpWordWrap) { + $indent += $this->_dumpIndent; + $indent = str_repeat(' ',$indent); + $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); + $value = ">\n".$indent.$wrapped; + } else { + if ($this->setting_dump_force_quotes && is_string ($value)) + $value = '"' . $value . '"'; + } + + + return $value; + } + +// LOADING FUNCTIONS + + function __load($input) { + $Source = $this->loadFromSource($input); + return $this->loadWithSource($Source); + } + + function __loadString($input) { + $Source = $this->loadFromString($input); + return $this->loadWithSource($Source); + } + + function loadWithSource($Source) { + if (empty ($Source)) return array(); + if ($this->setting_use_syck_is_possible && function_exists ('syck_load')) { + $array = syck_load (implode ('', $Source)); + return is_array($array) ? $array : array(); + } + + $this->path = array(); + $this->result = array(); + + $cnt = count($Source); + for ($i = 0; $i < $cnt; $i++) { + $line = $Source[$i]; + + $this->indent = strlen($line) - strlen(ltrim($line)); + $tempPath = $this->getParentPathByIndent($this->indent); + $line = $this->stripIndent($line, $this->indent); + if ($this->isComment($line)) continue; + if ($this->isEmpty($line)) continue; + $this->path = $tempPath; + + $literalBlockStyle = $this->startsLiteralBlock($line); + if ($literalBlockStyle) { + $line = rtrim ($line, $literalBlockStyle . " \n"); + $literalBlock = ''; + $line .= $this->LiteralPlaceHolder; + + while (++$i < $cnt && $this->literalBlockContinues($Source[$i], $this->indent)) { + $literalBlock = $this->addLiteralLine($literalBlock, $Source[$i], $literalBlockStyle); + } + $i--; + } + + while (++$i < $cnt && $this->greedilyNeedNextLine($line)) { + $line = rtrim ($line, " \n\t\r") . ' ' . ltrim ($Source[$i], " \t"); + } + $i--; + + + + if (strpos ($line, '#')) { + if (strpos ($line, '"') === false && strpos ($line, "'") === false) + $line = preg_replace('/\s+#(.+)$/','',$line); + } + + $lineArray = $this->_parseLine($line); + + if ($literalBlockStyle) + $lineArray = $this->revertLiteralPlaceHolder ($lineArray, $literalBlock); + + $this->addArray($lineArray, $this->indent); + + foreach ($this->delayedPath as $indent => $delayedPath) + $this->path[$indent] = $delayedPath; + + $this->delayedPath = array(); + + } + return $this->result; + } + + function loadFromSource ($input) { + if (!empty($input) && strpos($input, "\n") === false && file_exists($input)) + return file($input); + + return $this->loadFromString($input); + } + + function loadFromString ($input) { + $lines = explode("\n",$input); + foreach ($lines as $k => $_) { + $lines[$k] = rtrim ($_, "\r"); + } + return $lines; + } + + /** + * Parses YAML code and returns an array for a node + * @access private + * @return array + * @param string $line A line from the YAML file + */ + function _parseLine($line) { + if (!$line) return array(); + $line = trim($line); + + if (!$line) return array(); + $array = array(); + + $group = $this->nodeContainsGroup($line); + if ($group) { + $this->addGroup($line, $group); + $line = $this->stripGroup ($line, $group); + } + + if ($this->startsMappedSequence($line)) + return $this->returnMappedSequence($line); + + if ($this->startsMappedValue($line)) + return $this->returnMappedValue($line); + + if ($this->isArrayElement($line)) + return $this->returnArrayElement($line); + + if ($this->isPlainArray($line)) + return $this->returnPlainArray($line); + + + return $this->returnKeyValuePair($line); + + } + + /** + * Finds the type of the passed value, returns the value as the new type. + * @access private + * @param string $value + * @return mixed + */ + function _toType($value) { + if ($value === '') return null; + $first_character = $value[0]; + $last_character = substr($value, -1, 1); + + $is_quoted = false; + do { + if (!$value) break; + if ($first_character != '"' && $first_character != "'") break; + if ($last_character != '"' && $last_character != "'") break; + $is_quoted = true; + } while (0); + + if ($is_quoted) + return strtr(substr ($value, 1, -1), array ('\\"' => '"', '\'\'' => '\'', '\\\'' => '\'')); + + if (strpos($value, ' #') !== false) + $value = preg_replace('/\s+#(.+)$/','',$value); + + if ($first_character == '[' && $last_character == ']') { + // Take out strings sequences and mappings + $innerValue = trim(substr ($value, 1, -1)); + if ($innerValue === '') return array(); + $explode = $this->_inlineEscape($innerValue); + // Propagate value array + $value = array(); + foreach ($explode as $v) { + $value[] = $this->_toType($v); + } + return $value; + } + + if (strpos($value,': ')!==false && $first_character != '{') { + $array = explode(': ',$value); + $key = trim($array[0]); + array_shift($array); + $value = trim(implode(': ',$array)); + $value = $this->_toType($value); + return array($key => $value); + } + + if ($first_character == '{' && $last_character == '}') { + $innerValue = trim(substr ($value, 1, -1)); + if ($innerValue === '') return array(); + // Inline Mapping + // Take out strings sequences and mappings + $explode = $this->_inlineEscape($innerValue); + // Propagate value array + $array = array(); + foreach ($explode as $v) { + $SubArr = $this->_toType($v); + if (empty($SubArr)) continue; + if (is_array ($SubArr)) { + $array[key($SubArr)] = $SubArr[key($SubArr)]; continue; + } + $array[] = $SubArr; + } + return $array; + } + + if ($value == 'null' || $value == 'NULL' || $value == 'Null' || $value == '' || $value == '~') { + return null; + } + + if (intval($first_character) > 0 && preg_match ('/^[1-9]+[0-9]*$/', $value)) { + $intvalue = (int)$value; + if ($intvalue != PHP_INT_MAX) + $value = $intvalue; + return $value; + } + + if (in_array($value, + array('true', 'on', '+', 'yes', 'y', 'True', 'TRUE', 'On', 'ON', 'YES', 'Yes', 'Y'))) { + return true; + } + + if (in_array(strtolower($value), + array('false', 'off', '-', 'no', 'n'))) { + return false; + } + + if (is_numeric($value)) { + if ($value === '0') return 0; + if (trim ($value, 0) === $value) + $value = (float)$value; + return $value; + } + + return $value; + } + + /** + * Used in inlines to check for more inlines or quoted strings + * @access private + * @return array + */ + function _inlineEscape($inline) { + // There's gotta be a cleaner way to do this... + // While pure sequences seem to be nesting just fine, + // pure mappings and mappings with sequences inside can't go very + // deep. This needs to be fixed. + + $seqs = array(); + $maps = array(); + $saved_strings = array(); + + // Check for strings + $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; + if (preg_match_all($regex,$inline,$strings)) { + $saved_strings = $strings[0]; + $inline = preg_replace($regex,'YAMLString',$inline); + } + unset($regex); + + $i = 0; + do { + + // Check for sequences + while (preg_match('/\[([^{}\[\]]+)\]/U',$inline,$matchseqs)) { + $seqs[] = $matchseqs[0]; + $inline = preg_replace('/\[([^{}\[\]]+)\]/U', ('YAMLSeq' . (count($seqs) - 1) . 's'), $inline, 1); + } + + // Check for mappings + while (preg_match('/{([^\[\]{}]+)}/U',$inline,$matchmaps)) { + $maps[] = $matchmaps[0]; + $inline = preg_replace('/{([^\[\]{}]+)}/U', ('YAMLMap' . (count($maps) - 1) . 's'), $inline, 1); + } + + if ($i++ >= 10) break; + + } while (strpos ($inline, '[') !== false || strpos ($inline, '{') !== false); + + $explode = explode(', ',$inline); + $stringi = 0; $i = 0; + + while (1) { + + // Re-add the sequences + if (!empty($seqs)) { + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLSeq') !== false) { + foreach ($seqs as $seqk => $seq) { + $explode[$key] = str_replace(('YAMLSeq'.$seqk.'s'),$seq,$value); + $value = $explode[$key]; + } + } + } + } + + // Re-add the mappings + if (!empty($maps)) { + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLMap') !== false) { + foreach ($maps as $mapk => $map) { + $explode[$key] = str_replace(('YAMLMap'.$mapk.'s'), $map, $value); + $value = $explode[$key]; + } + } + } + } + + + // Re-add the strings + if (!empty($saved_strings)) { + foreach ($explode as $key => $value) { + while (strpos($value,'YAMLString') !== false) { + $explode[$key] = preg_replace('/YAMLString/',$saved_strings[$stringi],$value, 1); + unset($saved_strings[$stringi]); + ++$stringi; + $value = $explode[$key]; + } + } + } + + $finished = true; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLSeq') !== false) { + $finished = false; break; + } + if (strpos($value,'YAMLMap') !== false) { + $finished = false; break; + } + if (strpos($value,'YAMLString') !== false) { + $finished = false; break; + } + } + if ($finished) break; + + $i++; + if ($i > 10) + break; // Prevent infinite loops. + } + + return $explode; + } + + function literalBlockContinues ($line, $lineIndent) { + if (!trim($line)) return true; + if (strlen($line) - strlen(ltrim($line)) > $lineIndent) return true; + return false; + } + + function referenceContentsByAlias ($alias) { + do { + if (!isset($this->SavedGroups[$alias])) { echo "Bad group name: $alias."; break; } + $groupPath = $this->SavedGroups[$alias]; + $value = $this->result; + foreach ($groupPath as $k) { + $value = $value[$k]; + } + } while (false); + return $value; + } + + function addArrayInline ($array, $indent) { + $CommonGroupPath = $this->path; + if (empty ($array)) return false; + + foreach ($array as $k => $_) { + $this->addArray(array($k => $_), $indent); + $this->path = $CommonGroupPath; + } + return true; + } + + function addArray ($incoming_data, $incoming_indent) { + + // print_r ($incoming_data); + + if (count ($incoming_data) > 1) + return $this->addArrayInline ($incoming_data, $incoming_indent); + + $key = key ($incoming_data); + $value = isset($incoming_data[$key]) ? $incoming_data[$key] : null; + if ($key === '__!YAMLZero') $key = '0'; + + if ($incoming_indent == 0 && !$this->_containsGroupAlias && !$this->_containsGroupAnchor) { // Shortcut for root-level values. + if ($key || $key === '' || $key === '0') { + $this->result[$key] = $value; + } else { + $this->result[] = $value; end ($this->result); $key = key ($this->result); + } + $this->path[$incoming_indent] = $key; + return; + } + + + + $history = array(); + // Unfolding inner array tree. + $history[] = $_arr = $this->result; + foreach ($this->path as $k) { + $history[] = $_arr = $_arr[$k]; + } + + if ($this->_containsGroupAlias) { + $value = $this->referenceContentsByAlias($this->_containsGroupAlias); + $this->_containsGroupAlias = false; + } + + + // Adding string or numeric key to the innermost level or $this->arr. + if (is_string($key) && $key == '<<') { + if (!is_array ($_arr)) { $_arr = array (); } + $_arr = array_merge ($_arr, $value); + } else if ($key || $key === '' || $key === '0') { + $_arr[$key] = $value; + } else { + if (!is_array ($_arr)) { $_arr = array ($value); $key = 0; } + else { $_arr[] = $value; end ($_arr); $key = key ($_arr); } + } + + $reverse_path = array_reverse($this->path); + $reverse_history = array_reverse ($history); + $reverse_history[0] = $_arr; + $cnt = count($reverse_history) - 1; + for ($i = 0; $i < $cnt; $i++) { + $reverse_history[$i+1][$reverse_path[$i]] = $reverse_history[$i]; + } + $this->result = $reverse_history[$cnt]; + + $this->path[$incoming_indent] = $key; + + if ($this->_containsGroupAnchor) { + $this->SavedGroups[$this->_containsGroupAnchor] = $this->path; + if (is_array ($value)) { + $k = key ($value); + if (!is_int ($k)) { + $this->SavedGroups[$this->_containsGroupAnchor][$incoming_indent + 2] = $k; + } + } + $this->_containsGroupAnchor = false; + } + + } + + function startsLiteralBlock ($line) { + $lastChar = substr (trim($line), -1); + if ($lastChar != '>' && $lastChar != '|') return false; + if ($lastChar == '|') return $lastChar; + // HTML tags should not be counted as literal blocks. + if (preg_match ('#<.*?>$#', $line)) return false; + return $lastChar; + } + + function greedilyNeedNextLine($line) { + $line = trim ($line); + if (!strlen($line)) return false; + if (substr ($line, -1, 1) == ']') return false; + if ($line[0] == '[') return true; + if (preg_match ('#^[^:]+?:\s*\[#', $line)) return true; + return false; + } + + function addLiteralLine ($literalBlock, $line, $literalBlockStyle) { + $line = $this->stripIndent($line); + $line = rtrim ($line, "\r\n\t ") . "\n"; + if ($literalBlockStyle == '|') { + return $literalBlock . $line; + } + if (strlen($line) == 0) + return rtrim($literalBlock, ' ') . "\n"; + if ($line == "\n" && $literalBlockStyle == '>') { + return rtrim ($literalBlock, " \t") . "\n"; + } + if ($line != "\n") + $line = trim ($line, "\r\n ") . " "; + return $literalBlock . $line; + } + + function revertLiteralPlaceHolder ($lineArray, $literalBlock) { + foreach ($lineArray as $k => $_) { + if (is_array($_)) + $lineArray[$k] = $this->revertLiteralPlaceHolder ($_, $literalBlock); + else if (substr($_, -1 * strlen ($this->LiteralPlaceHolder)) == $this->LiteralPlaceHolder) + $lineArray[$k] = rtrim ($literalBlock, " \r\n"); + } + return $lineArray; + } + + function stripIndent ($line, $indent = -1) { + if ($indent == -1) $indent = strlen($line) - strlen(ltrim($line)); + return substr ($line, $indent); + } + + function getParentPathByIndent ($indent) { + if ($indent == 0) return array(); + $linePath = $this->path; + do { + end($linePath); $lastIndentInParentPath = key($linePath); + if ($indent <= $lastIndentInParentPath) array_pop ($linePath); + } while ($indent <= $lastIndentInParentPath); + return $linePath; + } + + + function clearBiggerPathValues ($indent) { + + + if ($indent == 0) $this->path = array(); + if (empty ($this->path)) return true; + + foreach ($this->path as $k => $_) { + if ($k > $indent) unset ($this->path[$k]); + } + + return true; + } + + + function isComment ($line) { + if (!$line) return false; + if ($line[0] == '#') return true; + if (trim($line, " \r\n\t") == '---') return true; + return false; + } + + function isEmpty ($line) { + return (trim ($line) === ''); + } + + + function isArrayElement ($line) { + if (!$line) return false; + if ($line[0] != '-') return false; + if (strlen ($line) > 3) + if (substr($line,0,3) == '---') return false; + + return true; + } + + function isHashElement ($line) { + return strpos($line, ':'); + } + + function isLiteral ($line) { + if ($this->isArrayElement($line)) return false; + if ($this->isHashElement($line)) return false; + return true; + } + + + function unquote ($value) { + if (!$value) return $value; + if (!is_string($value)) return $value; + if ($value[0] == '\'') return trim ($value, '\''); + if ($value[0] == '"') return trim ($value, '"'); + return $value; + } + + function startsMappedSequence ($line) { + return ($line[0] == '-' && substr ($line, -1, 1) == ':'); + } + + function returnMappedSequence ($line) { + $array = array(); + $key = $this->unquote(trim(substr($line,1,-1))); + $array[$key] = array(); + $this->delayedPath = array(strpos ($line, $key) + $this->indent => $key); + return array($array); + } + + function returnMappedValue ($line) { + $array = array(); + $key = $this->unquote (trim(substr($line,0,-1))); + $array[$key] = ''; + return $array; + } + + function startsMappedValue ($line) { + return (substr ($line, -1, 1) == ':'); + } + + function isPlainArray ($line) { + return ($line[0] == '[' && substr ($line, -1, 1) == ']'); + } + + function returnPlainArray ($line) { + return $this->_toType($line); + } + + function returnKeyValuePair ($line) { + $array = array(); + $key = ''; + if (strpos ($line, ':')) { + // It's a key/value pair most likely + // If the key is in double quotes pull it out + if (($line[0] == '"' || $line[0] == "'") && preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { + $value = trim(str_replace($matches[1],'',$line)); + $key = $matches[2]; + } else { + // Do some guesswork as to the key and the value + $explode = explode(':',$line); + $key = trim($explode[0]); + array_shift($explode); + $value = trim(implode(':',$explode)); + } + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + if ($key === '0') $key = '__!YAMLZero'; + $array[$key] = $value; + } else { + $array = array ($line); + } + return $array; + + } + + + function returnArrayElement ($line) { + if (strlen($line) <= 1) return array(array()); // Weird %) + $array = array(); + $value = trim(substr($line,1)); + $value = $this->_toType($value); + $array[] = $value; + return $array; + } + + + function nodeContainsGroup ($line) { + $symbolsForReference = 'A-z0-9_\-'; + if (strpos($line, '&') === false && strpos($line, '*') === false) return false; // Please die fast ;-) + if ($line[0] == '&' && preg_match('/^(&['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; + if ($line[0] == '*' && preg_match('/^(\*['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; + if (preg_match('/(&['.$symbolsForReference.']+)$/', $line, $matches)) return $matches[1]; + if (preg_match('/(\*['.$symbolsForReference.']+$)/', $line, $matches)) return $matches[1]; + if (preg_match ('#^\s*<<\s*:\s*(\*[^\s]+).*$#', $line, $matches)) return $matches[1]; + return false; + + } + + function addGroup ($line, $group) { + if ($group[0] == '&') $this->_containsGroupAnchor = substr ($group, 1); + if ($group[0] == '*') $this->_containsGroupAlias = substr ($group, 1); + //print_r ($this->path); + } + + function stripGroup ($line, $group) { + $line = trim(str_replace($group, '', $line)); + return $line; + } +} + +// Enable use of Spyc from command line +// The syntax is the following: php spyc.php spyc.yaml + +define ('SPYC_FROM_COMMAND_LINE', false); + +do { + if (!SPYC_FROM_COMMAND_LINE) break; + if (empty ($_SERVER['argc']) || $_SERVER['argc'] < 2) break; + if (empty ($_SERVER['PHP_SELF']) || $_SERVER['PHP_SELF'] != 'spyc.php') break; + $file = $argv[1]; + printf ("Spyc loading file: %s\n", $file); + print_r (spyc_load_file ($file)); +} while (0); \ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/php4/test.php4 b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/php4/test.php4 new file mode 100644 index 0000000..315f501 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/php4/test.php4 @@ -0,0 +1,162 @@ + "1.5ghz", "ram" => "1 gig", + "os" => "os x 10.4.1")) + die('Sequence 4 failed'); + +# Mapped sequence +if ($yaml['domains'] != array("yaml.org", "php.net")) + die("Key: 'domains' failed"); + +# A sequence like this. +if ($yaml[5] != array("program" => "Adium", "platform" => "OS X", + "type" => "Chat Client")) + die('Sequence 5 failed'); + +# A folded block as a mapped value +if ($yaml['no time'] != "There isn't any time for your tricks!\nDo you understand?") + die("Key: 'no time' failed"); + +# A literal block as a mapped value +if ($yaml['some time'] != "There is nothing but time\nfor your tricks.") + die("Key: 'some time' failed"); + +# Crazy combinations +if ($yaml['databases'] != array( array("name" => "spartan", "notes" => + array( "Needs to be backed up", + "Needs to be normalized" ), + "type" => "mysql" ))) + die("Key: 'databases' failed"); + +# You can be a bit tricky +if ($yaml["if: you'd"] != "like") + die("Key: 'if: you\'d' failed"); + +# Inline sequences +if ($yaml[6] != array("One", "Two", "Three", "Four")) + die("Sequence 6 failed"); + +# Nested Inline Sequences +if ($yaml[7] != array("One", array("Two", "And", "Three"), "Four", "Five")) + die("Sequence 7 failed"); + +# Nested Nested Inline Sequences +if ($yaml[8] != array( "This", array("Is", "Getting", array("Ridiculous", "Guys")), + "Seriously", array("Show", "Mercy"))) + die("Sequence 8 failed"); + +# Inline mappings +if ($yaml[9] != array("name" => "chris", "age" => "young", "brand" => "lucky strike")) + die("Sequence 9 failed"); + +# Nested inline mappings +if ($yaml[10] != array("name" => "mark", "age" => "older than chris", + "brand" => array("marlboro", "lucky strike"))) + die("Sequence 10 failed"); + +# References -- they're shaky, but functional +if ($yaml['dynamic languages'] != array('Perl', 'Python', 'PHP', 'Ruby')) + die("Key: 'dynamic languages' failed"); + +if ($yaml['compiled languages'] != array('C/C++', 'Java')) + die("Key: 'compiled languages' failed"); + +if ($yaml['all languages'] != array( + array('Perl', 'Python', 'PHP', 'Ruby'), + array('C/C++', 'Java') + )) + die("Key: 'all languages' failed"); + +# Added in .2.2: Escaped quotes +if ($yaml[11] != "you know, this shouldn't work. but it does.") + die("Sequence 11 failed."); + +if ($yaml[12] != "that's my value.") + die("Sequence 12 failed."); + +if ($yaml[13] != "again, that's my value.") + die("Sequence 13 failed."); + +if ($yaml[14] != "here's to \"quotes\", boss.") + die("Sequence 14 failed."); + +if ($yaml[15] != array( 'name' => "Foo, Bar's", 'age' => 20)) + die("Sequence 15 failed."); + +if ($yaml[16] != array( 0 => "a", 1 => array (0 => 1, 1 => 2), 2 => "b")) + die("Sequence 16 failed."); + +if ($yaml['endloop'] != "Does this line in the end indeed make Spyc go to an infinite loop?") + die("[endloop] failed."); + + +print "spyc.yaml parsed correctly\n"; + +?> \ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/phpunit.xml b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/phpunit.xml new file mode 100644 index 0000000..a6151af --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/phpunit.xml @@ -0,0 +1,8 @@ + + + + + tests/ + + + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/spyc.yaml b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/spyc.yaml new file mode 100644 index 0000000..489f28c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/spyc.yaml @@ -0,0 +1,219 @@ +# +# S P Y C +# a simple php yaml class +# +# authors: [vlad andersen (vlad.andersen@gmail.com), chris wanstrath (chris@ozmm.org)] +# websites: [http://www.yaml.org, http://spyc.sourceforge.net/] +# license: [MIT License, http://www.opensource.org/licenses/mit-license.php] +# copyright: (c) 2005-2006 Chris Wanstrath, 2006-2014 Vlad Andersen +# +# spyc.yaml - A file containing the YAML that Spyc understands. + +--- + +# Mappings - with proper types +String: Anyone's name, really. +Int: 13 +BadHex: f0xf3 +Hex: 0xf3 +True: true +False: false +Zero: 0 +Null: NULL +NotNull: 'null' +NotTrue: 'y' +NotBoolTrue: 'true' +NotInt: '5' +Float: 5.34 +Negative: -90 +SmallFloat: 0.7 +NewLine: \n +QuotedNewLine: "\n" + +# A sequence +- PHP Class +- Basic YAML Loader +- Very Basic YAML Dumper + +# A sequence of a sequence +- + - YAML is so easy to learn. + - Your config files will never be the same. + +# Sequence of mappings +- + cpu: 1.5ghz + ram: 1 gig + os : os x 10.4.1 + +# Mapped sequence +domains: + - yaml.org + - php.net + +# A sequence like this. +- program: Adium + platform: OS X + type: Chat Client + +# A folded block as a mapped value +no time: > + There isn't any time + for your tricks! + + Do you understand? + +# A literal block as a mapped value +some time: | + There is nothing but time + for your tricks. + +# Crazy combinations +databases: + - name: spartan + notes: + - Needs to be backed up + - Needs to be normalized + type: mysql + +# You can be a bit tricky +"if: you'd": like + +# Inline sequences +- [One, Two, Three, Four] + +# Nested Inline Sequences +- [One, [Two, And, Three], Four, Five] + +# Nested Nested Inline Sequences +- [This, [Is, Getting, [Ridiculous, Guys]], Seriously, [Show, Mercy]] + +# Inline mappings +- {name: chris, age: young, brand: lucky strike} + +# Nested inline mappings +- {name: mark, age: older than chris, brand: [marlboro, lucky strike]} + +# References -- they're shaky, but functional +dynamic languages: &DLANGS + - Perl + - Python + - PHP + - Ruby +compiled languages: &CLANGS + - C/C++ + - Java +all languages: + - *DLANGS + - *CLANGS + +# Added in .2.2: Escaped quotes +- you know, this shouldn't work. but it does. +- 'that''s my value.' +- 'again, that\'s my value.' +- "here's to \"quotes\", boss." + +# added in .2.3 +- {name: "Foo, Bar's", age: 20} + +# Added in .2.4: bug [ 1418193 ] Quote Values in Nested Arrays +- [a, ['1', "2"], b] + +# Add in .5.2: Quoted new line values. +- "First line\nSecond line\nThird line" + +# Added in .2.4: malformed YAML +all + javascripts: [dom1.js, dom.js] + +# Added in .2 +1040: Ooo, a numeric key! # And working comments? Wow! Colons in comments: a menace (0.3). + +hash_1: Hash #and a comment +hash_2: "Hash #and a comment" +"hash#3": "Hash (#) can appear in key too" + +float_test: 1.0 +float_test_with_quotes: '1.0' +float_inverse_test: 001 + +a_really_large_number: 115792089237316195423570985008687907853269984665640564039457584007913129639936 # 2^256 + +int array: [ 1, 2, 3 ] + +array on several lines: + [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ] + +morelesskey: "" + +array_of_zero: [0] +sophisticated_array_of_zero: {rx: {tx: [0]} } + +switches: + - { row: 0, col: 0, func: {tx: [0, 1]} } + +empty_sequence: [ ] +empty_hash: { } + +special_characters: "[{]]{{]]" + +asterisks: "*" + +empty_key: + : + key: value + +trailing_colon: "foo:" + +multiline_items: + - type: SomeItem + values: [blah, blah, blah, + blah] + ints: [2, 54, 12, + 2143] + +many_lines: | + A quick + fox + + + jumped + over + + + + + + a lazy + + + + dog + + +werte: + 1: nummer 1 + 0: Stunde 0 + +noindent_records: +- record1: value1 +- record2: value2 + +"a:1": [1000] +"a:2": + - 2000 +a:3: [3000] + +complex_unquoted_key: + a:b:''test': value + +array with commas: + ["0","1"] + +invoice: ["Something", "", '', "Something else"] +quotes: ['Something', "Nothing", 'Anything', "Thing"] + +# [Endloop] +endloop: | + Does this line in the end indeed make Spyc go to an infinite loop? diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/DumpTest.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/DumpTest.php new file mode 100644 index 0000000..ceb5c8e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/DumpTest.php @@ -0,0 +1,194 @@ +files_to_test = array (__DIR__.'/../spyc.yaml', 'failing1.yaml', 'indent_1.yaml', 'quotes.yaml'); + } + + public function testShortSyntax() { + $dump = spyc_dump(array ('item1', 'item2', 'item3')); + $awaiting = "- item1\n- item2\n- item3\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testDump() { + foreach ($this->files_to_test as $file) { + $yaml = spyc_load(file_get_contents($file)); + $dump = Spyc::YAMLDump ($yaml); + $yaml_after_dump = Spyc::YAMLLoad ($dump); + $this->assertEquals ($yaml, $yaml_after_dump); + } + } + + public function testDumpWithQuotes() { + $Spyc = new Spyc(); + $Spyc->setting_dump_force_quotes = true; + foreach ($this->files_to_test as $file) { + $yaml = $Spyc->load(file_get_contents($file)); + $dump = $Spyc->dump ($yaml); + $yaml_after_dump = Spyc::YAMLLoad ($dump); + $this->assertEquals ($yaml, $yaml_after_dump); + } + } + + public function testDumpArrays() { + $dump = Spyc::YAMLDump(array ('item1', 'item2', 'item3')); + $awaiting = "---\n- item1\n- item2\n- item3\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testNull() { + $dump = Spyc::YAMLDump(array('a' => 1, 'b' => null, 'c' => 3)); + $awaiting = "---\na: 1\nb: null\nc: 3\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testNext() { + $array = array("aaa", "bbb", "ccc"); + #set arrays internal pointer to next element + next($array); + $dump = Spyc::YAMLDump($array); + $awaiting = "---\n- aaa\n- bbb\n- ccc\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testDumpingMixedArrays() { + $array = array(); + $array[] = 'Sequence item'; + $array['The Key'] = 'Mapped value'; + $array[] = array('A sequence','of a sequence'); + $array[] = array('first' => 'A sequence','second' => 'of mapped values'); + $array['Mapped'] = array('A sequence','which is mapped'); + $array['A Note'] = 'What if your text is too long?'; + $array['Another Note'] = 'If that is the case, the dumper will probably fold your text by using a block. Kinda like this.'; + $array['The trick?'] = 'The trick is that we overrode the default indent, 2, to 4 and the default wordwrap, 40, to 60.'; + $array['Old Dog'] = "And if you want\n to preserve line breaks, \ngo ahead!"; + $array['key:withcolon'] = "Should support this to"; + + $yaml = Spyc::YAMLDump($array,4,60); + } + + public function testMixed() { + $dump = Spyc::YAMLDump(array(0 => 1, 'b' => 2, 1 => 3)); + $awaiting = "---\n0: 1\nb: 2\n1: 3\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testDumpNumerics() { + $dump = Spyc::YAMLDump(array ('404', '405', '500')); + $awaiting = "---\n- \"404\"\n- \"405\"\n- \"500\"\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testDumpAsterisks() { + $dump = Spyc::YAMLDump(array ('*')); + $awaiting = "---\n- '*'\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testDumpAmpersands() { + $dump = Spyc::YAMLDump(array ('some' => '&foo')); + $awaiting = "---\nsome: '&foo'\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testDumpExclamations() { + $dump = Spyc::YAMLDump(array ('some' => '!foo')); + $awaiting = "---\nsome: '!foo'\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testDumpExclamations2() { + $dump = Spyc::YAMLDump(array ('some' => 'foo!')); + $awaiting = "---\nsome: foo!\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testDumpApostrophes() { + $dump = Spyc::YAMLDump(array ('some' => "'Biz' pimpt bedrijventerreinen")); + $awaiting = "---\nsome: \"'Biz' pimpt bedrijventerreinen\"\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testDumpNumericHashes() { + $dump = Spyc::YAMLDump(array ("titel"=> array("0" => "", 1 => "Dr.", 5 => "Prof.", 6 => "Prof. Dr."))); + $awaiting = "---\ntitel:\n 0: \"\"\n 1: Dr.\n 5: Prof.\n 6: Prof. Dr.\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testEmpty() { + $dump = Spyc::YAMLDump(array("foo" => array())); + $awaiting = "---\nfoo: [ ]\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testHashesInKeys() { + $dump = Spyc::YAMLDump(array ('#color' => '#ffffff')); + $awaiting = "---\n\"#color\": '#ffffff'\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testParagraph() { + $dump = Spyc::YAMLDump(array ('key' => "|\n value")); + $awaiting = "---\nkey: |\n value\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testParagraphTwo() { + $dump = Spyc::YAMLDump(array ('key' => 'Congrats, pimpt bedrijventerreinen pimpt bedrijventerreinen pimpt bedrijventerreinen!')); + $awaiting = "---\nkey: >\n Congrats, pimpt bedrijventerreinen pimpt\n bedrijventerreinen pimpt\n bedrijventerreinen!\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testString() { + $dump = Spyc::YAMLDump(array ('key' => array('key_one' => 'Congrats, pimpt bedrijventerreinen!'))); + $awaiting = "---\nkey:\n key_one: Congrats, pimpt bedrijventerreinen!\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testStringLong() { + $dump = Spyc::YAMLDump(array ('key' => array('key_one' => 'Congrats, pimpt bedrijventerreinen pimpt bedrijventerreinen pimpt bedrijventerreinen!'))); + $awaiting = "---\nkey:\n key_one: >\n Congrats, pimpt bedrijventerreinen pimpt\n bedrijventerreinen pimpt\n bedrijventerreinen!\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testStringDoubleQuote() { + $dump = Spyc::YAMLDump(array ('key' => array('key_one' => array('key_two' => '"Système d\'e-réservation"')))); + $awaiting = "---\nkey:\n key_one:\n key_two: |\n Système d'e-réservation\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testLongStringDoubleQuote() { + $dump = Spyc::YAMLDump(array ('key' => array('key_one' => array('key_two' => '"Système d\'e-réservation bedrijventerreinen pimpt" bedrijventerreinen!')))); + $awaiting = "---\nkey:\n key_one:\n key_two: |\n \"Système d'e-réservation bedrijventerreinen pimpt\" bedrijventerreinen!\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testStringStartingWithSpace() { + $dump = Spyc::YAMLDump(array ('key' => array('key_one' => " Congrats, pimpt bedrijventerreinen \n pimpt bedrijventerreinen pimpt bedrijventerreinen!"))); + $awaiting = "---\nkey:\n key_one: |\n Congrats, pimpt bedrijventerreinen\n pimpt bedrijventerreinen pimpt bedrijventerreinen!\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testPerCentOne() { + $dump = Spyc::YAMLDump(array ('key' => "%name%, pimpts bedrijventerreinen!")); + $awaiting = "---\nkey: '%name%, pimpts bedrijventerreinen!'\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testPerCentAndSimpleQuote() { + $dump = Spyc::YAMLDump(array ('key' => "%name%, pimpt's bedrijventerreinen!")); + $awaiting = "---\nkey: \"%name%, pimpt's bedrijventerreinen!\"\n"; + $this->assertEquals ($awaiting, $dump); + } + + public function testPerCentAndDoubleQuote() { + $dump = Spyc::YAMLDump(array ('key' => '%name%, pimpt\'s "bed"rijventerreinen!')); + $awaiting = "---\nkey: |\n %name%, pimpt's \"bed\"rijventerreinen!\n"; + $this->assertEquals ($awaiting, $dump); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/IndentTest.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/IndentTest.php new file mode 100644 index 0000000..b7cdcab --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/IndentTest.php @@ -0,0 +1,68 @@ +Y = Spyc::YAMLLoad(__DIR__."/indent_1.yaml"); + } + + public function testIndent_1() { + $this->assertEquals (array ('child_1' => 2, 'child_2' => 0, 'child_3' => 1), $this->Y['root']); + } + + public function testIndent_2() { + $this->assertEquals (array ('child_1' => 1, 'child_2' => 2), $this->Y['root2']); + } + + public function testIndent_3() { + $this->assertEquals (array (array ('resolutions' => array (1024 => 768, 1920 => 1200), 'producer' => 'Nec')), $this->Y['display']); + } + + public function testIndent_4() { + $this->assertEquals (array ( + array ('resolutions' => array (1024 => 768)), + array ('resolutions' => array (1920 => 1200)), + ), $this->Y['displays']); + } + + public function testIndent_5() { + $this->assertEquals (array (array ( + 'row' => 0, + 'col' => 0, + 'headsets_affected' => array ( + array ( + 'ports' => array (0), + 'side' => 'left', + ) + ), + 'switch_function' => array ( + 'ics_ptt' => true + ) + )), $this->Y['nested_hashes_and_seqs']); + } + + public function testIndent_6() { + $this->assertEquals (array ( + 'h' => array ( + array ('a' => 'b', 'a1' => 'b1'), + array ('c' => 'd') + ) + ), $this->Y['easier_nest']); + } + + public function testIndent_space() { + $this->assertEquals ("By four\n spaces", $this->Y['one_space']); + } + + public function testListAndComment() { + $this->assertEquals (array ('one', 'two', 'three'), $this->Y['list_and_comment']); + } + + public function testAnchorAndAlias() { + $this->assertEquals (array ('database' => 'rails_dev', 'adapter' => 'mysql', 'host' => 'localhost'), $this->Y['development']); + $this->assertEquals (array (1 => 'abc'), $this->Y['zzz']); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/LoadTest.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/LoadTest.php new file mode 100644 index 0000000..a25d36f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/LoadTest.php @@ -0,0 +1,17 @@ +assertEquals ($yaml, $yaml_loaded); + } + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/ParseTest.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/ParseTest.php new file mode 100644 index 0000000..2ce77ca --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/ParseTest.php @@ -0,0 +1,413 @@ +yaml = spyc_load_file(__DIR__.'/../spyc.yaml'); + } + + public function testMergeHashKeys() { + $Expected = array ( + array ('step' => array('instrument' => 'Lasik 2000', 'pulseEnergy' => 5.4, 'pulseDuration' => 12, 'repetition' => 1000, 'spotSize' => '1mm')), + array ('step' => array('instrument' => 'Lasik 2000', 'pulseEnergy' => 5.4, 'pulseDuration' => 12, 'repetition' => 1000, 'spotSize' => '2mm')), + ); + $Actual = spyc_load_file (__DIR__.'/indent_1.yaml'); + $this->assertEquals ($Expected, $Actual['steps']); + } + + public function testDeathMasks() { + $Expected = array ('sad' => 2, 'magnificent' => 4); + $Actual = spyc_load_file (__DIR__.'/indent_1.yaml'); + $this->assertEquals ($Expected, $Actual['death masks are']); + } + + public function testDevDb() { + $Expected = array ('adapter' => 'mysql', 'host' => 'localhost', 'database' => 'rails_dev'); + $Actual = spyc_load_file (__DIR__.'/indent_1.yaml'); + $this->assertEquals ($Expected, $Actual['development']); + } + + public function testNumericKey() { + $this->assertEquals ("Ooo, a numeric key!", $this->yaml[1040]); + } + + public function testMappingsString() { + $this->assertEquals ("Anyone's name, really.", $this->yaml['String']); + } + + public function testMappingsInt() { + $this->assertSame (13, $this->yaml['Int']); + } + + public function testMappingsHex() { + $this->assertSame (243, $this->yaml['Hex']); + $this->assertSame ('f0xf3', $this->yaml['BadHex']); + } + + public function testMappingsBooleanTrue() { + $this->assertSame (true, $this->yaml['True']); + } + + public function testMappingsBooleanFalse() { + $this->assertSame (false, $this->yaml['False']); + } + + public function testMappingsZero() { + $this->assertSame (0, $this->yaml['Zero']); + } + + public function testMappingsNull() { + $this->assertSame (null, $this->yaml['Null']); + } + + public function testMappingsNotNull() { + $this->assertSame ('null', $this->yaml['NotNull']); + } + + public function testMappingsFloat() { + $this->assertSame (5.34, $this->yaml['Float']); + } + + public function testMappingsNegative() { + $this->assertSame (-90, $this->yaml['Negative']); + } + + public function testMappingsSmallFloat() { + $this->assertSame (0.7, $this->yaml['SmallFloat']); + } + + public function testNewline() { + $this->assertSame ('\n', $this->yaml['NewLine']); + } + + public function testQuotedNewline() { + $this->assertSame ("\n", $this->yaml['QuotedNewLine']); + } + + public function testSeq0() { + $this->assertEquals ("PHP Class", $this->yaml[0]); + } + + public function testSeq1() { + $this->assertEquals ("Basic YAML Loader", $this->yaml[1]); + } + + public function testSeq2() { + $this->assertEquals ("Very Basic YAML Dumper", $this->yaml[2]); + } + + public function testSeq3() { + $this->assertEquals (array("YAML is so easy to learn.", + "Your config files will never be the same."), $this->yaml[3]); + } + + public function testSeqMap() { + $this->assertEquals (array("cpu" => "1.5ghz", "ram" => "1 gig", + "os" => "os x 10.4.1"), $this->yaml[4]); + } + + public function testMappedSequence() { + $this->assertEquals (array("yaml.org", "php.net"), $this->yaml['domains']); + } + + public function testAnotherSequence() { + $this->assertEquals (array("program" => "Adium", "platform" => "OS X", + "type" => "Chat Client"), $this->yaml[5]); + } + + public function testFoldedBlock() { + $this->assertEquals ("There isn't any time for your tricks!\nDo you understand?", $this->yaml['no time']); + } + + public function testLiteralAsMapped() { + $this->assertEquals ("There is nothing but time\nfor your tricks.", $this->yaml['some time']); + } + + public function testCrazy() { + $this->assertEquals (array( array("name" => "spartan", "notes" => + array( "Needs to be backed up", + "Needs to be normalized" ), + "type" => "mysql" )), $this->yaml['databases']); + } + + public function testColons() { + $this->assertEquals ("like", $this->yaml["if: you'd"]); + } + + public function testInline() { + $this->assertEquals (array("One", "Two", "Three", "Four"), $this->yaml[6]); + } + + public function testNestedInline() { + $this->assertEquals (array("One", array("Two", "And", "Three"), "Four", "Five"), $this->yaml[7]); + } + + public function testNestedNestedInline() { + $this->assertEquals (array( "This", array("Is", "Getting", array("Ridiculous", "Guys")), + "Seriously", array("Show", "Mercy")), $this->yaml[8]); + } + + public function testInlineMappings() { + $this->assertEquals (array("name" => "chris", "age" => "young", "brand" => "lucky strike"), $this->yaml[9]); + } + + public function testNestedInlineMappings() { + $this->assertEquals (array("name" => "mark", "age" => "older than chris", + "brand" => array("marlboro", "lucky strike")), $this->yaml[10]); + } + + public function testReferences() { + $this->assertEquals (array('Perl', 'Python', 'PHP', 'Ruby'), $this->yaml['dynamic languages']); + } + + public function testReferences2() { + $this->assertEquals (array('C/C++', 'Java'), $this->yaml['compiled languages']); + } + + public function testReferences3() { + $this->assertEquals (array( + array('Perl', 'Python', 'PHP', 'Ruby'), + array('C/C++', 'Java') + ), $this->yaml['all languages']); + } + + public function testEscapedQuotes() { + $this->assertEquals ("you know, this shouldn't work. but it does.", $this->yaml[11]); + } + + public function testEscapedQuotes_2() { + $this->assertEquals ( "that's my value.", $this->yaml[12]); + } + + public function testEscapedQuotes_3() { + $this->assertEquals ("again, that's my value.", $this->yaml[13]); + } + + public function testQuotes() { + $this->assertEquals ("here's to \"quotes\", boss.", $this->yaml[14]); + } + + public function testQuoteSequence() { + $this->assertEquals ( array( 'name' => "Foo, Bar's", 'age' => 20), $this->yaml[15]); + } + + public function testShortSequence() { + $this->assertEquals (array( 0 => "a", 1 => array (0 => 1, 1 => 2), 2 => "b"), $this->yaml[16]); + } + + public function testQuotedNewlines() { + $this->assertEquals ("First line\nSecond line\nThird line", $this->yaml[17]); + } + + public function testHash_1() { + $this->assertEquals ("Hash", $this->yaml['hash_1']); + } + + public function testHash_2() { + $this->assertEquals ('Hash #and a comment', $this->yaml['hash_2']); + } + + public function testHash_3() { + $this->assertEquals ('Hash (#) can appear in key too', $this->yaml['hash#3']); + } + + public function testEndloop() { + $this->assertEquals ("Does this line in the end indeed make Spyc go to an infinite loop?", $this->yaml['endloop']); + } + + public function testReallyLargeNumber() { + $this->assertEquals ('115792089237316195423570985008687907853269984665640564039457584007913129639936', $this->yaml['a_really_large_number']); + } + + public function testFloatWithZeros() { + $this->assertSame ('1.0', $this->yaml['float_test']); + } + + public function testFloatWithQuotes() { + $this->assertSame ('1.0', $this->yaml['float_test_with_quotes']); + } + + public function testFloatInverse() { + $this->assertEquals ('001', $this->yaml['float_inverse_test']); + } + + public function testIntArray() { + $this->assertEquals (array (1, 2, 3), $this->yaml['int array']); + } + + public function testArrayOnSeveralLines() { + $this->assertEquals (array (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19), $this->yaml['array on several lines']); + } + + public function testArrayWithCommas() { + $this->assertEquals(array (0, 1), $this->yaml['array with commas']); + } + + public function testmoreLessKey() { + $this->assertEquals ('', $this->yaml['morelesskey']); + } + + public function testArrayOfZero() { + $this->assertSame (array(0), $this->yaml['array_of_zero']); + } + + public function testSophisticatedArrayOfZero() { + $this->assertSame (array('rx' => array ('tx' => array (0))), $this->yaml['sophisticated_array_of_zero']); + } + + public function testSwitches() { + $this->assertEquals (array (array ('row' => 0, 'col' => 0, 'func' => array ('tx' => array(0, 1)))), $this->yaml['switches']); + } + + public function testEmptySequence() { + $this->assertSame (array(), $this->yaml['empty_sequence']); + } + + public function testEmptyHash() { + $this->assertSame (array(), $this->yaml['empty_hash']); + } + + public function testEmptykey() { + $this->assertSame (array('' => array ('key' => 'value')), $this->yaml['empty_key']); + } + + public function testMultilines() { + $this->assertSame (array(array('type' => 'SomeItem', 'values' => array ('blah', 'blah', 'blah', 'blah'), 'ints' => array(2, 54, 12, 2143))), $this->yaml['multiline_items']); + } + + public function testManyNewlines() { + $this->assertSame ('A quick +fox + + +jumped +over + + + + + +a lazy + + + +dog', $this->yaml['many_lines']); + } + + public function testWerte() { + $this->assertSame (array ('1' => 'nummer 1', '0' => 'Stunde 0'), $this->yaml['werte']); + } + + /* public function testNoIndent() { + $this->assertSame (array( + array ('record1'=>'value1'), + array ('record2'=>'value2') + ) + , $this->yaml['noindent_records']); + } */ + + public function testColonsInKeys() { + $this->assertSame (array (1000), $this->yaml['a:1']); + } + + public function testColonsInKeys2() { + $this->assertSame (array (2000), $this->yaml['a:2']); + } + + public function testUnquotedColonsInKeys() { + $this->assertSame (array (3000), $this->yaml['a:3']); + } + + public function testComplicatedKeyWithColon() { + $this->assertSame(array("a:b:''test'" => 'value'), $this->yaml['complex_unquoted_key']); + } + + public function testKeysInMappedValueException() { + $this->setExpectedException('Exception'); + Spyc::YAMLLoad('x: y: z:'); + } + + public function testKeysInValueException() { + $this->setExpectedException('Exception'); + Spyc::YAMLLoad('x: y: z'); + } + + public function testSpecialCharacters() { + $this->assertSame ('[{]]{{]]', $this->yaml['special_characters']); + } + + public function testAngleQuotes() { + $Quotes = Spyc::YAMLLoad(__DIR__.'/quotes.yaml'); + $this->assertEquals (array ('html_tags' => array ('
    ', '

    '), 'html_content' => array ('

    hello world

    ', 'hello
    world'), 'text_content' => array ('hello world')), + $Quotes); + } + + public function testFailingColons() { + $Failing = Spyc::YAMLLoad(__DIR__.'/failing1.yaml'); + $this->assertSame (array ('MyObject' => array ('Prop1' => array ('key1:val1'))), + $Failing); + } + + public function testQuotesWithComments() { + $Expected = 'bar'; + $Actual = spyc_load_file (__DIR__.'/comments.yaml'); + $this->assertEquals ($Expected, $Actual['foo']); + } + + public function testArrayWithComments() { + $Expected = array ('x', 'y', 'z'); + $Actual = spyc_load_file (__DIR__.'/comments.yaml'); + $this->assertEquals ($Expected, $Actual['arr']); + } + + public function testAfterArrayWithKittens() { + $Expected = 'kittens'; + $Actual = spyc_load_file (__DIR__.'/comments.yaml'); + $this->assertEquals ($Expected, $Actual['bar']); + } + + // Plain characters http://www.yaml.org/spec/1.2/spec.html#id2789510 + public function testKai() { + $Expected = array('-example' => 'value'); + $Actual = spyc_load_file (__DIR__.'/indent_1.yaml'); + $this->assertEquals ($Expected, $Actual['kai']); + } + + public function testKaiList() { + $Expected = array ('-item', '-item', '-item'); + $Actual = spyc_load_file (__DIR__.'/indent_1.yaml'); + $this->assertEquals ($Expected, $Actual['kai_list_of_items']); + } + + public function testDifferentQuoteTypes() { + $expected = array ('Something', "", "", "Something else"); + $this->assertSame ($expected, $this->yaml['invoice']); + } + + public function testDifferentQuoteTypes2() { + $expected = array ('Something', "Nothing", "Anything", "Thing"); + $this->assertSame ($expected, $this->yaml['quotes']); + } + + // Separation spaces http://www.yaml.org/spec/1.2/spec.html#id2778394 + public function testMultipleArrays() { + $expected = array(array(array('x'))); + $this->assertSame($expected, Spyc::YAMLLoad("- - - x")); + } + + public function testElementWithEmptyHash() + { + $element = "hash: {}\narray: []"; + $yaml = Spyc::YAMLLoadString($element); + $this->assertEquals($yaml['hash'], []); + $this->assertEquals($yaml['array'], []); + + $yaml = Spyc::YAMLLoadString($element, [ + 'setting_empty_hash_as_object' => true + ]); + $this->assertInstanceOf(stdClass::class, $yaml['hash']); + $this->assertEquals($yaml['array'], []); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/RoundTripTest.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/RoundTripTest.php new file mode 100644 index 0000000..3e707df --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/RoundTripTest.php @@ -0,0 +1,76 @@ + $a))); } + + +class RoundTripTest extends PHPUnit_Framework_TestCase { + + protected function setUp() { + } + + public function testNull() { + $this->assertEquals (array ('x' => null), roundTrip (null)); + } + + public function testY() { + $this->assertEquals (array ('x' => 'y'), roundTrip ('y')); + } + + public function testExclam() { + $this->assertEquals (array ('x' => '!yeah'), roundTrip ('!yeah')); + } + + public function test5() { + $this->assertEquals (array ('x' => '5'), roundTrip ('5')); + } + + public function testSpaces() { + $this->assertEquals (array ('x' => 'x '), roundTrip ('x ')); + } + + public function testApostrophes() { + $this->assertEquals (array ('x' => "'biz'"), roundTrip ("'biz'")); + } + + public function testNewLines() { + $this->assertEquals (array ('x' => "\n"), roundTrip ("\n")); + } + + public function testHashes() { + $this->assertEquals (array ('x' => array ("#color" => '#fff')), roundTrip (array ("#color" => '#fff'))); + } + + public function testPreserveString() { + $result1 = roundTrip ('0'); + $result2 = roundTrip ('true'); + $this->assertTrue (is_string ($result1['x'])); + $this->assertTrue (is_string ($result2['x'])); + } + + public function testPreserveBool() { + $result = roundTrip (true); + $this->assertTrue (is_bool ($result['x'])); + } + + public function testPreserveInteger() { + $result = roundTrip (0); + $this->assertTrue (is_int ($result['x'])); + } + + public function testWordWrap() { + $this->assertEquals (array ('x' => "aaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), roundTrip ("aaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); + } + + public function testABCD() { + $this->assertEquals (array ('a', 'b', 'c', 'd'), Spyc::YAMLLoad(Spyc::YAMLDump(array('a', 'b', 'c', 'd')))); + } + + public function testABCD2() { + $a = array('a', 'b', 'c', 'd'); // Create a simple list + $b = Spyc::YAMLDump($a); // Dump the list as YAML + $c = Spyc::YAMLLoad($b); // Load the dumped YAML + $d = Spyc::YAMLDump($c); // Re-dump the data + $this->assertSame($b, $d); + } + +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/comments.yaml b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/comments.yaml new file mode 100644 index 0000000..c05012f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/comments.yaml @@ -0,0 +1,3 @@ +foo: 'bar' #Comment +arr: ['x', 'y', 'z'] # Comment here +bar: kittens \ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/failing1.yaml b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/failing1.yaml new file mode 100644 index 0000000..6906a51 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/failing1.yaml @@ -0,0 +1,2 @@ +MyObject: + Prop1: {key1:val1} \ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/indent_1.yaml b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/indent_1.yaml new file mode 100644 index 0000000..62d6ece --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/indent_1.yaml @@ -0,0 +1,70 @@ +root: + child_1: 2 + + child_2: 0 + child_3: 1 + +root2: + child_1: 1 +# A comment + child_2: 2 + +displays: + - resolutions: + 1024: 768 + - resolutions: + 1920: 1200 + +display: + - resolutions: + 1024: 768 + 1920: 1200 + producer: "Nec" + +nested_hashes_and_seqs: + - { row: 0, col: 0, headsets_affected: [{ports: [0], side: left}], switch_function: {ics_ptt: true} } + +easier_nest: { h: [{a: b, a1: b1}, {c: d}] } + +one_space: | + By four + spaces + +steps: + - step: &id001 + instrument: Lasik 2000 + pulseEnergy: 5.4 + pulseDuration: 12 + repetition: 1000 + spotSize: 1mm + - step: + <<: *id001 + spotSize: 2mm + +death masks are: + sad: 2 + <<: {magnificent: 4} + +login: &login + adapter: mysql + host: localhost + +development: + database: rails_dev + <<: *login + +"key": "value:" +colon_only: ":" + +list_and_comment: [one, two, three] # comment +kai: + -example: value +kai_list_of_items: + - -item + - '-item' + -item + +&foo bar: + 1: "abc" + +zzz: *foo diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/quotes.yaml b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/quotes.yaml new file mode 100644 index 0000000..2ceea86 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/mustangostang/spyc/tests/quotes.yaml @@ -0,0 +1,8 @@ +html_tags: + -
    + -

    +html_content: + -

    hello world

    + - hello
    world +text_content: + - hello world \ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/.editorconfig b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/.editorconfig new file mode 100644 index 0000000..a7c44dd --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/COMMITMENT b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/COMMITMENT new file mode 100644 index 0000000..a687e0d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/COMMITMENT @@ -0,0 +1,46 @@ +GPL Cooperation Commitment +Version 1.0 + +Before filing or continuing to prosecute any legal proceeding or claim +(other than a Defensive Action) arising from termination of a Covered +License, we commit to extend to the person or entity ('you') accused +of violating the Covered License the following provisions regarding +cure and reinstatement, taken from GPL version 3. As used here, the +term 'this License' refers to the specific Covered License being +enforced. + + 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. + +We intend this Commitment to be irrevocable, and binding and +enforceable against us and assignees of or successors to our +copyrights. + +Definitions + +'Covered License' means the GNU General Public License, version 2 +(GPLv2), the GNU Lesser General Public License, version 2.1 +(LGPLv2.1), or the GNU Library General Public License, version 2 +(LGPLv2), all as published by the Free Software Foundation. + +'Defensive Action' means a legal proceeding or claim that We bring +against you in response to a prior proceeding or claim initiated by +you or your affiliate. + +'We' means each contributor to this repository as of the date of +inclusion of this file, including subsidiaries of a corporate +contributor. + +This work is available under a Creative Commons Attribution-ShareAlike +4.0 International license (https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/LICENSE b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/LICENSE new file mode 100644 index 0000000..f166cc5 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/LICENSE @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! \ No newline at end of file diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/README.md b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/README.md new file mode 100644 index 0000000..07fe8c8 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/README.md @@ -0,0 +1,231 @@ +[![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://supportukrainenow.org/) + +![PHPMailer](https://raw.github.com/PHPMailer/PHPMailer/master/examples/images/phpmailer.png) + +# PHPMailer – A full-featured email creation and transfer class for PHP + +[![Test status](https://github.com/PHPMailer/PHPMailer/workflows/Tests/badge.svg)](https://github.com/PHPMailer/PHPMailer/actions) +[![codecov.io](https://codecov.io/gh/PHPMailer/PHPMailer/branch/master/graph/badge.svg?token=iORZpwmYmM)](https://codecov.io/gh/PHPMailer/PHPMailer) +[![Latest Stable Version](https://poser.pugx.org/phpmailer/phpmailer/v/stable.svg)](https://packagist.org/packages/phpmailer/phpmailer) +[![Total Downloads](https://poser.pugx.org/phpmailer/phpmailer/downloads)](https://packagist.org/packages/phpmailer/phpmailer) +[![License](https://poser.pugx.org/phpmailer/phpmailer/license.svg)](https://packagist.org/packages/phpmailer/phpmailer) +[![API Docs](https://github.com/phpmailer/phpmailer/workflows/Docs/badge.svg)](https://phpmailer.github.io/PHPMailer/) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/PHPMailer/PHPMailer/badge)](https://api.securityscorecards.dev/projects/github.com/PHPMailer/PHPMailer) + +## Features +- Probably the world's most popular code for sending email from PHP! +- Used by many open-source projects: WordPress, Drupal, 1CRM, SugarCRM, Yii, Joomla! and many more +- Integrated SMTP support – send without a local mail server +- Send emails with multiple To, CC, BCC, and Reply-to addresses +- Multipart/alternative emails for mail clients that do not read HTML email +- Add attachments, including inline +- Support for UTF-8 content and 8bit, base64, binary, and quoted-printable encodings +- SMTP authentication with LOGIN, PLAIN, CRAM-MD5, and XOAUTH2 mechanisms over SMTPS and SMTP+STARTTLS transports +- Validates email addresses automatically +- Protects against header injection attacks +- Error messages in over 50 languages! +- DKIM and S/MIME signing support +- Compatible with PHP 5.5 and later, including PHP 8.2 +- Namespaced to prevent name clashes +- Much more! + +## Why you might need it +Many PHP developers need to send email from their code. The only PHP function that supports this directly is [`mail()`](https://www.php.net/manual/en/function.mail.php). However, it does not provide any assistance for making use of popular features such as encryption, authentication, HTML messages, and attachments. + +Formatting email correctly is surprisingly difficult. There are myriad overlapping (and conflicting) standards, requiring tight adherence to horribly complicated formatting and encoding rules – the vast majority of code that you'll find online that uses the `mail()` function directly is just plain wrong, if not unsafe! + +The PHP `mail()` function usually sends via a local mail server, typically fronted by a `sendmail` binary on Linux, BSD, and macOS platforms, however, Windows usually doesn't include a local mail server; PHPMailer's integrated SMTP client allows email sending on all platforms without needing a local mail server. Be aware though, that the `mail()` function should be avoided when possible; it's both faster and [safer](https://exploitbox.io/paper/Pwning-PHP-Mail-Function-For-Fun-And-RCE.html) to use SMTP to localhost. + +*Please* don't be tempted to do it yourself – if you don't use PHPMailer, there are many other excellent libraries that +you should look at before rolling your own. Try [SwiftMailer](https://swiftmailer.symfony.com/) +, [Laminas/Mail](https://docs.laminas.dev/laminas-mail/), [ZetaComponents](https://github.com/zetacomponents/Mail), etc. + +## License +This software is distributed under the [LGPL 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) license, along with the [GPL Cooperation Commitment](https://gplcc.github.io/gplcc/). Please read [LICENSE](https://github.com/PHPMailer/PHPMailer/blob/master/LICENSE) for information on the software availability and distribution. + +## Installation & loading +PHPMailer is available on [Packagist](https://packagist.org/packages/phpmailer/phpmailer) (using semantic versioning), and installation via [Composer](https://getcomposer.org) is the recommended way to install PHPMailer. Just add this line to your `composer.json` file: + +```json +"phpmailer/phpmailer": "^6.9.2" +``` + +or run + +```sh +composer require phpmailer/phpmailer +``` + +Note that the `vendor` folder and the `vendor/autoload.php` script are generated by Composer; they are not part of PHPMailer. + +If you want to use XOAUTH2 authentication, you will also need to add a dependency on the `league/oauth2-client` and appropriate service adapters package in your `composer.json`, or take a look at +by @decomplexity's [SendOauth2 wrapper](https://github.com/decomplexity/SendOauth2), especially if you're using Microsoft services. + +Alternatively, if you're not using Composer, you +can [download PHPMailer as a zip file](https://github.com/PHPMailer/PHPMailer/archive/master.zip), (note that docs and examples are not included in the zip file), then copy the contents of the PHPMailer folder into one of the `include_path` directories specified in your PHP configuration and load each class file manually: + +```php +SMTPDebug = SMTP::DEBUG_SERVER; //Enable verbose debug output + $mail->isSMTP(); //Send using SMTP + $mail->Host = 'smtp.example.com'; //Set the SMTP server to send through + $mail->SMTPAuth = true; //Enable SMTP authentication + $mail->Username = 'user@example.com'; //SMTP username + $mail->Password = 'secret'; //SMTP password + $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; //Enable implicit TLS encryption + $mail->Port = 465; //TCP port to connect to; use 587 if you have set `SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS` + + //Recipients + $mail->setFrom('from@example.com', 'Mailer'); + $mail->addAddress('joe@example.net', 'Joe User'); //Add a recipient + $mail->addAddress('ellen@example.com'); //Name is optional + $mail->addReplyTo('info@example.com', 'Information'); + $mail->addCC('cc@example.com'); + $mail->addBCC('bcc@example.com'); + + //Attachments + $mail->addAttachment('/var/tmp/file.tar.gz'); //Add attachments + $mail->addAttachment('/tmp/image.jpg', 'new.jpg'); //Optional name + + //Content + $mail->isHTML(true); //Set email format to HTML + $mail->Subject = 'Here is the subject'; + $mail->Body = 'This is the HTML message body in bold!'; + $mail->AltBody = 'This is the body in plain text for non-HTML mail clients'; + + $mail->send(); + echo 'Message has been sent'; +} catch (Exception $e) { + echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}"; +} +``` + +You'll find plenty to play with in the [examples](https://github.com/PHPMailer/PHPMailer/tree/master/examples) folder, which covers many common scenarios including sending through Gmail, building contact forms, sending to mailing lists, and more. + +If you are re-using the instance (e.g. when sending to a mailing list), you may need to clear the recipient list to avoid sending duplicate messages. See [the mailing list example](https://github.com/PHPMailer/PHPMailer/blob/master/examples/mailing_list.phps) for further guidance. + +That's it. You should now be ready to use PHPMailer! + +## Localization +PHPMailer defaults to English, but in the [language](https://github.com/PHPMailer/PHPMailer/tree/master/language/) folder, you'll find many translations for PHPMailer error messages that you may encounter. Their filenames contain [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) language code for the translations, for example `fr` for French. To specify a language, you need to tell PHPMailer which one to use, like this: + +```php +//To load the French version +$mail->setLanguage('fr', '/optional/path/to/language/directory/'); +``` + +We welcome corrections and new languages – if you're looking for corrections, run the [Language/TranslationCompletenessTest.php](https://github.com/PHPMailer/PHPMailer/blob/master/test/Language/TranslationCompletenessTest.php) script in the tests folder and it will show any missing translations. + +## Documentation +Start reading at the [GitHub wiki](https://github.com/PHPMailer/PHPMailer/wiki). If you're having trouble, head for [the troubleshooting guide](https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting) as it's frequently updated. + +Examples of how to use PHPMailer for common scenarios can be found in the [examples](https://github.com/PHPMailer/PHPMailer/tree/master/examples) folder. If you're looking for a good starting point, we recommend you start with [the Gmail example](https://github.com/PHPMailer/PHPMailer/tree/master/examples/gmail.phps). + +To reduce PHPMailer's deployed code footprint, examples are not included if you load PHPMailer via Composer or via [GitHub's zip file download](https://github.com/PHPMailer/PHPMailer/archive/master.zip), so you'll need to either clone the git repository or use the above links to get to the examples directly. + +Complete generated API documentation is [available online](https://phpmailer.github.io/PHPMailer/). + +You can generate complete API-level documentation by running `phpdoc` in the top-level folder, and documentation will appear in the `docs` folder, though you'll need to have [PHPDocumentor](https://www.phpdoc.org) installed. You may find [the unit tests](https://github.com/PHPMailer/PHPMailer/blob/master/test/PHPMailer/PHPMailerTest.php) a good reference for how to do various operations such as encryption. + +If the documentation doesn't cover what you need, search the [many questions on Stack Overflow](https://stackoverflow.com/questions/tagged/phpmailer), and before you ask a question about "SMTP Error: Could not connect to SMTP host.", [read the troubleshooting guide](https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting). + +## Tests +[PHPMailer tests](https://github.com/PHPMailer/PHPMailer/tree/master/test/) use PHPUnit 9, with [a polyfill](https://github.com/Yoast/PHPUnit-Polyfills) to let 9-style tests run on older PHPUnit and PHP versions. + +[![Test status](https://github.com/PHPMailer/PHPMailer/workflows/Tests/badge.svg)](https://github.com/PHPMailer/PHPMailer/actions) + +If this isn't passing, is there something you can do to help? + +## Security +Please disclose any vulnerabilities found responsibly – report security issues to the maintainers privately. + +See [SECURITY](https://github.com/PHPMailer/PHPMailer/tree/master/SECURITY.md) and [PHPMailer's security advisories on GitHub](https://github.com/PHPMailer/PHPMailer/security). + +## Contributing +Please submit bug reports, suggestions, and pull requests to the [GitHub issue tracker](https://github.com/PHPMailer/PHPMailer/issues). + +We're particularly interested in fixing edge cases, expanding test coverage, and updating translations. + +If you found a mistake in the docs, or want to add something, go ahead and amend the wiki – anyone can edit it. + +If you have git clones from prior to the move to the PHPMailer GitHub organisation, you'll need to update any remote URLs referencing the old GitHub location with a command like this from within your clone: + +```sh +git remote set-url upstream https://github.com/PHPMailer/PHPMailer.git +``` + +Please *don't* use the SourceForge or Google Code projects any more; they are obsolete and no longer maintained. + +## Sponsorship +Development time and resources for PHPMailer are provided by [Smartmessages.net](https://info.smartmessages.net/), the world's only privacy-first email marketing system. + +Smartmessages.net privacy-first email marketing logo + +Donations are very welcome, whether in beer 🍺, T-shirts 👕, or cold, hard cash 💰. Sponsorship through GitHub is a simple and convenient way to say "thank you" to PHPMailer's maintainers and contributors – just click the "Sponsor" button [on the project page](https://github.com/PHPMailer/PHPMailer). If your company uses PHPMailer, consider taking part in Tidelift's enterprise support programme. + +## PHPMailer For Enterprise + +Available as part of the Tidelift Subscription. + +The maintainers of PHPMailer and thousands of other packages are working with Tidelift to deliver commercial +support and maintenance for the open-source packages you use to build your applications. Save time, reduce risk, and +improve code health, while paying the maintainers of the exact packages you +use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-phpmailer-phpmailer?utm_source=packagist-phpmailer-phpmailer&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) + +## Changelog +See [changelog](changelog.md). + +## History +- PHPMailer was originally written in 2001 by Brent R. Matzelle as a [SourceForge project](https://sourceforge.net/projects/phpmailer/). +- [Marcus Bointon](https://github.com/Synchro) (`coolbru` on SF) and Andy Prevost (`codeworxtech`) took over the project in 2004. +- Became an Apache incubator project on Google Code in 2010, managed by Jim Jagielski. +- Marcus created [his fork on GitHub](https://github.com/Synchro/PHPMailer) in 2008. +- Jim and Marcus decide to join forces and use GitHub as the canonical and official repo for PHPMailer in 2013. +- PHPMailer moves to [the PHPMailer organisation](https://github.com/PHPMailer) on GitHub in 2013. + +### What's changed since moving from SourceForge? +- Official successor to the SourceForge and Google Code projects. +- Test suite. +- Continuous integration with GitHub Actions. +- Composer support. +- Public development. +- Additional languages and language strings. +- CRAM-MD5 authentication support. +- Preserves full repo history of authors, commits, and branches from the original SourceForge project. diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/SECURITY.md b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/SECURITY.md new file mode 100644 index 0000000..4f34026 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/SECURITY.md @@ -0,0 +1,37 @@ +# Security notices relating to PHPMailer + +Please disclose any security issues or vulnerabilities found through [Tidelift's coordinated disclosure system](https://tidelift.com/security) or to the maintainers privately. + +PHPMailer 6.4.1 and earlier contain a vulnerability that can result in untrusted code being called (if such code is injected into the host project's scope by other means). If the `$patternselect` parameter to `validateAddress()` is set to `'php'` (the default, defined by `PHPMailer::$validator`), and the global namespace contains a function called `php`, it will be called in preference to the built-in validator of the same name. Mitigated in PHPMailer 6.5.0 by denying the use of simple strings as validator function names. Recorded as [CVE-2021-3603](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-3603). Reported by [Vikrant Singh Chauhan](mailto:vi@hackberry.xyz) via [huntr.dev](https://www.huntr.dev/). + +PHPMailer versions 6.4.1 and earlier contain a possible remote code execution vulnerability through the `$lang_path` parameter of the `setLanguage()` method. If the `$lang_path` parameter is passed unfiltered from user input, it can be set to [a UNC path](https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats#unc-paths), and if an attacker is also able to persuade the server to load a file from that UNC path, a script file under their control may be executed. This vulnerability only applies to systems that resolve UNC paths, typically only Microsoft Windows. +PHPMailer 6.5.0 mitigates this by no longer treating translation files as PHP code, but by parsing their text content directly. This approach avoids the possibility of executing unknown code while retaining backward compatibility. This isn't ideal, so the current translation format is deprecated and will be replaced in the next major release. Recorded as [CVE-2021-34551](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-34551). Reported by [Jilin Diting Information Technology Co., Ltd](https://listensec.com) via Tidelift. + +PHPMailer versions between 6.1.8 and 6.4.0 contain a regression of the earlier CVE-2018-19296 object injection vulnerability as a result of [a fix for Windows UNC paths in 6.1.8](https://github.com/PHPMailer/PHPMailer/commit/e2e07a355ee8ff36aba21d0242c5950c56e4c6f9). Recorded as [CVE-2020-36326](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-36326). Reported by Fariskhi Vidyan via Tidelift. 6.4.1 fixes this issue, and also enforces stricter checks for URL schemes in local path contexts. + +PHPMailer versions 6.1.5 and earlier contain an output escaping bug that occurs in `Content-Type` and `Content-Disposition` when filenames passed into `addAttachment` and other methods that accept attachment names contain double quote characters, in contravention of RFC822 3.4.1. No specific vulnerability has been found relating to this, but it could allow file attachments to bypass attachment filters that are based on matching filename extensions. Recorded as [CVE-2020-13625](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-13625). Reported by Elar Lang of Clarified Security. + +PHPMailer versions prior to 6.0.6 and 5.2.27 are vulnerable to an object injection attack by passing `phar://` paths into `addAttachment()` and other functions that may receive unfiltered local paths, possibly leading to RCE. Recorded as [CVE-2018-19296](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-19296). See [this article](https://knasmueller.net/5-answers-about-php-phar-exploitation) for more info on this type of vulnerability. Mitigated by blocking the use of paths containing URL-protocol style prefixes such as `phar://`. Reported by Sehun Oh of cyberone.kr. + +PHPMailer versions prior to 5.2.24 (released July 26th 2017) have an XSS vulnerability in one of the code examples, [CVE-2017-11503](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2017-11503). The `code_generator.phps` example did not filter user input prior to output. This file is distributed with a `.phps` extension, so it is not normally executable unless it is explicitly renamed, and the file is not included when PHPMailer is loaded through composer, so it is safe by default. There was also an undisclosed potential XSS vulnerability in the default exception handler (unused by default). Patches for both issues kindly provided by Patrick Monnerat of the Fedora Project. + +PHPMailer versions prior to 5.2.22 (released January 9th 2017) have a local file disclosure vulnerability, [CVE-2017-5223](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2017-5223). If content passed into `msgHTML()` is sourced from unfiltered user input, relative paths can map to absolute local file paths and added as attachments. Also note that `addAttachment` (just like `file_get_contents`, `passthru`, `unlink`, etc) should not be passed user-sourced params either! Reported by Yongxiang Li of Asiasecurity. + +PHPMailer versions prior to 5.2.20 (released December 28th 2016) are vulnerable to [CVE-2016-10045](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-10045) a remote code execution vulnerability, responsibly reported by [Dawid Golunski](https://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10045-Vuln-Patch-Bypass.html), and patched by Paul Buonopane (@Zenexer). + +PHPMailer versions prior to 5.2.18 (released December 2016) are vulnerable to [CVE-2016-10033](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-10033) a remote code execution vulnerability, responsibly reported by [Dawid Golunski](https://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10033-Vuln.html). + +PHPMailer versions prior to 5.2.14 (released November 2015) are vulnerable to [CVE-2015-8476](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-8476) an SMTP CRLF injection bug permitting arbitrary message sending. + +PHPMailer versions prior to 5.2.10 (released May 2015) are vulnerable to [CVE-2008-5619](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2008-5619), a remote code execution vulnerability in the bundled html2text library. This file was removed in 5.2.10, so if you are using a version prior to that and make use of the html2text function, it's vitally important that you upgrade and remove this file. + +PHPMailer versions prior to 2.0.7 and 2.2.1 are vulnerable to [CVE-2012-0796](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2012-0796), an email header injection attack. + +Joomla 1.6.0 uses PHPMailer in an unsafe way, allowing it to reveal local file paths, reported in [CVE-2011-3747](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2011-3747). + +PHPMailer didn't sanitise the `$lang_path` parameter in `SetLanguage`. This wasn't a problem in itself, but some apps (PHPClassifieds, ATutor) also failed to sanitise user-provided parameters passed to it, permitting semi-arbitrary local file inclusion, reported in [CVE-2010-4914](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2010-4914), [CVE-2007-2021](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2007-2021) and [CVE-2006-5734](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2006-5734). + +PHPMailer 1.7.2 and earlier contained a possible DDoS vulnerability reported in [CVE-2005-1807](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2005-1807). + +PHPMailer 1.7 and earlier (June 2003) have a possible vulnerability in the `SendmailSend` method where shell commands may not be sanitised. Reported in [CVE-2007-3215](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2007-3215). + diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/VERSION b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/VERSION new file mode 100644 index 0000000..5f54f91 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/VERSION @@ -0,0 +1 @@ +6.9.3 diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/composer.json b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/composer.json new file mode 100644 index 0000000..7b008b7 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/composer.json @@ -0,0 +1,80 @@ +{ + "name": "phpmailer/phpmailer", + "type": "library", + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "lock": false + }, + "require": { + "php": ">=5.5.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.7.2", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + }, + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "PHPMailer\\Test\\": "test/" + } + }, + "license": "LGPL-2.1-only", + "scripts": { + "check": "./vendor/bin/phpcs", + "test": "./vendor/bin/phpunit --no-coverage", + "coverage": "./vendor/bin/phpunit", + "lint": [ + "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . --show-deprecated -e php,phps --exclude vendor --exclude .git --exclude build" + ] + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/get_oauth_token.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/get_oauth_token.php new file mode 100644 index 0000000..0e54a00 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/get_oauth_token.php @@ -0,0 +1,182 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2020 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +/** + * Get an OAuth2 token from an OAuth2 provider. + * * Install this script on your server so that it's accessible + * as [https/http]:////get_oauth_token.php + * e.g.: http://localhost/phpmailer/get_oauth_token.php + * * Ensure dependencies are installed with 'composer install' + * * Set up an app in your Google/Yahoo/Microsoft account + * * Set the script address as the app's redirect URL + * If no refresh token is obtained when running this file, + * revoke access to your app and run the script again. + */ + +namespace PHPMailer\PHPMailer; + +/** + * Aliases for League Provider Classes + * Make sure you have added these to your composer.json and run `composer install` + * Plenty to choose from here: + * @see https://oauth2-client.thephpleague.com/providers/thirdparty/ + */ +//@see https://github.com/thephpleague/oauth2-google +use League\OAuth2\Client\Provider\Google; +//@see https://packagist.org/packages/hayageek/oauth2-yahoo +use Hayageek\OAuth2\Client\Provider\Yahoo; +//@see https://github.com/stevenmaguire/oauth2-microsoft +use Stevenmaguire\OAuth2\Client\Provider\Microsoft; +//@see https://github.com/greew/oauth2-azure-provider +use Greew\OAuth2\Client\Provider\Azure; + +if (!isset($_GET['code']) && !isset($_POST['provider'])) { + ?> + + +
    +

    Select Provider

    + +
    + +
    + +
    + +
    +

    Enter id and secret

    +

    These details are obtained by setting up an app in your provider's developer console. +

    +

    ClientId:

    +

    ClientSecret:

    +

    TenantID (only relevant for Azure):

    + +
    + + + $clientId, + 'clientSecret' => $clientSecret, + 'redirectUri' => $redirectUri, + 'accessType' => 'offline' +]; + +$options = []; +$provider = null; + +switch ($providerName) { + case 'Google': + $provider = new Google($params); + $options = [ + 'scope' => [ + 'https://mail.google.com/' + ] + ]; + break; + case 'Yahoo': + $provider = new Yahoo($params); + break; + case 'Microsoft': + $provider = new Microsoft($params); + $options = [ + 'scope' => [ + 'wl.imap', + 'wl.offline_access' + ] + ]; + break; + case 'Azure': + $params['tenantId'] = $tenantId; + + $provider = new Azure($params); + $options = [ + 'scope' => [ + 'https://outlook.office.com/SMTP.Send', + 'offline_access' + ] + ]; + break; +} + +if (null === $provider) { + exit('Provider missing'); +} + +if (!isset($_GET['code'])) { + //If we don't have an authorization code then get one + $authUrl = $provider->getAuthorizationUrl($options); + $_SESSION['oauth2state'] = $provider->getState(); + header('Location: ' . $authUrl); + exit; + //Check given state against previously stored one to mitigate CSRF attack +} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) { + unset($_SESSION['oauth2state']); + unset($_SESSION['provider']); + exit('Invalid state'); +} else { + unset($_SESSION['provider']); + //Try to get an access token (using the authorization code grant) + $token = $provider->getAccessToken( + 'authorization_code', + [ + 'code' => $_GET['code'] + ] + ); + //Use this to interact with an API on the users behalf + //Use this to get a new access token if the old one expires + echo 'Refresh Token: ', htmlspecialchars($token->getRefreshToken()); +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-af.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-af.php new file mode 100644 index 0000000..0b2a72d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-af.php @@ -0,0 +1,26 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'خطأ SMTP : لا يمكن تأكيد الهوية.'; +$PHPMAILER_LANG['connect_host'] = 'خطأ SMTP: لا يمكن الاتصال بالخادم SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'خطأ SMTP: لم يتم قبول المعلومات .'; +$PHPMAILER_LANG['empty_message'] = 'نص الرسالة فارغ'; +$PHPMAILER_LANG['encoding'] = 'ترميز غير معروف: '; +$PHPMAILER_LANG['execute'] = 'لا يمكن تنفيذ : '; +$PHPMAILER_LANG['file_access'] = 'لا يمكن الوصول للملف: '; +$PHPMAILER_LANG['file_open'] = 'خطأ في الملف: لا يمكن فتحه: '; +$PHPMAILER_LANG['from_failed'] = 'خطأ على مستوى عنوان المرسل : '; +$PHPMAILER_LANG['instantiate'] = 'لا يمكن توفير خدمة البريد.'; +$PHPMAILER_LANG['invalid_address'] = 'الإرسال غير ممكن لأن عنوان البريد الإلكتروني غير صالح: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' برنامج الإرسال غير مدعوم.'; +$PHPMAILER_LANG['provide_address'] = 'يجب توفير عنوان البريد الإلكتروني لمستلم واحد على الأقل.'; +$PHPMAILER_LANG['recipients_failed'] = 'خطأ SMTP: الأخطاء التالية فشل في الارسال لكل من : '; +$PHPMAILER_LANG['signing'] = 'خطأ في التوقيع: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() غير ممكن.'; +$PHPMAILER_LANG['smtp_error'] = 'خطأ على مستوى الخادم SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'لا يمكن تعيين أو إعادة تعيين متغير: '; +$PHPMAILER_LANG['extension_missing'] = 'الإضافة غير موجودة: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-as.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-as.php new file mode 100644 index 0000000..327dfba --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-as.php @@ -0,0 +1,35 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP ত্ৰুটি: প্ৰমাণীকৰণ কৰিব নোৱাৰি'; +$PHPMAILER_LANG['buggy_php'] = 'আপোনাৰ PHP সংস্কৰণ এটা বাগৰ দ্বাৰা প্ৰভাৱিত হয় যাৰ ফলত নষ্ট বাৰ্তা হব পাৰে । ইয়াক সমাধান কৰিবলে, প্ৰেৰণ কৰিবলে SMTP ব্যৱহাৰ কৰক, আপোনাৰ php.ini ত mail.add_x_header বিকল্প নিষ্ক্ৰিয় কৰক, MacOS বা Linux লৈ সলনি কৰক, বা আপোনাৰ PHP সংস্কৰণ 7.0.17+ বা 7.1.3+ লৈ সলনি কৰক ।'; +$PHPMAILER_LANG['connect_host'] = 'SMTP ত্ৰুটি: SMTP চাৰ্ভাৰৰ সৈতে সংযোগ কৰিবলে অক্ষম'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP ত্ৰুটি: তথ্য গ্ৰহণ কৰা হোৱা নাই'; +$PHPMAILER_LANG['empty_message'] = 'বাৰ্তাৰ মূখ্য অংশ খালী।'; +$PHPMAILER_LANG['encoding'] = 'অজ্ঞাত এনকোডিং: '; +$PHPMAILER_LANG['execute'] = 'এক্সিকিউট কৰিব নোৱাৰি: '; +$PHPMAILER_LANG['extension_missing'] = 'সম্প্ৰসাৰণ নোহোৱা হৈছে: '; +$PHPMAILER_LANG['file_access'] = 'ফাইল অভিগম কৰিবলে অক্ষম: '; +$PHPMAILER_LANG['file_open'] = 'ফাইল ত্ৰুটি: ফাইল খোলিবলৈ অক্ষম: '; +$PHPMAILER_LANG['from_failed'] = 'নিম্নলিখিত প্ৰেৰকৰ ঠিকনা(সমূহ) ব্যৰ্থ: '; +$PHPMAILER_LANG['instantiate'] = 'মেইল ফাংচনৰ এটা উদাহৰণ সৃষ্টি কৰিবলে অক্ষম'; +$PHPMAILER_LANG['invalid_address'] = 'প্ৰেৰণ কৰিব নোৱাৰি: অবৈধ ইমেইল ঠিকনা: '; +$PHPMAILER_LANG['invalid_header'] = 'অবৈধ হেডাৰৰ নাম বা মান'; +$PHPMAILER_LANG['invalid_hostentry'] = 'অবৈধ হোষ্টেন্ট্ৰি: '; +$PHPMAILER_LANG['invalid_host'] = 'অবৈধ হস্ট:'; +$PHPMAILER_LANG['mailer_not_supported'] = 'মেইলাৰ সমৰ্থিত নহয়।'; +$PHPMAILER_LANG['provide_address'] = 'আপুনি অন্ততঃ এটা গন্তব্য ইমেইল ঠিকনা দিব লাগিব'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP ত্ৰুটি: নিম্নলিখিত গন্তব্যস্থানসমূহ ব্যৰ্থ: '; +$PHPMAILER_LANG['signing'] = 'স্বাক্ষৰ কৰাত ব্যৰ্থ: '; +$PHPMAILER_LANG['smtp_code'] = 'SMTP কড: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'অতিৰিক্ত SMTP তথ্য: '; +$PHPMAILER_LANG['smtp_detail'] = 'বিৱৰণ:'; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP সংযোগ() ব্যৰ্থ'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP চাৰ্ভাৰৰ ত্ৰুটি: '; +$PHPMAILER_LANG['variable_set'] = 'চলক নিৰ্ধাৰণ কৰিব পৰা নগল: '; +$PHPMAILER_LANG['extension_missing'] = 'অনুপস্থিত সম্প্ৰসাৰণ: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-az.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-az.php new file mode 100644 index 0000000..552167e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-az.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Greška: Neuspjela prijava.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Greška: Nije moguće spojiti se sa SMTP serverom.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Greška: Podatci nisu prihvaćeni.'; +$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.'; +$PHPMAILER_LANG['encoding'] = 'Nepoznata kriptografija: '; +$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: '; +$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: '; +$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: '; +$PHPMAILER_LANG['from_failed'] = 'SMTP Greška: Slanje sa navedenih e-mail adresa nije uspjelo: '; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Greška: Slanje na navedene e-mail adrese nije uspjelo: '; +$PHPMAILER_LANG['instantiate'] = 'Ne mogu pokrenuti mail funkcionalnost.'; +$PHPMAILER_LANG['invalid_address'] = 'E-mail nije poslan. Neispravna e-mail adresa: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer nije podržan.'; +$PHPMAILER_LANG['provide_address'] = 'Definišite barem jednu adresu primaoca.'; +$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Spajanje na SMTP server nije uspjelo.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP greška: '; +$PHPMAILER_LANG['variable_set'] = 'Nije moguće postaviti varijablu ili je vratiti nazad: '; +$PHPMAILER_LANG['extension_missing'] = 'Nedostaje ekstenzija: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-be.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-be.php new file mode 100644 index 0000000..9e92dda --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-be.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Памылка SMTP: памылка ідэнтыфікацыі.'; +$PHPMAILER_LANG['connect_host'] = 'Памылка SMTP: нельга ўстанавіць сувязь з SMTP-серверам.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Памылка SMTP: звесткі непрынятыя.'; +$PHPMAILER_LANG['empty_message'] = 'Пустое паведамленне.'; +$PHPMAILER_LANG['encoding'] = 'Невядомая кадыроўка тэксту: '; +$PHPMAILER_LANG['execute'] = 'Нельга выканаць каманду: '; +$PHPMAILER_LANG['file_access'] = 'Няма доступу да файла: '; +$PHPMAILER_LANG['file_open'] = 'Нельга адкрыць файл: '; +$PHPMAILER_LANG['from_failed'] = 'Няправільны адрас адпраўніка: '; +$PHPMAILER_LANG['instantiate'] = 'Нельга прымяніць функцыю mail().'; +$PHPMAILER_LANG['invalid_address'] = 'Нельга даслаць паведамленне, няправільны email атрымальніка: '; +$PHPMAILER_LANG['provide_address'] = 'Запоўніце, калі ласка, правільны email атрымальніка.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' - паштовы сервер не падтрымліваецца.'; +$PHPMAILER_LANG['recipients_failed'] = 'Памылка SMTP: няправільныя атрымальнікі: '; +$PHPMAILER_LANG['signing'] = 'Памылка подпісу паведамлення: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Памылка сувязі з SMTP-серверам.'; +$PHPMAILER_LANG['smtp_error'] = 'Памылка SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Нельга ўстанавіць або перамяніць значэнне пераменнай: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-bg.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-bg.php new file mode 100644 index 0000000..c41f675 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-bg.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP грешка: Не може да се удостовери пред сървъра.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP грешка: Не може да се свърже с SMTP хоста.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP грешка: данните не са приети.'; +$PHPMAILER_LANG['empty_message'] = 'Съдържанието на съобщението е празно'; +$PHPMAILER_LANG['encoding'] = 'Неизвестно кодиране: '; +$PHPMAILER_LANG['execute'] = 'Не може да се изпълни: '; +$PHPMAILER_LANG['file_access'] = 'Няма достъп до файл: '; +$PHPMAILER_LANG['file_open'] = 'Файлова грешка: Не може да се отвори файл: '; +$PHPMAILER_LANG['from_failed'] = 'Следните адреси за подател са невалидни: '; +$PHPMAILER_LANG['instantiate'] = 'Не може да се инстанцира функцията mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Невалиден адрес: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' - пощенски сървър не се поддържа.'; +$PHPMAILER_LANG['provide_address'] = 'Трябва да предоставите поне един email адрес за получател.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP грешка: Следните адреси за Получател са невалидни: '; +$PHPMAILER_LANG['signing'] = 'Грешка при подписване: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP провален connect().'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP сървърна грешка: '; +$PHPMAILER_LANG['variable_set'] = 'Не може да се установи или възстанови променлива: '; +$PHPMAILER_LANG['extension_missing'] = 'Липсва разширение: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-bn.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-bn.php new file mode 100644 index 0000000..4736510 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-bn.php @@ -0,0 +1,35 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP ত্রুটি: প্রমাণীকরণ করতে অক্ষম৷'; +$PHPMAILER_LANG['buggy_php'] = 'আপনার PHP সংস্করণ একটি বাগ দ্বারা প্রভাবিত হয় যার ফলে দূষিত বার্তা হতে পারে। এটি ঠিক করতে, পাঠাতে SMTP ব্যবহার করুন, আপনার php.ini এ mail.add_x_header বিকল্পটি নিষ্ক্রিয় করুন, MacOS বা Linux-এ স্যুইচ করুন, অথবা আপনার PHP সংস্করণকে 7.0.17+ বা 7.1.3+ এ পরিবর্তন করুন।'; +$PHPMAILER_LANG['connect_host'] = 'SMTP ত্রুটি: SMTP সার্ভারের সাথে সংযোগ করতে অক্ষম৷'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP ত্রুটি: ডেটা গ্রহণ করা হয়নি৷'; +$PHPMAILER_LANG['empty_message'] = 'বার্তার অংশটি খালি।'; +$PHPMAILER_LANG['encoding'] = 'অজানা এনকোডিং: '; +$PHPMAILER_LANG['execute'] = 'নির্বাহ করতে অক্ষম: '; +$PHPMAILER_LANG['extension_missing'] = 'এক্সটেনশন অনুপস্থিত:'; +$PHPMAILER_LANG['file_access'] = 'ফাইল অ্যাক্সেস করতে অক্ষম: '; +$PHPMAILER_LANG['file_open'] = 'ফাইল ত্রুটি: ফাইল খুলতে অক্ষম: '; +$PHPMAILER_LANG['from_failed'] = 'নিম্নলিখিত প্রেরকের ঠিকানা(গুলি) ব্যর্থ হয়েছে: '; +$PHPMAILER_LANG['instantiate'] = 'মেল ফাংশনের একটি উদাহরণ তৈরি করতে অক্ষম৷'; +$PHPMAILER_LANG['invalid_address'] = 'পাঠাতে অক্ষম: অবৈধ ইমেল ঠিকানা: '; +$PHPMAILER_LANG['invalid_header'] = 'অবৈধ হেডার নাম বা মান'; +$PHPMAILER_LANG['invalid_hostentry'] = 'অবৈধ হোস্টেন্ট্রি: '; +$PHPMAILER_LANG['invalid_host'] = 'অবৈধ হোস্ট:'; +$PHPMAILER_LANG['mailer_not_supported'] = 'মেইলার সমর্থিত নয়।'; +$PHPMAILER_LANG['provide_address'] = 'আপনাকে অবশ্যই অন্তত একটি গন্তব্য ইমেল ঠিকানা প্রদান করতে হবে৷'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP ত্রুটি: নিম্নলিখিত গন্তব্যগুলি ব্যর্থ হয়েছে: '; +$PHPMAILER_LANG['signing'] = 'স্বাক্ষর করতে ব্যর্থ হয়েছে: '; +$PHPMAILER_LANG['smtp_code'] = 'SMTP কোড: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'অতিরিক্ত SMTP তথ্য:'; +$PHPMAILER_LANG['smtp_detail'] = 'বর্ণনা: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP সংযোগ() ব্যর্থ হয়েছে৷'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP সার্ভার ত্রুটি: '; +$PHPMAILER_LANG['variable_set'] = 'পরিবর্তনশীল সেট করা যায়নি: '; +$PHPMAILER_LANG['extension_missing'] = 'অনুপস্থিত এক্সটেনশন: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ca.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ca.php new file mode 100644 index 0000000..3468485 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ca.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Error SMTP: No s’ha pogut autenticar.'; +$PHPMAILER_LANG['connect_host'] = 'Error SMTP: No es pot connectar al servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Dades no acceptades.'; +$PHPMAILER_LANG['empty_message'] = 'El cos del missatge està buit.'; +$PHPMAILER_LANG['encoding'] = 'Codificació desconeguda: '; +$PHPMAILER_LANG['execute'] = 'No es pot executar: '; +$PHPMAILER_LANG['file_access'] = 'No es pot accedir a l’arxiu: '; +$PHPMAILER_LANG['file_open'] = 'Error d’Arxiu: No es pot obrir l’arxiu: '; +$PHPMAILER_LANG['from_failed'] = 'La(s) següent(s) adreces de remitent han fallat: '; +$PHPMAILER_LANG['instantiate'] = 'No s’ha pogut crear una instància de la funció Mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Adreça d’email invalida: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer no està suportat'; +$PHPMAILER_LANG['provide_address'] = 'S’ha de proveir almenys una adreça d’email com a destinatari.'; +$PHPMAILER_LANG['recipients_failed'] = 'Error SMTP: Els següents destinataris han fallat: '; +$PHPMAILER_LANG['signing'] = 'Error al signar: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Ha fallat el SMTP Connect().'; +$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'No s’ha pogut establir o restablir la variable: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-cs.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-cs.php new file mode 100644 index 0000000..e770a1a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-cs.php @@ -0,0 +1,28 @@ + + * Rewrite and extension of the work by Mikael Stokkebro + * + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP fejl: Login mislykkedes.'; +$PHPMAILER_LANG['buggy_php'] = 'Din version af PHP er berørt af en fejl, som gør at dine beskeder muligvis vises forkert. For at rette dette kan du skifte til SMTP, slå mail.add_x_header headeren i din php.ini fil fra, skifte til MacOS eller Linux eller opgradere din version af PHP til 7.0.17+ eller 7.1.3+.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP fejl: Forbindelse til SMTP serveren kunne ikke oprettes.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP fejl: Data blev ikke accepteret.'; +$PHPMAILER_LANG['empty_message'] = 'Meddelelsen er uden indhold'; +$PHPMAILER_LANG['encoding'] = 'Ukendt encode-format: '; +$PHPMAILER_LANG['execute'] = 'Kunne ikke afvikle: '; +$PHPMAILER_LANG['extension_missing'] = 'Udvidelse mangler: '; +$PHPMAILER_LANG['file_access'] = 'Kunne ikke tilgå filen: '; +$PHPMAILER_LANG['file_open'] = 'Fil fejl: Kunne ikke åbne filen: '; +$PHPMAILER_LANG['from_failed'] = 'Følgende afsenderadresse er forkert: '; +$PHPMAILER_LANG['instantiate'] = 'Email funktionen kunne ikke initialiseres.'; +$PHPMAILER_LANG['invalid_address'] = 'Udgyldig adresse: '; +$PHPMAILER_LANG['invalid_header'] = 'Ugyldig header navn eller værdi'; +$PHPMAILER_LANG['invalid_hostentry'] = 'Ugyldig hostentry: '; +$PHPMAILER_LANG['invalid_host'] = 'Ugyldig vært: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer understøttes ikke.'; +$PHPMAILER_LANG['provide_address'] = 'Indtast mindst en modtagers email adresse.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP fejl: Følgende modtagere fejlede: '; +$PHPMAILER_LANG['signing'] = 'Signeringsfejl: '; +$PHPMAILER_LANG['smtp_code'] = 'SMTP kode: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'Yderligere SMTP info: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fejlede.'; +$PHPMAILER_LANG['smtp_detail'] = 'Detalje: '; +$PHPMAILER_LANG['smtp_error'] = 'SMTP server fejl: '; +$PHPMAILER_LANG['variable_set'] = 'Kunne ikke definere eller nulstille variablen: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-de.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-de.php new file mode 100644 index 0000000..e7e59d2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-de.php @@ -0,0 +1,28 @@ + + * @author Crystopher Glodzienski Cardoso + * @author Daniel Cruz + */ + +$PHPMAILER_LANG['authenticate'] = 'Error SMTP: Imposible autentificar.'; +$PHPMAILER_LANG['buggy_php'] = 'Tu versión de PHP está afectada por un bug que puede resultar en mensajes corruptos. Para arreglarlo, cambia a enviar usando SMTP, deshabilita la opción mail.add_x_header en tu php.ini, cambia a MacOS o Linux, o actualiza tu PHP a la versión 7.0.17+ o 7.1.3+.'; +$PHPMAILER_LANG['connect_host'] = 'Error SMTP: Imposible conectar al servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Datos no aceptados.'; +$PHPMAILER_LANG['empty_message'] = 'El cuerpo del mensaje está vacío.'; +$PHPMAILER_LANG['encoding'] = 'Codificación desconocida: '; +$PHPMAILER_LANG['execute'] = 'Imposible ejecutar: '; +$PHPMAILER_LANG['extension_missing'] = 'Extensión faltante: '; +$PHPMAILER_LANG['file_access'] = 'Imposible acceder al archivo: '; +$PHPMAILER_LANG['file_open'] = 'Error de Archivo: Imposible abrir el archivo: '; +$PHPMAILER_LANG['from_failed'] = 'La(s) siguiente(s) direcciones de remitente fallaron: '; +$PHPMAILER_LANG['instantiate'] = 'Imposible crear una instancia de la función Mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Imposible enviar: dirección de email inválido: '; +$PHPMAILER_LANG['invalid_header'] = 'Nombre o valor de encabezado no válido'; +$PHPMAILER_LANG['invalid_hostentry'] = 'Hostentry inválido: '; +$PHPMAILER_LANG['invalid_host'] = 'Host inválido: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer no está soportado.'; +$PHPMAILER_LANG['provide_address'] = 'Debe proporcionar al menos una dirección de email de destino.'; +$PHPMAILER_LANG['recipients_failed'] = 'Error SMTP: Los siguientes destinos fallaron: '; +$PHPMAILER_LANG['signing'] = 'Error al firmar: '; +$PHPMAILER_LANG['smtp_code'] = 'Código del servidor SMTP: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'Información adicional del servidor SMTP: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falló.'; +$PHPMAILER_LANG['smtp_detail'] = 'Detalle: '; +$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'No se pudo configurar la variable: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-et.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-et.php new file mode 100644 index 0000000..93addc9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-et.php @@ -0,0 +1,28 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Viga: Autoriseerimise viga.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Viga: Ei õnnestunud luua ühendust SMTP serveriga.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Viga: Vigased andmed.'; +$PHPMAILER_LANG['empty_message'] = 'Tühi kirja sisu'; +$PHPMAILER_LANG["encoding"] = 'Tundmatu kodeering: '; +$PHPMAILER_LANG['execute'] = 'Tegevus ebaõnnestus: '; +$PHPMAILER_LANG['file_access'] = 'Pole piisavalt õiguseid järgneva faili avamiseks: '; +$PHPMAILER_LANG['file_open'] = 'Faili Viga: Faili avamine ebaõnnestus: '; +$PHPMAILER_LANG['from_failed'] = 'Järgnev saatja e-posti aadress on vigane: '; +$PHPMAILER_LANG['instantiate'] = 'mail funktiooni käivitamine ebaõnnestus.'; +$PHPMAILER_LANG['invalid_address'] = 'Saatmine peatatud, e-posti address vigane: '; +$PHPMAILER_LANG['provide_address'] = 'Te peate määrama vähemalt ühe saaja e-posti aadressi.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' maileri tugi puudub.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Viga: Järgnevate saajate e-posti aadressid on vigased: '; +$PHPMAILER_LANG["signing"] = 'Viga allkirjastamisel: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() ebaõnnestus.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP serveri viga: '; +$PHPMAILER_LANG['variable_set'] = 'Ei õnnestunud määrata või lähtestada muutujat: '; +$PHPMAILER_LANG['extension_missing'] = 'Nõutud laiendus on puudu: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-fa.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-fa.php new file mode 100644 index 0000000..295a47f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-fa.php @@ -0,0 +1,28 @@ + + * @author Mohammad Hossein Mojtahedi + */ + +$PHPMAILER_LANG['authenticate'] = 'خطای SMTP: احراز هویت با شکست مواجه شد.'; +$PHPMAILER_LANG['connect_host'] = 'خطای SMTP: اتصال به سرور SMTP برقرار نشد.'; +$PHPMAILER_LANG['data_not_accepted'] = 'خطای SMTP: داده‌ها نا‌درست هستند.'; +$PHPMAILER_LANG['empty_message'] = 'بخش متن پیام خالی است.'; +$PHPMAILER_LANG['encoding'] = 'کد‌گذاری نا‌شناخته: '; +$PHPMAILER_LANG['execute'] = 'امکان اجرا وجود ندارد: '; +$PHPMAILER_LANG['file_access'] = 'امکان دسترسی به فایل وجود ندارد: '; +$PHPMAILER_LANG['file_open'] = 'خطای File: امکان بازکردن فایل وجود ندارد: '; +$PHPMAILER_LANG['from_failed'] = 'آدرس فرستنده اشتباه است: '; +$PHPMAILER_LANG['instantiate'] = 'امکان معرفی تابع ایمیل وجود ندارد.'; +$PHPMAILER_LANG['invalid_address'] = 'آدرس ایمیل معتبر نیست: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer پشتیبانی نمی‌شود.'; +$PHPMAILER_LANG['provide_address'] = 'باید حداقل یک آدرس گیرنده وارد کنید.'; +$PHPMAILER_LANG['recipients_failed'] = 'خطای SMTP: ارسال به آدرس گیرنده با خطا مواجه شد: '; +$PHPMAILER_LANG['signing'] = 'خطا در امضا: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'خطا در اتصال به SMTP.'; +$PHPMAILER_LANG['smtp_error'] = 'خطا در SMTP Server: '; +$PHPMAILER_LANG['variable_set'] = 'امکان ارسال یا ارسال مجدد متغیر‌ها وجود ندارد: '; +$PHPMAILER_LANG['extension_missing'] = 'افزونه موجود نیست: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-fi.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-fi.php new file mode 100644 index 0000000..6d1e637 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-fi.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP feilur: Kundi ikki góðkenna.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP feilur: Kundi ikki knýta samband við SMTP vert.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP feilur: Data ikki góðkent.'; +//$PHPMAILER_LANG['empty_message'] = 'Message body empty'; +$PHPMAILER_LANG['encoding'] = 'Ókend encoding: '; +$PHPMAILER_LANG['execute'] = 'Kundi ikki útføra: '; +$PHPMAILER_LANG['file_access'] = 'Kundi ikki tilganga fílu: '; +$PHPMAILER_LANG['file_open'] = 'Fílu feilur: Kundi ikki opna fílu: '; +$PHPMAILER_LANG['from_failed'] = 'fylgjandi Frá/From adressa miseydnaðist: '; +$PHPMAILER_LANG['instantiate'] = 'Kuni ikki instantiera mail funktión.'; +//$PHPMAILER_LANG['invalid_address'] = 'Invalid address: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' er ikki supporterað.'; +$PHPMAILER_LANG['provide_address'] = 'Tú skal uppgeva minst móttakara-emailadressu(r).'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Feilur: Fylgjandi móttakarar miseydnaðust: '; +//$PHPMAILER_LANG['signing'] = 'Signing Error: '; +//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.'; +//$PHPMAILER_LANG['smtp_error'] = 'SMTP server error: '; +//$PHPMAILER_LANG['variable_set'] = 'Cannot set or reset variable: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-fr.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-fr.php new file mode 100644 index 0000000..a6d582d --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-fr.php @@ -0,0 +1,36 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Erro SMTP: Non puido ser autentificado.'; +$PHPMAILER_LANG['connect_host'] = 'Erro SMTP: Non puido conectar co servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Erro SMTP: Datos non aceptados.'; +$PHPMAILER_LANG['empty_message'] = 'Corpo da mensaxe vacía'; +$PHPMAILER_LANG['encoding'] = 'Codificación descoñecida: '; +$PHPMAILER_LANG['execute'] = 'Non puido ser executado: '; +$PHPMAILER_LANG['file_access'] = 'Nob puido acceder ó arquivo: '; +$PHPMAILER_LANG['file_open'] = 'Erro de Arquivo: No puido abrir o arquivo: '; +$PHPMAILER_LANG['from_failed'] = 'A(s) seguinte(s) dirección(s) de remitente(s) deron erro: '; +$PHPMAILER_LANG['instantiate'] = 'Non puido crear unha instancia da función Mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Non puido envia-lo correo: dirección de email inválida: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer non está soportado.'; +$PHPMAILER_LANG['provide_address'] = 'Debe engadir polo menos unha dirección de email coma destino.'; +$PHPMAILER_LANG['recipients_failed'] = 'Erro SMTP: Os seguintes destinos fallaron: '; +$PHPMAILER_LANG['signing'] = 'Erro ó firmar: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fallou.'; +$PHPMAILER_LANG['smtp_error'] = 'Erro do servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Non puidemos axustar ou reaxustar a variábel: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-he.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-he.php new file mode 100644 index 0000000..b123aa5 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-he.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'שגיאת SMTP: פעולת האימות נכשלה.'; +$PHPMAILER_LANG['connect_host'] = 'שגיאת SMTP: לא הצלחתי להתחבר לשרת SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'שגיאת SMTP: מידע לא התקבל.'; +$PHPMAILER_LANG['empty_message'] = 'גוף ההודעה ריק'; +$PHPMAILER_LANG['invalid_address'] = 'כתובת שגויה: '; +$PHPMAILER_LANG['encoding'] = 'קידוד לא מוכר: '; +$PHPMAILER_LANG['execute'] = 'לא הצלחתי להפעיל את: '; +$PHPMAILER_LANG['file_access'] = 'לא ניתן לגשת לקובץ: '; +$PHPMAILER_LANG['file_open'] = 'שגיאת קובץ: לא ניתן לגשת לקובץ: '; +$PHPMAILER_LANG['from_failed'] = 'כתובות הנמענים הבאות נכשלו: '; +$PHPMAILER_LANG['instantiate'] = 'לא הצלחתי להפעיל את פונקציית המייל.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' אינה נתמכת.'; +$PHPMAILER_LANG['provide_address'] = 'חובה לספק לפחות כתובת אחת של מקבל המייל.'; +$PHPMAILER_LANG['recipients_failed'] = 'שגיאת SMTP: הנמענים הבאים נכשלו: '; +$PHPMAILER_LANG['signing'] = 'שגיאת חתימה: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.'; +$PHPMAILER_LANG['smtp_error'] = 'שגיאת שרת SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'לא ניתן לקבוע או לשנות את המשתנה: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-hi.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-hi.php new file mode 100644 index 0000000..d2856e0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-hi.php @@ -0,0 +1,35 @@ + + * Rewrite and extension of the work by Jayanti Suthar + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP त्रुटि: प्रामाणिकता की जांच नहीं हो सका। '; +$PHPMAILER_LANG['buggy_php'] = 'PHP का आपका संस्करण एक बग से प्रभावित है जिसके परिणामस्वरूप संदेश दूषित हो सकते हैं. इसे ठीक करने हेतु, भेजने के लिए SMTP का उपयोग करे, अपने php.ini में mail.add_x_header विकल्प को अक्षम करें, MacOS या Linux पर जाए, या अपने PHP संस्करण को 7.0.17+ या 7.1.3+ बदले.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP त्रुटि: SMTP सर्वर से कनेक्ट नहीं हो सका। '; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP त्रुटि: डेटा स्वीकार नहीं किया जाता है। '; +$PHPMAILER_LANG['empty_message'] = 'संदेश खाली है। '; +$PHPMAILER_LANG['encoding'] = 'अज्ञात एन्कोडिंग प्रकार। '; +$PHPMAILER_LANG['execute'] = 'आदेश को निष्पादित करने में विफल। '; +$PHPMAILER_LANG['extension_missing'] = 'एक्सटेन्षन गायब है: '; +$PHPMAILER_LANG['file_access'] = 'फ़ाइल उपलब्ध नहीं है। '; +$PHPMAILER_LANG['file_open'] = 'फ़ाइल त्रुटि: फाइल को खोला नहीं जा सका। '; +$PHPMAILER_LANG['from_failed'] = 'प्रेषक का पता गलत है। '; +$PHPMAILER_LANG['instantiate'] = 'मेल फ़ंक्शन कॉल नहीं कर सकता है।'; +$PHPMAILER_LANG['invalid_address'] = 'पता गलत है। '; +$PHPMAILER_LANG['invalid_header'] = 'अमान्य हेडर नाम या मान'; +$PHPMAILER_LANG['invalid_hostentry'] = 'अमान्य hostentry: '; +$PHPMAILER_LANG['invalid_host'] = 'अमान्य होस्ट: '; +$PHPMAILER_LANG['mailer_not_supported'] = 'मेल सर्वर के साथ काम नहीं करता है। '; +$PHPMAILER_LANG['provide_address'] = 'आपको कम से कम एक प्राप्तकर्ता का ई-मेल पता प्रदान करना होगा।'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP त्रुटि: निम्न प्राप्तकर्ताओं को पते भेजने में विफल। '; +$PHPMAILER_LANG['signing'] = 'साइनअप त्रुटि: '; +$PHPMAILER_LANG['smtp_code'] = 'SMTP कोड: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'अतिरिक्त SMTP जानकारी: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP का connect () फ़ंक्शन विफल हुआ। '; +$PHPMAILER_LANG['smtp_detail'] = 'विवरण: '; +$PHPMAILER_LANG['smtp_error'] = 'SMTP सर्वर त्रुटि। '; +$PHPMAILER_LANG['variable_set'] = 'चर को बना या संशोधित नहीं किया जा सकता। '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-hr.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-hr.php new file mode 100644 index 0000000..cacb6c3 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-hr.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Greška: Neuspjela autentikacija.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Greška: Ne mogu se spojiti na SMTP poslužitelj.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Greška: Podatci nisu prihvaćeni.'; +$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.'; +$PHPMAILER_LANG['encoding'] = 'Nepoznati encoding: '; +$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: '; +$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: '; +$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: '; +$PHPMAILER_LANG['from_failed'] = 'SMTP Greška: Slanje s navedenih e-mail adresa nije uspjelo: '; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Greška: Slanje na navedenih e-mail adresa nije uspjelo: '; +$PHPMAILER_LANG['instantiate'] = 'Ne mogu pokrenuti mail funkcionalnost.'; +$PHPMAILER_LANG['invalid_address'] = 'E-mail nije poslan. Neispravna e-mail adresa: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer nije podržan.'; +$PHPMAILER_LANG['provide_address'] = 'Definirajte barem jednu adresu primatelja.'; +$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Spajanje na SMTP poslužitelj nije uspjelo.'; +$PHPMAILER_LANG['smtp_error'] = 'Greška SMTP poslužitelja: '; +$PHPMAILER_LANG['variable_set'] = 'Ne mogu postaviti varijablu niti ju vratiti nazad: '; +$PHPMAILER_LANG['extension_missing'] = 'Nedostaje proširenje: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-hu.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-hu.php new file mode 100644 index 0000000..e6b58b0 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-hu.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP -ի սխալ: չհաջողվեց ստուգել իսկությունը.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP -ի սխալ: չհաջողվեց կապ հաստատել SMTP սերվերի հետ.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP -ի սխալ: տվյալները ընդունված չեն.'; +$PHPMAILER_LANG['empty_message'] = 'Հաղորդագրությունը դատարկ է'; +$PHPMAILER_LANG['encoding'] = 'Կոդավորման անհայտ տեսակ: '; +$PHPMAILER_LANG['execute'] = 'Չհաջողվեց իրականացնել հրամանը: '; +$PHPMAILER_LANG['file_access'] = 'Ֆայլը հասանելի չէ: '; +$PHPMAILER_LANG['file_open'] = 'Ֆայլի սխալ: ֆայլը չհաջողվեց բացել: '; +$PHPMAILER_LANG['from_failed'] = 'Ուղարկողի հետևյալ հասցեն սխալ է: '; +$PHPMAILER_LANG['instantiate'] = 'Հնարավոր չէ կանչել mail ֆունկցիան.'; +$PHPMAILER_LANG['invalid_address'] = 'Հասցեն սխալ է: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' փոստային սերվերի հետ չի աշխատում.'; +$PHPMAILER_LANG['provide_address'] = 'Անհրաժեշտ է տրամադրել գոնե մեկ ստացողի e-mail հասցե.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP -ի սխալ: չի հաջողվել ուղարկել հետևյալ ստացողների հասցեներին: '; +$PHPMAILER_LANG['signing'] = 'Ստորագրման սխալ: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP -ի connect() ֆունկցիան չի հաջողվել'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP սերվերի սխալ: '; +$PHPMAILER_LANG['variable_set'] = 'Չի հաջողվում ստեղծել կամ վերափոխել փոփոխականը: '; +$PHPMAILER_LANG['extension_missing'] = 'Հավելվածը բացակայում է: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-id.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-id.php new file mode 100644 index 0000000..212a11f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-id.php @@ -0,0 +1,31 @@ + + * @author @januridp + * @author Ian Mustafa + */ + +$PHPMAILER_LANG['authenticate'] = 'Kesalahan SMTP: Tidak dapat mengotentikasi.'; +$PHPMAILER_LANG['connect_host'] = 'Kesalahan SMTP: Tidak dapat terhubung ke host SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Kesalahan SMTP: Data tidak diterima.'; +$PHPMAILER_LANG['empty_message'] = 'Isi pesan kosong'; +$PHPMAILER_LANG['encoding'] = 'Pengkodean karakter tidak dikenali: '; +$PHPMAILER_LANG['execute'] = 'Tidak dapat menjalankan proses: '; +$PHPMAILER_LANG['file_access'] = 'Tidak dapat mengakses berkas: '; +$PHPMAILER_LANG['file_open'] = 'Kesalahan Berkas: Berkas tidak dapat dibuka: '; +$PHPMAILER_LANG['from_failed'] = 'Alamat pengirim berikut mengakibatkan kesalahan: '; +$PHPMAILER_LANG['instantiate'] = 'Tidak dapat menginisialisasi fungsi surel.'; +$PHPMAILER_LANG['invalid_address'] = 'Gagal terkirim, alamat surel tidak sesuai: '; +$PHPMAILER_LANG['invalid_hostentry'] = 'Gagal terkirim, entri host tidak sesuai: '; +$PHPMAILER_LANG['invalid_host'] = 'Gagal terkirim, host tidak sesuai: '; +$PHPMAILER_LANG['provide_address'] = 'Harus tersedia minimal satu alamat tujuan'; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer tidak didukung'; +$PHPMAILER_LANG['recipients_failed'] = 'Kesalahan SMTP: Alamat tujuan berikut menyebabkan kesalahan: '; +$PHPMAILER_LANG['signing'] = 'Kesalahan dalam penandatangan SSL: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() gagal.'; +$PHPMAILER_LANG['smtp_error'] = 'Kesalahan pada pelayan SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Tidak dapat mengatur atau mengatur ulang variabel: '; +$PHPMAILER_LANG['extension_missing'] = 'Ekstensi PHP tidak tersedia: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-it.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-it.php new file mode 100644 index 0000000..08a6b73 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-it.php @@ -0,0 +1,28 @@ + + * @author Stefano Sabatini + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Impossibile autenticarsi.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Impossibile connettersi all\'host SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Dati non accettati dal server.'; +$PHPMAILER_LANG['empty_message'] = 'Il corpo del messaggio è vuoto'; +$PHPMAILER_LANG['encoding'] = 'Codifica dei caratteri sconosciuta: '; +$PHPMAILER_LANG['execute'] = 'Impossibile eseguire l\'operazione: '; +$PHPMAILER_LANG['file_access'] = 'Impossibile accedere al file: '; +$PHPMAILER_LANG['file_open'] = 'File Error: Impossibile aprire il file: '; +$PHPMAILER_LANG['from_failed'] = 'I seguenti indirizzi mittenti hanno generato errore: '; +$PHPMAILER_LANG['instantiate'] = 'Impossibile istanziare la funzione mail'; +$PHPMAILER_LANG['invalid_address'] = 'Impossibile inviare, l\'indirizzo email non è valido: '; +$PHPMAILER_LANG['provide_address'] = 'Deve essere fornito almeno un indirizzo ricevente'; +$PHPMAILER_LANG['mailer_not_supported'] = 'Mailer non supportato'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: I seguenti indirizzi destinatari hanno generato un errore: '; +$PHPMAILER_LANG['signing'] = 'Errore nella firma: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fallita.'; +$PHPMAILER_LANG['smtp_error'] = 'Errore del server SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Impossibile impostare o resettare la variabile: '; +$PHPMAILER_LANG['extension_missing'] = 'Estensione mancante: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ja.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ja.php new file mode 100644 index 0000000..d01869c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ja.php @@ -0,0 +1,37 @@ + + * @author Yoshi Sakai + * @author Arisophy + * @author ARAKI Musashi + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTPエラー: 認証できませんでした。'; +$PHPMAILER_LANG['buggy_php'] = 'ご利用のバージョンのPHPには不具合があり、メッセージが破損するおそれがあります。問題の解決は以下のいずれかを行ってください。SMTPでの送信に切り替える。php.iniのmail.add_x_headerをoffにする。MacOSまたはLinuxに切り替える。PHPバージョン7.0.17以降または7.1.3以降にアップグレードする。'; +$PHPMAILER_LANG['connect_host'] = 'SMTPエラー: SMTPホストに接続できませんでした。'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTPエラー: データが受け付けられませんでした。'; +$PHPMAILER_LANG['empty_message'] = 'メール本文が空です。'; +$PHPMAILER_LANG['encoding'] = '不明なエンコーディング: '; +$PHPMAILER_LANG['execute'] = '実行できませんでした: '; +$PHPMAILER_LANG['extension_missing'] = '拡張機能が見つかりません: '; +$PHPMAILER_LANG['file_access'] = 'ファイルにアクセスできません: '; +$PHPMAILER_LANG['file_open'] = 'ファイルエラー: ファイルを開けません: '; +$PHPMAILER_LANG['from_failed'] = 'Fromアドレスを登録する際にエラーが発生しました: '; +$PHPMAILER_LANG['instantiate'] = 'メール関数が正常に動作しませんでした。'; +$PHPMAILER_LANG['invalid_address'] = '不正なメールアドレス: '; +$PHPMAILER_LANG['invalid_header'] = '不正なヘッダー名またはその内容'; +$PHPMAILER_LANG['invalid_hostentry'] = '不正なホストエントリー: '; +$PHPMAILER_LANG['invalid_host'] = '不正なホスト: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' メーラーがサポートされていません。'; +$PHPMAILER_LANG['provide_address'] = '少なくとも1つメールアドレスを 指定する必要があります。'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTPエラー: 次の受信者アドレスに 間違いがあります: '; +$PHPMAILER_LANG['signing'] = '署名エラー: '; +$PHPMAILER_LANG['smtp_code'] = 'SMTPコード: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'SMTP追加情報: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP接続に失敗しました。'; +$PHPMAILER_LANG['smtp_detail'] = '詳細: '; +$PHPMAILER_LANG['smtp_error'] = 'SMTPサーバーエラー: '; +$PHPMAILER_LANG['variable_set'] = '変数が存在しません: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ka.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ka.php new file mode 100644 index 0000000..51fe403 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ka.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP შეცდომა: ავტორიზაცია შეუძლებელია.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP შეცდომა: SMTP სერვერთან დაკავშირება შეუძლებელია.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP შეცდომა: მონაცემები არ იქნა მიღებული.'; +$PHPMAILER_LANG['encoding'] = 'კოდირების უცნობი ტიპი: '; +$PHPMAILER_LANG['execute'] = 'შეუძლებელია შემდეგი ბრძანების შესრულება: '; +$PHPMAILER_LANG['file_access'] = 'შეუძლებელია წვდომა ფაილთან: '; +$PHPMAILER_LANG['file_open'] = 'ფაილური სისტემის შეცდომა: არ იხსნება ფაილი: '; +$PHPMAILER_LANG['from_failed'] = 'გამგზავნის არასწორი მისამართი: '; +$PHPMAILER_LANG['instantiate'] = 'mail ფუნქციის გაშვება ვერ ხერხდება.'; +$PHPMAILER_LANG['provide_address'] = 'გთხოვთ მიუთითოთ ერთი ადრესატის e-mail მისამართი მაინც.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' - საფოსტო სერვერის მხარდაჭერა არ არის.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP შეცდომა: შემდეგ მისამართებზე გაგზავნა ვერ მოხერხდა: '; +$PHPMAILER_LANG['empty_message'] = 'შეტყობინება ცარიელია'; +$PHPMAILER_LANG['invalid_address'] = 'არ გაიგზავნა, e-mail მისამართის არასწორი ფორმატი: '; +$PHPMAILER_LANG['signing'] = 'ხელმოწერის შეცდომა: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'შეცდომა SMTP სერვერთან დაკავშირებისას'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP სერვერის შეცდომა: '; +$PHPMAILER_LANG['variable_set'] = 'შეუძლებელია შემდეგი ცვლადის შექმნა ან შეცვლა: '; +$PHPMAILER_LANG['extension_missing'] = 'ბიბლიოთეკა არ არსებობს: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ko.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ko.php new file mode 100644 index 0000000..8c97dd9 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ko.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP 오류: 인증할 수 없습니다.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP 오류: SMTP 호스트에 접속할 수 없습니다.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 오류: 데이터가 받아들여지지 않았습니다.'; +$PHPMAILER_LANG['empty_message'] = '메세지 내용이 없습니다'; +$PHPMAILER_LANG['encoding'] = '알 수 없는 인코딩: '; +$PHPMAILER_LANG['execute'] = '실행 불가: '; +$PHPMAILER_LANG['file_access'] = '파일 접근 불가: '; +$PHPMAILER_LANG['file_open'] = '파일 오류: 파일을 열 수 없습니다: '; +$PHPMAILER_LANG['from_failed'] = '다음 From 주소에서 오류가 발생했습니다: '; +$PHPMAILER_LANG['instantiate'] = 'mail 함수를 인스턴스화할 수 없습니다'; +$PHPMAILER_LANG['invalid_address'] = '잘못된 주소: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' 메일러는 지원되지 않습니다.'; +$PHPMAILER_LANG['provide_address'] = '적어도 한 개 이상의 수신자 메일 주소를 제공해야 합니다.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP 오류: 다음 수신자에서 오류가 발생했습니다: '; +$PHPMAILER_LANG['signing'] = '서명 오류: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP 연결을 실패하였습니다.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP 서버 오류: '; +$PHPMAILER_LANG['variable_set'] = '변수 설정 및 초기화 불가: '; +$PHPMAILER_LANG['extension_missing'] = '확장자 없음: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ku.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ku.php new file mode 100644 index 0000000..cf3bda6 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ku.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'هەڵەی SMTP : نەتوانرا کۆدەکە پشتڕاست بکرێتەوە '; +$PHPMAILER_LANG['connect_host'] = 'هەڵەی SMTP: نەتوانرا پەیوەندی بە سێرڤەرەوە بکات SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'هەڵەی SMTP: ئەو زانیاریانە قبوڵ نەکرا.'; +$PHPMAILER_LANG['empty_message'] = 'پەیامەکە بەتاڵە'; +$PHPMAILER_LANG['encoding'] = 'کۆدکردنی نەزانراو : '; +$PHPMAILER_LANG['execute'] = 'ناتوانرێت جێبەجێ بکرێت: '; +$PHPMAILER_LANG['file_access'] = 'ناتوانرێت دەستت بگات بە فایلەکە: '; +$PHPMAILER_LANG['file_open'] = 'هەڵەی پەڕگە(فایل): ناتوانرێت بکرێتەوە: '; +$PHPMAILER_LANG['from_failed'] = 'هەڵە لە ئاستی ناونیشانی نێرەر: '; +$PHPMAILER_LANG['instantiate'] = 'ناتوانرێت خزمەتگوزاری پۆستە پێشکەش بکرێت.'; +$PHPMAILER_LANG['invalid_address'] = 'نەتوانرا بنێردرێت ، چونکە ناونیشانی ئیمەیڵەکە نادروستە: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' مەیلەر پشتگیری ناکات'; +$PHPMAILER_LANG['provide_address'] = 'دەبێت ناونیشانی ئیمەیڵی لانیکەم یەک وەرگر دابین بکرێت.'; +$PHPMAILER_LANG['recipients_failed'] = ' هەڵەی SMTP: ئەم هەڵانەی خوارەوەشکستی هێنا لە ناردن بۆ هەردووکیان: '; +$PHPMAILER_LANG['signing'] = 'هەڵەی واژۆ: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect()پەیوەندی شکستی هێنا .'; +$PHPMAILER_LANG['smtp_error'] = 'هەڵەی ئاستی سێرڤەری SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'ناتوانرێت بیگۆڕیت یان دوبارە بینێریتەوە: '; +$PHPMAILER_LANG['extension_missing'] = 'درێژکراوە نەماوە: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-lt.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-lt.php new file mode 100644 index 0000000..4f115b1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-lt.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP klaida: autentifikacija nepavyko.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP klaida: nepavyksta prisijungti prie SMTP stoties.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP klaida: duomenys nepriimti.'; +$PHPMAILER_LANG['empty_message'] = 'Laiško turinys tuščias'; +$PHPMAILER_LANG['encoding'] = 'Neatpažinta koduotė: '; +$PHPMAILER_LANG['execute'] = 'Nepavyko įvykdyti komandos: '; +$PHPMAILER_LANG['file_access'] = 'Byla nepasiekiama: '; +$PHPMAILER_LANG['file_open'] = 'Bylos klaida: Nepavyksta atidaryti: '; +$PHPMAILER_LANG['from_failed'] = 'Neteisingas siuntėjo adresas: '; +$PHPMAILER_LANG['instantiate'] = 'Nepavyko paleisti mail funkcijos.'; +$PHPMAILER_LANG['invalid_address'] = 'Neteisingas adresas: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' pašto stotis nepalaikoma.'; +$PHPMAILER_LANG['provide_address'] = 'Nurodykite bent vieną gavėjo adresą.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP klaida: nepavyko išsiųsti šiems gavėjams: '; +$PHPMAILER_LANG['signing'] = 'Prisijungimo klaida: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP susijungimo klaida'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP stoties klaida: '; +$PHPMAILER_LANG['variable_set'] = 'Nepavyko priskirti reikšmės kintamajam: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-lv.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-lv.php new file mode 100644 index 0000000..679b18c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-lv.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP kļūda: Autorizācija neizdevās.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Kļūda: Nevar izveidot savienojumu ar SMTP serveri.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Kļūda: Nepieņem informāciju.'; +$PHPMAILER_LANG['empty_message'] = 'Ziņojuma teksts ir tukšs'; +$PHPMAILER_LANG['encoding'] = 'Neatpazīts kodējums: '; +$PHPMAILER_LANG['execute'] = 'Neizdevās izpildīt komandu: '; +$PHPMAILER_LANG['file_access'] = 'Fails nav pieejams: '; +$PHPMAILER_LANG['file_open'] = 'Faila kļūda: Nevar atvērt failu: '; +$PHPMAILER_LANG['from_failed'] = 'Nepareiza sūtītāja adrese: '; +$PHPMAILER_LANG['instantiate'] = 'Nevar palaist sūtīšanas funkciju.'; +$PHPMAILER_LANG['invalid_address'] = 'Nepareiza adrese: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' sūtītājs netiek atbalstīts.'; +$PHPMAILER_LANG['provide_address'] = 'Lūdzu, norādiet vismaz vienu adresātu.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP kļūda: neizdevās nosūtīt šādiem saņēmējiem: '; +$PHPMAILER_LANG['signing'] = 'Autorizācijas kļūda: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP savienojuma kļūda'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP servera kļūda: '; +$PHPMAILER_LANG['variable_set'] = 'Nevar piešķirt mainīgā vērtību: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-mg.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-mg.php new file mode 100644 index 0000000..8a94f6a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-mg.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Hadisoana SMTP: Tsy nahomby ny fanamarinana.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Tsy afaka mampifandray amin\'ny mpampiantrano SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP diso: tsy voarakitra ny angona.'; +$PHPMAILER_LANG['empty_message'] = 'Tsy misy ny votoaty mailaka.'; +$PHPMAILER_LANG['encoding'] = 'Tsy fantatra encoding: '; +$PHPMAILER_LANG['execute'] = 'Tsy afaka manatanteraka ity baiko manaraka ity: '; +$PHPMAILER_LANG['file_access'] = 'Tsy nahomby ny fidirana amin\'ity rakitra ity: '; +$PHPMAILER_LANG['file_open'] = 'Hadisoana diso: Tsy afaka nanokatra ity file manaraka ity: '; +$PHPMAILER_LANG['from_failed'] = 'Ny adiresy iraka manaraka dia diso: '; +$PHPMAILER_LANG['instantiate'] = 'Tsy afaka nanomboka ny hetsika mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Tsy mety ny adiresy: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer tsy manohana.'; +$PHPMAILER_LANG['provide_address'] = 'Alefaso azafady iray adiresy iray farafahakeliny.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: Tsy mety ireo mpanaraka ireto: '; +$PHPMAILER_LANG['signing'] = 'Error nandritra ny sonia:'; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Tsy nahomby ny fifandraisana tamin\'ny server SMTP.'; +$PHPMAILER_LANG['smtp_error'] = 'Fahadisoana tamin\'ny server SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Tsy azo atao ny mametraka na mamerina ny variable: '; +$PHPMAILER_LANG['extension_missing'] = 'Tsy hita ny ampahany: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-mn.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-mn.php new file mode 100644 index 0000000..04d262c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-mn.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Ralat SMTP: Tidak dapat pengesahan.'; +$PHPMAILER_LANG['connect_host'] = 'Ralat SMTP: Tidak dapat menghubungi hos pelayan SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Ralat SMTP: Data tidak diterima oleh pelayan.'; +$PHPMAILER_LANG['empty_message'] = 'Tiada isi untuk mesej'; +$PHPMAILER_LANG['encoding'] = 'Pengekodan tidak diketahui: '; +$PHPMAILER_LANG['execute'] = 'Tidak dapat melaksanakan: '; +$PHPMAILER_LANG['file_access'] = 'Tidak dapat mengakses fail: '; +$PHPMAILER_LANG['file_open'] = 'Ralat Fail: Tidak dapat membuka fail: '; +$PHPMAILER_LANG['from_failed'] = 'Berikut merupakan ralat dari alamat e-mel: '; +$PHPMAILER_LANG['instantiate'] = 'Tidak dapat memberi contoh fungsi e-mel.'; +$PHPMAILER_LANG['invalid_address'] = 'Alamat emel tidak sah: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' jenis penghantar emel tidak disokong.'; +$PHPMAILER_LANG['provide_address'] = 'Anda perlu menyediakan sekurang-kurangnya satu alamat e-mel penerima.'; +$PHPMAILER_LANG['recipients_failed'] = 'Ralat SMTP: Penerima e-mel berikut telah gagal: '; +$PHPMAILER_LANG['signing'] = 'Ralat pada tanda tangan: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() telah gagal.'; +$PHPMAILER_LANG['smtp_error'] = 'Ralat pada pelayan SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Tidak boleh menetapkan atau menetapkan semula pembolehubah: '; +$PHPMAILER_LANG['extension_missing'] = 'Sambungan hilang: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-nb.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-nb.php new file mode 100644 index 0000000..c9621a1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-nb.php @@ -0,0 +1,33 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP-fout: authenticatie mislukt.'; +$PHPMAILER_LANG['buggy_php'] = 'PHP versie gededecteerd die onderhavig is aan een bug die kan resulteren in gecorrumpeerde berichten. Om dit te voorkomen, gebruik SMTP voor het verzenden van berichten, zet de mail.add_x_header optie in uw php.ini file uit, gebruik MacOS of Linux, of pas de gebruikte PHP versie aan naar versie 7.0.17+ or 7.1.3+.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP-fout: kon niet verbinden met SMTP-host.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP-fout: data niet geaccepteerd.'; +$PHPMAILER_LANG['empty_message'] = 'Berichttekst is leeg'; +$PHPMAILER_LANG['encoding'] = 'Onbekende codering: '; +$PHPMAILER_LANG['execute'] = 'Kon niet uitvoeren: '; +$PHPMAILER_LANG['extension_missing'] = 'Extensie afwezig: '; +$PHPMAILER_LANG['file_access'] = 'Kreeg geen toegang tot bestand: '; +$PHPMAILER_LANG['file_open'] = 'Bestandsfout: kon bestand niet openen: '; +$PHPMAILER_LANG['from_failed'] = 'Het volgende afzendersadres is mislukt: '; +$PHPMAILER_LANG['instantiate'] = 'Kon mailfunctie niet initialiseren.'; +$PHPMAILER_LANG['invalid_address'] = 'Ongeldig adres: '; +$PHPMAILER_LANG['invalid_header'] = 'Ongeldige header naam of waarde'; +$PHPMAILER_LANG['invalid_hostentry'] = 'Ongeldige hostentry: '; +$PHPMAILER_LANG['invalid_host'] = 'Ongeldige host: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer wordt niet ondersteund.'; +$PHPMAILER_LANG['provide_address'] = 'Er moet minstens één ontvanger worden opgegeven.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP-fout: de volgende ontvangers zijn mislukt: '; +$PHPMAILER_LANG['signing'] = 'Signeerfout: '; +$PHPMAILER_LANG['smtp_code'] = 'SMTP code: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'Aanvullende SMTP informatie: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Verbinding mislukt.'; +$PHPMAILER_LANG['smtp_detail'] = 'Detail: '; +$PHPMAILER_LANG['smtp_error'] = 'SMTP-serverfout: '; +$PHPMAILER_LANG['variable_set'] = 'Kan de volgende variabele niet instellen of resetten: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-pl.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-pl.php new file mode 100644 index 0000000..cb7b2c2 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-pl.php @@ -0,0 +1,33 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Erro do SMTP: Não foi possível realizar a autenticação.'; +$PHPMAILER_LANG['connect_host'] = 'Erro do SMTP: Não foi possível realizar ligação com o servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Erro do SMTP: Os dados foram rejeitados.'; +$PHPMAILER_LANG['empty_message'] = 'A mensagem no e-mail está vazia.'; +$PHPMAILER_LANG['encoding'] = 'Codificação desconhecida: '; +$PHPMAILER_LANG['execute'] = 'Não foi possível executar: '; +$PHPMAILER_LANG['file_access'] = 'Não foi possível aceder o ficheiro: '; +$PHPMAILER_LANG['file_open'] = 'Abertura do ficheiro: Não foi possível abrir o ficheiro: '; +$PHPMAILER_LANG['from_failed'] = 'Ocorreram falhas nos endereços dos seguintes remententes: '; +$PHPMAILER_LANG['instantiate'] = 'Não foi possível iniciar uma instância da função mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Não foi enviado nenhum e-mail para o endereço de e-mail inválido: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer não é suportado.'; +$PHPMAILER_LANG['provide_address'] = 'Tem de fornecer pelo menos um endereço como destinatário do e-mail.'; +$PHPMAILER_LANG['recipients_failed'] = 'Erro do SMTP: O endereço do seguinte destinatário falhou: '; +$PHPMAILER_LANG['signing'] = 'Erro ao assinar: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falhou.'; +$PHPMAILER_LANG['smtp_error'] = 'Erro de servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Não foi possível definir ou redefinir a variável: '; +$PHPMAILER_LANG['extension_missing'] = 'Extensão em falta: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-pt_br.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-pt_br.php new file mode 100644 index 0000000..5239865 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-pt_br.php @@ -0,0 +1,38 @@ + + * @author Lucas Guimarães + * @author Phelipe Alves + * @author Fabio Beneditto + * @author Geidson Benício Coelho + */ + +$PHPMAILER_LANG['authenticate'] = 'Erro de SMTP: Não foi possível autenticar.'; +$PHPMAILER_LANG['buggy_php'] = 'Sua versão do PHP é afetada por um bug que por resultar em messagens corrompidas. Para corrigir, mude para enviar usando SMTP, desative a opção mail.add_x_header em seu php.ini, mude para MacOS ou Linux, ou atualize seu PHP para versão 7.0.17+ ou 7.1.3+ '; +$PHPMAILER_LANG['connect_host'] = 'Erro de SMTP: Não foi possível conectar ao servidor SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Erro de SMTP: Dados rejeitados.'; +$PHPMAILER_LANG['empty_message'] = 'Mensagem vazia'; +$PHPMAILER_LANG['encoding'] = 'Codificação desconhecida: '; +$PHPMAILER_LANG['execute'] = 'Não foi possível executar: '; +$PHPMAILER_LANG['extension_missing'] = 'Extensão não existe: '; +$PHPMAILER_LANG['file_access'] = 'Não foi possível acessar o arquivo: '; +$PHPMAILER_LANG['file_open'] = 'Erro de Arquivo: Não foi possível abrir o arquivo: '; +$PHPMAILER_LANG['from_failed'] = 'Os seguintes remetentes falharam: '; +$PHPMAILER_LANG['instantiate'] = 'Não foi possível instanciar a função mail.'; +$PHPMAILER_LANG['invalid_address'] = 'Endereço de e-mail inválido: '; +$PHPMAILER_LANG['invalid_header'] = 'Nome ou valor de cabeçalho inválido'; +$PHPMAILER_LANG['invalid_hostentry'] = 'hostentry inválido: '; +$PHPMAILER_LANG['invalid_host'] = 'host inválido: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer não é suportado.'; +$PHPMAILER_LANG['provide_address'] = 'Você deve informar pelo menos um destinatário.'; +$PHPMAILER_LANG['recipients_failed'] = 'Erro de SMTP: Os seguintes destinatários falharam: '; +$PHPMAILER_LANG['signing'] = 'Erro de Assinatura: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falhou.'; +$PHPMAILER_LANG['smtp_code'] = 'Código do servidor SMTP: '; +$PHPMAILER_LANG['smtp_error'] = 'Erro de servidor SMTP: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'Informações adicionais do servidor SMTP: '; +$PHPMAILER_LANG['smtp_detail'] = 'Detalhes do servidor SMTP: '; +$PHPMAILER_LANG['variable_set'] = 'Não foi possível definir ou redefinir a variável: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ro.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ro.php new file mode 100644 index 0000000..45bef91 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ro.php @@ -0,0 +1,33 @@ + + * @author Foster Snowhill + * @author ProjectSoft + */ + +$PHPMAILER_LANG['authenticate'] = 'Ошибка SMTP: не удалось пройти аутентификацию.'; +$PHPMAILER_LANG['buggy_php'] = 'В вашей версии PHP есть ошибка, которая может привести к повреждению сообщений. Чтобы исправить, переключитесь на отправку по SMTP, отключите опцию mail.add_x_header в ваш php.ini, переключитесь на MacOS или Linux или обновите PHP до версии 7.0.17+ или 7.1.3+.'; +$PHPMAILER_LANG['connect_host'] = 'Ошибка SMTP: не удается подключиться к SMTP-серверу.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Ошибка SMTP: данные не приняты.'; +$PHPMAILER_LANG['empty_message'] = 'Пустое сообщение'; +$PHPMAILER_LANG['encoding'] = 'Неизвестная кодировка: '; +$PHPMAILER_LANG['execute'] = 'Невозможно выполнить команду: '; +$PHPMAILER_LANG['extension_missing'] = 'Расширение отсутствует: '; +$PHPMAILER_LANG['file_access'] = 'Нет доступа к файлу: '; +$PHPMAILER_LANG['file_open'] = 'Файловая ошибка: не удаётся открыть файл: '; +$PHPMAILER_LANG['from_failed'] = 'Неверный адрес отправителя: '; +$PHPMAILER_LANG['instantiate'] = 'Невозможно запустить функцию mail().'; +$PHPMAILER_LANG['invalid_address'] = 'Не отправлено из-за неправильного формата email-адреса: '; +$PHPMAILER_LANG['invalid_header'] = 'Неверное имя или значение заголовка'; +$PHPMAILER_LANG['invalid_hostentry'] = 'Неверная запись хоста: '; +$PHPMAILER_LANG['invalid_host'] = 'Неверный хост: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' — почтовый сервер не поддерживается.'; +$PHPMAILER_LANG['provide_address'] = 'Вы должны указать хотя бы один адрес электронной почты получателя.'; +$PHPMAILER_LANG['recipients_failed'] = 'Ошибка SMTP: Ошибка следующих получателей: '; +$PHPMAILER_LANG['signing'] = 'Ошибка подписи: '; +$PHPMAILER_LANG['smtp_code'] = 'Код SMTP: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'Дополнительная информация SMTP: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Ошибка соединения с SMTP-сервером.'; +$PHPMAILER_LANG['smtp_detail'] = 'Детали: '; +$PHPMAILER_LANG['smtp_error'] = 'Ошибка SMTP-сервера: '; +$PHPMAILER_LANG['variable_set'] = 'Невозможно установить или сбросить переменную: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-si.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-si.php new file mode 100644 index 0000000..dce502a --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-si.php @@ -0,0 +1,34 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP දෝෂය: සත්‍යාපනය අසාර්ථක විය.'; +$PHPMAILER_LANG['buggy_php'] = 'ඔබගේ PHP version එකෙහි පවතින දෝෂයක් නිසා email පණිවිඩ දෝෂ සහගත වීමේ හැකියාවක් ඇත. මෙය විසදීම සදහා SMTP භාවිතා කිරීම, mail.add_x_header INI setting එක අක්‍රීය කිරීම, MacOS හෝ Linux වලට මාරු වීම, හෝ ඔබගේ PHP version එක 7.0.17+ හෝ 7.1.3+ වලට අලුත් කිරීම කරගන්න.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP දෝෂය: සම්බන්ධ වීමට නොහැකි විය.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP දෝෂය: දත්ත පිළිගනු නොලැබේ.'; +$PHPMAILER_LANG['empty_message'] = 'පණිවිඩ අන්තර්ගතය හිස්'; +$PHPMAILER_LANG['encoding'] = 'නොදන්නා කේතනය: '; +$PHPMAILER_LANG['execute'] = 'ක්‍රියාත්මක කළ නොහැකි විය: '; +$PHPMAILER_LANG['extension_missing'] = 'Extension එක නොමැත: '; +$PHPMAILER_LANG['file_access'] = 'File එකට ප්‍රවේශ විය නොහැකි විය: '; +$PHPMAILER_LANG['file_open'] = 'File දෝෂය: File එක විවෘත කළ නොහැක: '; +$PHPMAILER_LANG['from_failed'] = 'පහත From ලිපිනයන් අසාර්ථක විය: '; +$PHPMAILER_LANG['instantiate'] = 'mail function එක ක්‍රියාත්මක කළ නොහැක.'; +$PHPMAILER_LANG['invalid_address'] = 'වලංගු නොවන ලිපිනය: '; +$PHPMAILER_LANG['invalid_header'] = 'වලංගු නොවන header නාමයක් හෝ අගයක්'; +$PHPMAILER_LANG['invalid_hostentry'] = 'වලංගු නොවන hostentry එකක්: '; +$PHPMAILER_LANG['invalid_host'] = 'වලංගු නොවන host එකක්: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer සහාය නොදක්වයි.'; +$PHPMAILER_LANG['provide_address'] = 'ඔබ අවම වශයෙන් එක් ලබන්නෙකුගේ ඊමේල් ලිපිනයක් සැපයිය යුතුය.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP දෝෂය: පහත ලබන්නන් අසමත් විය: '; +$PHPMAILER_LANG['signing'] = 'Sign කිරීමේ දෝෂය: '; +$PHPMAILER_LANG['smtp_code'] = 'SMTP කේතය: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'අමතර SMTP තොරතුරු: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP සම්බන්ධය අසාර්ථක විය.'; +$PHPMAILER_LANG['smtp_detail'] = 'තොරතුරු: '; +$PHPMAILER_LANG['smtp_error'] = 'SMTP දෝෂය: '; +$PHPMAILER_LANG['variable_set'] = 'Variable එක සැකසීමට හෝ නැවත සැකසීමට නොහැක: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sk.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sk.php new file mode 100644 index 0000000..028f5bc --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sk.php @@ -0,0 +1,30 @@ + + * @author Peter Orlický + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Chyba autentifikácie.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Nebolo možné nadviazať spojenie so SMTP serverom.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Dáta neboli prijaté'; +$PHPMAILER_LANG['empty_message'] = 'Prázdne telo správy.'; +$PHPMAILER_LANG['encoding'] = 'Neznáme kódovanie: '; +$PHPMAILER_LANG['execute'] = 'Nedá sa vykonať: '; +$PHPMAILER_LANG['file_access'] = 'Súbor nebol nájdený: '; +$PHPMAILER_LANG['file_open'] = 'File Error: Súbor sa otvoriť pre čítanie: '; +$PHPMAILER_LANG['from_failed'] = 'Následujúca adresa From je nesprávna: '; +$PHPMAILER_LANG['instantiate'] = 'Nedá sa vytvoriť inštancia emailovej funkcie.'; +$PHPMAILER_LANG['invalid_address'] = 'Neodoslané, emailová adresa je nesprávna: '; +$PHPMAILER_LANG['invalid_hostentry'] = 'Záznam hostiteľa je nesprávny: '; +$PHPMAILER_LANG['invalid_host'] = 'Hostiteľ je nesprávny: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' emailový klient nieje podporovaný.'; +$PHPMAILER_LANG['provide_address'] = 'Musíte zadať aspoň jednu emailovú adresu príjemcu.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: Adresy príjemcov niesu správne '; +$PHPMAILER_LANG['signing'] = 'Chyba prihlasovania: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() zlyhalo.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP chyba serveru: '; +$PHPMAILER_LANG['variable_set'] = 'Nemožno nastaviť alebo resetovať premennú: '; +$PHPMAILER_LANG['extension_missing'] = 'Chýba rozšírenie: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sl.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sl.php new file mode 100644 index 0000000..3e00c25 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sl.php @@ -0,0 +1,36 @@ + + * @author Filip Š + * @author Blaž Oražem + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP napaka: Avtentikacija ni uspela.'; +$PHPMAILER_LANG['buggy_php'] = 'Na vašo PHP različico vpliva napaka, ki lahko povzroči poškodovana sporočila. Če želite težavo odpraviti, preklopite na pošiljanje prek SMTP, onemogočite možnost mail.add_x_header v vaši php.ini datoteki, preklopite na MacOS ali Linux, ali nadgradite vašo PHP zaličico na 7.0.17+ ali 7.1.3+.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP napaka: Vzpostavljanje povezave s SMTP gostiteljem ni uspelo.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP napaka: Strežnik zavrača podatke.'; +$PHPMAILER_LANG['empty_message'] = 'E-poštno sporočilo nima vsebine.'; +$PHPMAILER_LANG['encoding'] = 'Nepoznan tip kodiranja: '; +$PHPMAILER_LANG['execute'] = 'Operacija ni uspela: '; +$PHPMAILER_LANG['extension_missing'] = 'Manjkajoča razširitev: '; +$PHPMAILER_LANG['file_access'] = 'Nimam dostopa do datoteke: '; +$PHPMAILER_LANG['file_open'] = 'Ne morem odpreti datoteke: '; +$PHPMAILER_LANG['from_failed'] = 'Neveljaven e-naslov pošiljatelja: '; +$PHPMAILER_LANG['instantiate'] = 'Ne morem inicializirati mail funkcije.'; +$PHPMAILER_LANG['invalid_address'] = 'E-poštno sporočilo ni bilo poslano. E-naslov je neveljaven: '; +$PHPMAILER_LANG['invalid_header'] = 'Neveljavno ime ali vrednost glave'; +$PHPMAILER_LANG['invalid_hostentry'] = 'Neveljaven vnos gostitelja: '; +$PHPMAILER_LANG['invalid_host'] = 'Neveljaven gostitelj: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer ni podprt.'; +$PHPMAILER_LANG['provide_address'] = 'Prosimo, vnesite vsaj enega naslovnika.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP napaka: Sledeči naslovniki so neveljavni: '; +$PHPMAILER_LANG['signing'] = 'Napaka pri podpisovanju: '; +$PHPMAILER_LANG['smtp_code'] = 'SMTP koda: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'Dodatne informacije o SMTP: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Ne morem vzpostaviti povezave s SMTP strežnikom.'; +$PHPMAILER_LANG['smtp_detail'] = 'Podrobnosti: '; +$PHPMAILER_LANG['smtp_error'] = 'Napaka SMTP strežnika: '; +$PHPMAILER_LANG['variable_set'] = 'Ne morem nastaviti oz. ponastaviti spremenljivke: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sr.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sr.php new file mode 100644 index 0000000..0b5280f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sr.php @@ -0,0 +1,28 @@ + + * @author Miloš Milanović + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP грешка: аутентификација није успела.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP грешка: повезивање са SMTP сервером није успело.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP грешка: подаци нису прихваћени.'; +$PHPMAILER_LANG['empty_message'] = 'Садржај поруке је празан.'; +$PHPMAILER_LANG['encoding'] = 'Непознато кодирање: '; +$PHPMAILER_LANG['execute'] = 'Није могуће извршити наредбу: '; +$PHPMAILER_LANG['file_access'] = 'Није могуће приступити датотеци: '; +$PHPMAILER_LANG['file_open'] = 'Није могуће отворити датотеку: '; +$PHPMAILER_LANG['from_failed'] = 'SMTP грешка: слање са следећих адреса није успело: '; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP грешка: слање на следеће адресе није успело: '; +$PHPMAILER_LANG['instantiate'] = 'Није могуће покренути mail функцију.'; +$PHPMAILER_LANG['invalid_address'] = 'Порука није послата. Неисправна адреса: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' мејлер није подржан.'; +$PHPMAILER_LANG['provide_address'] = 'Дефинишите бар једну адресу примаоца.'; +$PHPMAILER_LANG['signing'] = 'Грешка приликом пријаве: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Повезивање са SMTP сервером није успело.'; +$PHPMAILER_LANG['smtp_error'] = 'Грешка SMTP сервера: '; +$PHPMAILER_LANG['variable_set'] = 'Није могуће задати нити ресетовати променљиву: '; +$PHPMAILER_LANG['extension_missing'] = 'Недостаје проширење: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sr_latn.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sr_latn.php new file mode 100644 index 0000000..6213832 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sr_latn.php @@ -0,0 +1,28 @@ + + * @author Miloš Milanović + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP greška: autentifikacija nije uspela.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP greška: povezivanje sa SMTP serverom nije uspelo.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP greška: podaci nisu prihvaćeni.'; +$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.'; +$PHPMAILER_LANG['encoding'] = 'Nepoznato kodiranje: '; +$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: '; +$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: '; +$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: '; +$PHPMAILER_LANG['from_failed'] = 'SMTP greška: slanje sa sledećih adresa nije uspelo: '; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP greška: slanje na sledeće adrese nije uspelo: '; +$PHPMAILER_LANG['instantiate'] = 'Nije moguće pokrenuti mail funkciju.'; +$PHPMAILER_LANG['invalid_address'] = 'Poruka nije poslata. Neispravna adresa: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' majler nije podržan.'; +$PHPMAILER_LANG['provide_address'] = 'Definišite bar jednu adresu primaoca.'; +$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Povezivanje sa SMTP serverom nije uspelo.'; +$PHPMAILER_LANG['smtp_error'] = 'Greška SMTP servera: '; +$PHPMAILER_LANG['variable_set'] = 'Nije moguće zadati niti resetovati promenljivu: '; +$PHPMAILER_LANG['extension_missing'] = 'Nedostaje proširenje: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sv.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sv.php new file mode 100644 index 0000000..9872c19 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-sv.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP fel: Kunde inte autentisera.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP fel: Kunde inte ansluta till SMTP-server.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP fel: Data accepterades inte.'; +//$PHPMAILER_LANG['empty_message'] = 'Message body empty'; +$PHPMAILER_LANG['encoding'] = 'Okänt encode-format: '; +$PHPMAILER_LANG['execute'] = 'Kunde inte köra: '; +$PHPMAILER_LANG['file_access'] = 'Ingen åtkomst till fil: '; +$PHPMAILER_LANG['file_open'] = 'Fil fel: Kunde inte öppna fil: '; +$PHPMAILER_LANG['from_failed'] = 'Följande avsändaradress är felaktig: '; +$PHPMAILER_LANG['instantiate'] = 'Kunde inte initiera e-postfunktion.'; +$PHPMAILER_LANG['invalid_address'] = 'Felaktig adress: '; +$PHPMAILER_LANG['provide_address'] = 'Du måste ange minst en mottagares e-postadress.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' mailer stöds inte.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP fel: Följande mottagare är felaktig: '; +$PHPMAILER_LANG['signing'] = 'Signeringsfel: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() misslyckades.'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP serverfel: '; +$PHPMAILER_LANG['variable_set'] = 'Kunde inte definiera eller återställa variabel: '; +$PHPMAILER_LANG['extension_missing'] = 'Tillägg ej tillgängligt: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-tl.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-tl.php new file mode 100644 index 0000000..d15bed1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-tl.php @@ -0,0 +1,28 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Hindi mapatotohanan.'; +$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Hindi makakonekta sa SMTP host.'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Ang datos ay hindi naitanggap.'; +$PHPMAILER_LANG['empty_message'] = 'Walang laman ang mensahe'; +$PHPMAILER_LANG['encoding'] = 'Hindi alam ang encoding: '; +$PHPMAILER_LANG['execute'] = 'Hindi maisasagawa: '; +$PHPMAILER_LANG['file_access'] = 'Hindi ma-access ang file: '; +$PHPMAILER_LANG['file_open'] = 'File Error: Hindi mabuksan ang file: '; +$PHPMAILER_LANG['from_failed'] = 'Ang sumusunod na address ay nabigo: '; +$PHPMAILER_LANG['instantiate'] = 'Hindi maisimulan ang instance ng mail function.'; +$PHPMAILER_LANG['invalid_address'] = 'Hindi wasto ang address na naibigay: '; +$PHPMAILER_LANG['mailer_not_supported'] = 'Ang mailer ay hindi suportado.'; +$PHPMAILER_LANG['provide_address'] = 'Kailangan mong magbigay ng kahit isang email address na tatanggap.'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: Ang mga sumusunod na tatanggap ay nabigo: '; +$PHPMAILER_LANG['signing'] = 'Hindi ma-sign: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Ang SMTP connect() ay nabigo.'; +$PHPMAILER_LANG['smtp_error'] = 'Ang server ng SMTP ay nabigo: '; +$PHPMAILER_LANG['variable_set'] = 'Hindi matatakda o ma-reset ang mga variables: '; +$PHPMAILER_LANG['extension_missing'] = 'Nawawala ang extension: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-tr.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-tr.php new file mode 100644 index 0000000..3c45bc1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-tr.php @@ -0,0 +1,38 @@ + + * @fixed by Boris Yurchenko + */ + +$PHPMAILER_LANG['authenticate'] = 'Помилка SMTP: помилка авторизації.'; +$PHPMAILER_LANG['connect_host'] = 'Помилка SMTP: не вдається під\'єднатися до SMTP-серверу.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Помилка SMTP: дані не прийнято.'; +$PHPMAILER_LANG['encoding'] = 'Невідоме кодування: '; +$PHPMAILER_LANG['execute'] = 'Неможливо виконати команду: '; +$PHPMAILER_LANG['file_access'] = 'Немає доступу до файлу: '; +$PHPMAILER_LANG['file_open'] = 'Помилка файлової системи: не вдається відкрити файл: '; +$PHPMAILER_LANG['from_failed'] = 'Невірна адреса відправника: '; +$PHPMAILER_LANG['instantiate'] = 'Неможливо запустити функцію mail().'; +$PHPMAILER_LANG['provide_address'] = 'Будь ласка, введіть хоча б одну email-адресу отримувача.'; +$PHPMAILER_LANG['mailer_not_supported'] = ' - поштовий сервер не підтримується.'; +$PHPMAILER_LANG['recipients_failed'] = 'Помилка SMTP: не вдалося відправлення для таких отримувачів: '; +$PHPMAILER_LANG['empty_message'] = 'Пусте повідомлення'; +$PHPMAILER_LANG['invalid_address'] = 'Не відправлено через неправильний формат email-адреси: '; +$PHPMAILER_LANG['signing'] = 'Помилка підпису: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Помилка з\'єднання з SMTP-сервером'; +$PHPMAILER_LANG['smtp_error'] = 'Помилка SMTP-сервера: '; +$PHPMAILER_LANG['variable_set'] = 'Неможливо встановити або скинути змінну: '; +$PHPMAILER_LANG['extension_missing'] = 'Розширення відсутнє: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ur.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ur.php new file mode 100644 index 0000000..0b9de0f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-ur.php @@ -0,0 +1,30 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP خرابی: تصدیق کرنے سے قاصر۔'; +$PHPMAILER_LANG['connect_host'] = 'SMTP خرابی: سرور سے منسلک ہونے سے قاصر۔'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP خرابی: ڈیٹا قبول نہیں کیا گیا۔'; +$PHPMAILER_LANG['empty_message'] = 'پیغام کی باڈی خالی ہے۔'; +$PHPMAILER_LANG['encoding'] = 'نامعلوم انکوڈنگ: '; +$PHPMAILER_LANG['execute'] = 'عمل کرنے کے قابل نہیں '; +$PHPMAILER_LANG['file_access'] = 'فائل تک رسائی سے قاصر:'; +$PHPMAILER_LANG['file_open'] = 'فائل کی خرابی: فائل کو کھولنے سے قاصر:'; +$PHPMAILER_LANG['from_failed'] = 'درج ذیل بھیجنے والے کا پتہ ناکام ہو گیا:'; +$PHPMAILER_LANG['instantiate'] = 'میل فنکشن کی مثال بنانے سے قاصر۔'; +$PHPMAILER_LANG['invalid_address'] = 'بھیجنے سے قاصر: غلط ای میل پتہ:'; +$PHPMAILER_LANG['mailer_not_supported'] = ' میلر تعاون یافتہ نہیں ہے۔'; +$PHPMAILER_LANG['provide_address'] = 'آپ کو کم از کم ایک منزل کا ای میل پتہ فراہم کرنا چاہیے۔'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP خرابی: درج ذیل پتہ پر نہیں بھیجا جاسکا: '; +$PHPMAILER_LANG['signing'] = 'دستخط کی خرابی: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP ملنا ناکام ہوا'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP سرور کی خرابی: '; +$PHPMAILER_LANG['variable_set'] = 'متغیر سیٹ نہیں کیا جا سکا: '; +$PHPMAILER_LANG['extension_missing'] = 'ایکٹینشن موجود نہیں ہے۔ '; +$PHPMAILER_LANG['smtp_code'] = 'SMTP سرور کوڈ: '; +$PHPMAILER_LANG['smtp_code_ex'] = 'اضافی SMTP سرور کی معلومات:'; +$PHPMAILER_LANG['invalid_header'] = 'غلط ہیڈر کا نام یا قدر'; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-vi.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-vi.php new file mode 100644 index 0000000..d65576e --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-vi.php @@ -0,0 +1,27 @@ + + */ + +$PHPMAILER_LANG['authenticate'] = 'Lỗi SMTP: Không thể xác thực.'; +$PHPMAILER_LANG['connect_host'] = 'Lỗi SMTP: Không thể kết nối máy chủ SMTP.'; +$PHPMAILER_LANG['data_not_accepted'] = 'Lỗi SMTP: Dữ liệu không được chấp nhận.'; +$PHPMAILER_LANG['empty_message'] = 'Không có nội dung'; +$PHPMAILER_LANG['encoding'] = 'Mã hóa không xác định: '; +$PHPMAILER_LANG['execute'] = 'Không thực hiện được: '; +$PHPMAILER_LANG['file_access'] = 'Không thể truy cập tệp tin '; +$PHPMAILER_LANG['file_open'] = 'Lỗi Tập tin: Không thể mở tệp tin: '; +$PHPMAILER_LANG['from_failed'] = 'Lỗi địa chỉ gửi đi: '; +$PHPMAILER_LANG['instantiate'] = 'Không dùng được các hàm gửi thư.'; +$PHPMAILER_LANG['invalid_address'] = 'Đại chỉ emai không đúng: '; +$PHPMAILER_LANG['mailer_not_supported'] = ' trình gửi thư không được hỗ trợ.'; +$PHPMAILER_LANG['provide_address'] = 'Bạn phải cung cấp ít nhất một địa chỉ người nhận.'; +$PHPMAILER_LANG['recipients_failed'] = 'Lỗi SMTP: lỗi địa chỉ người nhận: '; +$PHPMAILER_LANG['signing'] = 'Lỗi đăng nhập: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'Lỗi kết nối với SMTP'; +$PHPMAILER_LANG['smtp_error'] = 'Lỗi máy chủ smtp '; +$PHPMAILER_LANG['variable_set'] = 'Không thể thiết lập hoặc thiết lập lại biến: '; +//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-zh.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-zh.php new file mode 100644 index 0000000..35e4e70 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-zh.php @@ -0,0 +1,29 @@ + + * @author Peter Dave Hello <@PeterDaveHello/> + * @author Jason Chiang + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP 錯誤:登入失敗。'; +$PHPMAILER_LANG['connect_host'] = 'SMTP 錯誤:無法連線到 SMTP 主機。'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 錯誤:無法接受的資料。'; +$PHPMAILER_LANG['empty_message'] = '郵件內容為空'; +$PHPMAILER_LANG['encoding'] = '未知編碼: '; +$PHPMAILER_LANG['execute'] = '無法執行:'; +$PHPMAILER_LANG['file_access'] = '無法存取檔案:'; +$PHPMAILER_LANG['file_open'] = '檔案錯誤:無法開啟檔案:'; +$PHPMAILER_LANG['from_failed'] = '發送地址錯誤:'; +$PHPMAILER_LANG['instantiate'] = '未知函數呼叫。'; +$PHPMAILER_LANG['invalid_address'] = '因為電子郵件地址無效,無法傳送: '; +$PHPMAILER_LANG['mailer_not_supported'] = '不支援的發信客戶端。'; +$PHPMAILER_LANG['provide_address'] = '必須提供至少一個收件人地址。'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP 錯誤:以下收件人地址錯誤:'; +$PHPMAILER_LANG['signing'] = '電子簽章錯誤: '; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP 連線失敗'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP 伺服器錯誤: '; +$PHPMAILER_LANG['variable_set'] = '無法設定或重設變數: '; +$PHPMAILER_LANG['extension_missing'] = '遺失模組 Extension: '; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-zh_cn.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-zh_cn.php new file mode 100644 index 0000000..03d4911 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/language/phpmailer.lang-zh_cn.php @@ -0,0 +1,36 @@ + + * @author young + * @author Teddysun + */ + +$PHPMAILER_LANG['authenticate'] = 'SMTP 错误:登录失败。'; +$PHPMAILER_LANG['buggy_php'] = '您的 PHP 版本存在漏洞,可能会导致消息损坏。为修复此问题,请切换到使用 SMTP 发送,在您的 php.ini 中禁用 mail.add_x_header 选项。切换到 MacOS 或 Linux,或将您的 PHP 升级到 7.0.17+ 或 7.1.3+ 版本。'; +$PHPMAILER_LANG['connect_host'] = 'SMTP 错误:无法连接到 SMTP 主机。'; +$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 错误:数据不被接受。'; +$PHPMAILER_LANG['empty_message'] = '邮件正文为空。'; +$PHPMAILER_LANG['encoding'] = '未知编码:'; +$PHPMAILER_LANG['execute'] = '无法执行:'; +$PHPMAILER_LANG['extension_missing'] = '缺少扩展名:'; +$PHPMAILER_LANG['file_access'] = '无法访问文件:'; +$PHPMAILER_LANG['file_open'] = '文件错误:无法打开文件:'; +$PHPMAILER_LANG['from_failed'] = '发送地址错误:'; +$PHPMAILER_LANG['instantiate'] = '未知函数调用。'; +$PHPMAILER_LANG['invalid_address'] = '发送失败,电子邮箱地址是无效的:'; +$PHPMAILER_LANG['mailer_not_supported'] = '发信客户端不被支持。'; +$PHPMAILER_LANG['provide_address'] = '必须提供至少一个收件人地址。'; +$PHPMAILER_LANG['recipients_failed'] = 'SMTP 错误:收件人地址错误:'; +$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP服务器连接失败。'; +$PHPMAILER_LANG['smtp_error'] = 'SMTP服务器出错:'; +$PHPMAILER_LANG['variable_set'] = '无法设置或重置变量:'; +$PHPMAILER_LANG['invalid_header'] = '无效的标题名称或值'; +$PHPMAILER_LANG['invalid_hostentry'] = '无效的hostentry: '; +$PHPMAILER_LANG['invalid_host'] = '无效的主机:'; +$PHPMAILER_LANG['signing'] = '签名错误:'; +$PHPMAILER_LANG['smtp_code'] = 'SMTP代码: '; +$PHPMAILER_LANG['smtp_code_ex'] = '附加SMTP信息: '; +$PHPMAILER_LANG['smtp_detail'] = '详情:'; diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/DSNConfigurator.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/DSNConfigurator.php new file mode 100644 index 0000000..7058c1f --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/DSNConfigurator.php @@ -0,0 +1,245 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2023 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * Configure PHPMailer with DSN string. + * + * @see https://en.wikipedia.org/wiki/Data_source_name + * + * @author Oleg Voronkovich + */ +class DSNConfigurator +{ + /** + * Create new PHPMailer instance configured by DSN. + * + * @param string $dsn DSN + * @param bool $exceptions Should we throw external exceptions? + * + * @return PHPMailer + */ + public static function mailer($dsn, $exceptions = null) + { + static $configurator = null; + + if (null === $configurator) { + $configurator = new DSNConfigurator(); + } + + return $configurator->configure(new PHPMailer($exceptions), $dsn); + } + + /** + * Configure PHPMailer instance with DSN string. + * + * @param PHPMailer $mailer PHPMailer instance + * @param string $dsn DSN + * + * @return PHPMailer + */ + public function configure(PHPMailer $mailer, $dsn) + { + $config = $this->parseDSN($dsn); + + $this->applyConfig($mailer, $config); + + return $mailer; + } + + /** + * Parse DSN string. + * + * @param string $dsn DSN + * + * @throws Exception If DSN is malformed + * + * @return array Configuration + */ + private function parseDSN($dsn) + { + $config = $this->parseUrl($dsn); + + if (false === $config || !isset($config['scheme']) || !isset($config['host'])) { + throw new Exception('Malformed DSN'); + } + + if (isset($config['query'])) { + parse_str($config['query'], $config['query']); + } + + return $config; + } + + /** + * Apply configuration to mailer. + * + * @param PHPMailer $mailer PHPMailer instance + * @param array $config Configuration + * + * @throws Exception If scheme is invalid + */ + private function applyConfig(PHPMailer $mailer, $config) + { + switch ($config['scheme']) { + case 'mail': + $mailer->isMail(); + break; + case 'sendmail': + $mailer->isSendmail(); + break; + case 'qmail': + $mailer->isQmail(); + break; + case 'smtp': + case 'smtps': + $mailer->isSMTP(); + $this->configureSMTP($mailer, $config); + break; + default: + throw new Exception( + sprintf( + 'Invalid scheme: "%s". Allowed values: "mail", "sendmail", "qmail", "smtp", "smtps".', + $config['scheme'] + ) + ); + } + + if (isset($config['query'])) { + $this->configureOptions($mailer, $config['query']); + } + } + + /** + * Configure SMTP. + * + * @param PHPMailer $mailer PHPMailer instance + * @param array $config Configuration + */ + private function configureSMTP($mailer, $config) + { + $isSMTPS = 'smtps' === $config['scheme']; + + if ($isSMTPS) { + $mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; + } + + $mailer->Host = $config['host']; + + if (isset($config['port'])) { + $mailer->Port = $config['port']; + } elseif ($isSMTPS) { + $mailer->Port = SMTP::DEFAULT_SECURE_PORT; + } + + $mailer->SMTPAuth = isset($config['user']) || isset($config['pass']); + + if (isset($config['user'])) { + $mailer->Username = $config['user']; + } + + if (isset($config['pass'])) { + $mailer->Password = $config['pass']; + } + } + + /** + * Configure options. + * + * @param PHPMailer $mailer PHPMailer instance + * @param array $options Options + * + * @throws Exception If option is unknown + */ + private function configureOptions(PHPMailer $mailer, $options) + { + $allowedOptions = get_object_vars($mailer); + + unset($allowedOptions['Mailer']); + unset($allowedOptions['SMTPAuth']); + unset($allowedOptions['Username']); + unset($allowedOptions['Password']); + unset($allowedOptions['Hostname']); + unset($allowedOptions['Port']); + unset($allowedOptions['ErrorInfo']); + + $allowedOptions = \array_keys($allowedOptions); + + foreach ($options as $key => $value) { + if (!in_array($key, $allowedOptions)) { + throw new Exception( + sprintf( + 'Unknown option: "%s". Allowed values: "%s"', + $key, + implode('", "', $allowedOptions) + ) + ); + } + + switch ($key) { + case 'AllowEmpty': + case 'SMTPAutoTLS': + case 'SMTPKeepAlive': + case 'SingleTo': + case 'UseSendmailOptions': + case 'do_verp': + case 'DKIM_copyHeaderFields': + $mailer->$key = (bool) $value; + break; + case 'Priority': + case 'SMTPDebug': + case 'WordWrap': + $mailer->$key = (int) $value; + break; + default: + $mailer->$key = $value; + break; + } + } + } + + /** + * Parse a URL. + * Wrapper for the built-in parse_url function to work around a bug in PHP 5.5. + * + * @param string $url URL + * + * @return array|false + */ + protected function parseUrl($url) + { + if (\PHP_VERSION_ID >= 50600 || false === strpos($url, '?')) { + return parse_url($url); + } + + $chunks = explode('?', $url); + if (is_array($chunks)) { + $result = parse_url($chunks[0]); + if (is_array($result)) { + $result['query'] = $chunks[1]; + } + return $result; + } + + return false; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/Exception.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/Exception.php new file mode 100644 index 0000000..09c1a2c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/Exception.php @@ -0,0 +1,40 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2020 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * PHPMailer exception handler. + * + * @author Marcus Bointon + */ +class Exception extends \Exception +{ + /** + * Prettify error message output. + * + * @return string + */ + public function errorMessage() + { + return '' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "
    \n"; + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/OAuth.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/OAuth.php new file mode 100644 index 0000000..a7e9588 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/OAuth.php @@ -0,0 +1,139 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2020 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +use League\OAuth2\Client\Grant\RefreshToken; +use League\OAuth2\Client\Provider\AbstractProvider; +use League\OAuth2\Client\Token\AccessToken; + +/** + * OAuth - OAuth2 authentication wrapper class. + * Uses the oauth2-client package from the League of Extraordinary Packages. + * + * @see https://oauth2-client.thephpleague.com + * + * @author Marcus Bointon (Synchro/coolbru) + */ +class OAuth implements OAuthTokenProvider +{ + /** + * An instance of the League OAuth Client Provider. + * + * @var AbstractProvider + */ + protected $provider; + + /** + * The current OAuth access token. + * + * @var AccessToken + */ + protected $oauthToken; + + /** + * The user's email address, usually used as the login ID + * and also the from address when sending email. + * + * @var string + */ + protected $oauthUserEmail = ''; + + /** + * The client secret, generated in the app definition of the service you're connecting to. + * + * @var string + */ + protected $oauthClientSecret = ''; + + /** + * The client ID, generated in the app definition of the service you're connecting to. + * + * @var string + */ + protected $oauthClientId = ''; + + /** + * The refresh token, used to obtain new AccessTokens. + * + * @var string + */ + protected $oauthRefreshToken = ''; + + /** + * OAuth constructor. + * + * @param array $options Associative array containing + * `provider`, `userName`, `clientSecret`, `clientId` and `refreshToken` elements + */ + public function __construct($options) + { + $this->provider = $options['provider']; + $this->oauthUserEmail = $options['userName']; + $this->oauthClientSecret = $options['clientSecret']; + $this->oauthClientId = $options['clientId']; + $this->oauthRefreshToken = $options['refreshToken']; + } + + /** + * Get a new RefreshToken. + * + * @return RefreshToken + */ + protected function getGrant() + { + return new RefreshToken(); + } + + /** + * Get a new AccessToken. + * + * @return AccessToken + */ + protected function getToken() + { + return $this->provider->getAccessToken( + $this->getGrant(), + ['refresh_token' => $this->oauthRefreshToken] + ); + } + + /** + * Generate a base64-encoded OAuth token. + * + * @return string + */ + public function getOauth64() + { + //Get a new token if it's not available or has expired + if (null === $this->oauthToken || $this->oauthToken->hasExpired()) { + $this->oauthToken = $this->getToken(); + } + + return base64_encode( + 'user=' . + $this->oauthUserEmail . + "\001auth=Bearer " . + $this->oauthToken . + "\001\001" + ); + } +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/OAuthTokenProvider.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/OAuthTokenProvider.php new file mode 100644 index 0000000..cbda1a1 --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/OAuthTokenProvider.php @@ -0,0 +1,44 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2020 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * OAuthTokenProvider - OAuth2 token provider interface. + * Provides base64 encoded OAuth2 auth strings for SMTP authentication. + * + * @see OAuth + * @see SMTP::authenticate() + * + * @author Peter Scopes (pdscopes) + * @author Marcus Bointon (Synchro/coolbru) + */ +interface OAuthTokenProvider +{ + /** + * Generate a base64-encoded OAuth token ensuring that the access token has not expired. + * The string to be base 64 encoded should be in the form: + * "user=\001auth=Bearer \001\001" + * + * @return string + */ + public function getOauth64(); +} diff --git a/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/PHPMailer.php b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/PHPMailer.php new file mode 100644 index 0000000..4a6077c --- /dev/null +++ b/CloudronStack/output/CloudronPackages-Artifacts/tirreno/libs/phpmailer/phpmailer/src/PHPMailer.php @@ -0,0 +1,5248 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2020 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +namespace PHPMailer\PHPMailer; + +/** + * PHPMailer - PHP email creation and transport class. + * + * @author Marcus Bointon (Synchro/coolbru) + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + */ +class PHPMailer +{ + const CHARSET_ASCII = 'us-ascii'; + const CHARSET_ISO88591 = 'iso-8859-1'; + const CHARSET_UTF8 = 'utf-8'; + + const CONTENT_TYPE_PLAINTEXT = 'text/plain'; + const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar'; + const CONTENT_TYPE_TEXT_HTML = 'text/html'; + const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative'; + const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed'; + const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related'; + + const ENCODING_7BIT = '7bit'; + const ENCODING_8BIT = '8bit'; + const ENCODING_BASE64 = 'base64'; + const ENCODING_BINARY = 'binary'; + const ENCODING_QUOTED_PRINTABLE = 'quoted-printable'; + + const ENCRYPTION_STARTTLS = 'tls'; + const ENCRYPTION_SMTPS = 'ssl'; + + const ICAL_METHOD_REQUEST = 'REQUEST'; + const ICAL_METHOD_PUBLISH = 'PUBLISH'; + const ICAL_METHOD_REPLY = 'REPLY'; + const ICAL_METHOD_ADD = 'ADD'; + const ICAL_METHOD_CANCEL = 'CANCEL'; + const ICAL_METHOD_REFRESH = 'REFRESH'; + const ICAL_METHOD_COUNTER = 'COUNTER'; + const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER'; + + /** + * Email priority. + * Options: null (default), 1 = High, 3 = Normal, 5 = low. + * When null, the header is not set at all. + * + * @var int|null + */ + public $Priority; + + /** + * The character set of the message. + * + * @var string + */ + public $CharSet = self::CHARSET_ISO88591; + + /** + * The MIME Content-type of the message. + * + * @var string + */ + public $ContentType = self::CONTENT_TYPE_PLAINTEXT; + + /** + * The message encoding. + * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable". + * + * @var string + */ + public $Encoding = self::ENCODING_8BIT; + + /** + * Holds the most recent mailer error message. + * + * @var string + */ + public $ErrorInfo = ''; + + /** + * The From email address for the message. + * + * @var string + */ + public $From = ''; + + /** + * The From name of the message. + * + * @var string + */ + public $FromName = ''; + + /** + * The envelope sender of the message. + * This will usually be turned into a Return-Path header by the receiver, + * and is the address that bounces will be sent to. + * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP. + * + * @var string + */ + public $Sender = ''; + + /** + * The Subject of the message. + * + * @var string + */ + public $Subject = ''; + + /** + * An HTML or plain text message body. + * If HTML then call isHTML(true). + * + * @var string + */ + public $Body = ''; + + /** + * The plain-text message body. + * This body can be read by mail clients that do not have HTML email + * capability such as mutt & Eudora. + * Clients that can read HTML will view the normal Body. + * + * @var string + */ + public $AltBody = ''; + + /** + * An iCal message part body. + * Only supported in simple alt or alt_inline message types + * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator. + * + * @see https://kigkonsult.se/iCalcreator/ + * + * @var string + */ + public $Ical = ''; + + /** + * Value-array of "method" in Contenttype header "text/calendar" + * + * @var string[] + */ + protected static $IcalMethods = [ + self::ICAL_METHOD_REQUEST, + self::ICAL_METHOD_PUBLISH, + self::ICAL_METHOD_REPLY, + self::ICAL_METHOD_ADD, + self::ICAL_METHOD_CANCEL, + self::ICAL_METHOD_REFRESH, + self::ICAL_METHOD_COUNTER, + self::ICAL_METHOD_DECLINECOUNTER, + ]; + + /** + * The complete compiled MIME message body. + * + * @var string + */ + protected $MIMEBody = ''; + + /** + * The complete compiled MIME message headers. + * + * @var string + */ + protected $MIMEHeader = ''; + + /** + * Extra headers that createHeader() doesn't fold in. + * + * @var string + */ + protected $mailHeader = ''; + + /** + * Word-wrap the message body to this number of chars. + * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance. + * + * @see static::STD_LINE_LENGTH + * + * @var int + */ + public $WordWrap = 0; + + /** + * Which method to use to send mail. + * Options: "mail", "sendmail", or "smtp". + * + * @var string + */ + public $Mailer = 'mail'; + + /** + * The path to the sendmail program. + * + * @var string + */ + public $Sendmail = '/usr/sbin/sendmail'; + + /** + * Whether mail() uses a fully sendmail-compatible MTA. + * One which supports sendmail's "-oi -f" options. + * + * @var bool + */ + public $UseSendmailOptions = true; + + /** + * The email address that a reading confirmation should be sent to, also known as read receipt. + * + * @var string + */ + public $ConfirmReadingTo = ''; + + /** + * The hostname to use in the Message-ID header and as default HELO string. + * If empty, PHPMailer attempts to find one with, in order, + * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value + * 'localhost.localdomain'. + * + * @see PHPMailer::$Helo + * + * @var string + */ + public $Hostname = ''; + + /** + * An ID to be used in the Message-ID header. + * If empty, a unique id will be generated. + * You can set your own, but it must be in the format "", + * as defined in RFC5322 section 3.6.4 or it will be ignored. + * + * @see https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4 + * + * @var string + */ + public $MessageID = ''; + + /** + * The message Date to be used in the Date header. + * If empty, the current date will be added. + * + * @var string + */ + public $MessageDate = ''; + + /** + * SMTP hosts. + * Either a single hostname or multiple semicolon-delimited hostnames. + * You can also specify a different port + * for each host by using this format: [hostname:port] + * (e.g. "smtp1.example.com:25;smtp2.example.com"). + * You can also specify encryption type, for example: + * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465"). + * Hosts will be tried in order. + * + * @var string + */ + public $Host = 'localhost'; + + /** + * The default SMTP server port. + * + * @var int + */ + public $Port = 25; + + /** + * The SMTP HELO/EHLO name used for the SMTP connection. + * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find + * one with the same method described above for $Hostname. + * + * @see PHPMailer::$Hostname + * + * @var string + */ + public $Helo = ''; + + /** + * What kind of encryption to use on the SMTP connection. + * Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS. + * + * @var string + */ + public $SMTPSecure = ''; + + /** + * Whether to enable TLS encryption automatically if a server supports it, + * even if `SMTPSecure` is not set to 'tls'. + * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid. + * + * @var bool + */ + public $SMTPAutoTLS = true; + + /** + * Whether to use SMTP authentication. + * Uses the Username and Password properties. + * + * @see PHPMailer::$Username + * @see PHPMailer::$Password + * + * @var bool + */ + public $SMTPAuth = false; + + /** + * Options array passed to stream_context_create when connecting via SMTP. + * + * @var array + */ + public $SMTPOptions = []; + + /** + * SMTP username. + * + * @var string + */ + public $Username = ''; + + /** + * SMTP password. + * + * @var string + */ + public $Password = ''; + + /** + * SMTP authentication type. Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2. + * If not specified, the first one from that list that the server supports will be selected. + * + * @var string + */ + public $AuthType = ''; + + /** + * SMTP SMTPXClient command attributes + * + * @var array + */ + protected $SMTPXClient = []; + + /** + * An implementation of the PHPMailer OAuthTokenProvider interface. + * + * @var OAuthTokenProvider + */ + protected $oauth; + + /** + * The SMTP server timeout in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. + * + * @var int + */ + public $Timeout = 300; + + /** + * Comma separated list of DSN notifications + * 'NEVER' under no circumstances a DSN must be returned to the sender. + * If you use NEVER all other notifications will be ignored. + * 'SUCCESS' will notify you when your mail has arrived at its destination. + * 'FAILURE' will arrive if an error occurred during delivery. + * 'DELAY' will notify you if there is an unusual delay in delivery, but the actual + * delivery's outcome (success or failure) is not yet decided. + * + * @see https://www.rfc-editor.org/rfc/rfc3461.html#section-4.1 for more information about NOTIFY + */ + public $dsn = ''; + + /** + * SMTP class debug output mode. + * Debug output level. + * Options: + * @see SMTP::DEBUG_OFF: No output + * @see SMTP::DEBUG_CLIENT: Client messages + * @see SMTP::DEBUG_SERVER: Client and server messages + * @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status + * @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed + * + * @see SMTP::$do_debug + * + * @var int + */ + public $SMTPDebug = 0; + + /** + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
    `, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise. + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * ```php + * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * ``` + * + * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` + * level output is used: + * + * ```php + * $mail->Debugoutput = new myPsr3Logger; + * ``` + * + * @see SMTP::$Debugoutput + * + * @var string|callable|\Psr\Log\LoggerInterface + */ + public $Debugoutput = 'echo'; + + /** + * Whether to keep the SMTP connection open after each message. + * If this is set to true then the connection will remain open after a send, + * and closing the connection will require an explicit call to smtpClose(). + * It's a good idea to use this if you are sending multiple messages as it reduces overhead. + * See the mailing list example for how to use it. + * + * @var bool + */ + public $SMTPKeepAlive = false; + + /** + * Whether to split multiple to addresses into multiple messages + * or send them all in one message. + * Only supported in `mail` and `sendmail` transports, not in SMTP. + * + * @var bool + * + * @deprecated 6.0.0 PHPMailer isn't a mailing list manager! + */ + public $SingleTo = false; + + /** + * Storage for addresses when SingleTo is enabled. + * + * @var array + */ + protected $SingleToArray = []; + + /** + * Whether to generate VERP addresses on send. + * Only applicable when sending via SMTP. + * + * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path + * @see https://www.postfix.org/VERP_README.html Postfix VERP info + * + * @var bool + */ + public $do_verp = false; + + /** + * Whether to allow sending messages with an empty body. + * + * @var bool + */ + public $AllowEmpty = false; + + /** + * DKIM selector. + * + * @var string + */ + public $DKIM_selector = ''; + + /** + * DKIM Identity. + * Usually the email address used as the source of the email. + * + * @var string + */ + public $DKIM_identity = ''; + + /** + * DKIM passphrase. + * Used if your key is encrypted. + * + * @var string + */ + public $DKIM_passphrase = ''; + + /** + * DKIM signing domain name. + * + * @example 'example.com' + * + * @var string + */ + public $DKIM_domain = ''; + + /** + * DKIM Copy header field values for diagnostic use. + * + * @var bool + */ + public $DKIM_copyHeaderFields = true; + + /** + * DKIM Extra signing headers. + * + * @example ['List-Unsubscribe', 'List-Help'] + * + * @var array + */ + public $DKIM_extraHeaders = []; + + /** + * DKIM private key file path. + * + * @var string + */ + public $DKIM_private = ''; + + /** + * DKIM private key string. + * + * If set, takes precedence over `$DKIM_private`. + * + * @var string + */ + public $DKIM_private_string = ''; + + /** + * Callback Action function name. + * + * The function that handles the result of the send email action. + * It is called out by send() for each email sent. + * + * Value can be any php callable: https://www.php.net/is_callable + * + * Parameters: + * bool $result result of the send action + * array $to email addresses of the recipients + * array $cc cc email addresses + * array $bcc bcc email addresses + * string $subject the subject + * string $body the email body + * string $from email address of sender + * string $extra extra information of possible use + * "smtp_transaction_id' => last smtp transaction id + * + * @var string + */ + public $action_function = ''; + + /** + * What to put in the X-Mailer header. + * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use. + * + * @var string|null + */ + public $XMailer = ''; + + /** + * Which validator to use by default when validating email addresses. + * May be a callable to inject your own validator, but there are several built-in validators. + * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option. + * + * @see PHPMailer::validateAddress() + * + * @var string|callable + */ + public static $validator = 'php'; + + /** + * An instance of the SMTP sender class. + * + * @var SMTP + */ + protected $smtp; + + /** + * The array of 'to' names and addresses. + * + * @var array + */ + protected $to = []; + + /** + * The array of 'cc' names and addresses. + * + * @var array + */ + protected $cc = []; + + /** + * The array of 'bcc' names and addresses. + * + * @var array + */ + protected $bcc = []; + + /** + * The array of reply-to names and addresses. + * + * @var array + */ + protected $ReplyTo = []; + + /** + * An array of all kinds of addresses. + * Includes all of $to, $cc, $bcc. + * + * @see PHPMailer::$to + * @see PHPMailer::$cc + * @see PHPMailer::$bcc + * + * @var array + */ + protected $all_recipients = []; + + /** + * An array of names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $all_recipients + * and one of $to, $cc, or $bcc. + * This array is used only for addresses with IDN. + * + * @see PHPMailer::$to + * @see PHPMailer::$cc + * @see PHPMailer::$bcc + * @see PHPMailer::$all_recipients + * + * @var array + */ + protected $RecipientsQueue = []; + + /** + * An array of reply-to names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $ReplyTo. + * This array is used only for addresses with IDN. + * + * @see PHPMailer::$ReplyTo + * + * @var array + */ + protected $ReplyToQueue = []; + + /** + * The array of attachments. + * + * @var array + */ + protected $attachment = []; + + /** + * The array of custom headers. + * + * @var array + */ + protected $CustomHeader = []; + + /** + * The most recent Message-ID (including angular brackets). + * + * @var string + */ + protected $lastMessageID = ''; + + /** + * The message's MIME type. + * + * @var string + */ + protected $message_type = ''; + + /** + * The array of MIME boundary strings. + * + * @var array + */ + protected $boundary = []; + + /** + * The array of available text strings for the current language. + * + * @var array + */ + protected $language = []; + + /** + * The number of errors encountered. + * + * @var int + */ + protected $error_count = 0; + + /** + * The S/MIME certificate file path. + * + * @var string + */ + protected $sign_cert_file = ''; + + /** + * The S/MIME key file path. + * + * @var string + */ + protected $sign_key_file = ''; + + /** + * The optional S/MIME extra certificates ("CA Chain") file path. + * + * @var string + */ + protected $sign_extracerts_file = ''; + + /** + * The S/MIME password for the key. + * Used only if the key is encrypted. + * + * @var string + */ + protected $sign_key_pass = ''; + + /** + * Whether to throw exceptions for errors. + * + * @var bool + */ + protected $exceptions = false; + + /** + * Unique ID used for message ID and boundaries. + * + * @var string + */ + protected $uniqueid = ''; + + /** + * The PHPMailer Version number. + * + * @var string + */ + const VERSION = '6.9.3'; + + /** + * Error severity: message only, continue processing. + * + * @var int + */ + const STOP_MESSAGE = 0; + + /** + * Error severity: message, likely ok to continue processing. + * + * @var int + */ + const STOP_CONTINUE = 1; + + /** + * Error severity: message, plus full stop, critical error reached. + * + * @var int + */ + const STOP_CRITICAL = 2; + + /** + * The SMTP standard CRLF line break. + * If you want to change line break format, change static::$LE, not this. + */ + const CRLF = "\r\n"; + + /** + * "Folding White Space" a white space string used for line folding. + */ + const FWS = ' '; + + /** + * SMTP RFC standard line ending; Carriage Return, Line Feed. + * + * @var string + */ + protected static $LE = self::CRLF; + + /** + * The maximum line length supported by mail(). + * + * Background: mail() will sometimes corrupt messages + * with headers longer than 65 chars, see #818. + * + * @var int + */ + const MAIL_MAX_LINE_LENGTH = 63; + + /** + * The maximum line length allowed by RFC 2822 section 2.1.1. + * + * @var int + */ + const MAX_LINE_LENGTH = 998; + + /** + * The lower maximum line length allowed by RFC 2822 section 2.1.1. + * This length does NOT include the line break + * 76 means that lines will be 77 or 78 chars depending on whether + * the line break format is LF or CRLF; both are valid. + * + * @var int + */ + const STD_LINE_LENGTH = 76; + + /** + * Constructor. + * + * @param bool $exceptions Should we throw external exceptions? + */ + public function __construct($exceptions = null) + { + if (null !== $exceptions) { + $this->exceptions = (bool) $exceptions; + } + //Pick an appropriate debug output format automatically + $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html'); + } + + /** + * Destructor. + */ + public function __destruct() + { + //Close any open SMTP connection nicely + $this->smtpClose(); + } + + /** + * Call mail() in a safe_mode-aware fashion. + * Also, unless sendmail_path points to sendmail (or something that + * claims to be sendmail), don't pass params (not a perfect fix, + * but it will do). + * + * @param string $to To + * @param string $subject Subject + * @param string $body Message Body + * @param string $header Additional Header(s) + * @param string|null $params Params + * + * @return bool + */ + private function mailPassthru($to, $subject, $body, $header, $params) + { + //Check overloading of mail function to avoid double-encoding + if ((int)ini_get('mbstring.func_overload') & 1) { + $subject = $this->secureHeader($subject); + } else { + $subject = $this->encodeHeader($this->secureHeader($subject)); + } + //Calling mail() with null params breaks + $this->edebug('Sending with mail()'); + $this->edebug('Sendmail path: ' . ini_get('sendmail_path')); + $this->edebug("Envelope sender: {$this->Sender}"); + $this->edebug("To: {$to}"); + $this->edebug("Subject: {$subject}"); + $this->edebug("Headers: {$header}"); + if (!$this->UseSendmailOptions || null === $params) { + $result = @mail($to, $subject, $body, $header); + } else { + $this->edebug("Additional params: {$params}"); + $result = @mail($to, $subject, $body, $header, $params); + } + $this->edebug('Result: ' . ($result ? 'true' : 'false')); + return $result; + } + + /** + * Output debugging info via a user-defined method. + * Only generates output if debug output is enabled. + * + * @see PHPMailer::$Debugoutput + * @see PHPMailer::$SMTPDebug + * + * @param string $str + */ + protected function edebug($str) + { + if ($this->SMTPDebug <= 0) { + return; + } + //Is this a PSR-3 logger? + if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { + $this->Debugoutput->debug(rtrim($str, "\r\n")); + + return; + } + //Avoid clash with built-in function names + if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) { + call_user_func($this->Debugoutput, $str, $this->SMTPDebug); + + return; + } + switch ($this->Debugoutput) { + case 'error_log': + //Don't output, just log + /** @noinspection ForgottenDebugOutputInspection */ + error_log($str); + break; + case 'html': + //Cleans up output a bit for a better looking, HTML-safe output + echo htmlentities( + preg_replace('/[\r\n]+/', '', $str), + ENT_QUOTES, + 'UTF-8' + ), "
    \n"; + break; + case 'echo': + default: + //Normalize line breaks + $str = preg_replace('/\r\n|\r/m', "\n", $str); + echo gmdate('Y-m-d H:i:s'), + "\t", + //Trim trailing space + trim( + //Indent for readability, except for trailing break + str_replace( + "\n", + "\n \t ", + trim($str) + ) + ), + "\n"; + } + } + + /** + * Sets message type to HTML or plain. + * + * @param bool $isHtml True for HTML mode + */ + public function isHTML($isHtml = true) + { + if ($isHtml) { + $this->ContentType = static::CONTENT_TYPE_TEXT_HTML; + } else { + $this->ContentType = static::CONTENT_TYPE_PLAINTEXT; + } + } + + /** + * Send messages using SMTP. + */ + public function isSMTP() + { + $this->Mailer = 'smtp'; + } + + /** + * Send messages using PHP's mail() function. + */ + public function isMail() + { + $this->Mailer = 'mail'; + } + + /** + * Send messages using $Sendmail. + */ + public function isSendmail() + { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (false === stripos($ini_sendmail_path, 'sendmail')) { + $this->Sendmail = '/usr/sbin/sendmail'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'sendmail'; + } + + /** + * Send messages using qmail. + */ + public function isQmail() + { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (false === stripos($ini_sendmail_path, 'qmail')) { + $this->Sendmail = '/var/qmail/bin/qmail-inject'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'qmail'; + } + + /** + * Add a "To" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addAddress($address, $name = '') + { + return $this->addOrEnqueueAnAddress('to', $address, $name); + } + + /** + * Add a "CC" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addCC($address, $name = '') + { + return $this->addOrEnqueueAnAddress('cc', $address, $name); + } + + /** + * Add a "BCC" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addBCC($address, $name = '') + { + return $this->addOrEnqueueAnAddress('bcc', $address, $name); + } + + /** + * Add a "Reply-To" address. + * + * @param string $address The email address to reply to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addReplyTo($address, $name = '') + { + return $this->addOrEnqueueAnAddress('Reply-To', $address, $name); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer + * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still + * be modified after calling this function), addition of such addresses is delayed until send(). + * Addresses that have been added already return false, but do not throw exceptions. + * + * @param string $kind One of 'to', 'cc', 'bcc', or 'Reply-To' + * @param string $address The email address + * @param string $name An optional username associated with the address + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + protected function addOrEnqueueAnAddress($kind, $address, $name) + { + $pos = false; + if ($address !== null) { + $address = trim($address); + $pos = strrpos($address, '@'); + } + if (false === $pos) { + //At-sign is missing. + $error_message = sprintf( + '%s (%s): %s', + $this->lang('invalid_address'), + $kind, + $address + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + if ($name !== null && is_string($name)) { + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + } else { + $name = ''; + } + $params = [$kind, $address, $name]; + //Enqueue addresses with IDN until we know the PHPMailer::$CharSet. + //Domain is assumed to be whatever is after the last @ symbol in the address + if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) { + if ('Reply-To' !== $kind) { + if (!array_key_exists($address, $this->RecipientsQueue)) { + $this->RecipientsQueue[$address] = $params; + + return true; + } + } elseif (!array_key_exists($address, $this->ReplyToQueue)) { + $this->ReplyToQueue[$address] = $params; + + return true; + } + + return false; + } + + //Immediately add standard addresses without IDN. + return call_user_func_array([$this, 'addAnAddress'], $params); + } + + /** + * Set the boundaries to use for delimiting MIME parts. + * If you override this, ensure you set all 3 boundaries to unique values. + * The default boundaries include a "=_" sequence which cannot occur in quoted-printable bodies, + * as suggested by https://www.rfc-editor.org/rfc/rfc2045#section-6.7 + * + * @return void + */ + public function setBoundaries() + { + $this->uniqueid = $this->generateId(); + $this->boundary[1] = 'b1=_' . $this->uniqueid; + $this->boundary[2] = 'b2=_' . $this->uniqueid; + $this->boundary[3] = 'b3=_' . $this->uniqueid; + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. + * Addresses that have been added already return false, but do not throw exceptions. + * + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + protected function addAnAddress($kind, $address, $name = '') + { + if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) { + $error_message = sprintf( + '%s: %s', + $this->lang('Invalid recipient kind'), + $kind + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + if (!static::validateAddress($address)) { + $error_message = sprintf( + '%s (%s): %s', + $this->lang('invalid_address'), + $kind, + $address + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + if ('Reply-To' !== $kind) { + if (!array_key_exists(strtolower($address), $this->all_recipients)) { + $this->{$kind}[] = [$address, $name]; + $this->all_recipients[strtolower($address)] = true; + + return true; + } + } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) { + $this->ReplyTo[strtolower($address)] = [$address, $name]; + + return true; + } + + return false; + } + + /** + * Parse and validate a string containing one or more RFC822-style comma-separated email addresses + * of the form "display name
    " into an array of name/address pairs. + * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available. + * Note that quotes in the name part are removed. + * + * @see https://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation + * + * @param string $addrstr The address list string + * @param bool $useimap Whether to use the IMAP extension to parse the list + * @param string $charset The charset to use when decoding the address list string. + * + * @return array + */ + public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591) + { + $addresses = []; + if ($useimap && function_exists('imap_rfc822_parse_adrlist')) { + //Use this built-in parser if it's available + $list = imap_rfc822_parse_adrlist($addrstr, ''); + // Clear any potential IMAP errors to get rid of notices being thrown at end of script. + imap_errors(); + foreach ($list as $address) { + if ( + '.SYNTAX-ERROR.' !== $address->host && + static::validateAddress($address->mailbox . '@' . $address->host) + ) { + //Decode the name part if it's present and encoded + if ( + property_exists($address, 'personal') && + //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled + defined('MB_CASE_UPPER') && + preg_match('/^=\?.*\?=$/s', $address->personal) + ) { + $origCharset = mb_internal_encoding(); + mb_internal_encoding($charset); + //Undo any RFC2047-encoded spaces-as-underscores + $address->personal = str_replace('_', '=20', $address->personal); + //Decode the name + $address->personal = mb_decode_mimeheader($address->personal); + mb_internal_encoding($origCharset); + } + + $addresses[] = [ + 'name' => (property_exists($address, 'personal') ? $address->personal : ''), + 'address' => $address->mailbox . '@' . $address->host, + ]; + } + } + } else { + //Use this simpler parser + $list = explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + //Is there a separate name part? + if (strpos($address, '<') === false) { + //No separate name, just use the whole thing + if (static::validateAddress($address)) { + $addresses[] = [ + 'name' => '', + 'address' => $address, + ]; + } + } else { + list($name, $email) = explode('<', $address); + $email = trim(str_replace('>', '', $email)); + $name = trim($name); + if (static::validateAddress($email)) { + //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled + //If this name is encoded, decode it + if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) { + $origCharset = mb_internal_encoding(); + mb_internal_encoding($charset); + //Undo any RFC2047-encoded spaces-as-underscores + $name = str_replace('_', '=20', $name); + //Decode the name + $name = mb_decode_mimeheader($name); + mb_internal_encoding($origCharset); + } + $addresses[] = [ + //Remove any surrounding quotes and spaces from the name + 'name' => trim($name, '\'" '), + 'address' => $email, + ]; + } + } + } + } + + return $addresses; + } + + /** + * Set the From and FromName properties. + * + * @param string $address + * @param string $name + * @param bool $auto Whether to also set the Sender address, defaults to true + * + * @throws Exception + * + * @return bool + */ + public function setFrom($address, $name = '', $auto = true) + { + $address = trim((string)$address); + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + //Don't validate now addresses with IDN. Will be done in send(). + $pos = strrpos($address, '@'); + if ( + (false === $pos) + || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported()) + && !static::validateAddress($address)) + ) { + $error_message = sprintf( + '%s (From): %s', + $this->lang('invalid_address'), + $address + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + $this->From = $address; + $this->FromName = $name; + if ($auto && empty($this->Sender)) { + $this->Sender = $address; + } + + return true; + } + + /** + * Return the Message-ID header of the last email. + * Technically this is the value from the last time the headers were created, + * but it's also the message ID of the last sent message except in + * pathological cases. + * + * @return string + */ + public function getLastMessageID() + { + return $this->lastMessageID; + } + + /** + * Check that a string looks like an email address. + * Validation patterns supported: + * * `auto` Pick best pattern automatically; + * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0; + * * `pcre` Use old PCRE implementation; + * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; + * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements. + * * `noregex` Don't use a regex: super fast, really dumb. + * Alternatively you may pass in a callable to inject your own validator, for example: + * + * ```php + * PHPMailer::validateAddress('user@example.com', function($address) { + * return (strpos($address, '@') !== false); + * }); + * ``` + * + * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator. + * + * @param string $address The email address to check + * @param string|callable $patternselect Which pattern to use + * + * @return bool + */ + public static function validateAddress($address, $patternselect = null) + { + if (null === $patternselect) { + $patternselect = static::$validator; + } + //Don't allow strings as callables, see SECURITY.md and CVE-2021-3603 + if (is_callable($patternselect) && !is_string($patternselect)) { + return call_user_func($patternselect, $address); + } + //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321 + if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) { + return false; + } + switch ($patternselect) { + case 'pcre': //Kept for BC + case 'pcre8': + /* + * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL + * is based. + * In addition to the addresses allowed by filter_var, also permits: + * * dotless domains: `a@b` + * * comments: `1234 @ local(blah) .machine .example` + * * quoted elements: `'"test blah"@example.org'` + * * numeric TLDs: `a@b.123` + * * unbracketed IPv4 literals: `a@192.168.0.1` + * * IPv6 literals: 'first.last@[IPv6:a1::]' + * Not all of these will necessarily work for sending! + * + * @copyright 2009-2010 Michael Rushton + * Feel free to use and redistribute this code. But please keep this copyright notice. + */ + return (bool) preg_match( + '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' . + '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' . + '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' . + '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' . + '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' . + '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' . + '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' . + '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' . + '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD', + $address + ); + case 'html5': + /* + * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements. + * + * @see https://html.spec.whatwg.org/#e-mail-state-(type=email) + */ + return (bool) preg_match( + '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' . + '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD', + $address + ); + case 'php': + default: + return filter_var($address, FILTER_VALIDATE_EMAIL) !== false; + } + } + + /** + * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the + * `intl` and `mbstring` PHP extensions. + * + * @return bool `true` if required functions for IDN support are present + */ + public static function idnSupported() + { + return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding'); + } + + /** + * Converts IDN in given email address to its ASCII form, also known as punycode, if possible. + * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet. + * This function silently returns unmodified address if: + * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form) + * - Conversion to punycode is impossible (e.g. required PHP functions are not available) + * or fails for any reason (e.g. domain contains characters not allowed in an IDN). + * + * @see PHPMailer::$CharSet + * + * @param string $address The email address to convert + * + * @return string The encoded address in ASCII form + */ + public function punyencodeAddress($address) + { + //Verify we have required functions, CharSet, and at-sign. + $pos = strrpos($address, '@'); + if ( + !empty($this->CharSet) && + false !== $pos && + static::idnSupported() + ) { + $domain = substr($address, ++$pos); + //Verify CharSet string is a valid one, and domain properly encoded in this CharSet. + if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) { + //Convert the domain from whatever charset it's in to UTF-8 + $domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet); + //Ignore IDE complaints about this line - method signature changed in PHP 5.4 + $errorcode = 0; + if (defined('INTL_IDNA_VARIANT_UTS46')) { + //Use the current punycode standard (appeared in PHP 7.2) + $punycode = idn_to_ascii( + $domain, + \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI | + \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII, + \INTL_IDNA_VARIANT_UTS46 + ); + } elseif (defined('INTL_IDNA_VARIANT_2003')) { + //Fall back to this old, deprecated/removed encoding + $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_2003); + } else { + //Fall back to a default we don't know about + $punycode = idn_to_ascii($domain, $errorcode); + } + if (false !== $punycode) { + return substr($address, 0, $pos) . $punycode; + } + } + } + + return $address; + } + + /** + * Create a message and send it. + * Uses the sending method specified by $Mailer. + * + * @throws Exception + * + * @return bool false on error - See the ErrorInfo property for details of the error + */ + public function send() + { + try { + if (!$this->preSend()) { + return false; + } + + return $this->postSend(); + } catch (Exception $exc) { + $this->mailHeader = ''; + $this->setError($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + } + + /** + * Prepare a message for sending. + * + * @throws Exception + * + * @return bool + */ + public function preSend() + { + if ( + 'smtp' === $this->Mailer + || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0)) + ) { + //SMTP mandates RFC-compliant line endings + //and it's also used with mail() on Windows + static::setLE(self::CRLF); + } else { + //Maintain backward compatibility with legacy Linux command line mailers + static::setLE(PHP_EOL); + } + //Check for buggy PHP versions that add a header with an incorrect line break + if ( + 'mail' === $this->Mailer + && ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017) + || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103)) + && ini_get('mail.add_x_header') === '1' + && stripos(PHP_OS, 'WIN') === 0 + ) { + trigger_error($this->lang('buggy_php'), E_USER_WARNING); + } + + try { + $this->error_count = 0; //Reset errors + $this->mailHeader = ''; + + //Dequeue recipient and Reply-To addresses with IDN + foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) { + $params[1] = $this->punyencodeAddress($params[1]); + call_user_func_array([$this, 'addAnAddress'], $params); + } + if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { + throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL); + } + + //Validate From, Sender, and ConfirmReadingTo addresses + foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) { + if ($this->{$address_kind} === null) { + $this->{$address_kind} = ''; + continue; + } + $this->{$address_kind} = trim($this->{$address_kind}); + if (empty($this->{$address_kind})) { + continue; + } + $this->{$address_kind} = $this->punyencodeAddress($this->{$address_kind}); + if (!static::validateAddress($this->{$address_kind})) { + $error_message = sprintf( + '%s (%s): %s', + $this->lang('invalid_address'), + $address_kind, + $this->{$address_kind} + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + } + + //Set whether the message is multipart/alternative + if ($this->alternativeExists()) { + $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE; + } + + $this->setMessageType(); + //Refuse to send an empty message unless we are specifically allowing it + if (!$this->AllowEmpty && empty($this->Body)) { + throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + } + + //Trim subject consistently + $this->Subject = trim($this->Subject); + //Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) + $this->MIMEHeader = ''; + $this->MIMEBody = $this->createBody(); + //createBody may have added some headers, so retain them + $tempheaders = $this->MIMEHeader; + $this->MIMEHeader = $this->createHeader(); + $this->MIMEHeader .= $tempheaders; + + //To capture the complete message when using mail(), create + //an extra header list which createHeader() doesn't fold in + if ('mail' === $this->Mailer) { + if (count($this->to) > 0) { + $this->mailHeader .= $this->addrAppend('To', $this->to); + } else { + $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;'); + } + $this->mailHeader .= $this->headerLine( + 'Subject', + $this->encodeHeader($this->secureHeader($this->Subject)) + ); + } + + //Sign with DKIM if enabled + if ( + !empty($this->DKIM_domain) + && !empty($this->DKIM_selector) + && (!empty($this->DKIM_private_string) + || (!empty($this->DKIM_private) + && static::isPermittedPath($this->DKIM_private) + && file_exists($this->DKIM_private) + ) + ) + ) { + $header_dkim = $this->DKIM_Add( + $this->MIMEHeader . $this->mailHeader, + $this->encodeHeader($this->secureHeader($this->Subject)), + $this->MIMEBody + ); + $this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader) . static::$LE . + static::normalizeBreaks($header_dkim) . static::$LE; + } + + return true; + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + } + + /** + * Actually send a message via the selected mechanism. + * + * @throws Exception + * + * @return bool + */ + public function postSend() + { + try { + //Choose the mailer and send through it + switch ($this->Mailer) { + case 'sendmail': + case 'qmail': + return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody); + case 'smtp': + return $this->smtpSend($this->MIMEHeader, $this->MIMEBody); + case 'mail': + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); + default: + $sendMethod = $this->Mailer . 'Send'; + if (method_exists($this, $sendMethod)) { + return $this->{$sendMethod}($this->MIMEHeader, $this->MIMEBody); + } + + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); + } + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); + if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true && $this->smtp->connected()) { + $this->smtp->reset(); + } + if ($this->exceptions) { + throw $exc; + } + } + + return false; + } + + /** + * Send mail using the $Sendmail program. + * + * @see PHPMailer::$Sendmail + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function sendmailSend($header, $body) + { + if ($this->Mailer === 'qmail') { + $this->edebug('Sending with qmail'); + } else { + $this->edebug('Sending with sendmail'); + } + $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; + //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver + //A space after `-f` is optional, but there is a long history of its presence + //causing problems, so we don't use one + //Exim docs: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html + //Sendmail docs: https://www.sendmail.org/~ca/email/man/sendmail.html + //Example problem: https://www.drupal.org/node/1057954 + + //PHP 5.6 workaround + $sendmail_from_value = ini_get('sendmail_from'); + if (empty($this->Sender) && !empty($sendmail_from_value)) { + //PHP config has a sender address we can use + $this->Sender = ini_get('sendmail_from'); + } + //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) { + if ($this->Mailer === 'qmail') { + $sendmailFmt = '%s -f%s'; + } else { + $sendmailFmt = '%s -oi -f%s -t'; + } + } else { + //allow sendmail to choose a default envelope sender. It may + //seem preferable to force it to use the From header as with + //SMTP, but that introduces new problems (see + //), and + //it has historically worked this way. + $sendmailFmt = '%s -oi -t'; + } + + $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender); + $this->edebug('Sendmail path: ' . $this->Sendmail); + $this->edebug('Sendmail command: ' . $sendmail); + $this->edebug('Envelope sender: ' . $this->Sender); + $this->edebug("Headers: {$header}"); + + if ($this->SingleTo) { + foreach ($this->SingleToArray as $toAddr) { + $mail = @popen($sendmail, 'w'); + if (!$mail) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + $this->edebug("To: {$toAddr}"); + fwrite($mail, 'To: ' . $toAddr . "\n"); + fwrite($mail, $header); + fwrite($mail, $body); + $result = pclose($mail); + $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); + $this->doCallback( + ($result === 0), + [[$addrinfo['address'], $addrinfo['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); + if (0 !== $result) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + } + } else { + $mail = @popen($sendmail, 'w'); + if (!$mail) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + fwrite($mail, $header); + fwrite($mail, $body); + $result = pclose($mail); + $this->doCallback( + ($result === 0), + $this->to, + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); + if (0 !== $result) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + } + + return true; + } + + /** + * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters. + * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows. + * + * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report + * + * @param string $string The string to be validated + * + * @return bool + */ + protected static function isShellSafe($string) + { + //It's not possible to use shell commands safely (which includes the mail() function) without escapeshellarg, + //but some hosting providers disable it, creating a security problem that we don't want to have to deal with, + //so we don't. + if (!function_exists('escapeshellarg') || !function_exists('escapeshellcmd')) { + return false; + } + + if ( + escapeshellcmd($string) !== $string + || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""]) + ) { + return false; + } + + $length = strlen($string); + + for ($i = 0; $i < $length; ++$i) { + $c = $string[$i]; + + //All other characters have a special meaning in at least one common shell, including = and +. + //Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here. + //Note that this does permit non-Latin alphanumeric characters based on the current locale. + if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { + return false; + } + } + + return true; + } + + /** + * Check whether a file path is of a permitted type. + * Used to reject URLs and phar files from functions that access local file paths, + * such as addAttachment. + * + * @param string $path A relative or absolute path to a file + * + * @return bool + */ + protected static function isPermittedPath($path) + { + //Matches scheme definition from https://www.rfc-editor.org/rfc/rfc3986#section-3.1 + return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path); + } + + /** + * Check whether a file path is safe, accessible, and readable. + * + * @param string $path A relative or absolute path to a file + * + * @return bool + */ + protected static function fileIsAccessible($path) + { + if (!static::isPermittedPath($path)) { + return false; + } + $readable = is_file($path); + //If not a UNC path (expected to start with \\), check read permission, see #2069 + if (strpos($path, '\\\\') !== 0) { + $readable = $readable && is_readable($path); + } + return $readable; + } + + /** + * Send mail using the PHP mail() function. + * + * @see https://www.php.net/manual/en/book.mail.php + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function mailSend($header, $body) + { + $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; + + $toArr = []; + foreach ($this->to as $toaddr) { + $toArr[] = $this->addrFormat($toaddr); + } + $to = trim(implode(', ', $toArr)); + + //If there are no To-addresses (e.g. when sending only to BCC-addresses) + //the following should be added to get a correct DKIM-signature. + //Compare with $this->preSend() + if ($to === '') { + $to = 'undisclosed-recipients:;'; + } + + $params = null; + //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver + //A space after `-f` is optional, but there is a long history of its presence + //causing problems, so we don't use one + //Exim docs: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html + //Sendmail docs: https://www.sendmail.org/~ca/email/man/sendmail.html + //Example problem: https://www.drupal.org/node/1057954 + //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + + //PHP 5.6 workaround + $sendmail_from_value = ini_get('sendmail_from'); + if (empty($this->Sender) && !empty($sendmail_from_value)) { + //PHP config has a sender address we can use + $this->Sender = ini_get('sendmail_from'); + } + if (!empty($this->Sender) && static::validateAddress($this->Sender)) { + if (self::isShellSafe($this->Sender)) { + $params = sprintf('-f%s', $this->Sender); + } + $old_from = ini_get('sendmail_from'); + ini_set('sendmail_from', $this->Sender); + } + $result = false; + if ($this->SingleTo && count($toArr) > 1) { + foreach ($toArr as $toAddr) { + $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); + $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); + $this->doCallback( + $result, + [[$addrinfo['address'], $addrinfo['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + } + } else { + $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); + $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); + } + if (isset($old_from)) { + ini_set('sendmail_from', $old_from); + } + if (!$result) { + throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL); + } + + return true; + } + + /** + * Get an instance to use for SMTP operations. + * Override this function to load your own SMTP implementation, + * or set one with setSMTPInstance. + * + * @return SMTP + */ + public function getSMTPInstance() + { + if (!is_object($this->smtp)) { + $this->smtp = new SMTP(); + } + + return $this->smtp; + } + + /** + * Provide an instance to use for SMTP operations. + * + * @return SMTP + */ + public function setSMTPInstance(SMTP $smtp) + { + $this->smtp = $smtp; + + return $this->smtp; + } + + /** + * Provide SMTP XCLIENT attributes + * + * @param string $name Attribute name + * @param ?string $value Attribute value + * + * @return bool + */ + public function setSMTPXclientAttribute($name, $value) + { + if (!in_array($name, SMTP::$xclient_allowed_attributes)) { + return false; + } + if (isset($this->SMTPXClient[$name]) && $value === null) { + unset($this->SMTPXClient[$name]); + } elseif ($value !== null) { + $this->SMTPXClient[$name] = $value; + } + + return true; + } + + /** + * Get SMTP XCLIENT attributes + * + * @return array + */ + public function getSMTPXclientAttributes() + { + return $this->SMTPXClient; + } + + /** + * Send mail via SMTP. + * Returns false if there is a bad MAIL FROM, RCPT, or DATA input. + * + * @see PHPMailer::setSMTPInstance() to use a different class. + * + * @uses \PHPMailer\PHPMailer\SMTP + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function smtpSend($header, $body) + { + $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; + $bad_rcpt = []; + if (!$this->smtpConnect($this->SMTPOptions)) { + throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); + } + //Sender already validated in preSend() + if ('' === $this->Sender) { + $smtp_from = $this->From; + } else { + $smtp_from = $this->Sender; + } + if (count($this->SMTPXClient)) { + $this->smtp->xclient($this->SMTPXClient); + } + if (!$this->smtp->mail($smtp_from)) { + $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); + throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); + } + + $callbacks = []; + //Attempt to send to all recipients + foreach ([$this->to, $this->cc, $this->bcc] as $togroup) { + foreach ($togroup as $to) { + if (!$this->smtp->recipient($to[0], $this->dsn)) { + $error = $this->smtp->getError(); + $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']]; + $isSent = false; + } else { + $isSent = true; + } + + $callbacks[] = ['issent' => $isSent, 'to' => $to[0], 'name' => $to[1]]; + } + } + + //Only send the DATA command if we have viable recipients + if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) { + throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL); + } + + $smtp_transaction_id = $this->smtp->getLastTransactionID(); + + if ($this->SMTPKeepAlive) { + $this->smtp->reset(); + } else { + $this->smtp->quit(); + $this->smtp->close(); + } + + foreach ($callbacks as $cb) { + $this->doCallback( + $cb['issent'], + [[$cb['to'], $cb['name']]], + [], + [], + $this->Subject, + $body, + $this->From, + ['smtp_transaction_id' => $smtp_transaction_id] + ); + } + + //Create error message for any bad addresses + if (count($bad_rcpt) > 0) { + $errstr = ''; + foreach ($bad_rcpt as $bad) { + $errstr .= $bad['to'] . ': ' . $bad['error']; + } + throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE); + } + + return true; + } + + /** + * Initiate a connection to an SMTP server. + * Returns false if the operation failed. + * + * @param array $options An array of options compatible with stream_context_create() + * + * @throws Exception + * + * @uses \PHPMailer\PHPMailer\SMTP + * + * @return bool + */ + public function smtpConnect($options = null) + { + if (null === $this->smtp) { + $this->smtp = $this->getSMTPInstance(); + } + + //If no options are provided, use whatever is set in the instance + if (null === $options) { + $options = $this->SMTPOptions; + } + + //Already connected? + if ($this->smtp->connected()) { + return true; + } + + $this->smtp->setTimeout($this->Timeout); + $this->smtp->setDebugLevel($this->SMTPDebug); + $this->smtp->setDebugOutput($this->Debugoutput); + $this->smtp->setVerp($this->do_verp); + if ($this->Host === null) { + $this->Host = 'localhost'; + } + $hosts = explode(';', $this->Host); + $lastexception = null; + + foreach ($hosts as $hostentry) { + $hostinfo = []; + if ( + !preg_match( + '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/', + trim($hostentry), + $hostinfo + ) + ) { + $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry)); + //Not a valid host entry + continue; + } + //$hostinfo[1]: optional ssl or tls prefix + //$hostinfo[2]: the hostname + //$hostinfo[3]: optional port number + //The host string prefix can temporarily override the current setting for SMTPSecure + //If it's not specified, the default value is used + + //Check the host name is a valid name or IP address before trying to use it + if (!static::isValidHost($hostinfo[2])) { + $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]); + continue; + } + $prefix = ''; + $secure = $this->SMTPSecure; + $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure); + if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) { + $prefix = 'ssl://'; + $tls = false; //Can't have SSL and TLS at the same time + $secure = static::ENCRYPTION_SMTPS; + } elseif ('tls' === $hostinfo[1]) { + $tls = true; + //TLS doesn't use a prefix + $secure = static::ENCRYPTION_STARTTLS; + } + //Do we need the OpenSSL extension? + $sslext = defined('OPENSSL_ALGO_SHA256'); + if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) { + //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled + if (!$sslext) { + throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL); + } + } + $host = $hostinfo[2]; + $port = $this->Port; + if ( + array_key_exists(3, $hostinfo) && + is_numeric($hostinfo[3]) && + $hostinfo[3] > 0 && + $hostinfo[3] < 65536 + ) { + $port = (int) $hostinfo[3]; + } + if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) { + try { + if ($this->Helo) { + $hello = $this->Helo; + } else { + $hello = $this->serverHostname(); + } + $this->smtp->hello($hello); + //Automatically enable TLS encryption if: + //* it's not disabled + //* we are not connecting to localhost + //* we have openssl extension + //* we are not already using SSL + //* the server offers STARTTLS + if ( + $this->SMTPAutoTLS && + $this->Host !== 'localhost' && + $sslext && + $secure !== 'ssl' && + $this->smtp->getServerExt('STARTTLS') + ) { + $tls = true; + } + if ($tls) { + if (!$this->smtp->startTLS()) { + $message = $this->getSmtpErrorMessage('connect_host'); + throw new Exception($message); + } + //We must resend EHLO after TLS negotiation + $this->smtp->hello($hello); + } + if ( + $this->SMTPAuth && !$this->smtp->authenticate( + $this->Username, + $this->Password, + $this->AuthType, + $this->oauth + ) + ) { + throw new Exception($this->lang('authenticate')); + } + + return true; + } catch (Exception $exc) { + $lastexception = $exc; + $this->edebug($exc->getMessage()); + //We must have connected, but then failed TLS or Auth, so close connection nicely + $this->smtp->quit(); + } + } + } + //If we get here, all connection attempts have failed, so close connection hard + $this->smtp->close(); + //As we've caught all exceptions, just report whatever the last one was + if ($this->exceptions && null !== $lastexception) { + throw $lastexception; + } + if ($this->exceptions) { + // no exception was thrown, likely $this->smtp->connect() failed + $message = $this->getSmtpErrorMessage('connect_host'); + throw new Exception($message); + } + + return false; + } + + /** + * Close the active SMTP session if one exists. + */ + public function smtpClose() + { + if ((null !== $this->smtp) && $this->smtp->connected()) { + $this->smtp->quit(); + $this->smtp->close(); + } + } + + /** + * Set the language for error messages. + * The default language is English. + * + * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") + * Optionally, the language code can be enhanced with a 4-character + * script annotation and/or a 2-character country annotation. + * @param string $lang_path Path to the language file directory, with trailing separator (slash) + * Do not set this from user input! + * + * @return bool Returns true if the requested language was loaded, false otherwise. + */ + public function setLanguage($langcode = 'en', $lang_path = '') + { + //Backwards compatibility for renamed language codes + $renamed_langcodes = [ + 'br' => 'pt_br', + 'cz' => 'cs', + 'dk' => 'da', + 'no' => 'nb', + 'se' => 'sv', + 'rs' => 'sr', + 'tg' => 'tl', + 'am' => 'hy', + ]; + + if (array_key_exists($langcode, $renamed_langcodes)) { + $langcode = $renamed_langcodes[$langcode]; + } + + //Define full set of translatable strings in English + $PHPMAILER_LANG = [ + 'authenticate' => 'SMTP Error: Could not authenticate.', + 'buggy_php' => 'Your version of PHP is affected by a bug that may result in corrupted messages.' . + ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' . + ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', + 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', + 'data_not_accepted' => 'SMTP Error: data not accepted.', + 'empty_message' => 'Message body empty', + 'encoding' => 'Unknown encoding: ', + 'execute' => 'Could not execute: ', + 'extension_missing' => 'Extension missing: ', + 'file_access' => 'Could not access file: ', + 'file_open' => 'File Error: Could not open file: ', + 'from_failed' => 'The following From address failed: ', + 'instantiate' => 'Could not instantiate mail function.', + 'invalid_address' => 'Invalid address: ', + 'invalid_header' => 'Invalid header name or value', + 'invalid_hostentry' => 'Invalid hostentry: ', + 'invalid_host' => 'Invalid host: ', + 'mailer_not_supported' => ' mailer is not supported.', + 'provide_address' => 'You must provide at least one recipient email address.', + 'recipients_failed' => 'SMTP Error: The following recipients failed: ', + 'signing' => 'Signing Error: ', + 'smtp_code' => 'SMTP code: ', + 'smtp_code_ex' => 'Additional SMTP info: ', + 'smtp_connect_failed' => 'SMTP connect() failed.', + 'smtp_detail' => 'Detail: ', + 'smtp_error' => 'SMTP server error: ', + 'variable_set' => 'Cannot set or reset variable: ', + ]; + if (empty($lang_path)) { + //Calculate an absolute path so it can work if CWD is not here + $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR; + } + + //Validate $langcode + $foundlang = true; + $langcode = strtolower($langcode); + if ( + !preg_match('/^(?P[a-z]{2})(?P +
    +
    Cross my heart by KilledByAPixel
    + +