feat(cloudron): add tirreno package artifacts

- Add CloudronStack/output/CloudronPackages-Artifacts/tirreno/ directory and its contents
- Includes package manifest, Dockerfile, source code, documentation, and build artifacts
- Add tirreno-1761840148.tar.gz as a build artifact
- Add tirreno-cloudron-package-1761841304.tar.gz as the Cloudron package
- Include all necessary files for the tirreno Cloudron package

This adds the complete tirreno Cloudron package artifacts to the repository.
This commit is contained in:
2025-10-30 11:43:06 -05:00
parent 0ce353ea9d
commit 91d52d2de5
1692 changed files with 202851 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class Access {
public static function CSRFTokenValid(array $params, \Base $f3): int|false {
$token = $params['token'] ?? null;
$csrf = $f3->get('SESSION.csrf');
if (!isset($token) || $token === '' || !isset($csrf) || $csrf === '' || $token !== $csrf) {
return \Utils\ErrorCodes::CSRF_ATTACK_DETECTED;
}
return false;
}
}

View File

@@ -0,0 +1,109 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class ApiResponseFormats {
// email and two phone responses??
public static function getErrorResponseFormat(): array {
return [
'value', // str
'type', // str
'error', // str
];
}
public static function getDomainFoundResponseFormat(): array {
return [
'domain', // str
'blockdomains', // bool
'disposable_domains', // bool
'free_email_provider', // bool
'creation_date', // date || null
'expiration_date', // date || null
'return_code', // int || null
'disabled', // bool
'closest_snapshot', // date || null
'mx_record', // bool
'ip', // IPvAnyAddress || null
'geo_ip', // str || null
'geo_html', // str || null
'web_server', // str || null
'hostname', // str || null
'emails', // str || null
'phone', // str || null
'discovery_date', // date
'tranco_rank', // int || null
];
}
public static function getDomainNotFoundResponseFormat(): array {
return [
'domain', // str
'blockdomains', // bool
'disposable_domains', // bool
'free_email_provider', // bool
'creation_date', // date || null
'expiration_date', // date || null
'return_code', // int || null
'disabled', // bool
'closest_snapshot', // date || null
'mx_record', // bool
];
}
public static function getIpResponseFormat(): array {
return [
'ip', // IPvAnyAddress
'country', // str
'asn', // int || null
'name', // str || null
'hosting', // bool
'vpn', // bool
'tor', // bool
'relay', // bool
'starlink', // bool
'description', // str || null
'blocklist', // bool
'domains_count', // list[str]
'cidr', // IPvAnyNetwork
'alert_list', // bool || null
];
}
public static function getUaResponseFormat(): array {
return [
'ua', // str
'device', // str || null
'browser_name', // str || null
'browser_version', // str || null
'os_name', // str || null
'os_version', // str || null
'modified', // bool
];
}
public static function matchResponse(array $arr, array $format): bool {
$allKeysPresent = true;
foreach ($format as $key) {
if (!isset($arr[$key])) {
$allKeysPresent = false;
break;
}
}
return $allKeysPresent;
}
}

View File

@@ -0,0 +1,370 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class Constants {
public static function get(string $key): array|string|int {
$const = __CLASS__ . '::' . $key;
if (!defined($const)) {
trigger_error('Undefined constant: ' . $key, E_USER_ERROR);
}
$value = constant($const);
$f3 = \Base::instance();
$f3key = 'EXTRA_' . $key;
if ($f3->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',
];
}

View File

@@ -0,0 +1,227 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
// can accept time params as `* * * * 0,1,2`, `0-15 * * 1 3`, but
// not step values like `23/4 10/2 * * *`
// comma-expressions should be wrapped in quotes like "0-10 0,12 * * *"
class Cron extends \Prefab {
public const HANDLER = 0;
public const EXPRESSION = 1;
public const RANGES = [
['min' => 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));
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class DictManager {
public static function load(string $file): void {
$f3 = \Base::instance();
$locale = $f3->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);
}
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class ElapsedDate {
//https://gist.github.com/fazlurr/473a46d6d2e967119e77b5339dd10bc2
public static function short(?string $dt): ?string {
return $dt ? date('d/m/Y H:i:s', strtotime($dt)) : null;
}
public static function date(?string $dt): ?string {
return $dt ? date('d/m/Y', strtotime($dt)) : null;
}
public static function long(string $dt): string {
$ret = [];
$secs = strtotime($dt);
$secs = time() - $secs;
$bit = [
' year' => $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);
}
}

View File

@@ -0,0 +1,133 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class ErrorCodes {
public const EVERYTHING_IS_FINE = 600;
public const CSRF_ATTACK_DETECTED = 601;
//Signup
public const EMAIL_DOES_NOT_EXIST = 602;
public const EMAIL_IS_NOT_CORRECT = 603;
public const EMAIL_ALREADY_EXIST = 604;
public const PASSWORD_DOES_NOT_EXIST = 605;
public const PASSWORD_IS_TO_SHORT = 606;
public const ACCOUNT_CREATED = 607;
//Activation
public const ACTIVATION_KEY_DOES_NOT_EXIST = 610;
public const ACTIVATION_KEY_IS_NOT_CORRECT = 611;
//Login
public const EMAIL_OR_PASSWORD_IS_NOT_CORRECT = 620;
//Api
public const API_KEY_ID_DOESNT_EXIST = 630;
public const API_KEY_WAS_CREATED_FOR_ANOTHER_USER = 631;
public const OPERATOR_ID_DOES_NOT_EXIST = 632;
public const OPERATOR_IS_NOT_A_CO_OWNER = 633;
public const UNKNOWN_ENRICHMENT_ATTRIBUTES = 634;
public const INVALID_API_RESPONSE = 635;
//Profile
public const FIRST_NAME_DOES_NOT_EXIST = 640;
public const LAST_NAME_DOES_NOT_EXIST = 641;
public const COUNTRY_DOES_NOT_EXIST = 642;
public const STREET_DOES_NOT_EXIST = 643;
public const CITY_DOES_NOT_EXIST = 644;
public const STATE_DOES_NOT_EXIST = 645;
public const ZIP_DOES_NOT_EXIST = 646;
public const TIME_ZONE_DOES_NOT_EXIST = 647;
public const RETENTION_POLICY_DOES_NOT_EXIST = 648;
public const UNREVIEWED_ITEMS_REMINDER_FREQUENCY_DOES_NOT_EXIST = 649;
//Settings
public const CURRENT_PASSWORD_DOES_NOT_EXIST = 650;
public const CURRENT_PASSWORD_IS_NOT_CORRECT = 651;
public const NEW_PASSWORD_DOES_NOT_EXIST = 652;
public const PASSWORD_CONFIRMATION_DOES_NOT_EXIST = 653;
public const PASSWORDS_ARE_NOT_EQUAL = 654;
public const EMAIL_IS_NOT_NEW = 655;
//Password recovering
public const RENEW_KEY_CREATED = 660;
public const RENEW_KEY_DOES_NOT_EXIST = 661;
public const RENEW_KEY_IS_NOT_CORRECT = 662;
public const RENEW_KEY_WAS_EXPIRED = 663;
public const ACCOUNT_ACTIVATED = 664;
//Account messages
public const THERE_ARE_NO_EVENTS_YET = 670;
public const THERE_ARE_NO_EVENTS_LAST_24_HOURS = 671;
public const CUSTOM_ERROR_FROM_DSHB_MESSAGES = 672;
//Watchlist
public const OPERATOR_DOES_NOT_HAVE_ACCESS_TO_ACCOUNT = 680;
public const USER_HAS_BEEN_SUCCESSFULLY_ADDED_TO_WATCH_LIST = 681;
public const USER_HAS_BEEN_SUCCESSFULLY_REMOVED_FROM_WATCH_LIST = 682;
public const USER_FRAUD_FLAG_HAS_BEEN_SET = 683;
public const USER_FRAUD_FLAG_HAS_BEEN_UNSET = 684;
public const USER_REVIEWED_FLAG_HAS_BEEN_SET = 685;
public const USER_REVIEWED_FLAG_HAS_BEEN_UNSET = 686;
public const USER_DELETION_FAILED = 687;
public const USER_BLACKLISTING_FAILED = 688;
public const USER_BLACKLISTING_QUEUED = 689;
//Change email
public const EMAIL_CHANGED = 690;
public const CHANGE_EMAIL_KEY_DOES_NOT_EXIST = 691;
public const CHANGE_EMAIL_KEY_IS_NOT_CORRECT = 692;
public const CHANGE_EMAIL_KEY_WAS_EXPIRED = 693;
//Rules
public const RULES_HAS_BEEN_SUCCESSFULLY_UPDATED = 800;
public const BLACKLIST_THRESHOLD_DOES_NOT_EXIST = 801;
public const REVIEW_QUEUE_THRESHOLD_DOES_NOT_EXIST = 802;
public const BLACKLIST_THRESHOLD_EXCEEDS_REVIEW_QUEUE_THRESHOLD = 803;
// REST API
public const REST_API_KEY_DOES_NOT_EXIST = 900;
public const REST_API_KEY_IS_NOT_CORRECT = 901;
public const REST_API_NOT_AUTHORIZED = 902;
public const REST_API_MISSING_PARAMETER = 903;
public const REST_API_VALIDATION_ERROR = 904;
public const REST_API_USER_ALREADY_SCHEDULED_FOR_DELETION = 905;
public const REST_API_USER_SUCCESSFULLY_ADDED_FOR_DELETION = 906;
// Manual check
public const ENRICHMENT_API_KEY_DOES_NOT_EXIST = 1000;
public const TYPE_DOES_NOT_EXIST = 1001;
public const SEARCH_QUERY_DOES_NOT_EXIST = 1002;
public const ENRICHMENT_API_UNKNOWN_ERROR = 1003;
public const ENRICHMENT_API_BOGON_IP = 1004;
public const ENRICHMENT_API_IP_NOT_FOUND = 1005;
public const RISK_SCORE_UPDATE_UNKNOWN_ERROR = 1006;
public const ENRICHMENT_API_KEY_OVERUSE = 1007;
public const ENRICHMENT_API_ATTRIBUTE_IS_UNAVAILABLE = 1008;
public const ENRICHMENT_API_IS_NOT_AVAILABLE = 1009;
//Blacklist
public const ITEM_HAS_BEEN_SUCCESSFULLY_REMOVED_FROM_BLACK_LIST = 1010;
//Subscription
public const SUBSCRIPTION_KEY_INVALID_UPDATE = 1100;
// Totals
public const TOTALS_INVALID_TYPE = 1200;
// Crons
public const CRON_JOB_MAY_BE_OFF = 1300;
}

View File

@@ -0,0 +1,178 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class ErrorHandler {
public static function getErrorDetails(\Base $f3): array {
$errorTraceArray = [];
$errorTraceString = $f3->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(['&gt;', '&lt;'], ['>', '<'], $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('<br>', $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('<br>', $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;
}
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class HotDebug {
public static function e($value, bool $shouldExit = false): void {
$html = '
<style type="text/css">TABLE{border-collapse: collapse;} TH {text-align: right; background-color: lightgrey;} TH, TD{ border: 1px solid #000; padding: 3px;}</style>
<table>
<caption><b>Debug</b></caption>
<tr>
<th>File:</th>
<td>%s</td>
</tr>
<tr>
<th>Line:</th>
<td>%s</td>
</tr>
<tr>
<th>Message:</th>
<td><pre>%s</pre></td>
</tr>
</table>';
$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;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class Logger {
public static function log(?string $title, string|array $message): void {
$f3 = \Base::instance();
$logFile = $f3->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);
}
}

View File

@@ -0,0 +1,134 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class Mailer {
public static function send(?string $toName, string $toAddress, string $subject, string $message): array {
$f3 = \Base::instance();
$canSendEmail = $f3->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,
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class Rules {
public static function checkPhoneCountryMatchIp(array $params): ?bool {
if (is_null($params['lp_country_code']) || $params['lp_country_code'] === 0) {
return null;
}
return in_array($params['lp_country_code'], $params['eip_country_id']);
}
public static function eventDeviceIsNew(array $params, int $idx): bool {
$deviceCreated = new \DateTime($params['event_device_created'][$idx]);
$deviceLastseen = new \DateTime($params['event_device_lastseen'][$idx]);
$interval = $deviceCreated->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;
}
}

View File

@@ -0,0 +1,184 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class RulesClasses {
private const RULES_WEIGHT = [
-20 => '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 ['&minus;', $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;
}
}

View File

@@ -0,0 +1,158 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class SystemMessages {
public static function get(int $apiKey): array {
$f3 = \Base::instance();
$messages = [
self::getNoEventsMessage($apiKey),
self::getOveruseMessage($apiKey),
];
// show no-crons warning only if events there are no valid incoming events
if (!array_filter($messages)) {
$messages[] = self::getInactiveCronMessage($apiKey);
}
$messages[] = self::getCustomErrorMessage($apiKey);
$msg = [];
$iters = count($messages);
for ($i = 0; $i < $iters; ++$i) {
$m = $messages[$i];
if ($m !== null) {
if ($m['id'] !== \Utils\ErrorCodes::CUSTOM_ERROR_FROM_DSHB_MESSAGES) {
$code = sprintf('error_%s', $m['id']);
$text = $f3->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;
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class TimeZones {
public const FORMAT = 'Y-m-d H:i:s';
public const EVENT_FORMAT = 'Y-m-d H:i:s.u';
public const DEFAULT = 'UTC';
public static function localizeTimeStamp(string $time, \DateTimeZone $from, \DateTimeZone $to, bool $useMilliseconds): string {
$format = ($useMilliseconds) ? self::EVENT_FORMAT : self::FORMAT;
$time = ($useMilliseconds) ? $time : explode('.', $time)[0];
$new = \DateTime::createFromFormat($format, $time, $from);
$new->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;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class Updates {
private const UPDATES_LIST = [
\Updates\Update001::class,
\Updates\Update002::class,
\Updates\Update003::class,
\Updates\Update004::class,
\Updates\Update005::class,
];
public static function syncUpdates() {
$updates = new \Models\Updates(\Base::instance());
$updates->checkDb('core', self::UPDATES_LIST);
}
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class Variables {
private static function getF3(): \Base {
return \Base::instance();
}
public static function getDB(): ?string {
return getenv('DATABASE_URL') ?: self::getF3()->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'));
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils;
class VersionControl {
public const VERSION_MAJOR = 0;
public const VERSION_MINOR = 9;
public const VERSION_REVISION = 9;
public static function versionString(): string {
return sprintf('%d.%d.%d', self::VERSION_MAJOR, self::VERSION_MINOR, self::VERSION_REVISION);
}
public static function fullVersionString(): string {
return sprintf('v%d.%d.%d', self::VERSION_MAJOR, self::VERSION_MINOR, self::VERSION_REVISION);
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils\WordsLists;
abstract class Base {
protected static string $extensionFile = '';
protected static array $words = [];
private static function getExtension(): ?array {
$filename = dirname(__DIR__, 3) . '/assets/lists/' . static::$extensionFile;
if (file_exists($filename) && is_readable($filename)) {
$data = include $filename;
if (is_array($data)) {
return $data;
}
}
return null;
}
public static function getWords(): array {
return self::getExtension() ?? static::$words;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils\WordsLists;
class Email extends Base {
protected static string $extensionFile = 'email.php';
protected static array $words = [
'spam',
'test',
'gummie',
'dummy',
'123',
'321',
'000',
'111',
'222',
'333',
'444',
'555',
'666',
'777',
'888',
'999',
];
}

View File

@@ -0,0 +1,157 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils\WordsLists;
class Url extends Base {
protected static string $extensionFile = 'url.php';
protected static array $words = [
'%00',
'%20AND%20',
'%20OR%20',
'%20and%20',
'%20or%20',
'%252e%252e%252f',
'%252e%252e%255c',
'%2e%2e%2f',
'%2e%2e%5c',
'%2e%2e/',
'%2e%2e\\',
'%c0%ae%c0%ae%c0%af',
'%uff0e%uff0e%u2215',
'%uff0e%uff0e%u2216',
'&exec',
'\' OR \'1\'=\'1',
'\'=\'',
'*/',
'..%255c',
'..%2f',
'..%5c',
'..%c0%af',
'..%c1%9c',
'../',
'..\\',
'..\/',
'./',
'.\\',
'.env',
'.env.example',
'.exe',
'.git',
'.htaccess',
'.htpasswd',
'.sh',
'.well-known/',
'/*',
'/Dix8',
'/FdOc',
'/Wp-includes',
'/app/index.js',
'/bY3o',
'/bc',
'/bk',
'/conf',
'/database',
'/etc',
'/graphql',
'/new',
'/old',
'/pki-validation',
'/proc',
'/sandbox',
'/secrets',
'/solr',
'/var',
'/var/www',
'/wp-admin',
'/wp-content',
'/wp-head',
'/wp-header',
'/wp-includes',
'/wp-info',
'/wp-login',
'/wso',
'1=1',
';',
'<script>',
'?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',
];
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Utils\WordsLists;
class UserAgent extends Base {
protected static string $extensionFile = 'user-agent.php';
protected static array $words = [
'--',
'/*',
'*/',
'pg_',
'\');', // should be %'%)%;% ?
'alter ',
'select',
'waitfor',
'delay',
'delete',
'drop',
'dbcc',
'schema',
'exists',
'cmdshell',
'%2A', // *
'%27', // '
'%22', // "
'%2D', // -
'%2F', // /
'%5C', // \
'%3B', // ;
'%23', // #
'%2B', // +
'%3D', // =
'%28', // (
'%29', // )
'/bin',
'%2Fbin',
'.sh',
'|sh',
'.exe',
];
}

View File

@@ -0,0 +1,3 @@
<?php
//