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