├── _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 | 
5 |
6 |
7 | 
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 |
--------------------------------------------------------------------------------