BlacklistPlugin.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. <?php
  2. /**
  3. * StatusNet, the distributed open-source microblogging tool
  4. *
  5. * Plugin to prevent use of nicknames or URLs on a blacklist
  6. *
  7. * PHP version 5
  8. *
  9. * LICENCE: This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as published by
  11. * the Free Software Foundation, either version 3 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. * @category Action
  23. * @package StatusNet
  24. * @author Evan Prodromou <evan@status.net>
  25. * @copyright 2010 StatusNet Inc.
  26. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  27. * @link http://status.net/
  28. */
  29. if (!defined('GNUSOCIAL')) { exit(1); }
  30. /**
  31. * Plugin to prevent use of nicknames or URLs on a blacklist
  32. *
  33. * @category Plugin
  34. * @package StatusNet
  35. * @author Evan Prodromou <evan@status.net>
  36. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  37. * @link http://status.net/
  38. */
  39. class BlacklistPlugin extends Plugin
  40. {
  41. const PLUGIN_VERSION = '2.0.0';
  42. public $nicknames = array();
  43. public $urls = array();
  44. public $canAdmin = true;
  45. function _getNicknamePatterns()
  46. {
  47. $confNicknames = $this->_configArray('blacklist', 'nicknames');
  48. $dbNicknames = Nickname_blacklist::getPatterns();
  49. return array_merge($this->nicknames,
  50. $confNicknames,
  51. $dbNicknames);
  52. }
  53. function _getUrlPatterns()
  54. {
  55. $confURLs = $this->_configArray('blacklist', 'urls');
  56. $dbURLs = Homepage_blacklist::getPatterns();
  57. return array_merge($this->urls,
  58. $confURLs,
  59. $dbURLs);
  60. }
  61. /**
  62. * Database schema setup
  63. *
  64. * @return boolean hook value
  65. */
  66. function onCheckSchema()
  67. {
  68. $schema = Schema::get();
  69. // For storing blacklist patterns for nicknames
  70. $schema->ensureTable('nickname_blacklist', Nickname_blacklist::schemaDef());
  71. $schema->ensureTable('homepage_blacklist', Homepage_blacklist::schemaDef());
  72. return true;
  73. }
  74. /**
  75. * Retrieve an array from configuration
  76. *
  77. * Carefully checks a section.
  78. *
  79. * @param string $section Configuration section
  80. * @param string $setting Configuration setting
  81. *
  82. * @return array configuration values
  83. */
  84. function _configArray($section, $setting)
  85. {
  86. $config = common_config($section, $setting);
  87. if (empty($config)) {
  88. return array();
  89. } else if (is_array($config)) {
  90. return $config;
  91. } else if (is_string($config)) {
  92. return explode("\r\n", $config);
  93. } else {
  94. // TRANS: Exception thrown if the Blacklist plugin configuration is incorrect.
  95. // TRANS: %1$s is a configuration section, %2$s is a configuration setting.
  96. throw new Exception(sprintf(_m('Unknown data type for config %1$s + %2$s.'),$section, $setting));
  97. }
  98. }
  99. /**
  100. * Hook profile update to prevent blacklisted homepages or nicknames
  101. *
  102. * Throws an exception if there's a blacklisted homepage or nickname.
  103. *
  104. * @param ManagedAction $action Action being called (usually register)
  105. *
  106. * @return boolean hook value
  107. */
  108. function onStartProfileSaveForm(ManagedAction $action)
  109. {
  110. $homepage = strtolower($action->trimmed('homepage'));
  111. if (!empty($homepage)) {
  112. if (!$this->_checkUrl($homepage)) {
  113. // TRANS: Validation failure for URL. %s is the URL.
  114. $msg = sprintf(_m("You may not use homepage \"%s\"."),
  115. $homepage);
  116. throw new ClientException($msg);
  117. }
  118. }
  119. $nickname = strtolower($action->trimmed('nickname'));
  120. if (!empty($nickname)) {
  121. if (!$this->_checkNickname($nickname)) {
  122. // TRANS: Validation failure for nickname. %s is the nickname.
  123. $msg = sprintf(_m("You may not use nickname \"%s\"."),
  124. $nickname);
  125. throw new ClientException($msg);
  126. }
  127. }
  128. return true;
  129. }
  130. /**
  131. * Hook notice save to prevent blacklisted urls
  132. *
  133. * Throws an exception if there's a blacklisted url in the content.
  134. *
  135. * @param Notice &$notice Notice being saved
  136. *
  137. * @return boolean hook value
  138. */
  139. public function onStartNoticeSave(&$notice)
  140. {
  141. common_replace_urls_callback($notice->content,
  142. array($this, 'checkNoticeUrl'));
  143. return true;
  144. }
  145. /**
  146. * Helper callback for notice save
  147. *
  148. * Throws an exception if there's a blacklisted url in the content.
  149. *
  150. * @param string $url URL in the notice content
  151. *
  152. * @return boolean hook value
  153. */
  154. function checkNoticeUrl($url)
  155. {
  156. // It comes in special'd, so we unspecial it
  157. // before comparing against patterns
  158. $url = htmlspecialchars_decode($url);
  159. if (!$this->_checkUrl($url)) {
  160. // TRANS: Validation failure for URL. %s is the URL.
  161. $msg = sprintf(_m("You may not use URL \"%s\" in notices."),
  162. $url);
  163. throw new ClientException($msg);
  164. }
  165. return $url;
  166. }
  167. /**
  168. * Helper for checking URLs
  169. *
  170. * Checks an URL against our patterns for a match.
  171. *
  172. * @param string $url URL to check
  173. *
  174. * @return boolean true means it's OK, false means it's bad
  175. */
  176. private function _checkUrl($url)
  177. {
  178. $patterns = $this->_getUrlPatterns();
  179. foreach ($patterns as $pattern) {
  180. if ($pattern != '' && preg_match("/$pattern/", $url)) {
  181. return false;
  182. }
  183. }
  184. return true;
  185. }
  186. public function onUrlBlacklistTest($url)
  187. {
  188. common_debug('Checking URL against blacklist: '._ve($url));
  189. if (!$this->_checkUrl($url)) {
  190. throw new ClientException('Forbidden URL', 403);
  191. }
  192. return true;
  193. }
  194. /**
  195. * Helper for checking nicknames
  196. *
  197. * Checks a nickname against our patterns for a match.
  198. *
  199. * @param string $nickname nickname to check
  200. *
  201. * @return boolean true means it's OK, false means it's bad
  202. */
  203. private function _checkNickname($nickname)
  204. {
  205. $patterns = $this->_getNicknamePatterns();
  206. foreach ($patterns as $pattern) {
  207. if ($pattern != '' && preg_match("/$pattern/", $nickname)) {
  208. return false;
  209. }
  210. }
  211. return true;
  212. }
  213. /**
  214. * Add our actions to the URL router
  215. *
  216. * @param URLMapper $m URL mapper for this hit
  217. *
  218. * @return boolean hook return
  219. */
  220. public function onRouterInitialized(URLMapper $m)
  221. {
  222. $m->connect('panel/blacklist',
  223. ['action' => 'blacklistadminpanel']);
  224. return true;
  225. }
  226. /**
  227. * Plugin version data
  228. *
  229. * @param array &$versions array of version blocks
  230. *
  231. * @return boolean hook value
  232. */
  233. public function onPluginVersion(array &$versions): bool
  234. {
  235. $versions[] = array('name' => 'Blacklist',
  236. 'version' => self::PLUGIN_VERSION,
  237. 'author' => 'Evan Prodromou',
  238. 'homepage' =>
  239. 'https://git.gnu.io/gnu/gnu-social/tree/master/plugins/Blacklist',
  240. 'description' =>
  241. // TRANS: Plugin description.
  242. _m('Keeps a blacklist of forbidden nickname '.
  243. 'and URL patterns.'));
  244. return true;
  245. }
  246. /**
  247. * Determines if our admin panel can be shown
  248. *
  249. * @param string $name name of the admin panel
  250. * @param boolean &$isOK result
  251. *
  252. * @return boolean hook value
  253. */
  254. function onAdminPanelCheck($name, &$isOK)
  255. {
  256. if ($name == 'blacklist') {
  257. $isOK = $this->canAdmin;
  258. return false;
  259. }
  260. return true;
  261. }
  262. /**
  263. * Add our tab to the admin panel
  264. *
  265. * @param Widget $nav Admin panel nav
  266. *
  267. * @return boolean hook value
  268. */
  269. function onEndAdminPanelNav(Menu $nav)
  270. {
  271. if (AdminPanelAction::canAdmin('blacklist')) {
  272. $action_name = $nav->action->trimmed('action');
  273. $nav->out->menuItem(common_local_url('blacklistadminpanel'),
  274. // TRANS: Menu item in admin panel.
  275. _m('MENU','Blacklist'),
  276. // TRANS: Tooltip for menu item in admin panel.
  277. _m('TOOLTIP','Blacklist configuration.'),
  278. $action_name == 'blacklistadminpanel',
  279. 'nav_blacklist_admin_panel');
  280. }
  281. return true;
  282. }
  283. function onEndDeleteUserForm(HTMLOutputter $out, User $user)
  284. {
  285. $scoped = $out->getScoped();
  286. if ($scoped === null || !$scoped->hasRight(Right::CONFIGURESITE)) {
  287. return true;
  288. }
  289. try {
  290. $profile = $user->getProfile();
  291. } catch (UserNoProfileException $e) {
  292. return true;
  293. }
  294. $out->elementStart('ul', 'form_data');
  295. $out->elementStart('li');
  296. $this->checkboxAndText($out,
  297. 'blacklistnickname',
  298. // TRANS: Checkbox label in the blacklist user form.
  299. _m('Add this nickname pattern to blacklist'),
  300. 'blacklistnicknamepattern',
  301. $this->patternizeNickname($profile->getNickname()));
  302. $out->elementEnd('li');
  303. if (!empty($profile->getHomepage())) {
  304. $out->elementStart('li');
  305. $this->checkboxAndText($out,
  306. 'blacklisthomepage',
  307. // TRANS: Checkbox label in the blacklist user form.
  308. _m('Add this homepage pattern to blacklist'),
  309. 'blacklisthomepagepattern',
  310. $this->patternizeHomepage($profile->getHomepage()));
  311. $out->elementEnd('li');
  312. }
  313. $out->elementEnd('ul');
  314. }
  315. function onEndDeleteUser(HTMLOutputter $out, User $user)
  316. {
  317. if ($out->boolean('blacklisthomepage')) {
  318. $pattern = $out->trimmed('blacklisthomepagepattern');
  319. Homepage_blacklist::ensurePattern($pattern);
  320. }
  321. if ($out->boolean('blacklistnickname')) {
  322. $pattern = $out->trimmed('blacklistnicknamepattern');
  323. Nickname_blacklist::ensurePattern($pattern);
  324. }
  325. return true;
  326. }
  327. function checkboxAndText(HTMLOutputter $out, $checkID, $label, $textID, $value)
  328. {
  329. $out->element('input', array('name' => $checkID,
  330. 'type' => 'checkbox',
  331. 'class' => 'checkbox',
  332. 'id' => $checkID));
  333. $out->text(' ');
  334. $out->element('label', array('class' => 'checkbox',
  335. 'for' => $checkID),
  336. $label);
  337. $out->text(' ');
  338. $out->element('input', array('name' => $textID,
  339. 'type' => 'text',
  340. 'id' => $textID,
  341. 'value' => $value));
  342. }
  343. function patternizeNickname($nickname)
  344. {
  345. return $nickname;
  346. }
  347. function patternizeHomepage($homepage)
  348. {
  349. $hostname = parse_url($homepage, PHP_URL_HOST);
  350. return $hostname;
  351. }
  352. function onStartHandleFeedEntry($activity)
  353. {
  354. return $this->_checkActivity($activity);
  355. }
  356. function onStartHandleSalmon($activity)
  357. {
  358. return $this->_checkActivity($activity);
  359. }
  360. function _checkActivity($activity)
  361. {
  362. $actor = $activity->actor;
  363. if (empty($actor)) {
  364. return true;
  365. }
  366. $homepage = strtolower($actor->link);
  367. if (!empty($homepage)) {
  368. if (!$this->_checkUrl($homepage)) {
  369. // TRANS: Exception thrown trying to post a notice while having set a blocked homepage URL. %s is the blocked URL.
  370. $msg = sprintf(_m("Users from \"%s\" are blocked."),
  371. $homepage);
  372. throw new ClientException($msg);
  373. }
  374. }
  375. if (!empty($actor->poco)) {
  376. $nickname = strtolower($actor->poco->preferredUsername);
  377. if (!empty($nickname)) {
  378. if (!$this->_checkNickname($nickname)) {
  379. // TRANS: Exception thrown trying to post a notice while having a blocked nickname. %s is the blocked nickname.
  380. $msg = sprintf(_m("Notices from nickname \"%s\" are disallowed."),
  381. $nickname);
  382. throw new ClientException($msg);
  383. }
  384. }
  385. }
  386. return true;
  387. }
  388. /**
  389. * Check URLs and homepages for blacklisted users.
  390. */
  391. function onStartSubscribe(Profile $subscriber, Profile $other)
  392. {
  393. foreach ([$other->getUrl(), $other->getHomepage()] as $url) {
  394. if (empty($url)) {
  395. continue;
  396. }
  397. $url = strtolower($url);
  398. if (!$this->_checkUrl($url)) {
  399. // TRANS: Client exception thrown trying to subscribe to a person with a blocked homepage or site URL. %s is the blocked URL.
  400. $msg = sprintf(_m("Users from \"%s\" are blocked."),
  401. $url);
  402. throw new ClientException($msg);
  403. }
  404. }
  405. $nickname = $other->getNickname();
  406. if (!empty($nickname)) {
  407. if (!$this->_checkNickname($nickname)) {
  408. // TRANS: Client exception thrown trying to subscribe to a person with a blocked nickname. %s is the blocked nickname.
  409. $msg = sprintf(_m("Cannot subscribe to nickname \"%s\"."),
  410. $nickname);
  411. throw new ClientException($msg);
  412. }
  413. }
  414. return true;
  415. }
  416. }