Passed
Push — dpa_spam3 ( d76b2d )
by David
09:28
created

delete_team()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 14
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 16
rs 9.7998
1
#!/usr/bin/env php
2
0 ignored issues
show
Coding Style introduced by
PHP files must only contain PHP code
Loading history...
3
<?php
0 ignored issues
show
Coding Style introduced by
The opening PHP tag must be the first content in the file
Loading history...
4
// This file is part of BOINC.
5
// http://boinc.berkeley.edu
6
// Copyright (C) 2024 University of California
7
//
8
// BOINC is free software; you can redistribute it and/or modify it
9
// under the terms of the GNU Lesser General Public License
10
// as published by the Free Software Foundation,
11
// either version 3 of the License, or (at your option) any later version.
12
//
13
// BOINC is distributed in the hope that it will be useful,
14
// but WITHOUT ANY WARRANTY; without even the implied warranty of
15
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16
// See the GNU Lesser General Public License for more details.
17
//
18
// You should have received a copy of the GNU Lesser General Public License
19
// along with BOINC.  If not, see <http://www.gnu.org/licenses/>.
20
// -----------------------------------------------
21
22
// delete_spammers.php [--test] [--min_days n] [--max_days n] command
23
//
24
// script to delete spammer accounts, profiles, forum posts, and/or teams.
25
// The various options delete different categories of spammers.
26
//
27
// USE WITH CARE.  You don't want to delete legit accounts.
28
// Run with --test and examine the results first.
29
30
// In this context, 'spam' is text that advertises something
31
// (e.g. viagra, porn sites etc.) and contains hyperlinks.
32
// Spammers can put such text in
33
// - profiles
34
// - team URLs or descriptions
35
// - account URLs
36
// - forum posts
37
//
38
// All the above can legitimately contain links,
39
// so to decide what's spam we look at attributes of the user:
40
// - whether they've attached a client to the project
41
//      Most spammers haven't, so they have no hosts.
42
//      Note: legit users might create an account just to participate
43
//      in the project forums (e.g. Science United users).
44
//      So we generally need to check for forum activity.
45
// - whether they've been granted any credit
46
//      This is more stringent.
47
//      But we need to take into account that it may take a month or two
48
//      to get credit because of validation
49
//
50
// When we identify spam, we delete everything associated with that user:
51
// - profile
52
// - forum stuff: post, thread, subscriptions etc.
53
// - private messages
54
// - friend links
55
// - badges
56
57
// options:
58
// --min_days N
59
//    Only delete accounts created at least N days ago
60
// --max_days N
61
//    Only delete accounts created at most N days ago
62
// --test
63
//    Show what accounts would be deleted, but don't delete them
64
//
65
// commands:
66
//
67
// --profiles
68
//   delete accounts that
69
//   - have a profile containing a link.
70
//   - have no hosts
71
//   - have no message-board posts
72
//
73
// --user_url
74
//   delete accounts that
75
//   - have a nonempty URL
76
//   - have no hosts
77
//   - have no message-board posts
78
//   Use for spammers who create accounts with commercial URLs.
79
//
80
// --user_null
81
//   delete accounts that
82
//   - have no hosts
83
//   - have no message-board posts
84
//   - don't belong to a team
85
//   Spammers may create accounts and attempt to create a profile but fail.
86
//   This cleans up those accounts.
87
//
88
// --forums
89
//   delete accounts that
90
//   - have no hosts
91
//   - have message-board posts
92
//   - don't belong to a team (to avoid deleting BOINC-wide team owners)
93
//   Use this for spammers who create accounts and post spam
94
//
95
// --profiles_strict
96
//  delete accounts that have a profile and no message-board posts.
97
//  For the BOINC message boards.
98
//
99
// --list filename
100
//   "filename" contains a list of user IDs, one per line.
101
//
102
// --id_range N M
103
//   delete users with ID N to M inclusive
104
//
105
// --teams
106
//   delete teams (and their owners and members) where the team
107
//   - has no total credit
108
//   - has description containing a link, or a URL
109
//   - is not a BOINC-Wide team
110
//   and the owner and members
111
//   - have no posts
112
//   - have no hosts
113
//
114
// --all (recommended for BOINC projects)
115
//   Does: --teams --user_url --profiles
116
//   Doesn't do --forums (see comments above).
117
//   Can use moderators for that.
118
119
error_reporting(E_ALL);
120
ini_set('display_errors', true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $value of ini_set(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

120
ini_set('display_errors', /** @scrutinizer ignore-type */ true);
Loading history...
121
ini_set('display_startup_errors', true);
122
ini_set('memory_limit', '4G');
123
124
require_once("../inc/db.inc");
125
require_once("../inc/profile.inc");
126
require_once("../inc/forum.inc");
127
require_once("../inc/user_util.inc");
128
db_init();
129
130
$min_days = 0;
131
$max_days = 0;
132
$test = false;
133
134
// delete a spammer account, and everything associated with it
135
//
136
function do_delete_user($user) {
137
    global $test;
138
    $age = (time() - $user->create_time) / 86400;
139
    echo "deleting user\n";
140
    echo "   ID: $user->id\n";
141
    echo "   email: $user->email_addr\n";
142
    echo "   name: $user->name\n";
143
    echo "   URL: $user->url\n";
144
    echo "   age:$age days\n";
145
    if ($test) {
146
        $n = count(BoincHost::enum("userid=$user->id"));
147
        $m = count(BoincPost::enum("user=$user->id"));
148
        echo "   $n hosts\n";
149
        echo "   $m posts\n";
150
        echo "   (test mode - nothing deleted)\n";
151
        return;
152
    }
153
    delete_user($user);
154
}
155
156
function delete_list($fname) {
157
    $f = fopen($fname, "r");
158
    if (!$f) die("no such file $fname\n");
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
introduced by
$f is of type resource, thus it always evaluated to false.
Loading history...
159
    while ($s = fgets($f)) {
160
        $s = trim($s);
161
        if (!is_numeric($s)) die("bad ID $s\n");
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
162
        $user = BoincUser::lookup_id((int)$s);
163
        if ($user) {
164
            do_delete_user($user);
165
        } else {
166
            echo "no user ID $s\n";
167
        }
168
    }
169
}
170
171
function has_link($x) {
172
    if (strstr($x, "<a ")) return true;
173
    if (strstr($x, "[url")) return true;
174
    if (strstr($x, "http://")) return true;
175
    if (strstr($x, "https://")) return true;
176
    if (strstr($x, "www.")) return true;
177
    return false;
178
}
179
180
function delete_forums() {
181
    global $min_days, $max_days;
182
    $prefs = BoincForumPrefs::enum("posts>0");
183
    $n = 0;
184
    foreach ($prefs as $p) {
185
        $user = BoincUser::lookup_id($p->userid);
186
        if (!$user) {
187
            echo "missing user $p->userid\n";
188
            continue;
189
        }
190
        if ($min_days) {
191
            if ($user->create_time > time() - $min_days*86400) continue;
192
        }
193
        if ($max_days) {
194
            if ($user->create_time < time() - $max_days*86400) continue;
195
        }
196
        if ($user->teamid) {
197
            continue;
198
        }
199
        $h = BoincHost::count("userid=$p->userid");
200
        if ($h) continue;
201
        do_delete_user($user);
202
        $n++;
203
    }
204
    echo "deleted $n users\n";
205
}
206
207
function delete_profiles() {
208
    global $test, $min_days, $max_days;
209
    $profiles = BoincProfile::enum("");
210
    $n = 0;
211
    foreach ($profiles as $p) {
212
        if (has_link($p->response1) || has_link($p->response2)) {
213
            $user = BoincUser::lookup_id($p->userid);
214
            if (!$user) {
215
                echo "profile has missing user: $p->userid\n";
216
                continue;
217
            }
218
219
            if ($min_days) {
220
                if ($user->create_time > time() - $min_days*86400) continue;
221
            }
222
            if ($max_days) {
223
                if ($user->create_time < time() - $max_days*86400) continue;
224
            }
225
226
            $m = BoincHost::count("userid=$p->userid");
227
            if ($m) continue;
228
            $m = BoincPost::count("user=$p->userid");
229
            if ($m) continue;
230
231
            do_delete_user($user);
232
            if ($test) {
233
                echo "\n$p->userid\n$p->response1\n$p->response2\n";
234
            }
235
            $n++;
236
        }
237
    }
238
    echo "deleted $n users\n";
239
}
240
241
function delete_profiles_strict() {
242
    global $test;
243
    $profiles = BoincProfile::enum("");
244
    foreach ($profiles as $p) {
245
        $user = BoincUser::lookup_id($p->userid);
246
        if (!$user) {
247
            echo "profile has missing user: $p->userid\n";
248
            continue;
249
        }
250
        $n = BoincPost::count("user=$p->userid");
251
        if ($n) continue;
252
        do_delete_user($user);
253
        if ($test) {
254
            echo "\n$p->userid\n$p->response1\n$p->response2\n";
255
        }
256
    }
257
}
258
259
function delete_users($no_hosts, $no_posts, $no_teams, $have_url) {
260
    global $test, $min_days, $max_days;
261
    $db = BoincDb::get();
262
    $query = "select a.* from user a ";
263
    if ($no_hosts) {
264
        $query .= " left join host c on c.userid=a.id ";
265
    }
266
    if ($no_posts) {
267
        $query .= " left join post b on a.id=b.user ";
268
    }
269
    if ($no_teams) {
270
        $query .= " left join team d on a.id=d.userid ";
271
    }
272
    $query .= " where true ";
273
    if ($no_hosts) {
274
        $query .= " and c.userid is null ";
275
    }
276
    if ($no_posts) {
277
        $query .= " and b.user is null ";
278
    }
279
    if ($no_teams) {
280
        $query .= " and d.userid is null ";
281
    }
282
    if ($min_days) {
283
        $t = time() - $min_days*86400;
284
        $query .= " and a.create_time < $t ";
285
    }
286
    if ($max_days) {
287
        $t = time() - $max_days*86400;
288
        $query .= " and a.create_time > $t ";
289
    }
290
291
    $result = $db->do_query($query);
292
    $n = 0;
293
    while ($u = $result->fetch_object()) {
294
        $user = BoincUser::lookup_id($u->id);
295
        if (!$user) {
296
            continue;
297
        }
298
        if ($have_url) {
299
            if (!strlen($user->url)) continue;
300
        }
301
        do_delete_user($user);
302
        $n++;
303
    }
304
    echo "deleted $n users\n";
305
}
306
307
function delete_banished() {
308
    global $min_days, $max_days;
309
    $fps = BoincForumPrefs::enum("banished_until>0");
310
    foreach ($fps as $fp) {
311
        $user = BoincUser::lookup_id($fp->userid);
312
        if (!$user) continue;
313
        if ($user->create_time > time() - $min_days*86400) continue;
314
        if ($user->create_time < time() - $max_days*86400) continue;
315
        do_delete_user($user);
316
    }
317
}
318
319
function delete_teams() {
320
    global $min_days, $max_days, $test;
321
    $query = "nusers < 2 and seti_id=0 and total_credit=0";
322
    if ($min_days) {
323
        $x = time() - $min_days*86400;
324
        $query .= " and create_time < $x";
325
    }
326
    if ($max_days) {
327
        $x = time() - $max_days*86400;
328
        $query .= " and create_time > $x";
329
    }
330
    $teams = BoincTeam::enum($query);
331
    $count = 0;
332
    foreach ($teams as $team) {
333
        $founder = null;
334
        if ($team->userid) {
335
            $founder = BoincUser::lookup_id($team->userid);
336
        }
337
338
        // delete teams with no founder
339
        if (!$founder) {
340
            delete_team($team, []);
341
            $count++;
342
            continue;
343
        }
344
345
        $n = team_count_members($team->id);
346
        if ($n > 1) continue;
347
        if (!has_link($team->description) && !$team->url) continue;
348
349
        // get list of team members
350
        //
351
        $users = BoincUser::enum("teamid = $team->id");
352
353
        // add team founder if not member
354
        //
355
        if ($founder->teamid != $team->id) {
356
            $users[] = $founder;
357
        }
358
359
        // if any of these users has signs of life, skip team
360
        //
361
        $life = false;
362
        foreach ($users as $user) {
363
            if ($user->seti_nresults) {
364
                // for SETI@home
365
                $life = true;
366
                break;
367
            }
368
            $n = BoincPost::count("user=$user->id");
369
            if ($n) {
370
                $life = true;
371
                break;
372
            }
373
            $n = BoincHost::count("userid=$user->id");
374
            if ($n) {
375
                $life = true;
376
                break;
377
            }
378
        }
379
        if ($life) {
380
            continue;
381
        }
382
383
        $count++;
384
        delete_team($team, $users);
385
    }
386
    echo "deleted $count teams\n";
387
}
388
389
function delete_team($team, $users) {
390
    global $test;
391
    if ($test) {
392
        echo "would delete team:\n";
393
        echo "   ID: $team->id\n";
394
        echo "   name: $team->name\n";
395
        echo "   description: $team->description\n";
396
        echo "   URL: $team->url\n";
397
        foreach ($users as $user) {
398
            echo "would delete user $user->id: $user->email_addr:\n";
399
        }
400
    } else {
401
        $team->delete();
402
        echo "deleted team ID $team->id name $team->name\n";
403
        foreach ($users as $user) {
404
            do_delete_user($user);
405
        }
406
    }
407
}
408
409
function delete_user_id($id) {
410
    $user = BoincUser::lookup_id($id);
411
    if ($user) {
412
        echo "deleting user $id\n";
413
        do_delete_user($user);
414
    } else {
415
        echo "no such user\n";
416
    }
417
}
418
419
function delete_user_id_range($id1, $id2) {
420
    for ($i=$id1; $i <= $id2; $i++) {
421
        $user = BoincUser::lookup_id($i);
422
        if ($user) {
423
            echo "deleting user $i\n";
424
            do_delete_user($user);
425
        }
426
    }
427
}
428
429
// this is for cleaning up BOINC-wide teams
430
//
431
function delete_team_id_range($id1, $id2) {
432
    for ($i=$id1; $i <= $id2; $i++) {
433
        echo "deleting team $i\n";
434
        $team = BoincTeam::lookup_id($i);
435
        if ($team) {
436
            $team->delete();
437
            $user = BoincUser::lookup_id($team->userid);
438
            if ($user) $user->delete();
439
        }
440
    }
441
}
442
443
echo "Starting: ".date(DATE_RFC2822)."\n";
444
445
// get settings first
446
//
447
for ($i=1; $i<$argc; $i++) {
448
    if ($argv[$i] == "--test") {
449
        $test = true;
450
    } else if ($argv[$i] == "--min_days") {
451
        $min_days = $argv[++$i];
452
    } else if ($argv[$i] == "--max_days") {
453
        $max_days = $argv[++$i];
454
    } else if ($argv[$i] == "--days") {     // deprecated
455
        $max_days = $argv[++$i];
456
    }
457
}
458
459
// then do actions
460
//
461
for ($i=1; $i<$argc; $i++) {
462
    if ($argv[$i] == "--list") {
463
        delete_list($argv[++$i]);
464
    } else if ($argv[$i] == "--profiles") {
465
        delete_profiles();
466
    } else if ($argv[$i] == "--profiles_strict") {
467
        delete_profiles_strict();
468
    } else if ($argv[$i] == "--forums") {
469
        delete_forums();
470
    } else if ($argv[$i] == "--id_range") {
471
        $id1 = $argv[++$i];
472
        $id2 = $argv[++$i];
473
        if (!is_numeric($id1) || !is_numeric($id2)) {
474
            die ("bad args\n");
475
        }
476
        if ($id2 < $id1) {
477
            die("bad args\n");
478
        }
479
        delete_user_id_range($id1, $id2);
480
    } else if ($argv[$i] == "--id") {
481
        $id = $argv[++$i];
482
        if (!is_numeric($id)) {
483
            die ("bad arg\n");
484
        }
485
        delete_user_id($id);
486
    } else if ($argv[$i] == "--team_id_range") {
487
        $id1 = $argv[++$i];
488
        $id2 = $argv[++$i];
489
        if (!is_numeric($id1) || !is_numeric($id2)) {
490
            die ("bad args\n");
491
        }
492
        if ($id2 < $id1) {
493
            die("bad args\n");
494
        }
495
        delete_team_id_range($id1, $id2);
496
    } else if ($argv[$i] == "--banished") {
497
        delete_banished();
498
    } else if ($argv[$i] == "--teams") {
499
        delete_teams();
500
    } else if ($argv[$i] == "--user_url") {
501
        delete_users(true, true, false, true);
502
    } else if ($argv[$i] == "--user_null") {
503
        delete_users(true, true, true, false);
504
    } else if ($argv[$i] == "--all") {
505
        delete_profiles();
506
        delete_teams();
507
        delete_users(true, true, false, true);
508
    }
509
}
510
echo "Finished: ".date(DATE_RFC2822)."\n";
511
512
?>
513