BxAntispamModule.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup Antispam Antispam
  7. * @ingroup UnaModules
  8. *
  9. * @{
  10. */
  11. class BxAntispamModule extends BxDolModule
  12. {
  13. private $_bLastSubmittedFormWasToxic;
  14. public function __construct(&$aModule)
  15. {
  16. parent::__construct($aModule);
  17. }
  18. public function serviceUpdateDisposableDomainsLists ()
  19. {
  20. $o = bx_instance('BxAntispamDisposableEmailDomains', array(), 'bx_antispam');
  21. $o->updateList('blacklist', 'https://raw.githubusercontent.com/martenson/disposable-email-domains/master/disposable_email_blocklist.conf');
  22. // TODO: uncomment after adding interface for whitelisting
  23. // $o->updateList('whitelist', 'https://raw.githubusercontent.com/martenson/disposable-email-domains/master/whitelist.conf');
  24. }
  25. public function serviceIpTable ()
  26. {
  27. $o = bx_instance('BxAntispamIP', array(), $this->_aModule);
  28. $s = _t('_bx_antispam_ip_table_status', mb_strtolower($o->getIpTableConfigTitle((int)getParam('bx_antispam_ip_list_type'))));
  29. $s .= $this->_grid('bx_antispam_grid_ip_table');
  30. return $s;
  31. }
  32. public function serviceDnsblList ()
  33. {
  34. $s = _t('_bx_antispam_dnsbl_status',
  35. BxTemplFunctions::getInstance()->statusOnOff((bool)$this->_oConfig->getAntispamOption('dnsbl_enable'), true),
  36. BxTemplFunctions::getInstance()->statusOnOff((bool)$this->_oConfig->getAntispamOption('uridnsbl_enable'), true),
  37. mb_strtolower(_t('_bx_antispam_dnsbl_behaviour_login_' . $this->_oConfig->getAntispamOption('dnsbl_behaviour_login'))),
  38. mb_strtolower(_t('_bx_antispam_dnsbl_behaviour_join_' . $this->_oConfig->getAntispamOption('dnsbl_behaviour_join')))
  39. );
  40. $s .= $this->_grid('bx_antispam_grid_dnsbl');
  41. return $s;
  42. }
  43. public function serviceBlockLog ()
  44. {
  45. return $this->_grid('bx_antispam_grid_block_log');
  46. }
  47. /**
  48. * Filter undesired words
  49. *
  50. * @param $sContent content to filter
  51. * @param $sIp IP address of content poster
  52. * @return modified or the same content.
  53. */
  54. public function serviceFilterSpam ($mContent, $sIp = '')
  55. {
  56. if (defined('BX_DOL_CRON_EXECUTE') || isAdmin() || 'on' != $this->_oConfig->getAntispamOption('profanity_enable'))
  57. return $mContent;
  58. $oProfanityFilter = bx_instance('BxAntispamProfanityFilter', array(), $this->_aModule);
  59. return $oProfanityFilter->censor($mContent);
  60. }
  61. /**
  62. * Check text for spam.
  63. * First it check if IP is whitelisted(or under cron execution or user is admin) - for whitelisted IPs check for spam isn't performed,
  64. * then it checks URLs found in text for DNSURI black lists (@see BxAntispamDNSURIBlacklists),
  65. * then it checks text in Akismet service (@see BxAntispamAkismet).
  66. * It can send report if spam is found or tries to inform caller to block the content (depending on configuration).
  67. *
  68. * @param $sContent content to check for spam
  69. * @param $sIp IP address of content poster
  70. * @param $isStripSlashes slashes parameter:
  71. * BX_SLASHES_AUTO - automatically detect magic_quotes_gpc setting
  72. * BX_SLASHES_NO_ACTION - do not perform any action with slashes
  73. * @return true if spam detected and content shouln't be recorded, false if content should be processed as usual.
  74. */
  75. public function serviceIsSpam ($sContent, $sIp = '', $isStripSlashes = BX_SLASHES_AUTO)
  76. {
  77. if (defined('BX_DOL_CRON_EXECUTE') || isAdmin())
  78. return false;
  79. if ($this->serviceIsIpWhitelisted($sIp))
  80. return false;
  81. if (version_compare(phpversion(), '7.4.0', '<') && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc() && $isStripSlashes == BX_SLASHES_AUTO)
  82. $sContent = stripslashes($sContent);
  83. $bRet = false;
  84. if ('on' == $this->_oConfig->getAntispamOption('uridnsbl_enable')) {
  85. $oDNSURIBlacklists = bx_instance('BxAntispamDNSURIBlacklists', array(), $this->_aModule);
  86. if ($oDNSURIBlacklists->isSpam($sContent)) {
  87. $oDNSURIBlacklists->onPositiveDetection($sContent);
  88. $bRet = true;
  89. }
  90. }
  91. if (!$bRet && 'on' == $this->_oConfig->getAntispamOption('akismet_enable')) {
  92. $oAkismet = bx_instance('BxAntispamAkismet', array(), $this->_aModule);
  93. if ($oAkismet->isSpam($sContent)) {
  94. $oAkismet->onPositiveDetection($sContent);
  95. $bRet = true;
  96. }
  97. }
  98. if ($bRet && 'on' == $this->_oConfig->getAntispamOption('antispam_report')) {
  99. $oProfile = BxDolProfile::getInstance();
  100. $aPlus = array(
  101. 'SpammerUrl' => $oProfile->getUrl(),
  102. 'SpammerNickName' => $oProfile->getDisplayName(),
  103. 'Page' => htmlspecialchars_adv($_SERVER['PHP_SELF']),
  104. 'Get' => print_r($_GET, true),
  105. 'Post' => print_r($_POST, true),
  106. 'SpamContent' => htmlspecialchars_adv($sContent),
  107. );
  108. $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('bx_antispam_spam_report', $aPlus);
  109. if (!$aTemplate)
  110. trigger_error('Email template or translation missing: bx_antispam_spam_report', E_USER_ERROR);
  111. sendMail(getParam('site_email'), $aTemplate['Subject'], $aTemplate['Body']);
  112. }
  113. if ($bRet && 'on' == $this->_oConfig->getAntispamOption('antispam_block'))
  114. return true;
  115. return false;
  116. }
  117. /**
  118. * Check text for toxicity.
  119. * It can send report if toxic content is found or tries to inform caller to block the content (depending on configuration).
  120. *
  121. * @param $sContent content to check for spam
  122. * @param $sIp IP address of content poster
  123. * @param $isStripSlashes slashes parameter:
  124. * BX_SLASHES_AUTO - automatically detect magic_quotes_gpc setting
  125. * BX_SLASHES_NO_ACTION - do not perform any action with slashes
  126. * @return true if spam detected and content shouln't be recorded, false if content should be processed as usual.
  127. */
  128. public function serviceIsToxic ($sContent, $sIp = '', $isStripSlashes = BX_SLASHES_AUTO)
  129. {
  130. if (defined('BX_DOL_CRON_EXECUTE') || isAdmin())
  131. return false;
  132. static $aQuickCache;
  133. $key = md5($sContent);
  134. if (isset($aQuickCache[$key]))
  135. return $aQuickCache[$key];
  136. $aQuickCache[$key] = false;
  137. if (!$this->serviceIsIpWhitelisted($sIp)) {
  138. if (version_compare(phpversion(), '7.4.0', '<') && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc() && $isStripSlashes == BX_SLASHES_AUTO)
  139. $sContent = stripslashes($sContent);
  140. if ('on' == $this->_oConfig->getAntispamOption('toxicity_enable')) {
  141. $oPerspectiveAPI = bx_instance('BxAntispamPerspectiveAPI', array(), $this->_aModule);
  142. if ($oPerspectiveAPI->isToxic($sContent)) {
  143. $oPerspectiveAPI->onPositiveDetection($sContent);
  144. $aQuickCache[$key] = true;
  145. }
  146. }
  147. }
  148. return $aQuickCache[$key];
  149. }
  150. public function serviceCheckFormForToxicity(&$oForm) {
  151. $this->_bLastSubmittedFormWasToxic = false;
  152. if (!$oForm->isValid() || !$oForm->isSubmitted()) return false;
  153. $sSubmitName = false;
  154. $sFormText = '';
  155. foreach ($oForm->aInputs as $k => $a) {
  156. if (isset($a['visible_for_levels']) && !BxDolForm::isVisible($a))
  157. continue;
  158. if (empty($a['name']) || 'submit' == $a['type'] || 'reset' == $a['type'] || 'button' == $a['type'] || 'value' == $a['type']) {
  159. if (isset($a['type']) && 'submit' == $a['type'])
  160. $sSubmitName = $k;
  161. continue;
  162. }
  163. if ('input_set' == $a['type'])
  164. foreach ($a as $r)
  165. if (isset($r['type']) && 'submit' == $r['type'])
  166. $sSubmitName = $k;
  167. $a['name'] = str_replace('[]', '', $a['name']);
  168. if ($a['type'] != 'textarea' && $a['type'] != 'text')
  169. continue;
  170. $val = BxDolForm::getSubmittedValue($a['name'], $oForm->aFormAttrs['method']);
  171. if (!$val)
  172. continue;
  173. $sFormText .= " ".$val; // collect all text fields into a single text
  174. }
  175. if ($this->serviceIsToxic($sFormText)) {
  176. $this->_bLastSubmittedFormWasToxic = true;
  177. $sActionrequired = $this->_oConfig->getAntispamOption('toxicity_action');
  178. if ($sActionrequired == 'block' || $sActionrequired == 'disapprove' && !$oForm->isStatusFieldSupported()) {
  179. $oForm->setValid(false);
  180. $this->serviceOnToxicContentBlocked($sFormText);
  181. if ($sSubmitName)
  182. $oForm->aInputs[$sSubmitName]['error'] = _t('_bx_antispam_form_submission_error');
  183. } elseif ($sActionrequired == 'disapprove' && $oForm->isStatusFieldSupported()) {
  184. $oForm->setForceSetToPending(true);
  185. return false;
  186. }
  187. }
  188. }
  189. public function serviceOnFormSubmitted($sModule, $iEntry) {
  190. if ($this->_bLastSubmittedFormWasToxic) {
  191. $this->serviceOnToxicContentPosted($sModule, $iEntry);
  192. }
  193. }
  194. public function serviceOnToxicContentBlocked(&$sText) {
  195. if ('on' == $this->_oConfig->getAntispamOption('toxicity_report')) {
  196. $oProfile = BxDolProfile::getInstance();
  197. if (!$oProfile) return;
  198. $aPlus = array(
  199. 'AuthorUrl' => $oProfile->getUrl(),
  200. 'AuthorNickName' => $oProfile->getDisplayName(),
  201. 'Page' => htmlspecialchars_adv($_SERVER['PHP_SELF']),
  202. 'Content' => htmlspecialchars_adv(strip_tags($sText)),
  203. );
  204. $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('bx_antispam_toxicity_blocked_report', $aPlus);
  205. if (!$aTemplate)
  206. trigger_error('Email template or translation missing: bx_antispam_toxicity_blocked_report', E_USER_ERROR);
  207. sendMail(getParam('site_email'), $aTemplate['Subject'], $aTemplate['Body']);
  208. }
  209. }
  210. public function serviceOnToxicContentPosted($sModule, $iContentId) {
  211. if ('on' == $this->_oConfig->getAntispamOption('toxicity_report') && $sModule) {
  212. $oModule = BxDolModule::getInstance($sModule);
  213. $CNF = &$oModule->_oConfig->CNF;
  214. $sContentUrl = isset($CNF['URI_VIEW_ENTRY']) ? BX_DOL_URL_ROOT.BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $iContentId) : false;
  215. $sManageContentUrl = isset($CNF['URL_MANAGE_ADMINISTRATION']) ? BX_DOL_URL_ROOT.BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URL_MANAGE_ADMINISTRATION']) : false;
  216. $oProfile = BxDolProfile::getInstance();
  217. if (!$oProfile) return;
  218. $aPlus = array(
  219. 'AuthorUrl' => $oProfile->getUrl(),
  220. 'AuthorNickName' => $oProfile->getDisplayName(),
  221. 'Page' => htmlspecialchars_adv($_SERVER['PHP_SELF']),
  222. 'bx_if:content_url' => [
  223. 'condition' => boolval($sContentUrl),
  224. 'content' => ['c_url' => $sContentUrl],
  225. ],
  226. 'bx_if:manage_content_url' => [
  227. 'condition' => boolval($sManageContentUrl),
  228. 'content' => ['m_url' => $sManageContentUrl],
  229. ],
  230. );
  231. $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('bx_antispam_toxicity_posted_report', $aPlus);
  232. if (!$aTemplate)
  233. trigger_error('Email template or translation missing: bx_antispam_toxicity_posted_report', E_USER_ERROR);
  234. sendMail(getParam('site_email'), $aTemplate['Subject'], $aTemplate['Body']);
  235. bx_alert('bx_antispam', 'toxic_content_posted', $iContentId, bx_get_logged_profile_id(), array(
  236. 'module' => $sModule,
  237. 'entry_id' => $iContentId,
  238. 'etrny_url' => $sContentUrl,
  239. ));
  240. }
  241. }
  242. /**
  243. * Perform complex check if user is allowed to login.
  244. * First it checks if IP is directly blocked (@see serviceIsIpBlocked),
  245. * then it checks in DNS black lists (@see serviceIsIpDnsBlacklisted).
  246. *
  247. * @param $sIp IP to check, or empty for current IP
  248. * @return empty string - if join should be allowed, error message - if join should be blocked
  249. */
  250. public function serviceCheckLogin ($sIp = '')
  251. {
  252. $bLoginBlock = ('block' == $this->_oConfig->getAntispamOption('dnsbl_behaviour_login'));
  253. $sErrorMsg = '';
  254. $sNote = $bLoginBlock ? 'login block' : 'login log';
  255. if (!$sIp)
  256. $sIp = getVisitorIP();
  257. if (!$sErrorMsg && $this->serviceIsIpBlocked($sIp))
  258. $sErrorMsg = $this->getErrorMessageIpBlocked();
  259. if (!$sErrorMsg && 'on' == $this->_oConfig->getAntispamOption('dnsbl_enable') && $this->serviceIsIpDnsBlacklisted($sIp, $sNote) && $bLoginBlock)
  260. $sErrorMsg = $this->getErrorMessageSpam();
  261. return $sErrorMsg;
  262. }
  263. /**
  264. * Perform complex check if user is allowed to join.
  265. * First it checks if IP is directly blocked (@see serviceIsIpBlocked),
  266. * then it checks in DNS black lists (@see serviceIsIpDnsBlacklisted),
  267. * then it checks in StopForumSpam service (@see BxAntispamStopForumSpam).
  268. *
  269. * @param $sCurIP IP to check, or empty for current IP
  270. * @return empty string - if join should be allowed, error message - if join should be blocked
  271. */
  272. public function serviceCheckJoin ($sEmail, &$bApproval, $sIp = '')
  273. {
  274. $bJoinBlock = ('block' == $this->_oConfig->getAntispamOption('dnsbl_behaviour_join'));
  275. $bApproval = false;
  276. $sErrorMsg = '';
  277. $sNote = $bJoinBlock ? 'join block' : 'join approval';
  278. if (!$sIp)
  279. $sIp = getVisitorIP();
  280. // check if IP is blocked
  281. if (!$sErrorMsg && $this->serviceIsIpBlocked($sIp))
  282. $sErrorMsg = $this->getErrorMessageIpBlocked();
  283. // check in DNSBL lists
  284. if (!$sErrorMsg && 'on' == $this->_oConfig->getAntispamOption('dnsbl_enable') && $this->serviceIsIpDnsBlacklisted($sIp, $sNote)) {
  285. if ('approval' == $this->_oConfig->getAntispamOption('dnsbl_behaviour_join'))
  286. $bApproval = true;
  287. else
  288. $sErrorMsg = $this->getErrorMessageSpam();
  289. }
  290. // check in StopForumSpam service
  291. if (!$sErrorMsg) {
  292. $oStopForumSpam = bx_instance('BxAntispamStopForumSpam', array(), $this->_aModule);
  293. if ($oStopForumSpam->isSpammer(array('email' => $sEmail, 'ip' => $sIp), $sNote))
  294. $sErrorMsg = $this->getErrorMessageSpam();
  295. }
  296. // check for disposable email domains
  297. if (!$sErrorMsg && 'disable' != ($sMode = $this->_oConfig->getAntispamOption('disposable_email_domains_mode'))) {
  298. $bDisposableEmailDomainsBehaviour = $this->_oConfig->getAntispamOption('disposable_email_domains_behaviour_join');
  299. $oDisposableEmailDomains = bx_instance('BxAntispamDisposableEmailDomains', array(), 'bx_antispam');
  300. if ('blacklist' == $sMode && $oDisposableEmailDomains->isBlacklisted($sEmail)) {
  301. if ('approval' == $bDisposableEmailDomainsBehaviour)
  302. $bApproval = true;
  303. else
  304. $sErrorMsg = $oDisposableEmailDomains->getErrorMessageBlacklisted();
  305. }
  306. elseif ('whitelist' == $sMode && !$oDisposableEmailDomains->isWhitelisted($sEmail)) {
  307. if ('approval' == $bDisposableEmailDomainsBehaviour)
  308. $bApproval = true;
  309. else
  310. $sErrorMsg = $oDisposableEmailDomains->getErrorMessageNotWhitelisted();
  311. }
  312. }
  313. return $sErrorMsg;
  314. }
  315. /**
  316. * Check if IP is blacklisted in some DNS chain (@see BxAntispamDNSBlacklists).
  317. *
  318. * @param $sCurIP IP to check, or empty for current IP
  319. * @param $sNote [optional] place where checking is performed, for example 'join', 'login'
  320. * @return true if IP blacklisted and not whiteloisted, or false if under cron execution or if IP isn't blacklisted
  321. */
  322. public function serviceIsIpDnsBlacklisted($sCurIP = '', $sNote = '')
  323. {
  324. if (defined('BX_DOL_CRON_EXECUTE'))
  325. return false;
  326. if (!$sCurIP)
  327. $sCurIP = getVisitorIP();
  328. if ($this->serviceIsIpWhitelisted($sCurIP))
  329. return false;
  330. $o = bx_instance('BxAntispamDNSBlacklists', array(), $this->_aModule);
  331. if (BX_DOL_DNSBL_POSITIVE == $o->dnsbl_lookup_ip(BX_DOL_DNSBL_CHAIN_SPAMMERS, $sCurIP) && BX_DOL_DNSBL_POSITIVE != $o->dnsbl_lookup_ip(BX_DOL_DNSBL_CHAIN_WHITELIST, $sCurIP)) {
  332. $o->onPositiveDetection ($sCurIP, $sNote);
  333. return true;
  334. }
  335. return false;
  336. }
  337. /**
  338. * @see BxAntispamIP::isIpWhitelisted
  339. */
  340. public function serviceIsIpWhitelisted($sIp = '')
  341. {
  342. $o = bx_instance('BxAntispamIP', array(), $this->_aModule);
  343. return $o->isIpWhitelisted($sIp);
  344. }
  345. /**
  346. * @see BxAntispamIP::isIpBlocked
  347. */
  348. public function serviceIsIpBlocked($sIp = '')
  349. {
  350. $o = bx_instance('BxAntispamIP', array(), $this->_aModule);
  351. return $o->isIpBlocked($sIp);
  352. }
  353. /**
  354. * @see BxAntispamIP::blockIp
  355. */
  356. public function serviceBlockIp($mixedIP, $iExpirationInSec = 86400, $sComment = '')
  357. {
  358. $o = bx_instance('BxAntispamIP', array(), $this->_aModule);
  359. return $o->blockIp($mixedIP, $iExpirationInSec, $sComment);
  360. }
  361. /**
  362. * @see BxAntispamIP::pruning
  363. */
  364. public function servicePruning()
  365. {
  366. $o = bx_instance('BxAntispamIP', array(), $this->_aModule);
  367. return $o->pruning();
  368. }
  369. /**
  370. * @see BxAntispamIP::pruning
  371. */
  372. public function serviceConfigValues($s)
  373. {
  374. switch ($s) {
  375. case 'ip_table':
  376. $o = bx_instance('BxAntispamIP', array(), $this->_aModule);
  377. return $o->getIpTableConfigValues();
  378. case 'dnsbl_login':
  379. $o = bx_instance('BxAntispamDNSBlacklists', array(), $this->_aModule);
  380. return $o->getDNSBLConfigValues();
  381. case 'dnsbl_join';
  382. $o = bx_instance('BxAntispamDNSURIBlacklists', array(), $this->_aModule);
  383. return $o->getURIDNSBLConfigValues();
  384. case 'disposable_email_domains_join';
  385. $o = bx_instance('BxAntispamDisposableEmailDomains', array(), $this->_aModule);
  386. return $o->getJoinBehaviourValues();
  387. case 'disposable_email_domains_mode';
  388. $o = bx_instance('BxAntispamDisposableEmailDomains', array(), $this->_aModule);
  389. return $o->getJoinBehaviourModes();
  390. }
  391. }
  392. /**
  393. * @return array with avaliable dictionaries languages
  394. */
  395. public function serviceGetProfanityFilterDicts ()
  396. {
  397. $oProfanityFilter = bx_instance('BxAntispamProfanityFilter', array(), $this->_aModule);
  398. return $oProfanityFilter->getDicts();
  399. }
  400. /**
  401. * @return array with avaliable toxicity filter actions
  402. */
  403. public function serviceGetToxicityFilterActions ()
  404. {
  405. return [
  406. 'none' => _t('_bx_antispam_option_toxicity_filter_action_none'),
  407. 'block' => _t('_bx_antispam_option_toxicity_filter_action_block'),
  408. 'disapprove' => _t('_bx_antispam_option_toxicity_filter_action_disapprove'),
  409. ];
  410. }
  411. protected function getErrorMessageIpBlocked ()
  412. {
  413. bx_import('BxDolLanguages');
  414. return _t('_bx_antispam_ip_blocked', $this->getErrorMessageSubmitFalsePositiveReport());
  415. }
  416. protected function getErrorMessageSpam ()
  417. {
  418. bx_import('BxDolLanguages');
  419. return _t('_bx_antispam_spam_detected', $this->getErrorMessageSubmitFalsePositiveReport());
  420. }
  421. protected function getErrorMessageSubmitFalsePositiveReport ()
  422. {
  423. if (BxDolRequest::serviceExists('bx_contact', 'get_contact_page_url') && ($sUrl = BxDolService::call('bx_contact', 'get_contact_page_url')))
  424. return _t('_bx_antispam_submit_false_positive_report', $sUrl);
  425. return '';
  426. }
  427. protected function _grid ($sObjectGrid)
  428. {
  429. $oGrid = BxDolGrid::getObjectInstance($sObjectGrid);
  430. return $oGrid ? $oGrid->getCode() : '';
  431. }
  432. }
  433. /** @} */