[PLUGINS] +set de base
[lhc/web/www.git] / www / plugins / nospam / nospam_pipelines.php
1 <?php
2 /**
3 * Plugin No-SPAM
4 * (c) 2008-2011 Cedric Morin Yterium.net
5 * Licence GPL
6 *
7 */
8
9 if (!defined("_ECRIRE_INC_VERSION")) return;
10 if (!defined('_SPAM_URL_MAX_OCCURENCES')) define('_SPAM_URL_MAX_OCCURENCES',3);
11
12 /**
13 * Lister les formulaires a prendre en charge contre le SPAM
14 * pour verifier le nobot et le jeton sur un formulaire, l'ajouter a cette liste
15 * par le pipeline nospam_lister_formulaires
16 * @return void
17 */
18 function nospam_lister_formulaires() {
19 if (!isset($GLOBALS['formulaires_no_spam']))
20 $GLOBALS['formulaires_no_spam'] = array();
21 $formulaires = array_merge($GLOBALS['formulaires_no_spam'], array('forum', 'ecrire_auteur', 'signature'));
22 return pipeline('nospam_lister_formulaires', $formulaires);
23 }
24
25 /**
26 * Ajouter le champ de formulaire 'nobot' au besoin
27 *
28 * @param array $flux
29 * @return array
30 */
31 function nospam_recuperer_fond($flux) {
32 // determiner le nom du formulaire
33 $fond = strval($flux['args']['fond']);
34 if (false !== $pos = strpos($fond, 'formulaires/')) {
35 $form = substr($fond, $pos + 12);
36 if (in_array($form, nospam_lister_formulaires())) {
37 // on ajoute le champ 'nobot' si pas present dans le formulaire
38 $texte = &$flux['data']['texte'];
39 if ((false === strpos($texte, 'name="nobot"'))
40 and (false !== $pos = strpos($texte, '</form>'))
41 ) {
42 $nobot = recuperer_fond("inclure/nobot", array('nobot' => ''));
43 $texte = substr_replace($texte, $nobot, $pos, 0);
44 }
45 }
46 }
47 return $flux;
48 }
49
50 /**
51 * Ajouter un jeton temporaire lie a l'heure et a l'IP pour limiter la reutilisation possible du formulaire
52 *
53 * @param array $flux
54 * @return array
55 */
56 function nospam_formulaire_charger($flux) {
57 $form = $flux['args']['form'];
58 if (in_array($form, nospam_lister_formulaires())
59 AND $flux['data']
60 AND is_array($flux['data'])
61 ) {
62 include_spip("inc/nospam");
63 $jeton = creer_jeton($form);
64 $flux['data']['_hidden'] .= "<input type='hidden' name='_jeton' value='$jeton' />";
65 }
66 return $flux;
67 }
68
69 /**
70 * Verifier le jeton temporaire lie a l'heure et a l'IP pour limiter la reutilisation possible du formulaire
71 *
72 * @param array $flux
73 * @return array
74 */
75 function nospam_formulaire_verifier($flux) {
76 $form = $flux['args']['form'];
77 if (in_array($form, nospam_lister_formulaires())) {
78 include_spip("inc/nospam");
79 $jeton = _request('_jeton');
80 // le jeton prend en compte l'heure et l'ip de l'internaute
81 if (_request('nobot') // trop facile !
82 OR (!verifier_jeton($jeton, $form))
83 ) {
84 #spip_log('pas de jeton pour '.var_export($flux,true),'nospam');
85 $flux['data']['message_erreur'] .= _T('nospam:erreur_jeton');
86 if ($form == 'forum')
87 unset($flux['data']['previsu']);
88 }
89
90 // pas la peine de filtrer les contenus postés par un admin
91 if (!isset($GLOBALS['visiteur_session']['statut']) OR $GLOBALS['visiteur_session']['statut'] != '0minirezo') {
92 if ($verifier_formulaire = charger_fonction("verifier_formulaire_$form", "nospam", true)) {
93 $flux = $verifier_formulaire($flux);
94 // recuperation de la liste des IPs blacklist/greylist
95 // async si on renvoie la previsu ou si erreur
96 $async = false;
97 if (count($flux['data']))
98 $async = true;
99 nospam_update_ip_list($async);
100 }
101 }
102 }
103 return $flux;
104 }
105
106 /**
107 * Au moment de decider du statut d'un forum,
108 * quelques verifications et une moderation si necessaire !
109 *
110 * @param array $flux
111 * @return array
112 */
113 function nospam_pre_edition($flux) {
114 if ($flux['args']['table'] == 'spip_forum'
115 AND $flux['args']['action'] == 'instituer'
116 ) {
117
118 // ne pas publier automatiquement certains messages suspects ...
119 // sauf si le posteur a de toute facon le pouvoir de moderer et de se publier
120 include_spip('inc/autoriser');
121 if ($flux['data']['statut'] == 'publie'
122 AND (!isset($GLOBALS['visiteur_session']['statut']) OR !autoriser('modererforum'))
123 ) {
124 // verifier le status de cette IP
125 nospam_check_ip_status($GLOBALS['ip']);
126
127 $email = strlen($flux['data']['email_auteur']) ? " OR email_auteur=" . sql_quote($flux['data']['email_auteur']) : "";
128 $spammeur_connu = (!isset($GLOBALS['visiteur_session']['statut'])
129 AND (
130 isset($GLOBALS['ip_greylist'][$GLOBALS['ip']])
131 OR isset($GLOBALS['ip_blacklist'][$GLOBALS['ip']])
132 OR sql_countsel('spip_forum', '(ip=' . sql_quote($GLOBALS['ip']) . "$email) AND statut='spam'") > 0
133 )
134 );
135
136 // activer aussi le flag spammeur connu en cas de flood, meme si aucune detection spam jusqu'ici
137 // on sera plus severe sur les liens dans ce cas
138 // cas du spammeur qui envoie que des messages a 3 liens a haute frequence (passe a travers tous les filtres)
139 // au bout du 5e message en <10min ou 10e en <30min on va moderer tout message avec un lien
140 if (!$spammeur_connu){
141 if (($nb=sql_countsel('spip_forum','(ip='.sql_quote($GLOBALS['ip']).$email.') AND '.sql_date_proche('date_heure','-30','minute')))>=7){
142 spip_log("[Flood] $nb message pour (ip=".$GLOBALS['ip']."$email) dans les 30 dernieres minutes",'nospam');
143 $spammeur_connu = true;
144 }
145 }
146 if (!$spammeur_connu){
147 if (($nb=sql_countsel('spip_forum','(ip='.sql_quote($GLOBALS['ip']).$email.') AND '.sql_date_proche('date_heure','-10','minute')))>=3){
148 spip_log("[Flood] $nb message pour (ip=".$GLOBALS['ip']."$email) dans les 10 dernieres minutes",'nospam');
149 $spammeur_connu = true;
150 }
151 }
152
153 $lang_suspecte = false;
154 // si c'est un spammeur connu,
155 // verifier que cette ip n'en est pas a son N-ieme spam en peu de temps
156 // a partir d'un moment on refuse carrement le spam massif, le posteur devra attendre pour reposter
157 if ($spammeur_connu) {
158 // plus de 30 spams dans les dernieres 2h, faut se calmer ...
159 // ou plus de 10 spams dans la dernieres 1h, faut se calmer ...
160 // ou IP blacklistee et plus de 5 messages prop/spam dans les dernieres 24h, faut se calmer ...
161 if (
162 (isset($GLOBALS['ip_blacklist'][$GLOBALS['ip']])
163 AND ($nb = sql_countsel('spip_forum', sql_in('statut',array('spam')).' AND (ip=' . sql_quote($GLOBALS['ip']).') AND ' . sql_date_proche('date_heure','-24','hour'))) >= 5
164 AND $h=24
165 )
166 OR
167 (($nb = sql_countsel('spip_forum', 'statut=\'spam\' AND (ip=' . sql_quote($GLOBALS['ip']) . $email . ') AND ' . sql_date_proche('date_heure','-120','minute'))) >= 30
168 AND $h=2)
169 OR
170 (($nb = sql_countsel('spip_forum', 'statut=\'spam\' AND (ip=' . sql_quote($GLOBALS['ip']) . $email .') AND ' . sql_date_proche('date_heure','-60','minute'))) >= 10
171 AND $h=1)
172 ){
173 $flux['data']['statut'] = ''; // on n'en veut pas !
174 spip_log("[Refuse] $nb spam pour (ip=" . $GLOBALS['ip'] . "$email) dans les $h dernieres heures", 'nospam');
175 return $flux;
176 }
177 }
178 // sinon regarder si l'objet a une langue, et si le post est dans la meme langue ou non
179 // en cas de langue differente, on se mefie
180 elseif ($flux['data']['objet']){
181 $table = table_objet_sql($flux['data']['objet']);
182 $trouver_table = charger_fonction("trouver_table","base");
183 if ($desc = $trouver_table($table)
184 AND isset($desc['field']['lang'])){
185 $primary = id_table_objet($flux['data']['objet']);
186 $lang_objet = sql_getfetsel("lang",$table,"$primary=".intval($flux['data']['id_objet']));
187 include_spip("inc/detecter_langue");
188 $lang_post = _detecter_langue($flux['data']['texte']);
189 if ($lang_post!==$lang_objet)
190 $lang_suspecte = true;
191 }
192 }
193
194 // si c'est un message bourre de liens, on le modere
195 // le seuil varie selon le champ et le fait que le spammeur est deja connu ou non
196 $seuils = array(
197 // seuils par defaut
198 0 => array(
199 0 => array(1 => 'prop', 3 => 'spam'), // seuils par defaut
200 'url_site' => array(2 => 'spam'), // 2 liens dans le champ url, c'est vraiment louche
201 'texte' => array(4 => 'prop', 20 => 'spam') // pour le champ texte
202 ),
203 // seuils severises pour les suspects : modere en prop des qu'il y a un lien, spam si plus de 5
204 'suspect' => array(
205 0 => array(1 => 'spam'),
206 'url_site' => array(2 => 'spam'), // 2 liens dans le champ url, c'est vraiment louche
207 'texte' => array(1 => 'prop', 5 => 'spam')
208 ),
209 // seuils pour les blacklist : si pas de lien on passe en prop par precaution, sinon en spam
210 'blacklist' => array(
211 0 => array(1 => 'spam'),
212 'url_site' => array(2 => 'spam'), // 2 liens dans le champ url, c'est vraiment louche
213 'texte' => array(0 => 'prop', 1 => 'spam')
214 )
215 );
216
217 $seuils = isset($GLOBALS['ip_blacklist'][$GLOBALS['ip']])? $seuils['blacklist'] : (($spammeur_connu OR $lang_suspecte) ? $seuils['suspect'] : $seuils[0]);
218 include_spip("inc/nospam"); // pour analyser_spams()
219 foreach ($flux['data'] as $champ => $valeur) {
220 $infos = analyser_spams($valeur);
221 if ($infos['contenu_cache']) {
222 // s'il y a du contenu caché avec des styles => spam direct
223 $flux['data']['statut'] = 'spam';
224 spip_log("\t" . $flux['data']['auteur'] . "\t" . $GLOBALS['ip'] . "\t" . "requalifié en spam car contenu cache", 'nospam');
225 }
226 elseif ($infos['nombre_liens'] > 0) {
227 // si un lien a un titre de moins de 3 caracteres, c'est louche...
228 if ($infos['caracteres_texte_lien_min'] < 3) {
229 $flux['data']['statut'] = 'prop'; // en dur en attendant une idee plus generique
230 spip_log("\t" . $flux['data']['auteur'] . "\t" . $GLOBALS['ip'] . "\t" . "requalifié en prop car moins de 3car hors liens", 'nospam');
231 }
232
233 if (isset($seuils[$champ]))
234 $seuil = $seuils[$champ];
235 else
236 $seuil = $seuils[0];
237
238 foreach ($seuil as $s => $stat)
239 if ($infos['nombre_liens'] >= $s) {
240 $flux['data']['statut'] = $stat;
241 spip_log("\t" . $flux['data']['auteur'] . "\t" . $GLOBALS['ip'] . "\t" . "requalifié en " . $stat . " car nombre_liens >= " . $s, 'nospam');
242 }
243
244 if ($flux['data']['statut'] != 'spam') {
245 $champs = array_unique(array('texte', $champ));
246 if ($h = rechercher_presence_liens_spammes($infos['liens'], _SPAM_URL_MAX_OCCURENCES, 'spip_forum', $champs)) {
247 $flux['data']['statut'] = 'spam';
248 spip_log("\t" . $flux['data']['auteur'] . "\t" . $GLOBALS['ip'] . "\t" . "requalifié en spam car lien $h deja dans un spam", 'nospam');
249 }
250 }
251 }
252 }
253
254
255 // verifier qu'un message identique n'a pas ete publie il y a peu
256 if ($flux['data']['statut'] != 'spam') {
257 if (sql_countsel('spip_forum', 'texte=' . sql_quote($flux['data']['texte']) . " AND statut IN ('publie','off','spam')") > 0){
258 $flux['data']['statut'] = 'spam';
259 spip_log("\t" . $flux['data']['auteur'] . "\t" . $GLOBALS['ip'] . "\t" . "requalifié en spam car message identique deja existant", 'nospam');
260 }
261 }
262 // verifier que cette ip n'en est pas a son N-ieme post en peu de temps
263 // plus de 5 messages en 5 minutes c'est suspect ...
264 if ($flux['data']['statut'] != 'spam') {
265 if (($nb = sql_countsel('spip_forum', 'ip=' . sql_quote($GLOBALS['ip']) . ' AND ' . sql_date_proche('date_heure','-5','minute'))) >= 5){
266 $flux['data']['statut'] = 'spam';
267 spip_log("[Flood2] $nb message pour (ip=".$GLOBALS['ip']."$email) dans les 5 dernieres minutes : requalif en spam",'nospam');
268 }
269 }
270 }
271 }
272 return $flux;
273 }
274
275
276
277 /**
278 * Fermer la connexion pour que le visiteur n'attende pas apres le curl sur nospam.spip.net
279 * @param $content
280 * @return mixed
281 */
282 function nospam_flush_close($content){
283 header("Content-Length: ".($l=ob_get_length()));
284 header("Connection: close");
285 return $content;
286 }
287
288 /**
289 * Flusher et lancer l'update de la liste des ip
290 */
291 function nospam_flush_and_update(){
292 chdir(_ROOT_CWD); // securite en cas de register_shutdown_function
293 // forcer le flush des tampons pas envoyes (declenche le content-length/conection:close envoye dans cache_cool_flush)
294 $flush_level = ob_get_level();
295 while ($flush_level--) ob_end_flush();
296 flush();
297 if (function_exists('fastcgi_finish_request'))
298 fastcgi_finish_request();
299 nospam_update_ip_list();
300 }
301
302 if (!defined('_NOSPAM_IP_LIST_CACHE')) define('_NOSPAM_IP_LIST_CACHE',10800);
303 /**
304 * Recuperer la liste des IP black ou grey sur nospam.spip.net
305 * si on a pas une liste a jour
306 * et la stocker dans un fichier
307 * @param bool $async
308 */
309 function nospam_update_ip_list($async=false){
310 $file = _DIR_TMP."nospam_ip_list.txt";
311 if (file_exists($file) AND filemtime($file)>time()-_NOSPAM_IP_LIST_CACHE)
312 return;
313 spip_log("nospam_update_ip_list:$async","nospam");
314
315 if ($async){
316 // indiquer de fermer la connexion dans la foulee
317 // pour faire le hit de recuperation async hors temps d'attente
318 ob_start("nospam_flush_close");
319 register_shutdown_function("nospam_flush_and_update");
320 return;
321 }
322
323 // on fait d'abord un touch car si le recuperer_page echoue (hebergeurs qui interdisent)
324 // on ne veut pas recommencer plein de fois de suite
325 @touch($file);
326 $url_api = "http://nospam.spip.net/spamsignal.api/list";
327 include_spip("inc/distant");
328 include_spip("inc/json");
329 $res = recuperer_page($url_api);
330 if ($res
331 AND function_exists("json_decode")
332 AND $liste = json_decode($res,true)){
333 ecrire_fichier($file,serialize($liste));
334 }
335 }
336
337 /**
338 * Verifier le status d'une IP et la noter dans la globale ip_blacklist ou ip_greylist si c'est une IP louche
339 * @param $ip
340 * @return string
341 * ok|grey|black
342 */
343 function nospam_check_ip_status($ip){
344 $file = _DIR_TMP."nospam_ip_list.txt";
345 if (!file_exists($file) OR filemtime($file)<time()-_NOSPAM_IP_LIST_CACHE)
346 return;
347
348 lire_fichier($file,$liste);
349 spip_log("nospam_check_ip_status:$ip","nospam");
350 if ($liste = unserialize($liste)){
351 #spip_log($liste,"nospam");
352 $now = date('Y-m-d H:i:s');
353 $ip_family = preg_replace(",([.:])[^.:]+$,","$1*",$ip);
354 spip_log("ip $ip famille $ip_family","nospam");
355 foreach(array("blacklist","greylist") AS $l){
356 if (isset($liste[$l][$ip])
357 AND $liste[$l][$ip]>$now){
358 $GLOBALS['ip_'.$l][$ip] = true;
359 spip_log("$ip ajoute a ip_$l","nospam");
360 return ($l=="blacklist"?"black":"grey");
361 }
362 if (isset($liste[$l][$ip_family])
363 AND $liste[$l][$ip_family]>$now){
364 $GLOBALS['ip_'.$l][$ip] = true;
365 spip_log("$ip ajoute a ip_$l (famille $ip_family)","nospam");
366 return ($l=="blacklist"?"black":"grey");
367 }
368 }
369 }
370 return "ok";
371 }
372 ?>