1<?php
2// This file is part of BOINC.
3// http://boinc.berkeley.edu
4// Copyright (C) 2008 University of California
5//
6// BOINC is free software; you can redistribute it and/or modify it
7// under the terms of the GNU Lesser General Public License
8// as published by the Free Software Foundation,
9// either version 3 of the License, or (at your option) any later version.
10//
11// BOINC is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14// See the GNU Lesser General Public License for more details.
15//
16// You should have received a copy of the GNU Lesser General Public License
17// along with BOINC.  If not, see <http://www.gnu.org/licenses/>.
18
19require_once("../inc/util.inc");
20require_once("../inc/boinc_db.inc");
21require_once("../inc/forum_db.inc");
22require_once("../inc/forum.inc");
23require_once("../inc/sanitize_html.inc");
24require_once("../inc/countries.inc");
25require_once("../inc/credit.inc");
26require_once("../inc/team_types.inc");
27require_once("../inc/time.inc");
28require_once("../inc/stats_sites.inc");
29
30function team_search_form($params) {
31    if (!$params) {
32        $params = new StdClass;
33        $params->keywords = "";
34        $params->country = "";
35        $params->type = "";
36        $params->active = false;
37    }
38    echo '<form name="form" action="team_search.php">';
39    start_table();
40    row2('<b>'.tra('Search criteria (use one or more)').'</b>', '');
41    row2(
42        tra('Key words').'<br><small>'.tra('Find teams with these words in their names or descriptions').'</small>',
43        '<input class="form-control" type="text" name="keywords" value="' . htmlspecialchars($params->keywords) . '">');
44    row2_init(tra('Country'), '');
45    echo '<select class="form-control" name="country"><option value="" selected>---</option>';
46    $country = $params->country;
47    if (!$country || $country == 'None') $country = "XXX";
48    echo country_select_options($country);
49    echo "</select></td></tr>\n";
50    row2(tra('Type of team'), team_type_select($params->type, true));
51    $checked = $params->active?"checked":"";
52    row2(tra('Show only active teams'), "<input type=checkbox name=active $checked>");
53    row2("", "<input class=\"btn btn-primary\" type=submit name=submit value=\"".tra('Search')."\">");
54    end_table();
55    echo '</form>';
56}
57
58function foundership_transfer_link($user, $team) {
59    $now = time();
60    if ($team->ping_user == $user->id) {
61        if (transfer_ok($team, $now)) {
62            return tra('Requested by you, and founder response deadline has passed.').'
63                <br>
64                <a href="team_founder_transfer_form.php">'.tra('Complete foundership transfer').'</a>.
65            ';
66        } else {
67            $deadline = date_str(transfer_ok_time($team));
68            return '<a href="team_founder_transfer_form.php">'.tra('Requested by you').'</a>; '.tra('founder response deadline is %1', $deadline);
69        }
70    }
71    if (new_transfer_request_ok($team, $now)) {
72        if ($team->userid == $user->id) {
73            return tra('None');
74        } else {
75            return '<a href="team_founder_transfer_form.php">'.tra('Initiate request').'</a>';
76        }
77    }
78    return '<a href="team_founder_transfer_form.php">'.tra('Deferred').'</a>';
79}
80
81// $team is the team record with a bunch of additional data
82// (see team_display.php)
83// $user is viewer (not necessarily team founder) or null
84//
85function display_team_page($team, $user) {
86    global $team_name_sites;
87    page_head("$team->name");
88
89    echo sanitize_html($team->name_html);
90    echo "<p>";
91    start_table();
92    row1(tra('Team info'));
93    if (strlen($team->description)) {
94        row2(tra('Description'), sanitize_html($team->description));
95    }
96    row2("Created", date_str($team->create_time));
97    if (defined("SHOW_NONVALIDATED_TEAMS")) {
98        $founder = $team->founder;
99        row2("Founder email validated", $founder->email_validated?"Yes":"No (team will not be exported)");
100    }
101    if (strlen($team->url)) {;
102        if (strstr($team->url, "http://")) {
103            $x = $team->url;
104        } else {
105            $x = "http://$team->url";
106        }
107        row2(tra('Web site'), "<a href=$x>$x</a>");
108    }
109
110    if (!NO_COMPUTING) {
111        row2(tra('Total credit'), format_credit_large($team->total_credit));
112        row2(tra('Recent average credit'), format_credit_large($team->expavg_credit));
113        if (function_exists('project_team_credit')) {
114            project_team_credit($team);
115        }
116        show_badges_row(false, $team);
117
118        $x = "";
119        shuffle($team_name_sites);
120        foreach ($team_name_sites as $t) {
121            $url = $t[0];
122            $site_name = $t[1];
123            $encoding = $t[2];
124            if ($encoding == "hashlc") {
125                $key = md5(strtolower($team->name));
126            } else if ($encoding == 'hash') {
127                $key = md5($team->name);
128            } else {
129                $key = urlencode($team->name);
130            }
131            $x .= "<a href=$url".$key.">$site_name</a><br>\n";
132        }
133        row2(tra('Cross-project stats'), $x);
134    }
135    row2(tra('Country'), $team->country);
136    row2(tra('Type'), team_type_name($team->type));
137
138    if ($team->forum && is_forum_visible_to_user($team->forum, $user)) {
139        $f = $team->forum;
140        row2('<a href="team_forum.php?teamid='.$team->id.'">'.tra('Message board').'</a>',
141            tra('Threads').': '.$f->threads.'<br>'.tra('Posts').': '.$f->posts.'<br>'.tra('Last post').': '.time_diff_str($f->timestamp, time())
142        );
143    }
144    if ($user) {
145        if ($user->teamid != $team->id) {
146            if ($team->joinable) {
147                $tokens = url_tokens($user->authenticator);
148                row2("",
149                    '<a href="team_join.php?'.$tokens.'&amp;teamid='.$team->id.'">'.tra('Join this team').'</a>
150                    <br><p class=\"text-muted\">'.tra('Note: if \'OK to email\' is set in your project preferences, joining a team gives its founder access to your email address.').'</p>'
151                );
152            } else {
153                row2(tra("Not accepting new members"), "");
154            }
155        }
156        if (($user->teamid == $team->id)) {
157            if (($user->id == $team->userid)) {
158                if ($team->ping_user) {
159                    $deadline = date_str(transfer_ok_time($team));
160                    row2(tra('Foundership change requested'),
161                        '<a href="team_change_founder_form.php?teamid='.$team->id.'">'.tra('Respond by %1', $deadline).'</a>'
162                    );
163                }
164            } else {
165                row2(tra('Team foundership change'), foundership_transfer_link($user, $team));
166            }
167        }
168    }
169    row1(tra('Members'));
170    row2(tra('Founder'),
171        $team->founder?user_links($team->founder, BADGE_HEIGHT_MEDIUM):"---"
172    );
173    if (count($team->admins)) {
174        $first = true;
175        $x = "";
176        foreach ($team->admins as $a) {
177            if ($first) {
178                $first = false;
179            } else {
180                $x .= " &middot; ";
181            }
182            $x .= user_links($a, BADGE_HEIGHT_MEDIUM);
183        }
184        row2(tra('Admins'), $x);
185    }
186    $x = "0";
187    if (count($team->new_members)) {
188        $first = true;
189        $x = "";
190        foreach ($team->new_members as $a) {
191            if ($first) {
192                $first = false;
193            } else {
194                $x .= " &middot; ";
195            }
196            $x .= user_links($a, BADGE_HEIGHT_MEDIUM);
197        }
198    }
199    row2(tra('New members in last day'), $x);
200    row2(tra('Total members'), "$team->nusers (<a href=team_members.php?teamid=$team->id&amp;offset=0&amp;sort_by=expavg_credit>".tra('view')."</a>)");
201    row2(tra('Active members'), "$team->nusers_active (<a href=team_members.php?teamid=$team->id&amp;offset=0&amp;sort_by=expavg_credit>".tra('view')."</a>)");
202    row2(tra('Members with credit'), "$team->nusers_worked (<a href=team_members.php?teamid=$team->id&amp;offset=0&amp;sort_by=total_credit>".tra('view')."</a>)");
203    end_table();
204}
205
206function display_team_members($team, $offset, $sort_by) {
207    $n = 20;
208
209    $admins = BoincTeamAdmin::enum("teamid=$team->id");
210
211    // there aren't indices to support sorting by credit.
212    // set the following variable to disable sorted output.
213    // (though since caching is generally used this shouldn't be needed)
214    //
215    $nosort = false;
216
217    if ($sort_by == "total_credit") {
218        $sort_clause = "total_credit desc";
219    } else {
220        $sort_clause = "expavg_credit desc";
221    }
222
223    start_table();
224    $x = array();
225    $x[] = tra('Name');
226    if (!NO_COMPUTING) {
227        if ($nosort) {
228            $x[] = tra('Total credit');
229            $x[] = tra('Recent average credit');
230        } else {
231            if ($sort_by == "total_credit") {
232                $x[] = tra('Total credit');
233            } else {
234                $x[] = "<href=team_members.php?teamid=$team->id&amp;sort_by=total_credit&amp;offset=$offset>".tra('Total credit')."</a>";
235            }
236            if ($sort_by == "expavg_credit") {
237                $x[] = tra('Recent average credit');
238            } else {
239                $x[] = "<href=team_members.php?teamid=$team->id&amp;sort_by=expavg_credit&amp;offset=$offset>".tra('Recent average credit').'</a>';
240            }
241        }
242    }
243
244    $x[] = tra('Country');
245    $a = array("", ALIGN_RIGHT, ALIGN_RIGHT, "");
246    row_heading_array($x, $a);
247
248    $cache_args = "teamid=".$team->id."&mosort=".$nosort."&order=".$sort_clause."&limit=".$offset."_".$n;
249    $users = unserialize(get_cached_data(TEAM_PAGE_TTL, $cache_args));
250    if (!$users) {
251        if ($nosort) {
252            $users = BoincUser::enum("teamid=$team->id limit $offset,$n");
253        } else {
254            $users = BoincUser::enum("teamid=$team->id order by $sort_clause limit $offset,$n");
255        }
256        set_cached_data(TEAM_PAGE_TTL, serialize($users), $cache_args);
257    }
258
259    $j = $offset + 1;
260    foreach ($users as $user) {
261        $user_total_credit = format_credit_large($user->total_credit);
262        $user_expavg_credit = format_credit($user->expavg_credit);
263        $x = user_links($user, BADGE_HEIGHT_MEDIUM);
264        if ($user->id == $team->userid) {
265            $x .= ' ['.tra('Founder').']';
266        } else if (is_team_admin_aux($user, $admins)) {
267            $x .= ' ['.tra('Admin').']';
268        }
269        echo "<tr class=row1>
270            <td align=left>$j) $x
271        </td>";
272        if (!NO_COMPUTING) {
273            echo "
274                <td align=right>$user_total_credit</td>
275                <td align=right>$user_expavg_credit</td>
276            ";
277        }
278        echo "
279            <td>$user->country</td>
280            </tr>
281        ";
282        $j++;
283    }
284    echo "</table>";
285
286    if ($offset > 0) {
287        $new_offset = $offset - $n;
288        echo "<a href=team_members.php?teamid=$team->id&amp;sort_by=$sort_by&amp;offset=$new_offset>".tra('Previous %1', $n)."</a> &middot; ";
289    }
290    if ($j == $offset + $n + 1) {
291        $new_offset = $offset + $n;
292        echo "<a href=team_members.php?teamid=$team->id&amp;sort_by=$sort_by&amp;offset=$new_offset>".tra('Next %1', $n)."</a>";
293    }
294}
295
296// check that the team exists
297//
298function require_team($team) {
299    if (!$team) {
300        error_page(tra('No such team.'));
301    }
302}
303
304function is_team_founder($user, $team) {
305    return $user->id == $team->userid;
306}
307
308// check that the user is founder of the team
309//
310function require_founder_login($user, $team) {
311    require_team($team);
312    if ($user->id != $team->userid) {
313        error_page(tra('This operation requires foundership.'));
314    }
315}
316
317function is_team_admin($user, $team) {
318    if (!$user) return false;
319    if ($user->id == $team->userid) return true;
320    $admin = BoincTeamAdmin::lookup($team->id, $user->id);
321    if ($admin) return true;
322    return false;
323}
324
325// use this when you're displaying a long list of users
326// and don't want to do a lookup for each one
327//
328function is_team_admin_aux($user, $admins) {
329    foreach ($admins as $a) {
330        if ($a->userid == $user->id) return true;
331    }
332    return false;
333}
334
335function require_admin($user, $team) {
336    if (!is_team_admin($user, $team)) {
337        error_page(tra('This operation requires team admin privileges'));
338    }
339}
340
341function new_member_list($teamid) {
342    $new_members = array();
343    $yesterday = time() - 86400;
344    $deltas = BoincTeamDelta::enum("teamid=$teamid and timestamp>$yesterday and joining=1 group by userid");
345    if (count($deltas)) {
346        foreach ($deltas as $delta) {
347            $u = BoincUser::lookup_id($delta->userid);
348            if ($u->teamid == $teamid) {
349                $new_members[] = $u;  // they might have later quit
350            }
351        }
352    }
353    return $new_members;
354}
355
356function admin_list($teamid) {
357    $u = array();
358    $admins = BoincTeamAdmin::enum("teamid=$teamid");
359    foreach ($admins as $admin) {
360        $user = BoincUser::lookup_id($admin->userid);
361        $u[] = $user;
362    }
363    return $u;
364}
365
366function team_table_start($sort_by, $type_url) {
367    $x = array();
368    $x[] = tra('Rank');
369    $x[] = tra('Name');
370    $x[] = tra('Members');
371    if ($sort_by == "total_credit") {
372        $x[] = "<a href=top_teams.php?sort_by=expavg_credit".$type_url.">".tra('Recent average credit')."</a>";
373        $x[] = tra('Total credit');
374    } else {
375        $x[] = tra('Recent average credit');
376        $x[] = "<a href=top_teams.php?sort_by=total_credit".$type_url.">".tra('Total credit')."</a>";
377    }
378    $x[] = tra('Country');
379    $x[] = tra("Type");
380
381    $a = array("", "", ALIGN_RIGHT, ALIGN_RIGHT, ALIGN_RIGHT, "", "");
382    row_heading_array($x, $a);
383}
384
385function team_links($team) {
386    $b = badges_string(false, $team, BADGE_HEIGHT_MEDIUM);
387    return "<a href=team_display.php?teamid=$team->id>$team->name</a> $b";
388}
389
390function show_team_row($team, $i) {
391    $team_expavg_credit = format_credit_large($team->expavg_credit);
392    $team_total_credit = format_credit_large($team->total_credit);
393    echo "<tr>
394        <td>$i</td>
395        <td>".team_links($team)."</td>
396        <td align=right>".$team->nusers."</td>
397        <td align=right>$team_expavg_credit</td>
398        <td align=right>$team_total_credit</td>
399        <td>$team->country</td>
400        <td>".team_type_name($team->type)."</td>
401        </tr>
402    ";
403}
404
405function user_join_team($team, $user) {
406    user_quit_team($user);
407    $res = $user->update("teamid=$team->id");
408    if ($res) {
409        $now = time();
410        BoincTeamDelta::insert("(userid, teamid, timestamp, joining, total_credit) values ($user->id, $team->id, $now, 1, $user->total_credit)");
411        return true;
412    }
413    return false;
414}
415
416function user_quit_team($user) {
417    if (!$user->teamid) return;
418    $user->update("teamid=0");
419    $team = BoincTeam::lookup_id($user->teamid);
420    if ($team && $team->ping_user=$user->id) {
421        $team->update("ping_user=-ping_user");
422    }
423    BoincTeamAdmin::delete("teamid=$user->teamid and userid=$user->id");
424    $now = time();
425    BoincTeamDelta::insert("(userid, teamid, timestamp, joining, total_credit) values ($user->id, $user->teamid, $now, 0, $user->total_credit)");
426}
427
428function team_edit_form($team, $label, $url) {
429    global $team_types, $recaptcha_public_key;
430    echo "<form method=post action=$url>\n";
431    if ($team) {
432        echo "<input type=hidden name=teamid value=$team->id>\n";
433        if ($team->seti_id) {
434            echo "<p class=\"text-danger\">".tra("WARNING: this is a BOINC-wide team. If you make changes here, they will soon be overwritten. Edit the %1BOINC-wide team%2 instead.", "<a href=https://boinc.berkeley.edu/teams/>", "</a>")
435            ."</p>";
436        }
437    }
438    echo '
439        <p>
440        '.tra('%1Privacy note%2: if you create a team, your project preferences (resource share, graphics preferences) will be visible to the public.', '<b>', '</b>').'
441        <p>
442    ';
443    start_table();
444    row2(tra('Team name, text version').'
445        <br><p class=\"text-muted\">'.tra('Don\'t use HTML tags.').'</p>',
446        '<input class="form-control" name="name" type="text" size="50" value="'.($team?$team->name:"").'">'
447    );
448    row2(tra('Team name, HTML version').'
449        <br><p class=\"text-muted\">
450        '.tra('You may use %1limited HTML tags%2.', '<a href="html.php" target="_new">', '</a>').'
451        '.tra('If you don\'t know HTML, leave this box blank.').'</p>',
452        '<input class="form-control" name="name_html" type="text" size="50" value="'.str_replace('"',"'",($team?$team->name_html:"")).'">'
453    );
454    row2(tra('URL of team web page, if any').':<br><font size=-2>('.tra('without "http://"').')
455        '.tra('This URL will be linked to from the team\'s page on this site.'),
456        '<input class="form-control" type="text" name="url" size="60" value="'.($team?$team->url:"").'">'
457    );
458    row2(tra('Description of team').':
459        <br><p class=\"text-muted\">
460        '.tra('You may use %1limited HTML tags%2.', '<a href="html.php" target="_new">', '</a>').'
461        </p>',
462        '<textarea class="form-control" name="description" rows=10>'.($team?$team->description:"").'</textarea>'
463    );
464
465    row2(tra('Type of team').':', team_type_select($team?$team->type:null));
466
467    row2_init(tra('Country'),
468        '<select class="form-control" name="country">'
469    );
470    echo country_select_options($team?$team->country:null);
471
472    echo "</select></td></tr>\n";
473    $x = (!$team || $team->joinable)?"checked":"";
474    row2(tra("Accept new members?"), "<input type=checkbox name=joinable $x>");
475    // Check if we're using reCaptcha to prevent spam accounts
476    //
477    if (!$team && $recaptcha_public_key) {
478        row2(
479            "",
480            boinc_recaptcha_get_html($recaptcha_public_key)
481        );
482    }
483    row2("",
484        "<input class=\"btn btn-primary\" type=submit name=new value='$label'>"
485    );
486    end_table();
487    echo "</form>\n";
488}
489
490// decay a team's average credit
491//
492function team_decay_credit($team) {
493    $avg = $team->expavg_credit;
494    $avg_time = $team->expavg_time;
495    $now = time(0);
496    update_average($now, 0, 0, $avg, $avg_time);
497    $team->update("expavg_credit=$avg, expavg_time=$now");
498
499}
500// if the team hasn't received new credit for ndays,
501// decay its average and return true
502//
503function team_inactive_ndays($team, $ndays) {
504    $diff = time() - $team->expavg_time;
505    if ($diff > $ndays*86400) {
506        team_decay_credit($team);
507        return true;
508    }
509    return false;
510}
511
512function team_count_members($teamid) {
513    return BoincUser::count("teamid=$teamid");
514}
515
516// These functions determine the rules for foundership transfer, namely:
517// - A transfer request is allowed if either:
518//   - there is no active request, and it's been at least 60 days
519//     since the last request (this protects the founder from
520//     being bombarded with frequest requests)
521//   - there's an active request older than 90 days
522//     (this lets a 2nd requester eventually get a chance)
523// - Suppose someone (X) requests foundership at time T.
524//   An email is sent to the founder (Y).
525//   The request is "active" (ping_user is set to X's ID)
526// - If Y declines the change, an email is sent to X,
527//   and the request is cleared.
528// - If Y accepts the change, an email is sent to X
529//   and the request is cleared.
530// - After T + 60 days, X can become founder
531// - After T + 90 days, new requests are allowed even if there's
532//   an active request, i.e. after the 60 days elapse X has another
533//   30 days to assume foundership before someone elase can request it
534//
535function new_transfer_request_ok($team, $now) {
536    if ($team->ping_user <= 0) {
537        if ($team->ping_time < $now - 60 * 86400) {
538            return true;
539        }
540        return false;
541    }
542    if ($team->ping_time < $now - 90 * 86400) {
543        return true;
544    }
545    return false;
546}
547
548// the time at which we can actually change foundership
549// if the founder hasn't responded
550//
551function transfer_ok_time($team) {
552    return $team->ping_time + 60*86400;
553}
554
555function transfer_ok($team, $now) {
556    if ($now > transfer_ok_time($team)) return true;
557    return false;
558}
559
560// Make a team; args are untrusted, so cleanse and validate them
561//
562function make_team(
563    $userid, $name, $url, $type, $name_html, $description, $country
564) {
565    $name = BoincDb::escape_string(sanitize_tags($name));
566    if (strlen($name) == 0) return null;
567    $name_lc = strtolower($name);
568    $url = BoincDb::escape_string(sanitize_tags($url));
569    if (strstr($url, "http://")) {
570        $url = substr($url, 7);
571    }
572    $name_html = BoincDb::escape_string($name_html);
573    $description = BoincDb::escape_string($description);
574    if (!is_valid_country($country)) {
575        $country = tra('None');
576    }
577    $country = BoincDb::escape_string($country);  // for Cote d'Ivoire
578
579    $clause = sprintf(
580        "(userid, create_time, name, name_lc, url, type, name_html, description, country, nusers, expavg_time) values(%d, %d, '%s', '%s', '%s', %d, '%s', '%s', '%s', %d, unix_timestamp())",
581        $userid,
582        time(),
583        $name,
584        $name_lc,
585        $url,
586        $type,
587        $name_html,
588        $description,
589        $country,
590        0
591    );
592    $id = BoincTeam::insert($clause);
593    if ($id) {
594        return BoincTeam::lookup_id($id);
595    } else {
596        return null;
597    }
598}
599
600$cvs_version_tracker[]="\$Id$";  //Generated automatically - do not edit
601
602?>
603