├── _meta ├── icons │ ├── 32x32.png │ ├── 64x64.png │ └── 128x128.png └── screenshots │ ├── 1.png │ ├── 2.png │ └── 3.png ├── htdocs ├── images │ ├── bad.png │ ├── bug.png │ ├── start.png │ ├── stop.png │ ├── disabled.png │ ├── enabled.png │ └── warning.png └── index.php ├── plib ├── scripts │ ├── virustotal-periodic-task.php │ ├── pre-uninstall.php │ └── post-install.php ├── views │ └── scripts │ │ └── index │ │ ├── settings.phtml │ │ ├── report.phtml │ │ └── about.phtml ├── hooks │ └── LongTasks.php ├── library │ ├── Task │ │ └── Scan.php │ ├── Promo │ │ └── Home.php │ ├── SettingsForm.php │ ├── PleskDomain.php │ └── Helper.php ├── resources │ └── locales │ │ ├── en-US.php │ │ └── ru-RU.php └── controllers │ └── IndexController.php ├── README.md ├── meta.xml ├── DESCRIPTION.md ├── CHANGES.md └── LICENSE /_meta/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/_meta/icons/32x32.png -------------------------------------------------------------------------------- /_meta/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/_meta/icons/64x64.png -------------------------------------------------------------------------------- /htdocs/images/bad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/htdocs/images/bad.png -------------------------------------------------------------------------------- /htdocs/images/bug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/htdocs/images/bug.png -------------------------------------------------------------------------------- /_meta/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/_meta/icons/128x128.png -------------------------------------------------------------------------------- /_meta/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/_meta/screenshots/1.png -------------------------------------------------------------------------------- /_meta/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/_meta/screenshots/2.png -------------------------------------------------------------------------------- /_meta/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/_meta/screenshots/3.png -------------------------------------------------------------------------------- /htdocs/images/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/htdocs/images/start.png -------------------------------------------------------------------------------- /htdocs/images/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/htdocs/images/stop.png -------------------------------------------------------------------------------- /htdocs/images/disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/htdocs/images/disabled.png -------------------------------------------------------------------------------- /htdocs/images/enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/htdocs/images/enabled.png -------------------------------------------------------------------------------- /htdocs/images/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plesk/ext-website-virus-check/HEAD/htdocs/images/warning.png -------------------------------------------------------------------------------- /htdocs/index.php: -------------------------------------------------------------------------------- 1 | run(); 8 | -------------------------------------------------------------------------------- /plib/scripts/virustotal-periodic-task.php: -------------------------------------------------------------------------------- 1 | removeAllTasks(); 6 | 7 | pm_Settings::clean(); -------------------------------------------------------------------------------- /plib/views/scripts/index/settings.phtml: -------------------------------------------------------------------------------- 1 | 4 | renderTabs($this->tabs); ?> 5 | help_tip; ?> 6 | form; ?> 7 | 8 | -------------------------------------------------------------------------------- /plib/hooks/LongTasks.php: -------------------------------------------------------------------------------- 1 | 4 | renderTabs($this->tabs); ?> 5 | 6 | RenderTools($this->scan); ?> 7 | 8 | summary; ?> 9 | 10 | renderList($this->list); ?> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plesk extension: VirusTotal Site Checker 2 | Check Plesk sites with VirusTotal API 3 | 4 | ![plesk-extensions-virustotal](https://raw.githubusercontent.com/oneumyvakin/plesk-extensions-virustotal/master/_meta/screenshots/1.png) 5 | 6 | 7 | ![plesk-extensions-virustotal](https://raw.githubusercontent.com/oneumyvakin/plesk-extensions-virustotal/master/_meta/screenshots/2.png) 8 | -------------------------------------------------------------------------------- /plib/views/scripts/index/about.phtml: -------------------------------------------------------------------------------- 1 | 4 | renderTabs($this->tabs); ?> 5 |

6 | about; ?> 7 |

8 |

9 | feedback; ?> 10 |

11 |

12 | faq; ?> 13 | question2; ?> 14 | question3; ?> 15 |

16 | -------------------------------------------------------------------------------- /meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | website-virus-check 4 | VirusTotal Website Check 5 | Check your websites for viruses automatically using multiple anti-virus engines. 6 | Автоматизируйте бесплатную проверку своих сайтов на вирусы. 7 | security 8 | 1.4.3 9 | 2 10 | Plesk 11 | https://github.com/plesk/ext-website-virus-check 12 | 12.5.30 13 | 14 | -------------------------------------------------------------------------------- /DESCRIPTION.md: -------------------------------------------------------------------------------- 1 | This extension scans all domains on your server for viruses, worms, trojans, and other malware. You can: 2 | 3 | - Scan all domains or select individual domains you want to scan. 4 | - View the scan report for each scanned domain. 5 | - Enable email notifications. 6 | - Add a widget with scan notifications to the Plesk Administrator home page. 7 | - Disable domain resolving during scanning. 8 | 9 | **Note:** To enable scanning, you need to 10 | 11 | 1. Register at [VirusTotal](https://virustotal.com/). 12 | 2. Get a free API key. 13 | 3. Type your free API key into the "VirusTotal Public API key" field. 14 | -------------------------------------------------------------------------------- /plib/scripts/post-install.php: -------------------------------------------------------------------------------- 1 | listTasks(); 8 | foreach ($tasks as $task) { 9 | if ('virustotal-periodic-task.php' == $task->getCmd()) { 10 | pm_Settings::set('virustotal_periodic_task_id', $task->getId()); 11 | return; 12 | } 13 | } 14 | $task = new pm_Scheduler_Task(); 15 | $task->setSchedule(pm_Scheduler::$EVERY_HOUR); 16 | $task->setCmd('virustotal-periodic-task.php'); 17 | pm_Scheduler::getInstance()->putTask($task); 18 | pm_Settings::set('virustotal_periodic_task_id', $task->getId()); 19 | 20 | -------------------------------------------------------------------------------- /plib/library/Task/Scan.php: -------------------------------------------------------------------------------- 1 | selectedDomains = $this->getParam('selectedDomains', []); 13 | Modules_WebsiteVirusCheck_Helper::check($this->selectedDomains); // scan_lock is acquired inside check() 14 | } 15 | 16 | public function statusMessage() 17 | { 18 | switch ($this->getStatus()) { 19 | case static::STATUS_RUNNING: 20 | return pm_Locale::lmsg('scanTaskRunning'); 21 | case static::STATUS_DONE: 22 | return pm_Locale::lmsg('scanTaskDone'); 23 | } 24 | return ''; 25 | } 26 | 27 | public function onDone() 28 | { 29 | pm_Settings::set('scan_lock', 0); // Just in case some troubles inside check() 30 | } 31 | } -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # 1.4.3 (10 Nov 2025) 2 | 3 | * [*] Updated the extension to be fully compatible with PHP 8.4. 4 | 5 | # 1.4.2 (6 May 2024) 6 | 7 | * [-] The "PHP Deprecated Construction: Creation of dynamic property Modules_WebsiteVirusCheck_PleskDomain" error no longer appears in /var/log/plesk/panel.log in Plesk for Linux and in %plesk_dir%\admin\logs\php_error.log in Plesk for Windows. (EXTPLESK-5512) 8 | 9 | # 1.4.1 (17 February 2023) 10 | 11 | * [*] Internal improvements. 12 | 13 | # 1.4 (29 May 2017) 14 | 15 | * [-] Fix issue [\#21](https://github.com/plesk/ext-website-virus-check/issues/21): Provide option to disable domain name resolving 16 | 17 | # 1.3 (3 May 2017) 18 | 19 | * [-] Fix issue [\#18](https://github.com/plesk/ext-website-virus-check/issues/18): Need to skip old VirusTotal detected urls and samples 20 | 21 | # 1.2 (3 April 2017) 22 | 23 | * [+] Selectively disabling scan for sites 24 | * [+] Show count for bad URLs and samples that communicate with this site 25 | * [-] Fix issue [\#4](https://github.com/plesk/ext-website-virus-check/issues/4): Need to gracefully handle HTTP time outs to virustotal.com 26 | 27 | # 1.1 (30 August 2016) 28 | 29 | * [+] E-mail notifications 30 | -------------------------------------------------------------------------------- /plib/library/Promo/Home.php: -------------------------------------------------------------------------------- 1 | lmsg('virustotalPromoTitle'); 10 | } 11 | public function getText() 12 | { 13 | pm_Context::init('website-virus-check'); 14 | 15 | $report = Modules_WebsiteVirusCheck_Helper::getDomainsReport(); 16 | 17 | $total_domains = $report['total']; 18 | $last_scan = pm_Settings::get('last_scan'); 19 | 20 | if ($last_scan) { 21 | $text = $this->lmsg('totalDomains') . $total_domains . ', ' . $this->lmsg('lastScan') . $last_scan; 22 | } else { 23 | $text = $this->lmsg('scanningWasNotPerformedYet'); 24 | } 25 | 26 | if (count($report['bad']) > 0) { 27 | $text = $this->lmsg('totalReports') . count($report['bad']) . $this->lmsg('ofTotalDomains') . $total_domains . ', ' . $this->lmsg('lastScan') . $last_scan; 28 | } 29 | 30 | return $text; 31 | } 32 | public function getButtonText() 33 | { 34 | pm_Context::init('website-virus-check'); 35 | return $this->lmsg('virustotalPromoButtonTitle'); 36 | } 37 | public function getButtonUrl() 38 | { 39 | pm_Context::init('website-virus-check'); 40 | return pm_Context::getBaseUrl(); 41 | } 42 | public function getIconUrl() 43 | { 44 | pm_Context::init('website-virus-check'); 45 | return pm_Context::getBaseUrl() . '/images/bug.png'; 46 | } 47 | } -------------------------------------------------------------------------------- /plib/library/SettingsForm.php: -------------------------------------------------------------------------------- 1 | getElement('virustotal_api_key')->getValue(); 17 | $virustotal_enabled = $this->getElement('virustotal_enabled')->getValue(); 18 | $promo_enabled = $this->getElement('_promo_admin_home')->getValue(); 19 | 20 | if ($virustotal_enabled) { 21 | if (!$baseValid) { 22 | return false; 23 | } 24 | 25 | $isKey = Modules_WebsiteVirusCheck_Helper::checkApiKey($virustotal_api_key); 26 | if ($isKey['valid']) { 27 | return true; 28 | } 29 | $msg = pm_Locale::lmsg( 30 | 'settingsFormApiCheckError', 31 | [ 32 | 'http_code' => (string)$isKey['http_code'], 33 | 'http_error' => (string)$isKey['http_error'], 34 | ] 35 | ); 36 | 37 | if ($isKey['http_code']) { 38 | $msg = pm_Locale::lmsg( 39 | 'settingsFormApiInvalid', 40 | [ 41 | 'http_code' => (string)$isKey['http_code'], 42 | 'http_error' => (string)$isKey['http_error'], 43 | ] 44 | ); 45 | } 46 | 47 | $this->getElement('virustotal_api_key')->addError($msg); 48 | $this->markAsError(); 49 | 50 | return false; 51 | } 52 | 53 | return true; 54 | } 55 | } -------------------------------------------------------------------------------- /plib/library/PleskDomain.php: -------------------------------------------------------------------------------- 1 | id = $id; 25 | $this->name = $name; 26 | $this->ascii_name = $ascii_name; 27 | $this->status = $status; 28 | $this->available = 'unknown'; 29 | $this->dns_ip_address = $dns_ip_address; 30 | $this->htype = $htype; 31 | $this->webspace_id = $webspace_id ? $webspace_id : $id; 32 | $this->enabled = true; 33 | $this->virustotal_positives = 0; 34 | $this->virustotal_bad_urls_and_samples = 0; 35 | } 36 | 37 | /** 38 | * @return bool 39 | */ 40 | private function isResolvingToPlesk() { 41 | /* 42 | array(5) { 43 | [0]=> 44 | array(5) { 45 | ["host"]=> string(9) "gmail.com" 46 | ["class"]=> string(2) "IN" 47 | ["ttl"]=> int(147) 48 | ["type"]=> string(1) "A" 49 | ["ip"]=> string(14) "173.194.222.17" 50 | } 51 | [4]=> 52 | array(5) { 53 | ["host"]=> string(9) "gmail.com" 54 | ["class"]=> string(2) "IN" 55 | ["ttl"]=> int(87) 56 | ["type"]=> string(4) "AAAA" 57 | ["ipv6"]=> string(22) "2a00:1450:4010:c07::11" 58 | } 59 | } 60 | */ 61 | if (!$this->ascii_name) { 62 | return false; 63 | } 64 | 65 | try { 66 | $records = @dns_get_record($this->ascii_name, DNS_A|DNS_AAAA); 67 | } catch (Exception $e) { 68 | pm_Log::debug(print_r($this, 1) . ' : ' . $e->getMessage()); 69 | return false; 70 | } 71 | pm_Log::debug('dns_get_record for ' . $this->ascii_name . ' : ' . print_r($records, 1)); 72 | 73 | if (!$records) { 74 | return false; 75 | } 76 | foreach ($records as $r) { 77 | $ip = ''; 78 | if (isset($r['ip'])) { 79 | $ip = $r['ip']; 80 | } elseif (isset($r['ipv6'])) { 81 | $ip = $r['ipv6']; 82 | } 83 | foreach ($this->dns_ip_address as $domain_ip) { 84 | if ($ip === $domain_ip) { 85 | return true; 86 | } 87 | } 88 | } 89 | return false; 90 | } 91 | 92 | /** 93 | * @return bool 94 | */ 95 | public function isAvailable() { 96 | $this->available = 'no'; 97 | if ($this->status > 0) { 98 | return false; 99 | } elseif (!(bool)pm_Settings::get('disableDomainResolving') && !$this->isResolvingToPlesk()) { 100 | return false; 101 | } 102 | 103 | $this->available = 'yes'; 104 | return true; 105 | } 106 | 107 | /** 108 | * @return string 109 | */ 110 | public function getAvailable() { 111 | return pm_Locale::lmsg($this->available); 112 | } 113 | } -------------------------------------------------------------------------------- /plib/resources/locales/en-US.php: -------------------------------------------------------------------------------- 1 | 'Reports', 5 | 'tabSettings' => 'Settings', 6 | 'tabAbout' => 'About', 7 | 'pageTitle' => 'VirusTotal Website Check', 8 | 'virustotalEnabled' => 'Enable scanning', 9 | 'virustotalPublicApiKey' => 'VirusTotal Public API key', 10 | 'adminHomeWidgetEnabled' => 'Add a widget with scan notifications to Administrator\'s home page', 11 | 'settingsWasSuccessfullySaved' => 'Settings successfully saved.', 12 | 'settingsFormApiCheckError' => 'Failed check API key. HTTP response: %%http_code%% %%http_error%%', 13 | 'settingsFormApiInvalid' => 'API key is invalid. HTTP response: %%http_code%% %%http_error%%', 14 | 'apiKeyBecameInvalid' => 'Last API request has finished with HTTP error 403', 15 | 'buttonStartScan' => 'Start', 16 | 'buttonStopScan' => 'Stop', 17 | 'buttonStartDesc' => 'Start Scanning for all domains', 18 | 'buttonStartSelectedDesc' => 'Start Scanning for selected domains', 19 | 'buttonStopDesc' => 'Stop Scanning', 20 | 'buttonDisable' => 'Disable', 21 | 'buttonDisableDesc' => 'Disable scanning for selected domains.', 22 | 'buttonDisableSuccess' => 'Scanning for domains was successfully disabled.', 23 | 'buttonEnable' => 'Enable', 24 | 'buttonEnableDesc' => 'Enable scanning for selected domains.', 25 | 'buttonEnableSuccess' => 'Scanning for domains was successfully enabled.', 26 | 'infoStartSuccess' => 'Scanning started', 27 | 'infoStopSuccess' => 'Scanning stopped', 28 | 'scanTaskRunning' => 'Scanning sites for viruses:', 29 | 'scanTaskDone' => 'Scanning of sites finished. Refresh page', 30 | 'errorScanAlreadyRunning' => 'Scanning is already running.', 31 | 'scanningState' => 'State', 32 | 'scanningEnabled' => 'Scanning Enabled', 33 | 'scanningDisabled' => 'Scanning Disabled', 34 | 'badReport' => 'Bad report', 35 | 'domain' => 'Domain', 36 | 'yes' => 'Yes', 37 | 'no' => 'No', 38 | 'unknown' => 'Unknown', 39 | 'domainInactiveOrCantbeResolvedInHostingIp' => 'Domain is "Suspended", "Disabled" or can\'t be resolved in hosting IP address', 40 | 'scanDate' => 'Last scan Date', 41 | 'checkResult' => 'Home page scan result (Detection ratio)', 42 | 'badUrlsAndSamples' => 'Bad URLs and samples', 43 | 'reportLink' => 'Link to scan report', 44 | 'virustotalReport' => 'Open', 45 | 'apikey_help' => 'You can get a free API key after you register at VirusTotal', 46 | 'virustotalPromoTitle' => 'VirusTotal Reports', 47 | 'virustotalPromoButtonTitle' => 'More info', 48 | 'scanningWasNotPerformedYet' => 'Scanning was not performed yet.', 49 | 'youCanStartTaskAt' => 'You can start scheduled task for scanning now at Scheduled Tasks', 50 | 'scanningWasNotPerformedYetForList' => 'Scanning was not performed yet', 51 | 'scanningRequestIsSent' => 'Scanning request is sent', 52 | 'virustotalCantScanDomain' => 'VirusTotal can\'t scan this domain', 53 | 'virustotalDomainIsNotScannedYet' => 'Domain is not scanned yet', 54 | 'httpError' => 'HTTP Error: %%message%%', 55 | 'httpErrorFailedToConnectVirusTotalUnknownError' => 'Failed to connect VirusTotal API server with Unknown error', 56 | 'totalDomains' => 'Domains scanned: ', 57 | 'ofTotalDomains' => ' of all domains selected for scanning ', 58 | 'totalReports' => 'Total "bad" domains: ', 59 | 'lastScan' => 'last scanning performed on ', 60 | 'about' => 'This extension uses the public API of VirusTotal to detect malicious scripts on your websites. API requests are executed using daily scheduled tasks at Scheduled Tasks', 61 | 'feedback' => 'If you have any questions or concerns about this extension, please feel free to submit issue in extension repository on GitHub', 62 | 'faq' => 'FAQ', 63 | 'question2' => '

Q: Why daily scheduled tasks take so long to execute?
A: Because of the limitations of the public API the extension sends the API requests at the speed of 3 domains per minute.

', 64 | 'question3' => '

Q: Can I execute daily scheduled task several times in a one day?
A: Yes, you can.

', 65 | 'disableDomainResolving' => 'Disable domain resolving', 66 | 'emailNotificationEnabled' => 'Enable email notifications', 67 | 'emailNotificationSubjectBadDomain' => 'VirusTotal.com reports "bad" domain %%domain%%', 68 | 'emailNotificationBodyBadDomain' => 'VirusTotal.com reports domain %%domain%% as "bad" %%url%%', 69 | ); -------------------------------------------------------------------------------- /plib/resources/locales/ru-RU.php: -------------------------------------------------------------------------------- 1 | 'Отчеты', 5 | 'tabSettings' => 'Настройки', 6 | 'tabAbout' => 'О программе', 7 | 'pageTitle' => 'VirusTotal Website Check', 8 | 'virustotalEnabled' => 'Проверка включена', 9 | 'virustotalPublicApiKey' => 'Публичный API ключ VirusTotal', 10 | 'adminHomeWidgetEnabled' => 'Виджет на домашней странице администратора включен', 11 | 'settingsWasSuccessfullySaved' => 'Настройки были сохранены.', 12 | 'settingsFormApiCheckError' => 'Ошибка проверки API ключа. Результат HTTP запроса: %%http_code%% %%http_error%%', 13 | 'settingsFormApiInvalid' => 'Неправильный API ключ. Результат HTTP запроса: %%http_code%% %%http_error%%', 14 | 'apiKeyBecameInvalid' => 'Последний запрос к API завершился с HTTP ошибкой 403', 15 | 'buttonStartScan' => 'Старт', 16 | 'buttonStopScan' => 'Стоп', 17 | 'buttonStartDesc' => 'Начать сканирование всех доменов', 18 | 'buttonStartSelectedDesc' => 'Начать сканирование выбранных доменов', 19 | 'buttonStopDesc' => 'Остановить сканирование', 20 | 'buttonDisable' => 'Отключить', 21 | 'buttonDisableDesc' => 'Отключить сканирование для выбранных доменов.', 22 | 'buttonDisableSuccess' => 'Сканирование доменов отключено.', 23 | 'buttonEnable' => 'Включить', 24 | 'buttonEnableDesc' => 'Включить сканирование для выбранных доменов.', 25 | 'buttonEnableSuccess' => 'Сканирование доменов было включено.', 26 | 'infoStartSuccess' => 'Сканирование началось', 27 | 'infoStopSuccess' => 'Сканирование остановлено', 28 | 'scanTaskRunning' => 'Выполняется сканирование сайтов на вирусы:', 29 | 'scanTaskDone' => 'Сканирование сайтов на вирусы завершено. Обновить страницу', 30 | 'errorScanAlreadyRunning' => 'Сканирование уже выполняется.', 31 | 'scanningState' => 'Состояние', 32 | 'scanningEnabled' => 'Сканирование включено', 33 | 'scanningDisabled' => 'Сканирование выключено', 34 | 'badReport' => 'Плохой отчет', 35 | 'domain' => 'Домен', 36 | 'yes' => 'Да', 37 | 'no' => 'Нет', 38 | 'unknown' => 'Неизвестно', 39 | 'domainInactiveOrCantbeResolvedInHostingIp' => 'Домен "Приостановлен", "Отключен" или не резолвится в присвоенный IP адрес', 40 | 'scanDate' => 'Дата сканирования', 41 | 'checkResult' => 'Результат сканирования домашней страницы (Срабатывания / Всего)', 42 | 'badUrlsAndSamples' => '"Плохие" URL и сэмплы', 43 | 'reportLink' => 'Сслыка на отчет', 44 | 'virustotalReport' => 'Отчет', 45 | 'apikey_help' => 'Вы можете бесплатно получить API-ключ после регистрации на сайте VirusTotal', 46 | 'virustotalPromoTitle' => 'Отчеты VirusTotal', 47 | 'virustotalPromoButtonTitle' => 'Подробнее', 48 | 'scanningWasNotPerformedYet' => 'Сканирование еще не выполнялось.', 49 | 'scanningWasNotPerformedYetForList' => 'Сканирование еще не выполнялось', 50 | 'youCanStartTaskAt' => 'Чтобы выполнить сканирование сейчас, Вы можете запустить запланированную задачу в Планировщике задач', 51 | 'scanningRequestIsSent' => 'Запрос на сканирование отправлен', 52 | 'virustotalCantScanDomain' => 'VirusTotal не может просканировать этот домен', 53 | 'virustotalDomainIsNotScannedYet' => 'Домен еще не отсканирован', 54 | 'httpError' => 'HTTP Error: %%message%%', 55 | 'httpErrorFailedToConnectVirusTotalUnknownError' => 'Неизветсная ошибка соединения с сервером VirusTotal API', 56 | 'totalDomains' => 'Всего доменов проверено: ', 57 | 'ofTotalDomains' => ' из всего проверенных доменов ', 58 | 'totalReports' => 'Всего "плохих" отчетов: ', 59 | 'lastScan' => 'последнее сканирование выполнено в ', 60 | 'about' => 'Это расширение использует публичное API VirusTotal, чтобы проверить Ваши сайты на вредоносные скрипты. Запросы выполняются через ежедневную задачу, которую Вы можете найти в Scheduled Tasks', 61 | 'feedback' => 'В случае каких-либо проблем с расширением Вы можете создать issue в репозитории на GitHub', 62 | 'faq' => 'FAQ', 63 | 'question2' => '

Q: Почему ежедневная задача выполняется так долго?
A: Из-за ограничений публичного API расширение отправляет API запросы со скоростью 3 домена в минуту.

', 64 | 'question3' => '

Q: Можно ли выполнять ежедневную задачу несколько раз в один день?
A: Да, можно.

', 65 | 'disableDomainResolving' => 'Не выполнять DNS-резолвинг доменов', 66 | 'emailNotificationEnabled' => 'Включить почтовые оповещения', 67 | 'emailNotificationSubjectBadDomain' => 'От VirusTotal.com получен "плохой" отчет для домена %%domain%%', 68 | 'emailNotificationBodyBadDomain' => '"Плохой" отчет VirusTotal.com для домена %%domain%% %%url%%', 69 | ); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Plesk International GmbH 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /plib/controllers/IndexController.php: -------------------------------------------------------------------------------- 1 | _accessLevel = 'admin'; 9 | 10 | parent::init(); 11 | 12 | 13 | $this->view->pageTitle = $this->lmsg('pageTitle'); 14 | 15 | $this->view->tabs = [ 16 | [ 17 | 'title' => $this->lmsg('tabReports'), 18 | 'action' => 'report', 19 | ], 20 | [ 21 | 'title' => $this->lmsg('tabSettings'), 22 | 'action' => 'settings', 23 | ], 24 | [ 25 | 'title' => $this->lmsg('tabAbout'), 26 | 'action' => 'about', 27 | ], 28 | ]; 29 | } 30 | 31 | public function indexAction() 32 | { 33 | if (!pm_Settings::get('virustotal_enabled') || pm_Settings::get('apiKeyBecameInvalid')) { 34 | $this->_forward('settings'); 35 | return; 36 | } 37 | 38 | $this->_forward('report'); 39 | } 40 | 41 | public function reportAction() 42 | { 43 | if (!pm_Settings::get('virustotal_enabled')) { 44 | $this->_forward('settings'); 45 | return; 46 | } 47 | 48 | if (pm_Settings::get('apiKeyBecameInvalid') && !$this->_status->hasMessage($this->lmsg('apiKeyBecameInvalid'))) { 49 | $this->_status->addError($this->lmsg('apiKeyBecameInvalid')); 50 | } 51 | 52 | $this->view->list = $this->_getDomainsReportList(); 53 | $this->view->scan = []; 54 | 55 | if (class_exists('pm_LongTask_Manager')) { // Since Plesk 17.0 56 | $isRunning = pm_Settings::get('scan_lock'); 57 | $action = $isRunning ? 'stop' : 'start'; 58 | 59 | $this->view->scan[] = [ 60 | 'title' => $isRunning ? $this->lmsg('buttonStopScan') : $this->lmsg('buttonStartScan'), 61 | 'description' => $isRunning ? $this->lmsg('buttonStopDesc') : $this->lmsg('buttonStartDesc'), 62 | 'icon' => pm_Context::getBaseUrl() . "/images/{$action}.png", 63 | 'link' => $this->view->getHelper('baseUrl')->moduleUrl(['action' => $action]), 64 | ]; 65 | 66 | } else { 67 | $this->view->summary = $this->_getReportSummary(); 68 | } 69 | } 70 | 71 | public function startAction() 72 | { 73 | $allDomains = Modules_WebsiteVirusCheck_Helper::getDomains(); 74 | $selectedDomainIds = (array)$this->_getParam('ids'); 75 | $selectedDomains = []; 76 | foreach ($allDomains as $domain) { 77 | if (in_array($domain->id, $selectedDomainIds)) { 78 | $selectedDomains[$domain->id] = $domain; 79 | } 80 | } 81 | 82 | $taskManager = new pm_LongTask_Manager(); 83 | $scanTask = new Modules_WebsiteVirusCheck_Task_Scan(); 84 | $scanTask->setParams(['selectedDomains' => $selectedDomains]); 85 | $taskManager->start($scanTask); 86 | 87 | for ($i = 1; $i < 5; $i++) { // wait for acquiring lock to keep UI consistent 88 | if (pm_Settings::get('scan_lock')) { 89 | break; 90 | } 91 | sleep(1); 92 | } 93 | 94 | $this->view->status->addInfo($this->lmsg('infoStartSuccess')); 95 | $this->_redirect(pm_Context::getBaseUrl()); 96 | } 97 | 98 | public function stopAction() 99 | { 100 | $taskManager = new pm_LongTask_Manager(); 101 | $taskManager->cancelAllTasks(); 102 | 103 | pm_Settings::set('scan_lock', 0); 104 | 105 | $this->view->status->addInfo($this->lmsg('infoStopSuccess')); 106 | $this->_redirect(pm_Context::getBaseUrl()); 107 | } 108 | 109 | public function reportDataAction() 110 | { 111 | $list = $this->_getDomainsReportList(); 112 | // Json data from pm_View_List_Simple 113 | $this->_helper->json($list->fetchData()); 114 | } 115 | 116 | public function settingsAction() 117 | { 118 | if (pm_Settings::get('apiKeyBecameInvalid') && !$this->_status->hasMessage($this->lmsg('apiKeyBecameInvalid'))) { 119 | $this->_status->addError($this->lmsg('apiKeyBecameInvalid')); 120 | } 121 | 122 | $this->view->help_tip = $this->lmsg('apikey_help'); 123 | 124 | $form = new Modules_WebsiteVirusCheck_SettingsForm(); 125 | 126 | $form->addElement('checkbox', 'virustotal_enabled', [ 127 | 'label' => $this->lmsg('virustotalEnabled'), 128 | 'value' => pm_Settings::get('virustotal_enabled'), 129 | ]); 130 | 131 | $form->addElement('text', 'virustotal_api_key', [ 132 | 'label' => $this->lmsg('virustotalPublicApiKey'), 133 | 'value' => pm_Settings::get('virustotal_api_key'), 134 | 'required' => true, 135 | 'validators' => [ 136 | ['NotEmpty', true], 137 | ], 138 | ]); 139 | 140 | $form->addElement('checkbox', 'disableDomainResolving', [ 141 | 'label' => $this->lmsg('disableDomainResolving'), 142 | 'value' => pm_Settings::get('disableDomainResolving'), 143 | ]); 144 | 145 | $form->addElement('checkbox', 'emailNotificationEnabled', [ 146 | 'label' => $this->lmsg('emailNotificationEnabled'), 147 | 'value' => pm_Settings::get('emailNotificationEnabled'), 148 | ]); 149 | 150 | $form->addElement('checkbox', '_promo_admin_home', [ 151 | 'label' => $this->lmsg('adminHomeWidgetEnabled'), 152 | 'value' => pm_Settings::get('_promo_admin_home'), 153 | ]); 154 | 155 | $form->addControlButtons([ 156 | 'cancelLink' => pm_Context::getModulesListUrl(), 157 | ]); 158 | 159 | if ($this->getRequest()->isPost() && $form->isValid($this->getRequest()->getPost())) { 160 | 161 | pm_Settings::set('apiKeyBecameInvalid', ''); 162 | pm_Settings::set('virustotal_enabled', $form->getValue('virustotal_enabled')); 163 | pm_Settings::set('virustotal_api_key', $form->getValue('virustotal_api_key')); 164 | pm_Settings::set('disableDomainResolving', $form->getValue('disableDomainResolving')); 165 | pm_Settings::set('emailNotificationEnabled', $form->getValue('emailNotificationEnabled')); 166 | pm_Settings::set('_promo_admin_home', $form->getValue('_promo_admin_home')); 167 | 168 | $this->_status->addMessage('info', $this->lmsg('settingsWasSuccessfullySaved')); 169 | $this->_helper->json(['redirect' => pm_Context::getBaseUrl()]); 170 | } 171 | 172 | $this->view->form = $form; 173 | } 174 | 175 | public function aboutAction() 176 | { 177 | if (pm_Settings::get('apiKeyBecameInvalid') && !$this->_status->hasMessage($this->lmsg('apiKeyBecameInvalid'))) { 178 | $this->_status->addError($this->lmsg('apiKeyBecameInvalid')); 179 | } 180 | 181 | $this->view->about = $this->lmsg('about'); 182 | $this->view->feedback = $this->lmsg('feedback'); 183 | $this->view->faq = $this->lmsg('faq'); 184 | $this->view->question1 = $this->lmsg('question1'); 185 | $this->view->question2 = $this->lmsg('question2'); 186 | $this->view->question3 = $this->lmsg('question3'); 187 | } 188 | 189 | private function _getReportSummary() 190 | { 191 | $report = Modules_WebsiteVirusCheck_Helper::getDomainsReport(); 192 | 193 | $total_domains = $report['total']; 194 | $last_scan = pm_Settings::get('last_scan'); 195 | 196 | if ($last_scan) { 197 | $text = $this->lmsg('totalDomains') . $total_domains . ', ' . $this->lmsg('lastScan') . $last_scan; 198 | } else { 199 | $text = $this->lmsg('scanningWasNotPerformedYet') . ' ' . $this->lmsg('youCanStartTaskAt'); 200 | } 201 | 202 | if (count($report['bad']) > 0) { 203 | $text = $this->lmsg('totalReports') . count($report['bad']) . $this->lmsg('ofTotalDomains') . $total_domains . ', ' . $this->lmsg('lastScan') . $last_scan; 204 | } 205 | 206 | return $text; 207 | } 208 | 209 | private function _getDomainsReportList() 210 | { 211 | $data = []; 212 | $report = Modules_WebsiteVirusCheck_Helper::getDomainsReport(); 213 | foreach ($report['all'] as $domain) { 214 | $colScanDate = isset($domain->virustotal_scan_date) ? $domain->virustotal_scan_date : ''; 215 | $colScanResult = pm_Locale::lmsg('domainInactiveOrCantbeResolvedInHostingIp'); 216 | $colBadUrlsAndSamples = $domain->virustotal_bad_urls_and_samples; 217 | $colReportLink = ''; 218 | $isDomainAvailable = $domain->isAvailable(); 219 | if ($isDomainAvailable) { 220 | if (isset($domain->no_scanning_results)) { 221 | $colScanResult = $domain->no_scanning_results; 222 | } else { 223 | $colScanResult = $domain->virustotal_positives . ' / ' . $domain->virustotal_total; 224 | $colReportLink = '' . $this->lmsg('virustotalReport') . ''; 225 | } 226 | } 227 | 228 | if (!$isDomainAvailable) { 229 | $stateImgSrc = pm_Context::getBaseUrl() . '/images/warning.png'; 230 | $stateImgAlt = $this->lmsg('domainInactiveOrCantbeResolvedInHostingIp'); 231 | } else if ($domain->enabled) { 232 | $stateImgSrc = pm_Context::getBaseUrl() . '/images/enabled.png'; 233 | $stateImgAlt = $this->lmsg('scanningEnabled'); 234 | if ((int)$domain->virustotal_positives > 0 || $domain->virustotal_bad_urls_and_samples > 0) { 235 | $stateImgSrc = pm_Context::getBaseUrl() . '/images/bad.png'; 236 | $stateImgAlt = $this->lmsg('badReport'); 237 | } 238 | } else { 239 | $stateImgSrc = pm_Context::getBaseUrl() . '/images/disabled.png'; 240 | $stateImgAlt = $this->lmsg('scanningDisabled'); 241 | } 242 | 243 | $colScanningState = ''; 244 | $colDomain = '' . $domain->name . ''; 245 | $data[$domain->id] = [ 246 | 'column-1' => $colScanningState, 247 | 'column-2' => $colDomain, 248 | 'column-3' => $colScanDate, 249 | 'column-4' => $colScanResult, 250 | 'column-5' => $colBadUrlsAndSamples, 251 | 'column-6' => $colReportLink, 252 | ]; 253 | } 254 | 255 | if (!count($data) > 0) { 256 | return new pm_View_List_Simple($this->view, $this->_request); 257 | } 258 | 259 | $options = [ 260 | 'defaultSortField' => 'column-2', 261 | 'defaultSortDirection' => pm_View_List_Simple::SORT_DIR_DOWN, 262 | ]; 263 | $list = new pm_View_List_Simple($this->view, $this->_request, $options); 264 | $list->setData($data); 265 | $list->setColumns([ 266 | pm_View_List_Simple::COLUMN_SELECTION, 267 | 'column-1' => [ 268 | 'title' => $this->lmsg('scanningState'), 269 | 'noEscape' => true, 270 | 'searchable' => false, 271 | 'sortable' => true, 272 | ], 273 | 'column-2' => [ 274 | 'title' => $this->lmsg('domain'), 275 | 'noEscape' => true, 276 | 'searchable' => true, 277 | 'sortable' => true, 278 | ], 279 | 'column-3' => [ 280 | 'title' => $this->lmsg('scanDate'), 281 | 'sortable' => true, 282 | ], 283 | 'column-4' => [ 284 | 'title' => $this->lmsg('checkResult'), 285 | 'sortable' => true, 286 | ], 287 | 'column-5' => [ 288 | 'title' => $this->lmsg('badUrlsAndSamples'), 289 | 'sortable' => true, 290 | ], 291 | 'column-6' => [ 292 | 'title' => $this->lmsg('reportLink'), 293 | 'noEscape' => true, 294 | 'searchable' => false, 295 | 'sortable' => false, 296 | 297 | ], 298 | ]); 299 | 300 | $listTools = []; 301 | if (class_exists('pm_LongTask_Manager')) { // Since Plesk 17.0 302 | $isRunning = pm_Settings::get('scan_lock'); 303 | $action = $isRunning ? 'stop' : 'start'; 304 | if ($action == 'start') { 305 | $listTools[] = [ 306 | 'title' => $isRunning ? $this->lmsg('buttonStopScan') : $this->lmsg('buttonStartScan'), 307 | 'description' => $isRunning ? $this->lmsg('buttonStopDesc') : $this->lmsg('buttonStartSelectedDesc'), 308 | 'class' => "sb-{$action}", 309 | 'execGroupOperation' => $this->_helper->url($action), 310 | ]; 311 | } else { 312 | $listTools[] = [ 313 | 'title' => $isRunning ? $this->lmsg('buttonStopScan') : $this->lmsg('buttonStartScan'), 314 | 'description' => $isRunning ? $this->lmsg('buttonStopDesc') : $this->lmsg('buttonStartSelectedDesc'), 315 | 'class' => "sb-{$action}", 316 | 'link' => $this->view->getHelper('baseUrl')->moduleUrl(['action' => $action]), 317 | ]; 318 | } 319 | } 320 | $listTools[] = [ 321 | 'title' => $this->lmsg('buttonEnable'), 322 | 'description' => $this->lmsg('buttonEnableDesc'), 323 | 'class' => 'sb-make-visible', 324 | 'execGroupOperation' => $this->_helper->url('enable'), 325 | ]; 326 | $listTools[] = [ 327 | 'title' => $this->lmsg('buttonDisable'), 328 | 'description' => $this->lmsg('buttonDisableDesc'), 329 | 'class' => 'sb-make-invisible', 330 | 'execGroupOperation' => $this->_helper->url('disable'), 331 | ]; 332 | 333 | $list->setTools($listTools); 334 | 335 | $list->setDataUrl(['action' => 'report-data']); 336 | return $list; 337 | } 338 | 339 | public function enableAction() 340 | { 341 | foreach ((array)$this->_getParam('ids') as $domainId) { 342 | $report = json_decode(pm_Settings::get('domain_id_' . $domainId), true); 343 | if ($report) { 344 | $report['domain']['enabled'] = true; 345 | pm_Settings::set('domain_id_' . $domainId, json_encode($report)); 346 | } 347 | } 348 | $messages[] = ['status' => 'info', 'content' => $this->lmsg('buttonEnableSuccess')]; 349 | $this->_helper->json(['status' => 'success', 'statusMessages' => $messages]); 350 | } 351 | 352 | public function disableAction() 353 | { 354 | foreach ((array)$this->_getParam('ids') as $domainId) { 355 | $report = json_decode(pm_Settings::get('domain_id_' . $domainId), true); 356 | if ($report) { 357 | $report['domain']['enabled'] = false; 358 | pm_Settings::set('domain_id_' . $domainId, json_encode($report)); 359 | } 360 | } 361 | $messages[] = ['status' => 'info', 'content' => $this->lmsg('buttonDisableSuccess')]; 362 | $this->_helper->json(['status' => 'success', 'statusMessages' => $messages]); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /plib/library/Helper.php: -------------------------------------------------------------------------------- 1 | diff($last_scan); 33 | if ($interval->h < 1) { 34 | pm_Log::debug(pm_Locale::lmsg('errorScanAlreadyRunning')); 35 | return; 36 | } 37 | } else { 38 | pm_Settings::set('scan_lock', 1); // Also set to 0 after check self::is_enough() 39 | } 40 | 41 | pm_Settings::set('last_scan', date('d/M/Y G:i')); // Has dependency with scan_lock 42 | 43 | self::report($domains); 44 | $i = 1; 45 | if (count($domains) == 0) { 46 | $domains = self::getDomains(); 47 | } 48 | foreach ($domains as $domain) { 49 | $i++; 50 | 51 | if (!self::is_last_domain('check', $domain)) { 52 | continue; 53 | } 54 | 55 | $report = self::getDomainReport($domain->id); 56 | if ($report 57 | && isset($report['virustotal_request_done']) 58 | && !$report['virustotal_request_done']) { 59 | continue; 60 | } 61 | if ($report && isset($report['domain']['enabled']) && !$report['domain']['enabled']) { 62 | continue; 63 | } 64 | if (!$report) { 65 | $report = []; 66 | } 67 | 68 | self::set_progress($i, count($domains) + 1, 2, 2); 69 | 70 | if (!$domain->isAvailable()) { 71 | $report['domain'] = $domain; 72 | self::setDomainReport($domain->id, $report); 73 | continue; 74 | } 75 | 76 | if (self::is_enough() || !pm_Settings::get('scan_lock')) { 77 | pm_Settings::set('scan_lock', 0); 78 | exit(0); 79 | } 80 | $report['domain'] = $domain; 81 | $report['virustotal_request_done'] = false; 82 | 83 | $request = self::virustotal_scan_url_request($domain->ascii_name); 84 | if (isset($request['http_error'])) { 85 | $report['http_error'] = $request['http_error']; 86 | } else { 87 | $report['virustotal_request'] = array( 88 | 'response_code' => isset($request['response_code']) ? $request['response_code'] : 0, 89 | 'scan_date' => isset($request['scan_date']) ? $request['scan_date'] : '', 90 | ); 91 | } 92 | 93 | self::setDomainReport($domain->id, $report); 94 | } 95 | 96 | pm_Settings::set('scan_lock', 0); 97 | 98 | self::cleanup_last_domains(); 99 | self::cleanup_deleted_domains(); 100 | } 101 | 102 | 103 | 104 | /** 105 | * VirusTotal API has restriction in 4 req/min, for safety we have limit to 3 req/min (180 req/hour, 4320 req/day) 106 | * 107 | * @return bool 108 | */ 109 | public static function is_enough() 110 | { 111 | static $counter = 0; 112 | if ($counter >= self::virustotal_api_hour_limit) { 113 | return true; 114 | } 115 | $counter++; 116 | return false; 117 | } 118 | 119 | /** 120 | * Update progress for long task 121 | * @param $current int 122 | * @param $total int 123 | * @param $phase int 124 | * @param $phases int 125 | * @return int 126 | */ 127 | public static function set_progress($current, $total, $phase, $phases) 128 | { 129 | $current_amplification = ($current * ($phase / $phases)) + ($total - ( $total * ($phases - $phase))); 130 | $total_amplification = $total * ($phases * ($phase / $phases) ); 131 | 132 | $progress = ($current_amplification / $total_amplification) * 100; 133 | 134 | pm_Settings::set('scan_progress', $progress); 135 | 136 | if (class_exists('pm_LongTask_Manager')) { // Since Plesk 17.0 137 | $taskManager = new pm_LongTask_Manager(); 138 | 139 | $tasks = $taskManager->getTasks(['task_scan']); 140 | foreach ($tasks as $task) { 141 | $task->updateProgress($progress); 142 | } 143 | } 144 | 145 | return $progress; 146 | } 147 | 148 | /** 149 | * @param $operation string 150 | * @param $domain Modules_WebsiteVirusCheck_PleskDomain 151 | * @return bool 152 | */ 153 | public static function is_last_domain($operation, $domain) 154 | { 155 | $last = json_decode(pm_Settings::get('last_domain_' . $operation), true); 156 | if (!$last) { 157 | pm_Settings::set('last_domain_' . $operation, json_encode($domain)); 158 | return true; 159 | } 160 | 161 | if ($domain->id < $last['id']) { 162 | return false; 163 | } 164 | 165 | pm_Settings::set('last_domain_' . $operation, json_encode($domain)); 166 | return true; 167 | } 168 | 169 | /** 170 | * @param $domains Modules_WebsiteVirusCheck_PleskDomain[] 171 | * @return void 172 | */ 173 | public static function report($domains = []) 174 | { 175 | $i = 1; 176 | if (count($domains) == 0) { 177 | $domains = self::getDomains(); 178 | } 179 | foreach ($domains as $domain) { 180 | $i++; 181 | 182 | if (!self::is_last_domain('report', $domain)) { 183 | continue; 184 | } 185 | $request = self::getDomainReport($domain->id); 186 | if (!$request) { 187 | continue; 188 | } 189 | 190 | if (isset($request['domain']['enabled']) && !$request['domain']['enabled']) { 191 | continue; 192 | } 193 | 194 | self::set_progress($i, count($domains) + 1, 1, 2); 195 | 196 | if (self::is_enough() || !pm_Settings::get('scan_lock')) { 197 | pm_Settings::set('scan_lock', 0); 198 | exit(0); 199 | } 200 | $report = self::virustotal_scan_url_report($domain->ascii_name); 201 | pm_Log::debug(print_r($report, 1)); 202 | 203 | $reportDomain = self::virustotal_scan_domain_report($domain->ascii_name); 204 | $report['detected_urls'] = self::filterVirusTotalReportDomainOldItems($reportDomain)['detected_urls']; 205 | $report['detected_communicating_samples'] = self::filterVirusTotalReportDomainOldItems($reportDomain)['detected_communicating_samples']; 206 | $report['detected_referrer_samples'] = self::filterVirusTotalReportDomainOldItems($reportDomain)['detected_referrer_samples']; 207 | 208 | self::report_domain($domain, $report); 209 | } 210 | } 211 | 212 | /** 213 | * @param $reportDomain array 214 | * @return array 215 | */ 216 | private static function filterVirusTotalReportDomainOldItems($reportDomain) 217 | { 218 | $filtered = [ 219 | 'detected_urls' => 0, 220 | 'detected_communicating_samples' => 0, 221 | 'detected_referrer_samples' => 0, 222 | ]; 223 | 224 | $now = new DateTime(); 225 | 226 | if (isset($reportDomain['detected_urls'])) { 227 | foreach ($reportDomain['detected_urls'] as $item) { 228 | if (!isset($item['scan_date'])) { 229 | continue; 230 | } 231 | 232 | $scanDate = DateTime::createFromFormat('Y-m-d G:i:s', $item['scan_date']); // "2013-04-07 07:18:09" 233 | if ($scanDate === false) { 234 | continue; 235 | } 236 | 237 | $interval = $now->diff($scanDate); 238 | if ((int)$interval->format('%a') < 7) { 239 | pm_Log::debug("Item detected days ago $interval->d:\n" . print_r($item, 1)); 240 | $filtered['detected_urls']++; 241 | } 242 | } 243 | } 244 | 245 | if (isset($reportDomain['detected_communicating_samples'])) { 246 | foreach ($reportDomain['detected_communicating_samples'] as $item) { 247 | if (!isset($item['date'])) { 248 | continue; 249 | } 250 | 251 | $scanDate = DateTime::createFromFormat('Y-m-d G:i:s', $item['date']); // "2013-04-07 07:18:09" 252 | if ($scanDate === false) { 253 | continue; 254 | } 255 | 256 | $interval = $now->diff($scanDate); 257 | if ((int)$interval->format('%a') < 7) { 258 | pm_Log::debug("Item detected days ago $interval->d:\n" . print_r($item, 1)); 259 | $filtered['detected_communicating_samples']++; 260 | } 261 | } 262 | } 263 | 264 | $filtered['detected_referrer_samples'] = isset($reportDomain['detected_referrer_samples']) ? count($reportDomain['detected_referrer_samples']) : 0; 265 | 266 | return $filtered; 267 | } 268 | 269 | public static function cleanup_last_domains() 270 | { 271 | $ops = ['report', 'check']; 272 | foreach ($ops as $operation) { 273 | pm_Settings::set('last_domain_' . $operation, false); 274 | } 275 | } 276 | 277 | public static function cleanup_deleted_domains() 278 | { 279 | pm_Bootstrap::init(); 280 | $module_id = pm_Bootstrap::getDbAdapter()->fetchOne("select module_id from ModuleSettings where name ='virustotal_enabled'"); 281 | if (!$module_id) { 282 | return; 283 | } 284 | $reports = pm_Bootstrap::getDbAdapter()->fetchAssoc("select name, value from ModuleSettings where module_id = " . $module_id . " and name like 'domain_id_%'"); 285 | //pm_Log::debug(print_r($reports, 1)); 286 | 287 | foreach ($reports as $row) { 288 | $report = json_decode($row['value'], true); 289 | try { 290 | $domain = new pm_Domain($report['domain']['id']); 291 | } catch (pm_Exception $e) { 292 | pm_Bootstrap::getDbAdapter()->delete('ModuleSettings', "module_id = " . $module_id . " AND name = '{$row['name']}'"); 293 | } 294 | } 295 | } 296 | 297 | /** 298 | * @param $domain Modules_WebsiteVirusCheck_PleskDomain 299 | * @param $new_report array 300 | * @return null 301 | */ 302 | public static function report_domain($domain, $new_report) 303 | { 304 | $report = self::getDomainReport($domain->id); 305 | if (!$report) { 306 | $report = []; 307 | } 308 | if (isset($new_report['http_error'])) { 309 | $report['http_error'] = $new_report['http_error']; 310 | } 311 | $report['virustotal_request_done'] = true; 312 | $report['virustotal_response_code'] = isset($new_report['response_code']) ? (int)$new_report['response_code'] : 0; 313 | $report['virustotal_positives'] = isset($new_report['positives']) ? (int)$new_report['positives'] : 0; 314 | $report['virustotal_total'] = isset($new_report['total']) ? (int)$new_report['total'] : ''; 315 | $report['virustotal_scan_date'] = isset($new_report['scan_date']) ? $new_report['scan_date'] : ''; 316 | $report['detected_urls'] = $new_report['detected_urls']; 317 | $report['detected_communicating_samples'] = $new_report['detected_communicating_samples']; 318 | $report['detected_referrer_samples'] = $new_report['detected_referrer_samples']; 319 | 320 | if ((int)$report['virustotal_positives'] > 0 321 | || $report['detected_urls'] > 0 322 | || $report['detected_communicating_samples'] > 0 323 | || $report['detected_referrer_samples'] > 0) { 324 | self::sendNotification($domain); 325 | } 326 | 327 | self::setDomainReport($domain->id, $report); 328 | 329 | return; 330 | } 331 | 332 | /** 333 | * @param $client Zend_Http_Client 334 | * @param $method string 335 | * @return Zend_Http_Response|Zend_Http_Client_Adapter_Exception|false 336 | */ 337 | static function send_http_request(Zend_Http_Client $client, $method = Zend_Http_Client::GET) { 338 | $response = false; 339 | 340 | for ($try = 5; $try > 0; $try--) { 341 | pm_Log::debug('Try to connect ' . self::virustotal_scan_url); 342 | try { 343 | $response = $client->request($method); 344 | pm_Log::debug('Successfully request ' . $method . ' ' . self::virustotal_scan_url); 345 | break; 346 | } catch (Zend_Http_Client_Adapter_Exception $e) { 347 | pm_Log::err('Failed to request ' . $method . ' ' . self::virustotal_scan_url . $e->getMessage()); 348 | sleep(5); 349 | return $e; 350 | } 351 | } 352 | 353 | return $response; 354 | } 355 | 356 | /** 357 | * @param $url string 358 | * @return array 359 | */ 360 | public static function virustotal_scan_url_request($url) 361 | { 362 | $client = new Zend_Http_Client(self::virustotal_scan_url); 363 | 364 | $client->setParameterPost('url', $url); 365 | $client->setParameterPost('apikey', pm_Settings::get('virustotal_api_key')); 366 | sleep(self::virustotal_api_timeout); 367 | 368 | $response = self::send_http_request($client, Zend_Http_Client::POST); 369 | if ($response === false) { 370 | return array ( 371 | 'http_error' => pm_Locale::lmsg('httpErrorFailedToConnectVirusTotalUnknownError'), 372 | ); 373 | } 374 | if ($response instanceof Zend_Http_Client_Adapter_Exception) { 375 | return array ( 376 | 'http_error' => $response->getMessage(), 377 | ); 378 | } 379 | 380 | if ($response->getStatus() == 403) { 381 | pm_Settings::set('apiKeyBecameInvalid', '1'); 382 | } 383 | 384 | return json_decode($response->getBody(), true); 385 | } 386 | 387 | /** 388 | * https://virustotal.com/ru/documentation/public-api/#getting-url-scans 389 | * 390 | * @param $url string 391 | * @return array 392 | */ 393 | public static function virustotal_scan_url_report($url) 394 | { 395 | $client = new Zend_Http_Client(self::virustotal_report_url); 396 | 397 | $client->setParameterPost('resource', $url); 398 | $client->setParameterPost('apikey', pm_Settings::get('virustotal_api_key')); 399 | 400 | sleep(self::virustotal_api_timeout); 401 | 402 | $response = self::send_http_request($client, Zend_Http_Client::POST); 403 | if ($response === false) { 404 | return array ( 405 | 'http_error' => pm_Locale::lmsg('failedToConnectVirusTotalUnknownError'), 406 | ); 407 | } 408 | if ($response instanceof Zend_Http_Client_Adapter_Exception) { 409 | return array ( 410 | 'http_error' => $response->getMessage(), 411 | ); 412 | } 413 | 414 | if ($response->getStatus() == 403) { 415 | pm_Settings::set('apiKeyBecameInvalid', '1'); 416 | } 417 | 418 | return json_decode($response->getBody(), true); 419 | } 420 | 421 | /** 422 | * https://virustotal.com/ru/documentation/public-api/#getting-domain-reports 423 | * 424 | * @param $domainAsciiName string 425 | * @return array 426 | */ 427 | public static function virustotal_scan_domain_report($domainAsciiName) 428 | { 429 | $client = new Zend_Http_Client(self::virustotal_report_domain); 430 | 431 | $client->setParameterGet('domain', $domainAsciiName); 432 | $client->setParameterGet('apikey', pm_Settings::get('virustotal_api_key')); 433 | 434 | sleep(self::virustotal_api_timeout); 435 | 436 | $response = self::send_http_request($client, Zend_Http_Client::GET); 437 | if ($response === false) { 438 | return array ( 439 | 'http_error' => pm_Locale::lmsg('failedToConnectVirusTotalUnknownError'), 440 | ); 441 | } 442 | if ($response instanceof Zend_Http_Client_Adapter_Exception) { 443 | return array ( 444 | 'http_error' => $response->getMessage(), 445 | ); 446 | } 447 | 448 | if ($response->getStatus() == 403) { 449 | pm_Settings::set('apiKeyBecameInvalid', '1'); 450 | } 451 | 452 | return json_decode($response->getBody(), true); 453 | } 454 | 455 | 456 | /** 457 | * @return array[string] 458 | * ['all'] Modules_WebsiteVirusCheck_PleskDomain[] 459 | * ['bad'] Modules_WebsiteVirusCheck_PleskDomain[] 460 | * ['total'] int 461 | */ 462 | public static function getDomainsReport() 463 | { 464 | static $domains = [ 465 | 'all' => [], 466 | 'bad' => [], 467 | 'total' => 0, 468 | ]; 469 | if ($domains['total'] > 0) { 470 | return $domains; 471 | } 472 | foreach (self::getDomains() as $domain) { 473 | $report = self::getDomainReport($domain->id); 474 | $domain->no_scanning_results = pm_Locale::lmsg('scanningWasNotPerformedYetForList'); 475 | if (!$report) { 476 | $report = []; 477 | $report['domain'] = $domain; 478 | $report['detected_urls'] = 0; 479 | $report['detected_communicating_samples'] = 0; 480 | $report['detected_referrer_samples'] = 0; 481 | self::setDomainReport($domain->id, $report); 482 | } else { 483 | if (isset($report['domain']['enabled'])) { 484 | $domain->enabled = $report['domain']['enabled']; 485 | } else { 486 | $domain->enabled = true; 487 | } 488 | $domain->available = $report['domain']['available']; 489 | if ($domain->available == 'no') { 490 | $domain->no_scanning_results = pm_Locale::lmsg('domainInactiveOrCantbeResolvedInHostingIp'); 491 | } 492 | if (isset($report['http_error'])) { 493 | $domain->no_scanning_results = pm_Locale::lmsg('httpError', array('message' => $report['http_error'])); 494 | } 495 | } 496 | 497 | if (isset($report['virustotal_response_code']) && $report['virustotal_response_code'] == 0) { 498 | $domain->no_scanning_results = pm_Locale::lmsg('virustotalDomainIsNotScannedYet'); 499 | if (isset($report['virustotal_request']['response_code'])) { 500 | if ($report['virustotal_request']['response_code'] == -1) { 501 | $domain->no_scanning_results = pm_Locale::lmsg('virustotalCantScanDomain'); 502 | } 503 | if ($report['virustotal_request']['response_code'] == 0) { 504 | $domain->no_scanning_results = pm_Locale::lmsg('scanningRequestIsSent'); 505 | } 506 | } 507 | } 508 | 509 | if (isset($report['virustotal_response_code']) && $report['virustotal_response_code'] > 0) { 510 | unset($domain->no_scanning_results); 511 | $domain->virustotal_scan_date = $report['virustotal_scan_date']; 512 | $domain->virustotal_positives = $report['virustotal_positives']; 513 | $domain->virustotal_total = $report['virustotal_total']; 514 | $detectedUrls = isset($report['detected_urls']) ? $report['detected_urls'] : 0; 515 | $detectedCommunicatingSamples = isset($report['detected_communicating_samples']) ? $report['detected_communicating_samples'] : 0; 516 | $detectedReferrerSamples = isset($report['detected_referrer_samples']) ? $report['detected_referrer_samples'] : 0; 517 | $domain->virustotal_bad_urls_and_samples = $detectedUrls + $detectedCommunicatingSamples + $detectedReferrerSamples; 518 | $domain->virustotal_domain_info_url = sprintf(self::virustotal_domain_info_url, $domain->ascii_name); 519 | } 520 | 521 | $domains['all'][$domain->id] = $domain; 522 | $domains['total']++; 523 | 524 | if (!isset($report['virustotal_positives']) || $report['virustotal_positives'] <= 0) { 525 | continue; 526 | } 527 | 528 | $domains['bad'][$domain->id] = $domain; 529 | } 530 | 531 | pm_Log::debug('Reports: ' . print_r($domains, 1)); 532 | return $domains; 533 | } 534 | 535 | /** 536 | * @return Modules_WebsiteVirusCheck_PleskDomain[] 537 | */ 538 | public static function getDomains() 539 | { 540 | static $domains = []; 541 | if ($domains) { 542 | return $domains; 543 | } 544 | $sites_request = ''; 545 | $websp_request = ''; 546 | $api = pm_ApiRpc::getService(); 547 | // site->get->result->[ id, data -> gen_info ( [cr_date] , [name] , [ascii-name] , [status] => 0 , [dns_ip_address] , [htype] ) 548 | $sites_response = $api->call($sites_request); 549 | $websp_response = $api->call($websp_request); 550 | 551 | $sites = json_decode(json_encode($sites_response->site->get)); 552 | $websp = json_decode(json_encode($websp_response->webspace->get)); 553 | 554 | $sites_array = is_array($sites->result) ? $sites->result : array($sites->result); 555 | $websp_array = is_array($websp->result) ? $websp->result : array($websp->result); 556 | 557 | $tmp_list = array_merge($sites_array, $websp_array); 558 | 559 | foreach ($tmp_list as $domain) { 560 | if (!isset($domain->id)) { 561 | continue; 562 | } 563 | 564 | $domains[$domain->id] = new Modules_WebsiteVirusCheck_PleskDomain( 565 | $domain->id, 566 | $domain->data->gen_info->name, 567 | $domain->data->gen_info->{'ascii-name'}, 568 | $domain->data->gen_info->status, 569 | (array)($domain->data->gen_info->dns_ip_address ?? []), 570 | $domain->data->gen_info->htype, 571 | isset($domain->data->gen_info->{'webspace-id'}) ? $domain->data->gen_info->{'webspace-id'} : $domain->id 572 | ); 573 | } 574 | 575 | ksort($domains); 576 | pm_Log::debug('PleskDomains : ' . print_r($domains, 1)); 577 | return $domains; 578 | } 579 | 580 | /** 581 | * https://virustotal.com/ru/documentation/public-api/#getting-domain-reports 582 | * 583 | * @param $key string 584 | * @return array 585 | */ 586 | public static function checkApiKey($key) 587 | { 588 | $client = new Zend_Http_Client(self::virustotal_report_url); 589 | 590 | $client->setParameterPost('resource', 'www.virustotal.com'); 591 | $client->setParameterPost('apikey', $key); 592 | 593 | $response = self::send_http_request($client, Zend_Http_Client::POST); 594 | if ($response === false) { 595 | return [ 596 | 'valid' => false, 597 | 'http_code' => pm_Locale::lmsg('failedToConnectVirusTotalUnknownError'), 598 | ]; 599 | } 600 | if ($response instanceof Zend_Http_Client_Adapter_Exception) { 601 | return array ( 602 | 'valid' => false, 603 | 'http_code' => $response->getStatus(), 604 | 'http_error' => $response->getMessage(), 605 | ); 606 | } 607 | 608 | if ($response->getStatus() == 403) { 609 | pm_Settings::set('apiKeyBecameInvalid', '1'); 610 | } 611 | 612 | $body = json_decode($response->getBody(), true); 613 | pm_Log::debug('API key check result: ' . print_r($response, 1) . "\n" . print_r($body, 1)); 614 | 615 | if (isset($body['response_code'])) { 616 | return [ 617 | 'valid' => true, 618 | 'http_code' => $response->getStatus(), 619 | 'http_error' => $response->getMessage(), 620 | ]; 621 | } 622 | 623 | return [ 624 | 'valid' => false, 625 | 'http_code' => $response->getStatus(), 626 | 'http_error' => $response->getMessage(), 627 | ]; 628 | } 629 | 630 | /** 631 | * @param $mail Zend_Mail 632 | * @return Zend_Mail|false 633 | */ 634 | static function sendMailRequest($mail) 635 | { 636 | $response = false; 637 | try { 638 | $response = $mail->send(); 639 | } catch (Zend_Mail_Transport_Exception $e) { 640 | pm_Log::debug('Failed to send mail'); 641 | pm_Log::err($e); 642 | } 643 | 644 | return $response; 645 | } 646 | 647 | /** Send notification to admin 648 | * @param $domain Modules_WebsiteVirusCheck_PleskDomain 649 | * @return null 650 | */ 651 | public static function sendNotification($domain) 652 | { 653 | if (!pm_Settings::get('emailNotificationEnabled')) { 654 | return; 655 | } 656 | $today = date('d/M/Y'); 657 | if (pm_Settings::get('notified_id_' . $domain->id) === $today) { 658 | return; 659 | } 660 | 661 | pm_Settings::set('notified_id_' . $domain->id, date('d/M/Y')); 662 | 663 | $admin = pm_Client::getByLogin('admin'); 664 | $adminEmail = $admin->getProperty('email'); 665 | $cnameEmail = $admin->getProperty('cname'); 666 | 667 | $mail = new Zend_Mail(); 668 | $mail->setBodyText( 669 | pm_Locale::lmsg( 670 | 'emailNotificationBodyBadDomain', 671 | [ 672 | 'domain' => $domain->ascii_name, 673 | 'url' => sprintf(self::virustotal_domain_info_url, $domain->ascii_name) 674 | ] 675 | ) 676 | ); 677 | $mail->setFrom($adminEmail, $cnameEmail); 678 | $mail->addTo($adminEmail, $cnameEmail); 679 | $mail->setSubject(pm_Locale::lmsg('emailNotificationSubjectBadDomain', ['domain' => $domain->ascii_name])); 680 | self::sendMailRequest($mail); 681 | 682 | return; 683 | } 684 | 685 | /** Get domain report by domain id 686 | * @param $domainId 687 | * @return mixed 688 | */ 689 | static function getDomainReport($domainId) { 690 | $report = json_decode(pm_Settings::get('domain_id_' . $domainId, ''), true); 691 | return $report; 692 | } 693 | 694 | /** Set domain report by domain id 695 | * @param $domainId string 696 | * @param $report array 697 | * @return void 698 | */ 699 | static function setDomainReport($domainId, $report) { 700 | pm_Settings::set('domain_id_' . $domainId, json_encode($report)); 701 | } 702 | } 703 | --------------------------------------------------------------------------------