OwlCyberSecurity - MANAGER
Edit File: Manager.php
<?php /** * Copyright (с) Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2022 All Rights Reserved * * Licensed under CLOUD LINUX LICENSE AGREEMENT * https://www.cloudlinux.com/legal/ */ namespace CloudLinux\SmartAdvice\App\Advice; use CloudLinux\SmartAdvice\App\Option; use CloudLinux\SmartAdvice\App\Notice\Model as NoticeModel; /** * Advice manager */ class Manager { /** * Notification status enabled. */ const NOTIFICATION_STATUS_ENABLED = 'enabled'; /** * Reminder period in days. */ const REMINDERS_PERIOD = 30; /** * Api. * * @var Api */ private $api; /** * Option. * * @var Option */ private $option; /** * Site home URL. * * @var string */ private $home = ''; /** * Site domain. * * @var string */ private $domain = ''; /** * WordPress path. * * @var string */ private $website = ''; /** * Advices. * * @var array<Model> */ protected $advices; /** * User options. * * @var array<string,array> */ protected $user_options = array(); /** * Constructor. * * @param Api $api api. * @param Option $option api. */ public function __construct( Api $api, Option $option ) { $this->api = $api; $this->option = $option; $this->advices = $this->cache(); $this->env(); add_action( 'cl_smart_advice_sync', array( $this, 'sync' ), 10, 1 ); add_action( 'cl_smart_advice_sync_statuses', array( $this, 'syncStatuses' ), 10, 1 ); add_action( 'cl_smart_advice_status', array( $this, 'status' ), 10, 2 ); add_action( 'cl_smart_advice_apply', array( $this, 'apply' ), 10, 2 ); add_action( 'cl_smart_advice_apply_type', array( $this, 'applyType' ), 10, 2 ); add_action( 'cl_smart_advice_rollback', array( $this, 'rollback' ), 10, 3 ); add_action( 'cl_smart_advice_subscription', array( $this, 'subscription' ), 10, 1 ); add_action( 'cl_smart_advice_email_new_advices', array( $this, 'email_new_advices' ), 10, 1 ); add_action( 'cl_smart_advice_email_reminders', array( $this, 'email_reminders' ), 10, 0 ); add_filter( 'cl_smart_advice_js_data', array( $this, 'js_data' ), 10, 1 ); $this->create_cron_jobs(); } /** * Maybe create cron event. */ protected function create_cron_jobs() { if ( false === wp_next_scheduled( 'cl_smart_advice_email_reminders' ) ) { $timestamp = strtotime( 'today midnight' ); if ( false !== $timestamp ) { wp_schedule_event( $timestamp, 'daily', 'cl_smart_advice_email_reminders' ); } } } /** * Evn info. * * @return void */ protected function env() { $home = $this->home(); $parse = wp_parse_url( $home ); if ( ! is_array( $parse ) ) { $parse = array(); } $domain = array_key_exists( 'host', $parse ) ? $parse['host'] : ''; $path = array_key_exists( 'path', $parse ) ? $parse['path'] : ''; $this->domain = str_replace( 'www.', '', $domain ); $this->website = ! empty( $path ) ? $path : '/'; } /** * Domain. * * @return string */ public function domain() { return $this->domain; } /** * Website. * * @return string */ public function website() { return $this->website; } /** * Home url. * * @return string */ public function home() { if ( ! empty( $this->home ) ) { return $this->home; } if ( defined( 'WP_HOME' ) ) { $home = (string) WP_HOME; } else { $home = (string) get_option( 'home' ); } $this->home = $home; return $this->home; } /** * Api interface. * * @return Api */ protected function api() { return $this->api; } /** * Option instance. * * @return Option */ protected function option() { return $this->option; } /** * Advices array. * * @return array<Model> */ public function advices() { $advices = $this->advices; uasort( $advices, function ( $a, $b ) { if ( 'review' === $a->status ) { return - 1; } return 1; } ); return $advices; } /** * Get advice by ID. * * @param string $advice_id Advice ID. * @param bool $fresh Get fresh data of advice. * * @return ?Model * @since 0.1-6 */ public function get( $advice_id, $fresh = false ) { $advices = $this->advices(); foreach ( $advices as $key => $advice ) { if ( (string) $advice->id === (string) $advice_id ) { if ( true === $fresh ) { $this->setDetails( $advice ); $this->advices[ $key ] = $advice; $this->save(); } return $advice; } } return null; } /** * Get advice by type. * * @param string $advice_type Advice type. * * @return ?Model * @since 0.1-6 */ public function get_advice_by_type( $advice_type ) { $advices = $this->advices(); foreach ( $advices as $advice ) { if ( $advice->type === $advice_type ) { return $advice; } } return null; } /** * Check domain and WordPress path. * * @param array $item data. * * @return boolean */ protected function checkMetadata( $item ) { if ( strtolower( $this->domain() ) !== strtolower( $item['metadata']['domain'] ) ) { do_action( 'cl_smart_advice_set_error', E_WARNING, 'Received wrong advice. Domain mismatch.', __FILE__, __LINE__, array( 'advice' => $item, 'domain' => $this->domain(), ) ); return false; } if ( strtolower( $this->website() ) !== strtolower( $item['metadata']['website'] ) ) { do_action( 'cl_smart_advice_set_error', E_WARNING, 'Received wrong advice. Website mismatch.', __FILE__, __LINE__, array( 'advice' => $item, 'website' => $this->website(), ) ); return false; } return true; } /** * Check advice. * * @param array $advice advice. * * @return boolean */ private function checkAdvice( $advice ) { if ( 'outdated' === $advice['status'] ) { return false; } return true; } /** * Add or update advice. * * @param array $advice data. * * @return Model */ private function set( $advice ) { if ( array_key_exists( $advice['id'], $this->advices() ) ) { $model = $this->advices()[ $advice['id'] ]; } else { $model = new Model(); } $model->fill( $advice ); $this->advices[ $model->id ] = $model; return $model; } /** * Sync advice. * * @param array $response advices. * * @return void */ public function sync( $response ) { if ( is_array( $response ) && array_key_exists( 'data', $response ) ) { $this->setAdvices( $response ); $this->notifyNew(); $this->save(); } else { add_filter( 'cl_smart_advice_sync_failed', '__return_true' ); } } /** * Set advices. * * @param mixed $response advices. * * @return void */ public function setAdvices( $response ) { $this->advices = array(); $items = array_filter( $response['data'], array( $this, 'checkMetadata' ) ); foreach ( $items as $item ) { $advice = $item['advice']; $advice['username'] = $item['metadata']['username']; foreach ( array( 'created_at', 'updated_at' ) as $key ) { $advice[ $key ] = array_key_exists( $key, $item ) ? $item[ $key ] : ''; } if ( ! $this->checkAdvice( $advice ) ) { continue; } $model = $this->set( $advice ); $this->setDetails( $model ); } } /** * Set details. * * @param Model $advice advice. * * @return void */ public function setDetails( $advice ) { if ( false === $advice->is_socket_available() ) { return; } $json = $this->api()->details( $advice->id ); if ( ! empty( $json ) ) { $details = json_decode( $json, true ); if ( is_array( $details ) && array_key_exists( 'data', $details ) && array_key_exists( 'advice', $details['data'] ) && is_array( $details['data']['advice'] ) ) { $data = $details['data']['advice']; if ( isset( $details['data']['created_at'] ) ) { $advice->created_at = $details['data']['created_at']; } if ( isset( $details['data']['updated_at'] ) ) { $advice->updated_at = $details['data']['updated_at']; } if ( isset( $details['data']['metadata']['username'] ) ) { $advice->username = $details['data']['metadata']['username']; } if ( isset( $data['status'] ) ) { $advice->status = $data['status']; } if ( isset( $data['description'] ) ) { $advice->description = $data['description']; } if ( isset( $data['detailed_description'] ) ) { $advice->detailed_description = $data['detailed_description']; } if ( isset( $data['apply_advice_button_text'] ) ) { $advice->apply_advice_button_text = $data['apply_advice_button_text']; } if ( isset( $data['upgrade_to_apply_button_text'] ) ) { $advice->upgrade_to_apply_button_text = $data['upgrade_to_apply_button_text']; } if ( isset( $data['email_view_advice_text'] ) ) { $advice->email_view_advice_text = $data['email_view_advice_text']; } if ( isset( $data['email_subject'] ) ) { $advice->email_subject = $data['email_subject']; } $advice->requests = array(); if ( array_key_exists( 'requests', $data ) && is_array( $data['requests'] ) ) { foreach ( $data['requests'] as $request ) { $advice->requests[ $request['id'] ] = $request['url']; } } } } } /** * Sync statuses. * * @param ?string $advice_id id. * * @return void */ public function syncStatuses( $advice_id = '' ) { if ( ! empty( $advice_id ) ) { $this->status( $advice_id, true ); } else { $advices = $this->advices(); foreach ( $advices as $advice ) { $this->status( $advice->id, false ); } $this->save(); } } /** * Advice modifier. * * @param string $advice_id advice. * @param callable $callback command. * @param bool $save advice. * @param string $reason advice rollback reason. * * @return void */ private function modifier( $advice_id, $callback, $save = true, $reason = '' ) { $advices = $this->advices(); if ( ! array_key_exists( $advice_id, $advices ) ) { do_action( 'cl_smart_advice_notice_add', $advice_id, NoticeModel::ERROR_ADVICE_NOT_FOUND['type'], array( 'advice_id' => $advice_id ) ); return; } $advice = $advices[ $advice_id ]; if ( is_callable( $callback ) ) { $callback( $advice, $reason ); } $this->set( $advice->toArray() ); if ( true === $save ) { $this->save(); } } /** * Apply advice, set status pending. * * @param string $advice_id to apply. * @param bool $save advices. * * @return void */ public function apply( $advice_id, $save = true ) { $this->modifier( $advice_id, function ( $advice ) { if ( false === $advice->is_socket_available() ) { return; } $json = $this->api()->apply( $advice->id ); if ( ! $json ) { do_action( 'cl_smart_advice_set_error', E_USER_WARNING, 'Advice manager: failed modifier apply for ' . $advice->id, __FILE__, __LINE__ ); do_action( 'cl_smart_advice_notice_add', $advice->id, NoticeModel::ERROR_ADVICE_APPLY['type'], array( 'advice_type' => $advice->type ) ); } else { $response = json_decode( $json, true ); $this->errorHandler( $response, $advice, NoticeModel::ERROR_ADVICE_APPLY['type'] ); $advice->status = 'pending'; } }, $save ); } /** * Apply advice by type, set status pending. * * @param string $type to apply. * @param bool $save advices. * * @return void */ public function applyType( $type, $save = true ) { $advice = current( array_filter( $this->advices(), function ( $advice ) use ( $type ) { return strtolower( $advice->type ) === strtolower( $type ); } ) ); if ( $advice ) { $this->apply( $advice->id, $save ); } } /** * Rollback advice, set status pending. * * @param string $advice_id id. * @param bool $save advices. * @param string $reason advice rollback reason. * * @return void */ public function rollback( $advice_id, $save = true, $reason = '' ) { $this->modifier( $advice_id, function ( $advice, $reason ) { if ( false === $advice->is_socket_available() ) { return; } $json = $this->api()->rollback( $advice->id, $reason ); if ( ! $json ) { do_action( 'cl_smart_advice_set_error', E_USER_WARNING, 'Advice manager: failed modifier rollback for ' . $advice->id, __FILE__, __LINE__ ); do_action( 'cl_smart_advice_notice_add', $advice->id, NoticeModel::ERROR_ADVICE_ROLLBACK['type'], array( 'advice_type' => $advice->type ) ); } else { $response = json_decode( $json, true ); $this->errorHandler( $response, $advice, NoticeModel::ERROR_ADVICE_ROLLBACK['type'] ); $advice->status = 'pending'; } }, $save, $reason ); } /** * Subscription advice payment status. * * @param string $advice_id id. * * @return void */ public function subscription( $advice_id ) { $this->api()->subscription( $advice_id ); } /** * Update advice status. * * @param string $advice_id id. * @param bool $save advices. * * @return void */ public function status( $advice_id, $save = true ) { $this->modifier( $advice_id, function ( $advice ) { if ( false === $advice->is_socket_available() ) { return; } $json = $this->api()->status( $advice->id ); $response = json_decode( $json, true ); if ( is_array( $response ) ) { if ( array_key_exists( 'status', $response ) ) { $advice->status = $response['status']; } if ( array_key_exists( 'total_stages', $response ) ) { $advice->total_stages = $response['total_stages']; } if ( array_key_exists( 'completed_stages', $response ) ) { $advice->completed_stages = $response['completed_stages']; } if ( array_key_exists( 'subscription', $response ) ) { $module_name = ! empty( $advice->module_name ) ? $advice->module_name : strtolower( $advice->type ); if ( array_key_exists( $module_name, $response['subscription'] ) ) { $advice->subscription_status = $response['subscription'][ $module_name ]; } } if ( array_key_exists( 'upgrade_url', $response ) ) { $advice->subscription_upgrade_url = $response['upgrade_url']; } $this->errorHandler( $response, $advice, NoticeModel::ERROR_ADVICE_STATUS['type'] ); } }, $save ); } /** * Error handler. * * @param array $response data. * @param Model $advice advice. * @param string $error_type type. * * @return void */ private function errorHandler( $response, $advice, $error_type ) { if ( ! is_array( $response ) ) { return; } $errors = array(); // Result. if ( array_key_exists( 'result', $response ) && 'success' !== $response['result'] ) { $context = array(); $description = $response['result']; if ( array_key_exists( 'context', $response ) && is_array( $response['context'] ) ) { $context = $response['context']; } $errors[] = $this->errorContext( $description, $context ); } // Features. $this->errorFeature( $response, $errors ); // Data result. if ( array_key_exists( 'data', $response ) && array_key_exists( 'result', $response['data'] ) && 'success' !== $response['data']['result'] ) { $context = array(); $description = $response['data']['result']; if ( 'PAYMENT_REQUIRED' !== $description ) { if ( array_key_exists( 'context', $response['data'] ) && is_array( $response['data']['context'] ) ) { $context = $response['data']['context']; } $errors[] = $this->errorContext( $description, $context ); // Data features. if ( array_key_exists( 'feature', $response['data'] ) ) { $this->errorFeature( $response['data'], $errors ); } } } // Warning. if ( array_key_exists( 'warning', $response ) && ! empty( $response['warning'] ) ) { $context = array(); $description = $response['warning']; if ( array_key_exists( 'context', $response ) && is_array( $response['context'] ) ) { $context = $response['context']; } $errors[] = $this->errorContext( $description, $context ); } $error = implode( PHP_EOL, $errors ); if ( ! empty( $error ) ) { do_action( 'cl_smart_advice_notice_add', $advice->id, $error_type, array( 'advice_type' => $advice->type, 'description' => $error, ) ); } } /** * Find error features. * * @param array $response data. * @param array $errors output. * * @return void */ private function errorFeature( $response, &$errors ) { if ( array_key_exists( 'feature', $response ) && array_key_exists( 'issues', $response['feature'] ) && ! empty( $response['feature']['issues'] ) ) { foreach ( $response['feature']['issues'] as $issue ) { $context = array(); $descriptions = array(); if ( array_key_exists( 'header', $issue ) ) { $descriptions[] = $issue['header']; } if ( array_key_exists( 'description', $issue ) ) { // Too long to show. if ( strpos( $issue['description'], 'wp-cli' ) === false ) { $descriptions[] = PHP_EOL . $issue['description']; } } if ( array_key_exists( 'fix_tip', $issue ) ) { $descriptions[] = PHP_EOL . $issue['fix_tip']; } if ( array_key_exists( 'context', $issue ) && is_array( $issue['context'] ) ) { $context = $issue['context']; } $description = implode( PHP_EOL, $descriptions ); $errors[] = $this->errorContext( $description, $context ); } } } /** * Error parse. * * @param string $description error string. * @param array $context context. * * @return string */ private function errorContext( $description, $context = array() ) { $search = array(); $replace = array(); foreach ( $context as $key => $val ) { $search[] = '%(' . $key . ')s'; $replace[] = $val; } return str_replace( $search, $replace, $description ); } /** * All cached advices. * * @return array<Model> */ private function cache() { return $this->option()->get( 'advices', true ); } /** * Time sync at. * * @return int|null */ public function syncAt() { return $this->option()->get( 'sync_at' ); } /** * Update advices' cache. * * @return void */ public function save() { $advices = $this->advices(); $this->option()->save( 'advices', $advices ); $this->option()->save( 'sync_at', time() ); } /** * Notify if advice is new * * @return void */ private function notifyNew() { $current = array_map( function ( $advice ) { return $advice->id; }, $this->cache() ); $new = array(); array_filter( $this->advices(), function ( $advice ) use ( &$new, $current ) { if ( ! in_array( $advice->id, $current ) ) { $new[] = $advice->id; return true; } return true; } ); $count = count( $new ); if ( $count > 0 ) { $type = NoticeModel::NEW_ADVICES['type']; do_action( 'cl_smart_advice_notice_add', $type, $type, array( 'count' => $count, 'list' => implode( ',', $new ), ) ); do_action( 'cl_smart_advice_email_new_advices', $new ); } } /** * Get options by user via SmartAdvice api. * * @param string $user name. * * @return array */ protected function user_options( $user ) { if ( ! array_key_exists( $user, $this->user_options ) ) { $response = $this->api()->get_options( $user ); $data = json_decode( $response, true ); if ( is_array( $data ) ) { $this->user_options[ $user ] = $data; } else { return array(); } } return $this->user_options[ $user ]; } /** * Notification email status. * * @param string $user name. * * @return string|null */ public function notifications_email_status( $user ) { $data = $this->user_options( $user ); if ( ! isset( $data['notifications']['email_status'] ) ) { return null; } return (string) $data['notifications']['email_status']; } /** * Notification reminders status. * * @param string $user name. * * @return string|null */ public function notifications_reminders_status( $user ) { $data = $this->user_options( $user ); if ( ! isset( $data['notifications']['reminders_status'] ) ) { return null; } return (string) $data['notifications']['reminders_status']; } /** * Checks if a reminder should be sent for advice. * * @param Model $advice advice. * * @return bool */ public function is_reminder_needed( $advice ) { if ( false === $advice->is_email_available() ) { return false; } $timestamp = $this->get_sending_timestamp( $advice->id ); if ( false === $timestamp ) { $timestamp = strtotime( $advice->created_at ); } $window = DAY_IN_SECONDS * self::REMINDERS_PERIOD; if ( defined( 'CL_SMART_ADVICE_REMINDER_WINDOW' ) ) { $window = (int) CL_SMART_ADVICE_REMINDER_WINDOW; } if ( time() - $timestamp > $window ) { return true; } return false; } /** * Get email sending timestamp. * * @param string $advice_id Advice ID. * * @return false|int */ public function get_sending_timestamp( $advice_id ) { /** * All logs. * * @var LogModel[] $items */ $items = $this->option()->get( 'logs', true ); if ( is_array( $items ) ) { foreach ( $items as $item ) { if ( 'mailer' === $item->type && (string) $item->id === (string) $advice_id ) { return $item->created_at; } } } return false; } /** * Send email notifications. * * @param array $ids of advice. * * @return void */ public function email_new_advices( $ids ) { if ( ! is_array( $ids ) || empty( $ids ) ) { return; } foreach ( $ids as $key => $id ) { $advice = $this->get( $id, false ); unset( $ids[ $key ] ); if ( ! $this->is_email_notifications_enabled( $advice ) ) { continue; } // Only one letter. do_action( 'cl_smart_advice_email', 'advices', array( 'advice' => $advice ) ); $this->set_sending_timestamp( $advice, time() ); break; } if ( ! empty( $ids ) ) { wp_schedule_single_event( time() + DAY_IN_SECONDS, 'cl_smart_advice_email_new_advices', array( $ids ) ); } } /** * Notify if advice is in review status for more than 30 days * * @return void */ public function email_reminders() { $advices = $this->cache(); foreach ( $advices as $advice ) { $advice = $this->get( $advice->id, true ); if ( ! $this->is_email_notifications_enabled( $advice, true ) || ! $this->is_reminder_needed( $advice ) ) { continue; } // Only one letter. do_action( 'cl_smart_advice_email', 'reminders', array( 'advice' => $advice ) ); $this->set_sending_timestamp( $advice, time() ); break; } } /** * Is email notifications enabled, globally and with reminders. * * @param ?Model $advice model. * @param bool $with_reminders with reminders status. * * @return bool */ public function is_email_notifications_enabled( $advice = null, $with_reminders = false ) { if ( is_null( $advice ) ) { do_action( 'cl_smart_advice_set_error', E_WARNING, 'Email notifications are disabled: advice is empty', __FILE__, __LINE__, array( 'is_reminder' => $with_reminders, ) ); return false; } if ( ! $advice->is_email_available() ) { return false; } if ( empty( $advice->username ) ) { do_action( 'cl_smart_advice_set_error', E_WARNING, 'Email notifications are disabled: no username in advice', __FILE__, __LINE__, array( 'advice_data' => wp_json_encode( $advice->toArray() ), 'advice_id' => $advice->id, 'advice_type' => $advice->type, 'advice_status' => $advice->status, 'is_reminder' => $with_reminders, ) ); return false; } $emails_status = $this->notifications_email_status( $advice->username ); if ( null === $emails_status ) { do_action( 'cl_smart_advice_set_error', E_WARNING, 'Email notifications are disabled: no emails status in user options', __FILE__, __LINE__, array( 'advice_id' => $advice->id, 'advice_type' => $advice->type, 'advice_status' => $advice->status, 'is_reminder' => $with_reminders, ) ); } if ( self::NOTIFICATION_STATUS_ENABLED !== $emails_status ) { return false; } if ( true === $with_reminders ) { $reminders_status = $this->notifications_reminders_status( $advice->username ); if ( null === $reminders_status ) { do_action( 'cl_smart_advice_set_error', E_WARNING, 'Email notifications are disabled: no reminders status in user options', __FILE__, __LINE__, array( 'advice_id' => $advice->id, 'advice_type' => $advice->type, 'advice_status' => $advice->status, 'is_reminder' => $with_reminders, ) ); } if ( self::NOTIFICATION_STATUS_ENABLED !== $reminders_status ) { return false; } } return true; } /** * Has pending advices. * * @return bool */ public function hasPending() { $advices = $this->advices(); $pending = array_filter( $advices, function ( $advice ) { return 'pending' === $advice->statusUi(); } ); return count( $pending ) > 0; } /** * Need subscription. * * @param string $advice_id id. * * @return false|string */ public function subscriptionType( $advice_id ) { $advices = $this->advices(); if ( array_key_exists( $advice_id, $advices ) ) { return $advices[ $advice_id ]->subscriptionType(); } return false; } /** * Need agreement. * * @param string $advice_id id. * * @return false|string */ public function agreementType( $advice_id ) { $advices = $this->advices(); if ( array_key_exists( $advice_id, $advices ) ) { return $advices[ $advice_id ]->agreementType(); } return false; } /** * Plugin compatible. * * @return bool */ public function compatible() { return function_exists( 'stream_socket_client' ); } /** * Agreement text. * * @param string $type advice type string, e.g. 'cdn'. * * @return string */ public function agreementText( $type ) { $json = $this->api()->agreement( $type ); if ( ! empty( $json ) ) { $data = json_decode( $json, true ); if ( is_array( $data ) && array_key_exists( 'text', $data ) ) { return $data['text']; } } return ''; } /** * Save time to send an advice letter. * * @param Model $advice Model. * @param int $timestamp Timestamp. * * @return void */ public function set_sending_timestamp( $advice, $timestamp ) { $ids = array(); /** * All logs. * * @var LogModel[] $items */ $items = $this->option()->get( 'logs', true ); if ( ! $items ) { $items = array(); } else { foreach ( $items as $key => $item ) { $ids[ $item->id ] = $key; if ( 'mailer' !== $item->type ) { unset( $items[ $key ] ); } } } if ( array_key_exists( $advice->id, $ids ) ) { $items[ $ids[ $advice->id ] ]->created_at = $timestamp; } else { $items[] = ( new LogModel() )->fill( array( 'id' => $advice->id, 'type' => 'mailer', 'created_at' => $timestamp, ) ); } $this->option()->save( 'logs', $items ); } /** * Filter admin js data. * * @param array $data for js. * * @return array */ public function js_data( $data ) { $data['advices'] = array(); $advices = $this->cache(); foreach ( $advices as $advice ) { $data['advices'][ $advice->id ] = array( 'id' => $advice->id, 'type' => $advice->type, 'is_analytics_available' => $advice->is_analytics_available(), ); } return $data; } }