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.'&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 .= " · "; 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 .= " · "; 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&offset=0&sort_by=expavg_credit>".tra('view')."</a>)"); 201 row2(tra('Active members'), "$team->nusers_active (<a href=team_members.php?teamid=$team->id&offset=0&sort_by=expavg_credit>".tra('view')."</a>)"); 202 row2(tra('Members with credit'), "$team->nusers_worked (<a href=team_members.php?teamid=$team->id&offset=0&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&sort_by=total_credit&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&sort_by=expavg_credit&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&sort_by=$sort_by&offset=$new_offset>".tra('Previous %1', $n)."</a> · "; 289 } 290 if ($j == $offset + $n + 1) { 291 $new_offset = $offset + $n; 292 echo "<a href=team_members.php?teamid=$team->id&sort_by=$sort_by&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