BlacklistPlugin.php 15 KB

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