BlacklistPlugin.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  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', array('action' => 'blacklistadminpanel'));
  223. return true;
  224. }
  225. /**
  226. * Plugin version data
  227. *
  228. * @param array &$versions array of version blocks
  229. *
  230. * @return boolean hook value
  231. */
  232. function onPluginVersion(array &$versions)
  233. {
  234. $versions[] = array('name' => 'Blacklist',
  235. 'version' => self::PLUGIN_VERSION,
  236. 'author' => 'Evan Prodromou',
  237. 'homepage' =>
  238. 'https://git.gnu.io/gnu/gnu-social/tree/master/plugins/Blacklist',
  239. 'description' =>
  240. // TRANS: Plugin description.
  241. _m('Keeps a blacklist of forbidden nickname '.
  242. 'and URL patterns.'));
  243. return true;
  244. }
  245. /**
  246. * Determines if our admin panel can be shown
  247. *
  248. * @param string $name name of the admin panel
  249. * @param boolean &$isOK result
  250. *
  251. * @return boolean hook value
  252. */
  253. function onAdminPanelCheck($name, &$isOK)
  254. {
  255. if ($name == 'blacklist') {
  256. $isOK = $this->canAdmin;
  257. return false;
  258. }
  259. return true;
  260. }
  261. /**
  262. * Add our tab to the admin panel
  263. *
  264. * @param Widget $nav Admin panel nav
  265. *
  266. * @return boolean hook value
  267. */
  268. function onEndAdminPanelNav(Menu $nav)
  269. {
  270. if (AdminPanelAction::canAdmin('blacklist')) {
  271. $action_name = $nav->action->trimmed('action');
  272. $nav->out->menuItem(common_local_url('blacklistadminpanel'),
  273. // TRANS: Menu item in admin panel.
  274. _m('MENU','Blacklist'),
  275. // TRANS: Tooltip for menu item in admin panel.
  276. _m('TOOLTIP','Blacklist configuration.'),
  277. $action_name == 'blacklistadminpanel',
  278. 'nav_blacklist_admin_panel');
  279. }
  280. return true;
  281. }
  282. function onEndDeleteUserForm(HTMLOutputter $out, User $user)
  283. {
  284. $scoped = $out->getScoped();
  285. if ($scoped === null || !$scoped->hasRight(Right::CONFIGURESITE)) {
  286. return true;
  287. }
  288. try {
  289. $profile = $user->getProfile();
  290. } catch (UserNoProfileException $e) {
  291. return true;
  292. }
  293. $out->elementStart('ul', 'form_data');
  294. $out->elementStart('li');
  295. $this->checkboxAndText($out,
  296. 'blacklistnickname',
  297. // TRANS: Checkbox label in the blacklist user form.
  298. _m('Add this nickname pattern to blacklist'),
  299. 'blacklistnicknamepattern',
  300. $this->patternizeNickname($profile->getNickname()));
  301. $out->elementEnd('li');
  302. if (!empty($profile->getHomepage())) {
  303. $out->elementStart('li');
  304. $this->checkboxAndText($out,
  305. 'blacklisthomepage',
  306. // TRANS: Checkbox label in the blacklist user form.
  307. _m('Add this homepage pattern to blacklist'),
  308. 'blacklisthomepagepattern',
  309. $this->patternizeHomepage($profile->getHomepage()));
  310. $out->elementEnd('li');
  311. }
  312. $out->elementEnd('ul');
  313. }
  314. function onEndDeleteUser(HTMLOutputter $out, User $user)
  315. {
  316. if ($out->boolean('blacklisthomepage')) {
  317. $pattern = $out->trimmed('blacklisthomepagepattern');
  318. Homepage_blacklist::ensurePattern($pattern);
  319. }
  320. if ($out->boolean('blacklistnickname')) {
  321. $pattern = $out->trimmed('blacklistnicknamepattern');
  322. Nickname_blacklist::ensurePattern($pattern);
  323. }
  324. return true;
  325. }
  326. function checkboxAndText(HTMLOutputter $out, $checkID, $label, $textID, $value)
  327. {
  328. $out->element('input', array('name' => $checkID,
  329. 'type' => 'checkbox',
  330. 'class' => 'checkbox',
  331. 'id' => $checkID));
  332. $out->text(' ');
  333. $out->element('label', array('class' => 'checkbox',
  334. 'for' => $checkID),
  335. $label);
  336. $out->text(' ');
  337. $out->element('input', array('name' => $textID,
  338. 'type' => 'text',
  339. 'id' => $textID,
  340. 'value' => $value));
  341. }
  342. function patternizeNickname($nickname)
  343. {
  344. return $nickname;
  345. }
  346. function patternizeHomepage($homepage)
  347. {
  348. $hostname = parse_url($homepage, PHP_URL_HOST);
  349. return $hostname;
  350. }
  351. function onStartHandleFeedEntry($activity)
  352. {
  353. return $this->_checkActivity($activity);
  354. }
  355. function onStartHandleSalmon($activity)
  356. {
  357. return $this->_checkActivity($activity);
  358. }
  359. function _checkActivity($activity)
  360. {
  361. $actor = $activity->actor;
  362. if (empty($actor)) {
  363. return true;
  364. }
  365. $homepage = strtolower($actor->link);
  366. if (!empty($homepage)) {
  367. if (!$this->_checkUrl($homepage)) {
  368. // TRANS: Exception thrown trying to post a notice while having set a blocked homepage URL. %s is the blocked URL.
  369. $msg = sprintf(_m("Users from \"%s\" are blocked."),
  370. $homepage);
  371. throw new ClientException($msg);
  372. }
  373. }
  374. if (!empty($actor->poco)) {
  375. $nickname = strtolower($actor->poco->preferredUsername);
  376. if (!empty($nickname)) {
  377. if (!$this->_checkNickname($nickname)) {
  378. // TRANS: Exception thrown trying to post a notice while having a blocked nickname. %s is the blocked nickname.
  379. $msg = sprintf(_m("Notices from nickname \"%s\" are disallowed."),
  380. $nickname);
  381. throw new ClientException($msg);
  382. }
  383. }
  384. }
  385. return true;
  386. }
  387. /**
  388. * Check URLs and homepages for blacklisted users.
  389. */
  390. function onStartSubscribe(Profile $subscriber, Profile $other)
  391. {
  392. foreach ([$other->getUrl(), $other->getHomepage()] as $url) {
  393. if (empty($url)) {
  394. continue;
  395. }
  396. $url = strtolower($url);
  397. if (!$this->_checkUrl($url)) {
  398. // TRANS: Client exception thrown trying to subscribe to a person with a blocked homepage or site URL. %s is the blocked URL.
  399. $msg = sprintf(_m("Users from \"%s\" are blocked."),
  400. $url);
  401. throw new ClientException($msg);
  402. }
  403. }
  404. $nickname = $other->getNickname();
  405. if (!empty($nickname)) {
  406. if (!$this->_checkNickname($nickname)) {
  407. // TRANS: Client exception thrown trying to subscribe to a person with a blocked nickname. %s is the blocked nickname.
  408. $msg = sprintf(_m("Cannot subscribe to nickname \"%s\"."),
  409. $nickname);
  410. throw new ClientException($msg);
  411. }
  412. }
  413. return true;
  414. }
  415. }