+
+
+
+
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('| File: | +%s | +
|---|---|
| Line: | +%s | +
| Message: | +%s |
+
' . $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(
+ '/(