index : flyspray | |
Archlinux32 customized Flyspray installation | gitolite user |
summaryrefslogtreecommitdiff |
-rw-r--r-- | scripts/activity.php | 64 | ||||
-rw-r--r-- | scripts/admin.php | 185 | ||||
-rw-r--r-- | scripts/authenticate.php | 104 | ||||
-rw-r--r-- | scripts/depends.php | 196 | ||||
-rw-r--r-- | scripts/details.php | 768 | ||||
-rw-r--r-- | scripts/editcomment.php | 28 | ||||
-rw-r--r-- | scripts/index.php | 534 | ||||
-rw-r--r-- | scripts/langdiff.php | 192 | ||||
-rw-r--r-- | scripts/langedit.php | 329 | ||||
-rw-r--r-- | scripts/lostpw.php | 32 | ||||
-rw-r--r-- | scripts/myprofile.php | 41 | ||||
-rw-r--r-- | scripts/newmultitasks.php | 20 | ||||
-rw-r--r-- | scripts/newtask.php | 53 | ||||
-rw-r--r-- | scripts/oauth.php | 201 | ||||
-rw-r--r-- | scripts/pm.php | 61 | ||||
-rw-r--r-- | scripts/register.php | 63 | ||||
-rw-r--r-- | scripts/reports.php | 122 | ||||
-rw-r--r-- | scripts/roadmap.php | 84 | ||||
-rw-r--r-- | scripts/toplevel.php | 103 | ||||
-rw-r--r-- | scripts/user.php | 43 |
diff --git a/scripts/activity.php b/scripts/activity.php new file mode 100644 index 0000000..7968c19 --- /dev/null +++ b/scripts/activity.php @@ -0,0 +1,64 @@ +<?php +/*****************************\ +| Activity Graph Maker | +| Renders a graph for topview | +\*****************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +$data=''; + +# Project Graph +if ((Get::has('project_id') && Get::val('graph', 'project') == 'project')) { + if ($user->can_view_project(Get::num('project_id'))) { + $today = date('Y-m-d'); + $thirtyone_days = date('U' , strtotime("-31 day", strtotime($today))); + $sixtyone_days = date('U' , strtotime("-61 day", strtotime($today))); + + //look 30 + days and if found scale + $projectCheck = Project::getActivityProjectCount($sixtyone_days, $thirtyone_days, Get::num('project_id')); + + if($projectCheck > 0) { + $data = Project::getDayActivityByProject($sixtyone_days, date('U', strtotime(date('Y-m-d'))), Get::num('project_id')); + } else { + $data = Project::getDayActivityByProject($thirtyone_days, date('U', strtotime(date('Y-m-d'))), Get::num('project_id')); + } + + $data = implode(',', $data); + } else { + # and make the zero-line 'invisible' + $_GET['line']='fff'; + } +# User Graph +} else if(Get::has('user_id') && Get::has('project_id') && Get::val('graph') == 'user') { + if ($user->can_view_project(Get::num('project_id'))) { + $today = date('Y-m-d'); + $thirtyone_days = date('U' , strtotime("-31 day", strtotime($today))); + $sixtyone_days = date('U' , strtotime("-61 day", strtotime($today))); + + //look 30 + days and if found scale + $projectCheck = Project::getActivityProjectCount($sixtyone_days, $thirtyone_days, Get::num('project_id')); + + if($projectCheck > 0) { + $data = User::getDayActivityByUser($sixtyone_days, date('U', strtotime(date('Y-m-d'))), Get::num('project_id'), Get::num('user_id')); + } else { + $data = User::getDayActivityByUser($thirtyone_days, date('U', strtotime(date('Y-m-d'))), Get::num('project_id'), Get::num('user_id')); + } + + $data = implode(',', $data); + } else { + # and make the zero-line 'invisible' + $_GET['line']='fff'; + } +} else { + # make the zero-line 'invisible' + $_GET['line']='fff'; +} + +// Not pretty but gets the job done. +$_SERVER['QUERY_STRING'] = 'size=160x25&data='. $data; +$_GET['size'] = '160x25'; +$_GET['data'] = $data; +require dirname(__DIR__) . '/vendor/jamiebicknell/Sparkline/sparkline.php'; diff --git a/scripts/admin.php b/scripts/admin.php new file mode 100644 index 0000000..08c0caa --- /dev/null +++ b/scripts/admin.php @@ -0,0 +1,185 @@ +<?php + + /***********************************************\ + | Administrator's Toolbox | + | ~~~~~~~~~~~~~~~~~~~~~~~~ | + | This script allows members of a global Admin | + | group to modify the global preferences, user | + | profiles, global lists, global groups, pretty | + | much everything global. | + \***********************************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +if (!$user->perms('is_admin')) { + Flyspray::show_error(4); +} + +$proj = new Project(0); +#I $proj->setCookie(); + +$page->pushTpl('admin.menu.tpl'); + +switch ($area = Req::val('area', 'prefs')) { + case 'users': + $id = Flyspray::usernameToId(Req::val('user_name')); + if (!$id) { + $id = is_numeric(Req::val('user_id')) ? Req::val('user_id') : 0; + } + $theuser = new User($id, $proj); + if ($theuser->isAnon()) { + Flyspray::show_error(5, true, null, $_SESSION['prev_page']); + } + $page->assign('theuser', $theuser); + case 'cat': + case 'editgroup': + // yeah, utterly stupid, is changed in 1.0 already + if (Req::val('area') == 'editgroup') { + $group_details = Flyspray::getGroupDetails(Req::num('id')); + if (!$group_details || $group_details['project_id'] != $proj->id) { + Flyspray::show_error(L('groupnotexist')); + Flyspray::redirect(createURL('pm', 'groups', $proj->id)); + } + $page->uses('group_details'); + } + case 'groups': + case 'newuser': + case 'newuserbulk': + case 'editallusers': + $page->assign('groups', Flyspray::listGroups()); + case 'userrequest': + $sql = $db->query("SELECT * + FROM {admin_requests} + WHERE request_type = 3 AND project_id = 0 AND resolved_by = 0 + ORDER BY time_submitted ASC"); + + $page->assign('pendings', $db->fetchAllArray($sql)); + case 'newproject': + case 'os': + case 'prefs': + case 'resolution': + case 'tasktype': + case 'tag': + case 'status': + case 'version': + case 'newgroup': + $page->setTitle($fs->prefs['page_title'] . L('admintoolboxlong')); + $page->pushTpl('admin.'.$area.'.tpl'); + break; + + case 'translations': + require_once(BASEDIR.'/scripts/langdiff.php'); + break; + + case 'checks': + $hashtypes=$db->query(' + SELECT COUNT(*) c, LENGTH(user_pass) l, + CASE WHEN SUBSTRING(user_pass FROM 1 FOR 1)=\'$\' THEN 1 ELSE 0 END AS s, + SUM(CASE WHEN (SUBSTRING(user_pass FROM 1 FOR 2)=\'$2\' AND SUBSTRING(user_pass FROM 3 FOR 1)=\'$\' ) THEN 1 ELSE 0 END) cr, + SUM(CASE WHEN (SUBSTRING(user_pass FROM 1 FOR 2)=\'$2\' AND SUBSTRING(user_pass FROM 3 FOR 1) IN( \'a\', \'x\', \'y\' ) ) THEN 1 ELSE 0 END) bcr, + SUM(CASE WHEN SUBSTRING(user_pass FROM 1 FOR 3)=\'$1$\' THEN 1 ELSE 0 END) md5crypt, + SUM(CASE WHEN SUBSTRING(user_pass FROM 1 FOR 8)=\'$argon2i\' THEN 1 ELSE 0 END) argon2i + FROM {users} + GROUP BY LENGTH(user_pass), CASE WHEN SUBSTRING(user_pass FROM 1 FOR 1)=\'$\' THEN 1 ELSE 0 END + ORDER BY l ASC, s ASC'); + $hashlengths='<table><thead><tr><th>strlen</th><th>count</th><th>salted?</th><th>options</th><th>hash algo</th></tr></thead><tbody>'; + $warnhash=0; + $warnhash2=0; + while ($r = $db->fetchRow($hashtypes)){ + $alert=''; + if( $r['l']==32 && $r['s']==0){ $maybe='md5'; $warnhash+=$r['c']; $alert=' style="background-color:#f99"';} + elseif($r['l']==13 && $r['s']==0){ $maybe='CRYPT_STD_DES'; $r['s']=2; $warnhash2+=$r['c']; $alert=' style="background-color:#fc9"';} + elseif($r['l']==40 && $r['s']==0){ $maybe='sha1'; $warnhash+=$r['c']; $alert=' style="background-color:#f99"';} + elseif($r['l']==128 && $r['s']==0){ $maybe='sha512'; $warnhash+=$r['c']; $alert=' style="background-color:#f99"';} + elseif($r['l']==34 && $r['s']==1){ $maybe='CRYPT_MD5';$warnhash2+=$r['c'];$alert=' style="background-color:#fc9"';} + elseif($r['l']==60){$maybe='CRYPT_BLOWFISH';} + elseif($r['s']==1){ + $maybe='other pw hashes'; + if($r['argon2i']>0){$maybe.=': '.$r['argon2i'].' argon2i'; } + }else{$maybe='not detected';} + $hashlengths.='<tr'.$alert.'><td>'.$r['l'].'</td><td> '.$r['c'].'</td><td>'.$r['s'].'</td><td>'.$r['bcr'].' '.$r['cr'].' '.$r['md5crypt'].' '.$r['argon2i'].'</td><td>'.$maybe.'</td></tr>'; + } + $hashlengths.='</tbody></table>'; + if($warnhash>0){ + $hashlengths.='<div class="error">'.$warnhash." users with unsalted password hashes.</div>"; + } + if($warnhash2>0){ + $hashlengths.='<div class="error">'.$warnhash2." users with salted password hashes, but considered bad algorithms for password hashing.</div>"; + } + $page->assign('passwdcrypt', $conf['general']['passwdcrypt']); + $page->assign('hashlengths', $hashlengths); + + # info of old temporary unfinished user registration entries, for insights into register bot pattern or for cleanup old entries to free unused usernames as available again. + $statregistrations=$db->query('SELECT COUNT(*) FROM {registrations}'); + $regcount=$db->fetchOne($statregistrations); + $page->assign('regcount', $regcount); + + # show oldest unfinished user registrations + $registrations=$db->query('SELECT reg_time, user_name, email_address FROM {registrations} + ORDER BY reg_time ASC + LIMIT 50'); + $page->assign('registrations', $db->fetchAllArray($registrations)); + + $sinfo=$db->dblink->serverInfo(); + if( ($db->dbtype=='mysqli' || $db->dbtype=='mysql') && isset($sinfo['version'])){ + $fsdb=$db->query("SELECT default_character_set_name, default_collation_name + FROM INFORMATION_SCHEMA.SCHEMATA + WHERE SCHEMA_NAME=?", array($db->dblink->database) + ); + $page->assign('fsdb', $db->fetchRow($fsdb)); + + # TODO Test if Flyspray tables really have default charset utf8mb4 and default collation utf8mb4_unicode_ci. + # TODO Test if the TEXT/CHAR/VARCHAR fields that should have utf8mb_unicode_ci really have it. + # TODO Test if the TEXT/CHAR/VARCHAR fields that should have other collations really have that other collation. + # utf8mb4_unicode_ci may be not optimal for every TEXT/CHAR/VARCHAR field of Flyspray. + # Must be defined explicit for fields that differs from the default in the xmlschemas in the setup/upgrade/* files. + # At the moment (in 2019) the current ADODB 5.20.14 release does not handle that stuff yet. + + if(version_compare($sinfo['version'], '5.5.3')>=0 ){ + $page->assign('utf8mb4upgradable', "Your MySQL supports full utf-8 since 5.5.3. You are using ".$sinfo['version']." and Flyspray tables could be upgraded."); + } else{ + $page->assign('oldmysqlversion', "Your MySQL version ".$sinfo['version']." does not support full utf-8, only up to 3 Byte chars. No emojis for instance. Consider upgrading your MySQL server version."); + } + + $fstables=$db->query("SELECT table_name, table_collation, engine as table_type, create_options, table_comment + FROM INFORMATION_SCHEMA.tables + WHERE table_schema=? AND table_name LIKE '".$db->dbprefix."%' + ORDER BY table_name ASC", array($db->dblink->database) + ); + $page->assign('fstables', $db->fetchAllArray($fstables)); + + $fsfields=$db->query(" + SELECT table_name, column_name, column_default, data_type, character_set_name, collation_name, column_type, column_comment + FROM INFORMATION_SCHEMA.columns + WHERE table_schema=? AND table_name LIKE '".$db->dbprefix."%' + ORDER BY table_name ASC, ordinal_position ASC", array($db->dblink->database) + ); + $page->assign('fsfields', $db->fetchAllArray($fsfields)); + + } elseif($db->dbtype=='pgsql'){ + $fstables=$db->query("SELECT table_name, '' AS table_collation, table_type, '' AS create_options, '-' AS table_comment + FROM INFORMATION_SCHEMA.tables + WHERE table_catalog=? AND table_name LIKE '".$db->dbprefix."%' + ORDER BY table_name ASC", array($db->dblink->database) + ); + $page->assign('fstables', $db->fetchAllArray($fstables)); + + $fsfields=$db->query(" + SELECT table_name, column_name, column_default, data_type as column_type, character_set_name, collation_name, '-' AS column_comment + FROM INFORMATION_SCHEMA.columns + WHERE table_catalog=? AND table_name LIKE '".$db->dbprefix."%' + ORDER BY table_name ASC, ordinal_position ASC", array($db->dblink->database) + ); + $page->assign('fsfields', $db->fetchAllArray($fsfields)); + } + $page->assign('adodbversion', $db->dblink->version()); + $page->assign('htmlpurifierversion', HTMLPurifier::VERSION); + $page->pushTpl('admin.'.$area.'.tpl'); + break; + default: + Flyspray::show_error(6); +} + +?> diff --git a/scripts/authenticate.php b/scripts/authenticate.php new file mode 100644 index 0000000..dbcd882 --- /dev/null +++ b/scripts/authenticate.php @@ -0,0 +1,104 @@ +<?php + + /********************************************************\ + | User authentication (no output) | + | ~~~~~~~~~~~~~~~~~~~ | + \********************************************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +if (Req::val('logout')) { + $user->logout(); + Flyspray::redirect($baseurl); +} + +if (Req::val('user_name') != '' && Req::val('password') != '') { + // Otherwise, they requested login. See if they provided the correct credentials... + // FIXME: Do not do clean_username. Should not autostrip stuff + // $username = Backend::clean_username(Req::val('user_name')); + $username = Req::val('user_name'); + $password = Req::val('password'); + + // Run the username and password through the login checker + if (($user_id = Flyspray::checkLogin($username, $password)) < 1) { + $_SESSION['failed_login'] = Req::val('user_name'); + if($user_id === -2) { + Flyspray::show_error(L('usernotexist')); + }elseif ($user_id === -1) { + Flyspray::show_error(23); + } else /* $user_id == 0 */ { + // just some extra check here so that never ever an account can get locked when it's already disabled + // ... that would make it easy to get enabled + $db->query('UPDATE {users} SET login_attempts = login_attempts+1 WHERE account_enabled = 1 AND user_name = ?', + array($username)); + // Lock account if failed too often for a limited amount of time + $db->query('UPDATE {users} SET lock_until = ?, account_enabled = 0 WHERE login_attempts > ? AND user_name = ?', + array(time() + 60 * $fs->prefs['lock_for'], LOGIN_ATTEMPTS, $username)); + + if ($db->affectedRows()) { + Flyspray::show_error(sprintf(L('error71'), $fs->prefs['lock_for'])); + Flyspray::redirect($baseurl); + } else { + Flyspray::show_error(7); + } + } + } else { + // Determine if the user should be remembered on this machine + if (Req::has('remember_login')) { + $cookie_time = time() + (60 * 60 * 24 * 30); // Set cookies for 30 days + } else { + $cookie_time = 0; // Set cookies to expire when session ends (browser closes) + } + + $user = new User($user_id); + + # check if user still has an outdated password hash and upgrade it + if( $conf['general']['passwdcrypt']!='md5' + && $conf['general']['passwdcrypt']!='sha1' + && $conf['general']['passwdcrypt']!='sha512' + ){ + if( substr($user->infos['user_pass'],0,1)!='$' + && ( strlen($user->infos['user_pass'])==32 + || strlen($user->infos['user_pass'])==40 + || strlen($user->infos['user_pass'])==128 + ) + ){ + # upgrade from unsalted md5 or unsalted sha1 or unsalted sha512 to better + if($conf['general']['passwdcrypt']=='argon2i'){ + $newhash=password_hash($password, PASSWORD_ARGON2I); + }else{ + $cryptoptions=array('cost'=>12); + $newhash=password_hash($password, PASSWORD_BCRYPT, $cryptoptions); + } + # save the new hash + $db->query("UPDATE {users} SET user_pass=? WHERE user_id=?", array($newhash, $user_id)); + # reload the user with updated data + $user= new User($user_id); + } + } + + // Set a couple of cookies + $passweirded = crypt($user->infos['user_pass'], $conf['general']['cookiesalt']); + Flyspray::setCookie('flyspray_userid', $user->id, $cookie_time,null,null,null,true); + Flyspray::setCookie('flyspray_passhash', $passweirded, $cookie_time,null,null,null,true); + // If the user had previously requested a password change, remove the magic url + $remove_magic = $db->query("UPDATE {users} SET magic_url = '' WHERE user_id = ?", + array($user->id)); + // Save for displaying + if ($user->infos['login_attempts'] > 0) { + $_SESSION['login_attempts'] = $user->infos['login_attempts']; + } + $db->query('UPDATE {users} SET login_attempts = 0, last_login = ? WHERE user_id = ?', array(time(), $user->id)); + + $_SESSION['SUCCESS'] = L('loginsuccessful'); + } +} +else { + // If the user didn't provide both a username and a password, show this error: + Flyspray::show_error(8); +} + +Flyspray::redirect(Req::val('return_to')); +?> diff --git a/scripts/depends.php b/scripts/depends.php new file mode 100644 index 0000000..0621a4a --- /dev/null +++ b/scripts/depends.php @@ -0,0 +1,196 @@ +<?php + + /********************************************************\ + | Task Dependancy Graph | + | ~~~~~~~~~~~~~~~~~~~~~ | + \********************************************************/ + +/** + * XXX: This stuff looks incredible ugly, rewrite me for 1.0 + */ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +if ( !($task_details = Flyspray::getTaskDetails(Req::num('task_id'))) + || !$user->can_view_task($task_details)) +{ + Flyspray::show_error(9); +} + +$id = Req::num('task_id'); +$page->assign('task_id', $id); + +$prunemode = Req::num('prune', 0); +$selfurl = createURL('depends', $id); +$pmodes = array(L('none'), L('pruneclosedlinks'), L('pruneclosedtasks')); + +foreach ($pmodes as $mode => $desc) { + if ($mode == $prunemode) { + $strlist[] = $desc; + } else { + $strlist[] = "<a href='". htmlspecialchars($selfurl, ENT_QUOTES, 'utf-8') . + ($mode !=0 ? "&prune=$mode" : "") . "'>$desc</a>\n"; + } +} + +$page->uses('strlist'); + +$starttime = microtime(); + +$sql= 'SELECT t1.task_id AS id1, t1.item_summary AS sum1, + t1.percent_complete AS pct1, t1.is_closed AS clsd1, + lst1.status_name AS stat1, t1.task_severity AS sev1, + t1.task_priority AS pri1, + t1.closure_comment AS com1, u1c.real_name AS clsdby1, + r1.resolution_name as res1, + t2.task_id AS id2, t2.item_summary AS sum2, + t2.percent_complete AS pct2, t2.is_closed AS clsd2, + lst2.status_name AS stat2, t2.task_severity AS sev2, + t2.task_priority AS pri2, + t2.closure_comment AS com2, u2c.real_name AS clsdby2, + r2.resolution_name as res2 + FROM {dependencies} AS d + JOIN {tasks} AS t1 ON d.task_id=t1.task_id + LEFT JOIN {users} AS u1c ON t1.closed_by=u1c.user_id + LEFT JOIN {list_status} AS lst1 ON t1.item_status = lst1.status_id + LEFT JOIN {list_resolution} AS r1 ON t1.resolution_reason=r1.resolution_id + JOIN {tasks} AS t2 ON d.dep_task_id=t2.task_id + LEFT JOIN {list_status} AS lst2 ON t2.item_status = lst2.status_id + LEFT JOIN {users} AS u2c ON t2.closed_by=u2c.user_id + LEFT JOIN {list_resolution} AS r2 ON t2.resolution_reason=r2.resolution_id + WHERE t1.project_id= ? + ORDER BY d.task_id, d.dep_task_id'; + +$get_edges = $db->query($sql, array($proj->id)); + +$edge_list = array(); +$rvrs_list = array(); +$node_list = array(); +while ($row = $db->fetchRow($get_edges)) { + extract($row, EXTR_REFS); + $edge_list[$id1][] = $id2; + $rvrs_list[$id2][] = $id1; + if (!isset($node_list[$id1])) { + $node_list[$id1] = + array('id'=>$id1, 'sum'=>$sum1, 'pct'=>$pct1, 'clsd'=>$clsd1, + 'status_name'=>$stat1, 'sev'=>$sev1, 'pri'=>$pri1, + 'com'=>$com1, 'clsdby'=>$clsdby1, 'res'=>$res1); + } + if (!isset($node_list[$id2])) { + $node_list[$id2] = + array('id'=>$id2, 'sum'=>$sum2, 'pct'=>$pct2, 'clsd'=>$clsd2, + 'status_name'=>$stat2, 'sev'=>$sev2, 'pri'=>$pri2, + 'com'=>$com2, 'clsdby'=>$clsdby2, 'res'=>$res2); + } +} + +// Now we have our lists of nodes and edges, along with a helper +// list of reverse edges. Time to do the graph coloring, so we know +// which ones are in our particular connected graph. We'll set up a +// list and fill it up as we visit nodes that are connected to our +// main task. + +$connected = array(); +$levelsdown = 0; +$levelsup = 0; +function connectsTo($id, $down, $up) { + global $connected, $edge_list, $rvrs_list, $levelsdown, $levelsup; + global $prunemode, $node_list; + if (!isset($connected[$id])) { $connected[$id]=1; } + if ($down > $levelsdown) { $levelsdown = $down; } + if ($up > $levelsup ) { $levelsup = $up ; } + +/* +echo '<pre><code>'; +echo "$id ($down d, $up u) => $levelsdown d $levelsup u<br>\n"; +echo 'nodes:';print_r($node_list); +echo 'edges:';print_r($edge_list); +echo 'rvrs:';print_r($rvrs_list); +echo 'levelsdown:';print_r($levelsdown); +echo "\n".'levelsup';print_r($levelsup); +echo '<code></pre>'; +*/ + if (empty($node_list)){ return; } + if (!isset($node_list[$id])){ return; } + $selfclosed = $node_list[$id]['clsd']; + if (isset($edge_list[$id])) { + foreach ($edge_list[$id] as $neighbor) { + $neighborclosed = $node_list[$neighbor]['clsd']; + if (!isset($connected[$neighbor]) && + !($prunemode==1 && $selfclosed && $neighborclosed) && + !($prunemode==2 && $neighborclosed)) { + connectsTo($neighbor, $down, $up+1); + } + } + } + if (isset($rvrs_list[$id])) { + foreach ($rvrs_list[$id] as $neighbor) { + $neighborclosed = $node_list[$neighbor]['clsd']; + if (!isset($connected[$neighbor]) && + !($prunemode==1 && $selfclosed && $neighborclosed) && + !($prunemode==2 && $neighborclosed)) { + connectsTo($neighbor, $down+1, $up); + } + } + } +} + +connectsTo($id, 0, 0); +$connected_nodes = array_keys($connected); +sort($connected_nodes); + +// Now lets get rid of the extra junk in our arrays. +// In prunemode 0, we know we're only going to have to get rid of +// whole lists, and not elements in the lists, because if they were +// in the list, they'd be connected, so we wouldn't be removing them. +// In prunemode 1 or 2, we may have to remove stuff from the list, because +// you can have an edge to a node that didn't end up connected. +foreach (array("edge_list", "rvrs_list", "node_list") as $l) { + foreach (${$l} as $n => $list) { + if (!isset($connected[$n])) { + unset(${$l}[$n]); + } + if ($prunemode!=0 && $l!="node_list" && isset(${$l}[$n])) { + // Only keep entries that appear in the $connected_nodes list + ${$l}[$n] = array_intersect(${$l}[$n], $connected_nodes); + } + } +} + +// Now we've got everything we need... prepare JSON data +$resultData = array(); +foreach ($node_list as $task_id => $taskInfo) { + $adjacencies = array(); + if (isset($edge_list[$task_id])) { + foreach ($edge_list[$task_id] as $dst) { + array_push($adjacencies, array('nodeTo' => $dst, 'nodeFrom' => $task_id)); + } + } + + if ($task_id == $id) { + $color = '#5F9729'; + } else if ($taskInfo['clsd']) { + $color = '#808080'; + } else { + $color = '#83548B'; + } + + $newTask = array('id' => $task_id, + 'name' => tpl_tasklink($task_id), + 'data' => array('$color' => $color, + '$type' => 'circle', + '$dim' => 15), + 'adjacencies' => $adjacencies); + + array_push($resultData, $newTask); +} + +$jasonData = json_encode($resultData); +$page->assign('jasonData', $jasonData); +$page->assign('task_id', $id); + +$page->setTitle(sprintf('FS#%d : %s', $id, L('dependencygraph'))); +$page->pushTpl('depends.tpl'); +?> diff --git a/scripts/details.php b/scripts/details.php new file mode 100644 index 0000000..421a68a --- /dev/null +++ b/scripts/details.php @@ -0,0 +1,768 @@ +<?php + + /*************************************************************\ + | Details a task (and edit it) | + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | + | This script displays task details when in view mode, | + | and allows the user to edit task details when in edit mode. | + | It also shows comments, attachments, notifications etc. | + \*************************************************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +$task_id = Req::num('task_id'); + +if ( !($task_details = Flyspray::GetTaskDetails($task_id)) ) { + Flyspray::show_error(10); +} +if (!$user->can_view_task($task_details)) { + Flyspray::show_error( $user->isAnon() ? 102 : 101, false); +} else{ + + require_once(BASEDIR . '/includes/events.inc.php'); + + if($proj->prefs['use_effort_tracking']){ + require_once(BASEDIR . '/includes/class.effort.php'); + $effort = new effort($task_id,$user->id); + $effort->populateDetails(); + $page->assign('effort',$effort); + } + + $page->uses('task_details'); + + // Send user variables to the template + $page->assign('assigned_users', $task_details['assigned_to']); + $page->assign('old_assigned', implode(' ', $task_details['assigned_to'])); + $page->assign('tags', $task_details['tags']); + + $page->setTitle(sprintf('FS#%d : %s', $task_details['task_id'], $task_details['item_summary'])); + + + if ((Get::val('edit') || (Post::has('item_summary') && !isset($_SESSION['SUCCESS']))) && $user->can_edit_task($task_details)) { + + if(isset($move) && $move==1){ + if( !$user->perms('modify_all_tasks', $toproject->id)){ + Flyspray::show_error('invalidtargetproject'); + } + } + + $result = $db->query(' + SELECT g.project_id, u.user_id, u.user_name, u.real_name, g.group_id, g.group_name + FROM {users} u + JOIN {users_in_groups} uig ON u.user_id = uig.user_id + JOIN {groups} g ON g.group_id = uig.group_id + WHERE (g.show_as_assignees = 1 OR g.is_admin = 1) + AND (g.project_id = 0 OR g.project_id = ?) + AND u.account_enabled = 1 + ORDER BY g.project_id ASC, g.group_name ASC, u.user_name ASC', + ($proj->id ? $proj->id : -1) + ); // FIXME: -1 is a hack. when $proj->id is 0 the query fails + + $userlist = array(); + $userids = array(); + while ($row = $db->fetchRow($result)) { + if( !in_array($row['user_id'], $userids) ){ + $userlist[$row['group_id']][] = array( + 0 => $row['user_id'], + 1 => sprintf('%s (%s)', $row['user_name'], $row['real_name']), + 2 => $row['project_id'], + 3 => $row['group_name'] + ); + $userids[]=$row['user_id']; + } else{ + # user is probably in a global group with assignee permission listed, so no need to show second time in a project group. + } + } + + if (is_array(Post::val('rassigned_to'))) { + $page->assign('assignees', Post::val('rassigned_to')); + } else { + $assignees = $db->query('SELECT user_id FROM {assigned} WHERE task_id = ?', $task_details['task_id']); + $page->assign('assignees', $db->fetchCol($assignees)); + } + $page->assign('userlist', $userlist); + + # Build the select arrays, for 'move task' or normal taskedit + # Then in the template just use tpl_select($xxxselect); + + # keep last selections + $catselected=Req::val('product_category', $task_details['product_category']); + $osselected=Req::val('operating_system', $task_details['operating_system']); + $ttselected=Req::val('task_type', $task_details['task_type']); + $stselected=Req::val('item_status', $task_details['item_status']); + $repverselected=Req::val('reportedver', $task_details['product_version']); + $dueverselected=Req::val('closedby_version', $task_details['closedby_version']); + if(isset($move) && $move==1){ + # get global categories + $gcats=$proj->listCategories(0); + if( count($gcats)>0){ + foreach($gcats as $cat){ + $gcatopts[]=array('value'=>$cat['category_id'], 'label'=>$cat['category_name']); + if($catselected==$cat['category_id']){ + $gcatopts[count($gcatopts)-1]['selected']=1; + } + } + #$catsel['options'][]=array('optgroup'=>1, 'label'=>L('categoriesglobal'), 'options'=>$gcatopts); + $catsel['options'][]=array('optgroup'=>1, 'label'=>L('globaloptions'), 'options'=>$gcatopts); + } + # get project categories + $pcats=$proj->listCategories($proj->id); + if( count($pcats)>0){ + foreach($pcats as $cat){ + $pcatopts[]=array('value'=>$cat['category_id'], 'label'=>$cat['category_name']); + if($catselected==$cat['category_id']){ + $pcatopts[count($pcatopts)-1]['selected']=1; + } + } + #$catsel['options'][]=array('optgroup'=>1, 'label'=>L('categoriesproject').' '.$proj->prefs['project_title'], 'options'=>$pcatopts); + $catsel['options'][]=array('optgroup'=>1, 'label'=>L('currentproject').' '.$proj->prefs['project_title'], 'options'=>$pcatopts); + } + # get target categories + $tcats=$toproject->listCategories($toproject->id); + if( count($tcats)>0){ + foreach($tcats as $cat){ + $tcatopts[]=array('value'=>$cat['category_id'], 'label'=>$cat['category_name']); + if($catselected==$cat['category_id']){ + $tcatopts[count($tcatopts)-1]['selected']=1; + } + } + #$catsel['options'][]=array('optgroup'=>1, 'label'=>L('categoriestarget').' '.$toproject->prefs['project_title'], 'options'=>$tcatopts); + $catsel['options'][]=array('optgroup'=>1, 'label'=>L('targetproject').' '.$toproject->prefs['project_title'], 'options'=>$tcatopts); + } + + + # get global task statuses + $resgst=$db->query("SELECT status_id, status_name, list_position, show_in_list FROM {list_status} WHERE project_id=0 ORDER BY list_position"); + $gsts=$db->fetchAllArray($resgst); + if(count($gsts)>0){ + foreach($gsts as $gst){ + $gstopts[]=array('value'=>$gst['status_id'], 'label'=>$gst['status_name']); + if($stselected==$gst['status_id']){ + $gstopts[count($gstopts)-1]['selected']=1; + } + if($gst['show_in_list']==0){ + $gstopts[count($gstopts)-1]['disabled']=1; + } + } + $statussel['options'][]=array('optgroup'=>1, 'label'=>L('globaloptions'), 'options'=>$gstopts); + } + # get current project task statuses + $rescst=$db->query("SELECT status_id, status_name, list_position, show_in_list FROM {list_status} WHERE project_id=? ORDER BY list_position", array($proj->id)); + $csts=$db->fetchAllArray($rescst); + if(count($csts)>0){ + foreach($csts as $cst){ + $cstopts[]=array('value'=>$cst['status_id'], 'label'=>$cst['status_name']); + if($stselected==$cst['status_id']){ + $cstopts[count($cstopts)-1]['selected']=1; + } + if($cst['show_in_list']==0){ + $cstopts[count($cstopts)-1]['disabled']=1; + } + } + $statussel['options'][]=array('optgroup'=>1, 'label'=>L('currentproject').' '.$proj->prefs['project_title'], 'options'=>$cstopts); + } + # get target project task statuses + $restst=$db->query("SELECT status_id, status_name, list_position, show_in_list FROM {list_status} WHERE project_id=? ORDER BY list_position", array($toproject->id)); + $tsts=$db->fetchAllArray($restst); + if(count($tsts)>0){ + foreach($tsts as $tst){ + $tstopts[]=array('value'=>$tst['status_id'], 'label'=>$tst['status_name']); + if($stselected==$tst['status_id']){ + $tstopts[count($tstopts)-1]['selected']=1; + } + if($tst['show_in_list']==0){ + $tstopts[count($tstopts)-1]['disabled']=1; + } + } + $statussel['options'][]=array('optgroup'=>1, 'label'=>L('targetproject').' '.$toproject->prefs['project_title'], 'options'=>$tstopts); + } + + + # get list global tasktypes + $resgtt=$db->query("SELECT tasktype_id, tasktype_name, list_position, show_in_list FROM {list_tasktype} WHERE project_id=0 ORDER BY list_position"); + $gtts=$db->fetchAllArray($resgtt); + if(count($gtts)>0){ + foreach($gtts as $gtt){ + $gttopts[]=array('value'=>$gtt['tasktype_id'], 'label'=>$gtt['tasktype_name']); + if($ttselected==$gtt['tasktype_id']){ + $gttopts[count($gttopts)-1]['selected']=1; + } + } + $tasktypesel['options'][]=array('optgroup'=>1, 'label'=>L('globaloptions'), 'options'=>$gttopts); + } + # get current project tasktypes + $resctt=$db->query("SELECT tasktype_id, tasktype_name, list_position, show_in_list FROM {list_tasktype} WHERE project_id=? ORDER BY list_position", array($proj->id)); + $ctts=$db->fetchAllArray($resctt); + if(count($ctts)>0){ + foreach($ctts as $ctt){ + $cttopts[]=array('value'=>$ctt['tasktype_id'], 'label'=>$ctt['tasktype_name']); + if($ttselected==$ctt['tasktype_id']){ + $cttopts[count($cttopts)-1]['selected']=1; + } + } + $tasktypesel['options'][]=array('optgroup'=>1, 'label'=>L('currentproject').' '.$proj->prefs['project_title'], 'options'=>$cttopts); + } + # get target project tasktypes + $resttt=$db->query("SELECT tasktype_id, tasktype_name, list_position, show_in_list FROM {list_tasktype} WHERE project_id=? ORDER BY list_position", array($toproject->id)); + $ttts=$db->fetchAllArray($resttt); + if(count($ttts)>0){ + foreach($ttts as $ttt){ + $tttopts[]=array('value'=>$ttt['tasktype_id'], 'label'=>$ttt['tasktype_name']); + if($ttselected==$ttt['tasktype_id']){ + $tttopts[count($tttopts)-1]['selected']=1; + } + } + $tasktypesel['options'][]=array('optgroup'=>1, 'label'=>L('targetproject').' '.$toproject->prefs['project_title'], 'options'=>$tttopts); + } + + + # allow unset (0) value (field os_id currently defined with NOT NULL by flyspray-install.xml, so must use 0 instead null) + $osfound=0; + $ossel['options'][]=array('value'=>0, 'label'=>L('undecided')); + # get global operating systems + $resgos=$db->query("SELECT os_id, os_name, list_position, show_in_list FROM {list_os} WHERE project_id=0 AND show_in_list=1 ORDER BY list_position"); + $goses=$db->fetchAllArray($resgos); + if(count($goses)>0){ + foreach($goses as $gos){ + $gosopts[]=array('value'=>$gos['os_id'], 'label'=>$gos['os_name']); + if($osselected==$gos['os_id']){ + $gosopts[count($gosopts)-1]['selected']=1; + $osfound=1; + } + } + $ossel['options'][]=array('optgroup'=>1, 'label'=>L('globaloptions'), 'options'=>$gosopts); + } + # get current project operating systems + $rescos=$db->query("SELECT os_id, os_name, list_position, show_in_list FROM {list_os} WHERE project_id=? AND show_in_list=1 ORDER BY list_position", array($proj->id)); + $coses=$db->fetchAllArray($rescos); + if(count($coses)>0){ + foreach($coses as $cos){ + $cosopts[]=array('value'=>$cos['os_id'], 'label'=>$cos['os_name']); + if($osselected==$cos['os_id']){ + $cosopts[count($cosopts)-1]['selected']=1; + $osfound=1; + } + } + $ossel['options'][]=array('optgroup'=>1, 'label'=>L('currentproject').' '.$proj->prefs['project_title'], 'options'=>$cosopts); + } + # get target project operating systems + $restos=$db->query("SELECT os_id, os_name, list_position, show_in_list FROM {list_os} WHERE project_id=? AND show_in_list=1 ORDER BY list_position", array($toproject->id)); + $toses=$db->fetchAllArray($restos); + if(count($toses)>0){ + foreach($toses as $tos){ + $tosopts[]=array('value'=>$tos['os_id'], 'label'=>$tos['os_name']); + if($osselected==$tos['os_id']){ + $tosopts[count($tosopts)-1]['selected']=1; + $osfound=1; + } + } + $ossel['options'][]=array('optgroup'=>1, 'label'=>L('targetproject').' '.$toproject->prefs['project_title'], 'options'=>$tosopts); + } + # keep existing operating_system entry choosable even if would not currently selectable by current settings + if($osfound==0 && $task_details['operating_system']>0){ + # get operating_system of that existing old entry, even if show_in_list=0 or other project + $resexistos=$db->query(" + SELECT os.os_id, os.os_name, os.list_position, os.show_in_list, os.project_id, p.project_id AS p_project_id FROM {list_os} os + LEFT JOIN {projects} p ON p.project_id=os.project_id + WHERE os.os_id=?", array($task_details['operating_system'])); + $existos=$db->fetchRow($resexistos); + if($existos['project_id']==$proj->id){ + $existosgrouplabel=$proj->prefs['project_title'].': existing reported version'; + } elseif($existos['project_id']==$toproject->id){ + $existosgrouplabel=$toproject->prefs['project_title'].': existing reported version'; + } else{ + # maybe version_id from other/hidden/forbidden/deleted project, so only show project_id as hint. + # if user has view permission of this other project, then showing project_title would be ok -> extra sql required + $existosgrouplabel='existing os of project '.($existos['p_project_id']->id); + } + $existosopts[]=array('value'=>$task_details['operating_system'], 'label'=>$existos['os_name']); + if($osselected==$task_details['operating_system']){ + $existosopts[count($existosopts)-1]['selected']=1; + } + + #$ossel['options'][]=array('optgroup'=>1, 'label'=>$existosgrouplabel, 'options'=>$existosopts); + # put existing at beginning + $ossel['options']=array_merge(array(array('optgroup'=>1, 'label'=>$existosgrouplabel, 'options'=>$existosopts)), $ossel['options']); + } + + + + # get list global reported versions + # FIXME/TODO: Should we use 'show_in_list' list setting here to filter them out here? Or distinguish between editor/projectmanager/admin roles? + # FIXME/TODO: All Flyspray version up to 1.0-rc8 only versions with tense=2 were shown for edit. + # But what if someone edits an old tasks (maybe reopened an old closed), and that old task is connected with an old reported version (tense=1) + # Or that {list_version} entry has now show_in_list=0 set ? + # In both cases that version would not be selectable for editing the task, although it is the correct reported version. + $reportedversionfound=0; + $repversel['options'][]=array('value'=>0, 'label'=>L('undecided')); + $resgrepver=$db->query("SELECT version_id, version_name, list_position, show_in_list FROM {list_version} + WHERE project_id=0 + AND version_tense=2 + AND show_in_list=1 + ORDER BY list_position"); + $grepvers=$db->fetchAllArray($resgrepver); + if(count($grepvers)>0){ + foreach($grepvers as $grepver){ + $grepveropts[]=array('value'=>$grepver['version_id'], 'label'=>$grepver['version_name']); + if($repverselected==$grepver['version_id']){ + $grepveropts[count($grepveropts)-1]['selected']=1; + $reportedversionfound=1; + } + } + $repversel['options'][]=array('optgroup'=>1, 'label'=>L('globaloptions'), 'options'=>$grepveropts); + } + # get current project reported versions + $rescrepver=$db->query("SELECT version_id, version_name, list_position, show_in_list FROM {list_version} + WHERE project_id=? + AND version_tense=2 + AND show_in_list=1 + ORDER BY list_position", array($proj->id)); + $crepvers=$db->fetchAllArray($rescrepver); + if(count($crepvers)>0){ + foreach($crepvers as $crepver){ + $crepveropts[]=array('value'=>$crepver['version_id'], 'label'=>$crepver['version_name']); + if($repverselected==$crepver['version_id']){ + $crepveropts[count($crepveropts)-1]['selected']=1; + $reportedversionfound=1; + } + } + $repversel['options'][]=array('optgroup'=>1, 'label'=>L('currentproject').' '.$proj->prefs['project_title'], 'options'=>$crepveropts); + } + # get target project reported versions + $restrepver=$db->query("SELECT version_id, version_name, list_position, show_in_list FROM {list_version} + WHERE project_id=? + AND version_tense=2 + AND show_in_list=1 + ORDER BY list_position", array($toproject->id)); + $trepvers=$db->fetchAllArray($restrepver); + if(count($trepvers)>0){ + foreach($trepvers as $trepver){ + $trepveropts[]=array('value'=>$trepver['version_id'], 'label'=>$trepver['version_name']); + if($repverselected==$trepver['version_id']){ + $trepveropts[count($trepveropts)-1]['selected']=1; + $reportedversionfound=1; + } + } + $repversel['options'][]=array('optgroup'=>1, 'label'=>L('targetproject').' '.$toproject->prefs['project_title'], 'options'=>$trepveropts); + } + # keep existing reportedversion(product_version) choosable even if would not currently selectable by current settings + if($reportedversionfound==0 && $task_details['product_version']>0){ + # get version_name of that existing old entry, even if tense is past or show_in_list=0 or other project + $resexistrepver=$db->query(" + SELECT v.version_id, v.version_name, v.list_position, v.show_in_list, v.project_id, p.project_id AS p_project_id FROM {list_version} v + LEFT JOIN {projects} p ON p.project_id=v.project_id + WHERE v.version_id=?", array($task_details['product_version'])); + $existrepver=$db->fetchRow($resexistrepver); + if($existrepver['project_id']==$proj->id){ + $existgrouplabel=$proj->prefs['project_title'].': existing reported version'; + } elseif($existrepver['project_id']==$toproject->id){ + $existgrouplabel=$toproject->prefs['project_title'].': existing reported version'; + } else{ + # maybe version_id from other/hidden/forbidden/deleted project, so only show project_id as hint. + # if user has view permission of this other project, then showing project_title would be ok -> extra sql required + $existgrouplabel='existing reported version of project '.($existrepver['p_project_id']); + } + $existrepveropts[]=array('value'=>$task_details['product_version'], 'label'=>$existrepver['version_name']); + if($repverselected==$task_details['product_version']){ + $existrepveropts[count($existrepveropts)-1]['selected']=1; + } + + #$repversel['options'][]=array('optgroup'=>1, 'label'=>$existgrouplabel, 'options'=>$existrepveropts); + # put existing at beginning + $repversel['options']=array_merge(array(array('optgroup'=>1, 'label'=>$existgrouplabel, 'options'=>$existrepveropts)), $repversel['options']); + } + + + # get list global due versions + # FIXME/TODO: Should we use 'show_in_list' list setting here to filter them out here? Or distinguish between editor/projectmanager/admin roles? + $dueversel['options'][]=array('value'=>0, 'label'=>L('undecided')); + $resgduever=$db->query("SELECT version_id, version_name, list_position, show_in_list FROM {list_version} + WHERE project_id=0 + AND version_tense=3 + AND show_in_list=1 + ORDER BY list_position"); + $gduevers=$db->fetchAllArray($resgduever); + if(count($gduevers)>0){ + foreach($gduevers as $gduever){ + $gdueveropts[]=array('value'=>$gduever['version_id'], 'label'=>$gduever['version_name']); + if($dueverselected==$gduever['version_id']){ + $gdueveropts[count($gdueveropts)-1]['selected']=1; + } + } + $dueversel['options'][]=array('optgroup'=>1, 'label'=>L('globaloptions'), 'options'=>$gdueveropts); + } + # get current project due versions + $rescduever=$db->query("SELECT version_id, version_name, list_position, show_in_list FROM {list_version} + WHERE project_id=? + AND version_tense=3 + AND show_in_list=1 + ORDER BY list_position", array($proj->id)); + $cduevers=$db->fetchAllArray($rescduever); + if(count($cduevers)>0){ + foreach($cduevers as $cduever){ + $cdueveropts[]=array('value'=>$cduever['version_id'], 'label'=>$cduever['version_name']); + if($dueverselected==$cduever['version_id']){ + $cdueveropts[count($cdueveropts)-1]['selected']=1; + } + } + $dueversel['options'][]=array('optgroup'=>1, 'label'=>L('currentproject').' '.$proj->prefs['project_title'], 'options'=>$cdueveropts); + } + # get target project due versions + $restduever=$db->query("SELECT version_id, version_name, list_position, show_in_list FROM {list_version} + WHERE project_id=? + AND version_tense=3 + AND show_in_list=1 + ORDER BY list_position", array($toproject->id)); + $tduevers=$db->fetchAllArray($restduever); + if(count($tduevers)>0){ + foreach($tduevers as $tduever){ + $tdueveropts[]=array('value'=>$tduever['version_id'], 'label'=>$tduever['version_name']); + if($dueverselected==$tduever['version_id']){ + $tdueveropts[count($tdueveropts)-1]['selected']=1; + } + } + $dueversel['options'][]=array('optgroup'=>1, 'label'=>L('targetproject').' '.$toproject->prefs['project_title'], 'options'=>$tdueveropts); + } + + + }else{ + # just the normal merged global/project categories + $cats=$proj->listCategories(); + if( count($cats)>0){ + foreach($cats as $cat){ + $catopts[]=array('value'=>$cat['category_id'], 'label'=>$cat['category_name']); + if($catselected==$cat['category_id']){ + $catopts[count($catopts)-1]['selected']=1; + } + } + $catsel['options']=$catopts; + } + + # just the normal merged global/project statuses + $sts=$proj->listTaskStatuses(); + if( count($sts)>0){ + foreach($sts as $st){ + $stopts[]=array('value'=>$st['status_id'], 'label'=>$st['status_name']); + if($stselected==$st['status_id']){ + $stopts[count($stopts)-1]['selected']=1; + } + } + $statussel['options']=$stopts; + } + + # just the normal merged global/project tasktypes + $tts=$proj->listTaskTypes(); + if( count($tts)>0){ + foreach($tts as $tt){ + $ttopts[]=array('value'=>$tt['tasktype_id'], 'label'=>$tt['tasktype_name']); + if($ttselected==$tt['tasktype_id']){ + $ttopts[count($ttopts)-1]['selected']=1; + } + } + $tasktypesel['options']=$ttopts; + } + + # just the normal merged global/project os + $osses=$proj->listOs(); + # also allow unsetting operating system entry + $osopts[]=array('value'=>0, 'label'=>L('undecided')); + if( count($osses)>0){ + foreach($osses as $os){ + $osopts[]=array('value'=>$os['os_id'], 'label'=>$os['os_name']); + if($osselected==$os['os_id']){ + $osopts[count($osopts)-1]['selected']=1; + } + } + $ossel['options']=$osopts; + } + + # just the normal merged global/project reported version + $repversions=$proj->listVersions(false, 2, $task_details['product_version']); + # also allow unsetting dueversion system entry + $repveropts[]=array('value'=>0, 'label'=>L('undecided')); + if( count($repversions)>0){ + foreach($repversions as $repver){ + $repveropts[]=array('value'=>$repver['version_id'], 'label'=>$repver['version_name']); + if($repverselected==$repver['version_id']){ + $repveropts[count($repveropts)-1]['selected']=1; + } + } + $repversel['options']=$repveropts; + } + + # just the normal merged global/project dueversion + $dueversions=$proj->listVersions(false, 3); # future (tense=3) with 'shown_in_list' set + # also allow unsetting dueversion system entry + $dueveropts[]=array('value'=>0, 'label'=>L('undecided')); + if( count($dueversions)>0){ + foreach($dueversions as $duever){ + $dueveropts[]=array('value'=>$duever['version_id'], 'label'=>$duever['version_name']); + if($dueverselected==$duever['version_id']){ + $dueveropts[count($dueveropts)-1]['selected']=1; + } + } + $dueversel['options']=$dueveropts; + } + } + $catsel['name']='product_category'; + $catsel['attr']['id']='category'; + $page->assign('catselect', $catsel); + + $statussel['name']='item_status'; + $statussel['attr']['id']='status'; + $page->assign('statusselect', $statussel); + + $tasktypesel['name']='task_type'; + $tasktypesel['attr']['id']='tasktype'; + $page->assign('tasktypeselect', $tasktypesel); + + $ossel['name']='operating_system'; + $ossel['attr']['id']='os'; + $page->assign('osselect', $ossel); + + $repversel['name']='reportedver'; + $repversel['attr']['id']='reportedver'; + $page->assign('reportedversionselect', $repversel); + + $dueversel['name']='closedby_version'; + $dueversel['attr']['id']='dueversion'; + $page->assign('dueversionselect', $dueversel); + + # user tries to move a task to a different project: + if(isset($move) && $move==1){ + $page->assign('move', 1); + $page->assign('toproject', $toproject); + } + $page->pushTpl('details.edit.tpl'); + } else { + $prev_id = $next_id = 0; + + if (isset($_SESSION['tasklist']) && ($id_list = $_SESSION['tasklist']) + && ($i = array_search($task_id, $id_list)) !== false) { + $prev_id = isset($id_list[$i - 1]) ? $id_list[$i - 1] : ''; + $next_id = isset($id_list[$i + 1]) ? $id_list[$i + 1] : ''; + } + + // Sub-Tasks + $subtasks = $db->query('SELECT t.*, p.project_title + FROM {tasks} t + LEFT JOIN {projects} p ON t.project_id = p.project_id + WHERE t.supertask_id = ?', + array($task_id)); + $subtasks_cleaned = Flyspray::weedOutTasks($user, $db->fetchAllArray($subtasks)); + + for($i=0;$i<count($subtasks_cleaned);$i++){ + $subtasks_cleaned[$i]['assigned_to']=array(); + if ($assignees = Flyspray::getAssignees($subtasks_cleaned[$i]["task_id"], false)) { + for($j=0;$j<count($assignees);$j++){ + $subtasks_cleaned[$i]['assigned_to'][$j] = tpl_userlink($assignees[$j]); + } + } + } + + // Parent categories + $parent = $db->query('SELECT * + FROM {list_category} + WHERE lft < ? AND rgt > ? AND project_id = ? AND lft != 1 + ORDER BY lft ASC', + array($task_details['lft'], $task_details['rgt'], $task_details['cproj'])); + // Check for task dependencies that block closing this task + $check_deps = $db->query('SELECT t.*, s.status_name, r.resolution_name, d.depend_id, p.project_title + FROM {dependencies} d + LEFT JOIN {tasks} t on d.dep_task_id = t.task_id + LEFT JOIN {list_status} s ON t.item_status = s.status_id + LEFT JOIN {list_resolution} r ON t.resolution_reason = r.resolution_id + LEFT JOIN {projects} p ON t.project_id = p.project_id + WHERE d.task_id = ?', array($task_id)); + $check_deps_cleaned = Flyspray::weedOutTasks($user, $db->fetchAllArray($check_deps)); + + + for($i=0;$i<count($check_deps_cleaned);$i++){ + $check_deps_cleaned[$i]['assigned_to']=array(); + if ($assignees = Flyspray::getAssignees($check_deps_cleaned[$i]["task_id"], false)) { + for($j=0;$j<count($assignees);$j++){ + $check_deps_cleaned[$i]['assigned_to'][$j] = tpl_userlink($assignees[$j]); + } + } + } + + // Check for tasks that this task blocks + $check_blocks = $db->query('SELECT t.*, s.status_name, r.resolution_name, d.depend_id, p.project_title + FROM {dependencies} d + LEFT JOIN {tasks} t on d.task_id = t.task_id + LEFT JOIN {list_status} s ON t.item_status = s.status_id + LEFT JOIN {list_resolution} r ON t.resolution_reason = r.resolution_id + LEFT JOIN {projects} p ON t.project_id = p.project_id + WHERE d.dep_task_id = ?', array($task_id)); + $check_blocks_cleaned = Flyspray::weedOutTasks($user, $db->fetchAllArray($check_blocks)); + + + for($i=0;$i<count($check_blocks_cleaned);$i++){ + $check_blocks_cleaned[$i]['assigned_to']=array(); + if ($assignees = Flyspray::getAssignees($check_blocks_cleaned[$i]["task_id"], false)) { + for($j=0;$j<count($assignees);$j++){ + $check_blocks_cleaned[$i]['assigned_to'][$j] = tpl_userlink($assignees[$j]); + } + } + } + + // Check for pending PM requests + $get_pending = $db->query("SELECT * + FROM {admin_requests} + WHERE task_id = ? AND resolved_by = 0", + array($task_id)); + + // Get info on the dependencies again + $open_deps = $db->query('SELECT COUNT(*) - SUM(is_closed) + FROM {dependencies} d + LEFT JOIN {tasks} t on d.dep_task_id = t.task_id + WHERE d.task_id = ?', array($task_id)); + + $watching = $db->query('SELECT COUNT(*) + FROM {notifications} + WHERE task_id = ? AND user_id = ?', + array($task_id, $user->id)); + + // Check if task has been reopened some time + $reopened = $db->query('SELECT COUNT(*) + FROM {history} + WHERE task_id = ? AND event_type = 13', + array($task_id)); + + // Check for cached version + $cached = $db->query("SELECT content, last_updated + FROM {cache} + WHERE topic = ? AND type = 'task'", + array($task_details['task_id'])); + $cached = $db->fetchRow($cached); + + // List of votes + $get_votes = $db->query('SELECT u.user_id, u.user_name, u.real_name, v.date_time + FROM {votes} v + LEFT JOIN {users} u ON v.user_id = u.user_id + WHERE v.task_id = ? + ORDER BY v.date_time DESC', + array($task_id)); + + if ($task_details['last_edited_time'] > $cached['last_updated'] || !defined('FLYSPRAY_USE_CACHE')) { + $task_text = TextFormatter::render($task_details['detailed_desc'], 'task', $task_details['task_id']); + } else { + $task_text = TextFormatter::render($task_details['detailed_desc'], 'task', $task_details['task_id'], $cached['content']); + } + + $page->assign('prev_id', $prev_id); + $page->assign('next_id', $next_id); + $page->assign('task_text', $task_text); + $page->assign('subtasks', $subtasks_cleaned); + $page->assign('deps', $check_deps_cleaned); + $page->assign('parent', $db->fetchAllArray($parent)); + $page->assign('blocks', $check_blocks_cleaned); + $page->assign('votes', $db->fetchAllArray($get_votes)); + $page->assign('penreqs', $db->fetchAllArray($get_pending)); + $page->assign('d_open', $db->fetchOne($open_deps)); + $page->assign('watched', $db->fetchOne($watching)); + $page->assign('reopened', $db->fetchOne($reopened)); + $page->pushTpl('details.view.tpl'); + + /////////////// + // tabbed area + + // Comments + cache + $sql = $db->query('SELECT * FROM {comments} c + LEFT JOIN {cache} ca ON (c.comment_id = ca.topic AND ca.type = ?) + WHERE task_id = ? + ORDER BY date_added ASC', + array('comm', $task_id)); + $page->assign('comments', $db->fetchAllArray($sql)); + + // Comment events + $sql = get_events($task_id, ' AND (event_type = 3 OR event_type = 14)'); + $comment_changes = array(); + while ($row = $db->fetchRow($sql)) { + $comment_changes[$row['event_date']][] = $row; + } + $page->assign('comment_changes', $comment_changes); + + // Comment attachments + $attachments = array(); + $sql = $db->query('SELECT * + FROM {attachments} a, {comments} c + WHERE c.task_id = ? AND a.comment_id = c.comment_id', + array($task_id)); + while ($row = $db->fetchRow($sql)) { + $attachments[$row['comment_id']][] = $row; + } + $page->assign('comment_attachments', $attachments); + + // Comment links + $links = array(); + $sql = $db->query('SELECT * + FROM {links} l, {comments} c + WHERE c.task_id = ? AND l.comment_id = c.comment_id', + array($task_id)); + while ($row = $db->fetchRow($sql)) { + $links[$row['comment_id']][] = $row; + } + $page->assign('comment_links', $links); + + // Relations, notifications and reminders + $sql = $db->query('SELECT t.*, r.*, s.status_name, res.resolution_name + FROM {related} r + LEFT JOIN {tasks} t ON (r.related_task = t.task_id AND r.this_task = ? OR r.this_task = t.task_id AND r.related_task = ?) + LEFT JOIN {list_status} s ON t.item_status = s.status_id + LEFT JOIN {list_resolution} res ON t.resolution_reason = res.resolution_id + WHERE t.task_id is NOT NULL AND is_duplicate = 0 AND ( t.mark_private = 0 OR ? = 1 ) + ORDER BY t.task_id ASC', + array($task_id, $task_id, $user->perms('manage_project'))); + $related_cleaned = Flyspray::weedOutTasks($user, $db->fetchAllArray($sql)); + $page->assign('related', $related_cleaned); + + $sql = $db->query('SELECT t.*, r.*, s.status_name, res.resolution_name + FROM {related} r + LEFT JOIN {tasks} t ON r.this_task = t.task_id + LEFT JOIN {list_status} s ON t.item_status = s.status_id + LEFT JOIN {list_resolution} res ON t.resolution_reason = res.resolution_id + WHERE is_duplicate = 1 AND r.related_task = ? + ORDER BY t.task_id ASC', + array($task_id)); + $duplicates_cleaned = Flyspray::weedOutTasks($user, $db->fetchAllArray($sql)); + $page->assign('duplicates', $duplicates_cleaned); + + $sql = $db->query('SELECT * + FROM {notifications} n + LEFT JOIN {users} u ON n.user_id = u.user_id + WHERE n.task_id = ?', array($task_id)); + $page->assign('notifications', $db->fetchAllArray($sql)); + + $sql = $db->query('SELECT * + FROM {reminders} r + LEFT JOIN {users} u ON r.to_user_id = u.user_id + WHERE task_id = ? + ORDER BY reminder_id', array($task_id)); + $page->assign('reminders', $db->fetchAllArray($sql)); + + $page->pushTpl('details.tabs.tpl'); + + if ($user->perms('view_comments') || $proj->prefs['others_view'] || ($user->isAnon() && $task_details['task_token'] && Get::val('task_token') == $task_details['task_token'])) { + $page->pushTpl('details.tabs.comment.tpl'); + } + + $page->pushTpl('details.tabs.related.tpl'); + + if ($user->perms('manage_project')) { + $page->pushTpl('details.tabs.notifs.tpl'); + $page->pushTpl('details.tabs.remind.tpl'); + } + + if ($proj->prefs['use_effort_tracking']) { + $page->pushTpl('details.tabs.efforttracking.tpl'); + } + + $page->pushTpl('details.tabs.history.tpl'); + + } # endif can_edit_task + +} # endif can_view_task +?> diff --git a/scripts/editcomment.php b/scripts/editcomment.php new file mode 100644 index 0000000..a74ee30 --- /dev/null +++ b/scripts/editcomment.php @@ -0,0 +1,28 @@ +<?php + + /************************************\ + | Edit comment | + | ~~~~~~~~~~~~ | + | This script allows users | + | to edit comments attached to tasks | + \************************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +$sql = $db->query("SELECT c.*, u.real_name + FROM {comments} c + INNER JOIN {users} u ON c.user_id = u.user_id + WHERE comment_id = ? AND task_id = ?", + array(Get::num('id', 0), Get::num('task_id', 0))); + +$page->assign('comment', $comment = $db->fetchRow($sql)); + +if (!$user->can_edit_comment($comment)) { + Flyspray::show_error(11); +} + +$page->pushTpl('editcomment.tpl'); + +?> diff --git a/scripts/index.php b/scripts/index.php new file mode 100644 index 0000000..28e194e --- /dev/null +++ b/scripts/index.php @@ -0,0 +1,534 @@ +<?php + +/* + This script sets up and shows the tasklist page. + It is for historical reason called index.php, because it was also the frontpage. + But now there can be a different pagetype set up as frontpage in Flyspray. +*/ + + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +// Need to get function ConvertSeconds +require_once(BASEDIR . '/includes/class.effort.php'); + +if (!$user->can_select_project($proj->id)) { + $proj = new Project(0); +} + +$perpage = '50'; +if (isset($user->infos['tasks_perpage']) && $user->infos['tasks_perpage'] > 0) { + $perpage = $user->infos['tasks_perpage']; +} + +$pagenum = Get::num('pagenum', 1); +if ($pagenum < 1) { + $pagenum = 1; +} +$offset = $perpage * ($pagenum - 1); + +// Get the visibility state of all columns +$visible = explode(' ', trim($proj->id ? $proj->prefs['visible_columns'] : $fs->prefs['visible_columns'])); +if (!is_array($visible) || !count($visible) || !$visible[0]) { + $visible = array('id'); +} + +// Remove columns the user is not allowed to see +if (in_array('estimated_effort', $visible) && !$user->perms('view_estimated_effort')) { + unset($visible[array_search('estimated_effort', $visible)]); +} +if (in_array('effort', $visible) && !$user->perms('view_current_effort_done')) { + unset($visible[array_search('effort', $visible)]); +} + +# for csv export no paging limits +if (Get::has('export_list')) { + $offset = -1; + $perpage = -1; +} + +list($tasks, $id_list, $totalcount, $forbiddencount) = Backend::get_task_list($_GET, $visible, $offset, $perpage); + +if (Get::has('export_list')) { + export_task_list(); +} + +$page->uses('tasks', 'offset', 'perpage', 'pagenum', 'visible'); + +// List of task IDs for next/previous links +# Mmh the result is persistent in $_SESSION a bit for the length of each user session and can lead to a DOS quite fast on bigger installs? +# Do we really need prev-next on task details view or can we find an alternative solution? +# And using the $_SESSION for that is currently not working correct if someone uses 2 browser tabs for 2 different projects. +$_SESSION['tasklist'] = $id_list; + +$page->assign('total', $totalcount); +$page->assign('forbiddencount', $forbiddencount); + +// Send user variables to the template + +$result = $db->query('SELECT DISTINCT u.user_id, u.user_name, u.real_name, g.group_name, g.project_id + FROM {users} u + LEFT JOIN {users_in_groups} uig ON u.user_id = uig.user_id + LEFT JOIN {groups} g ON g.group_id = uig.group_id + WHERE (g.show_as_assignees = 1 OR g.is_admin = 1) + AND (g.project_id = 0 OR g.project_id = ?) AND u.account_enabled = 1 + ORDER BY g.project_id ASC, g.group_name ASC, u.user_name ASC', ($proj->id || -1)); // FIXME: -1 is a hack. when $proj->id is 0 the query fails +$userlist = array(); +while ($row = $db->fetchRow($result)) { + $userlist[$row['group_name']][] = array(0 => $row['user_id'], + 1 => sprintf('%s (%s)', $row['user_name'], $row['real_name'])); +} + +$page->assign('userlist', $userlist); + +/** + * tpl function that Displays a header cell for report list + */ +function tpl_list_heading($colname, $format = "<th%s>%s</th>") +{ + global $proj, $page; + $imgbase = '<img src="%s" alt="%s" />'; + $class = $colname; + $html = eL($colname); +/* + if ($colname == 'comments' || $colname == 'attachments') { + $html = sprintf($imgbase, $page->get_image(substr($colname, 0, -1)), $html); + } +*/ + if ($colname == 'attachments') { + $html='<i class="fa fa-paperclip fa-lg" title="'.$html.'"></i>'; + } + if ($colname == 'comments') { + $html='<i class="fa fa-comments fa-lg" title="'.$html.'"></i>'; + } + if ($colname == 'votes') { + $html='<i class="fa fa-star-o fa-lg" title="'.$html.'"></i>'; + } + + if (Get::val('order') == $colname) { + $class .= ' orderby'; + $sort1 = Get::safe('sort', 'desc') == 'desc' ? 'asc' : 'desc'; + $sort2 = Get::safe('sort2', 'desc'); + $order2 = Get::safe('order2'); + $html .= ' '.sprintf($imgbase, $page->get_image(Get::val('sort')), Get::safe('sort')); + } + else { + $sort1 = 'desc'; + if (in_array($colname, + array('project', 'tasktype', 'category', 'openedby', 'assignedto'))) + { + $sort1 = 'asc'; + } + $sort2 = Get::safe('sort', 'desc'); + $order2 = Get::safe('order'); + } + + + $new_order = array('order' => $colname, 'sort' => $sort1, 'order2' => $order2, 'sort2' => $sort2); + # unneeded params from $_GET for the sort links + $params=array_merge($_GET, $new_order); + unset($params['do']); + unset($params['project']); + unset($params['switch']); + $html = sprintf('<a title="%s" href="%s">%s</a>', + eL('sortthiscolumn'), Filters::noXSS(createURL('tasklist', $proj->id, null, $params )), $html); + + return sprintf($format, ' class="'.$class.'"', $html); +} + + +/** + * tpl function that draws a cell + */ +function tpl_draw_cell($task, $colname, $format = "<td class='%s'>%s</td>") { + global $fs, $db, $proj, $page, $user; + + $indexes = array ( + 'id' => 'task_id', + 'project' => 'project_title', + 'tasktype' => 'task_type', + 'tasktypename'=> 'tasktype_name', + 'category' => 'category_name', + 'severity' => '', + 'priority' => '', + 'summary' => 'item_summary', + 'dateopened' => 'date_opened', + 'status' => 'status_name', + 'openedby' => 'opened_by', + 'openedbyname'=> 'opened_by_name', + 'assignedto' => 'assigned_to_name', + 'lastedit' => 'max_date', + 'editedby' => 'last_edited_by', + 'reportedin' => 'product_version_name', + 'dueversion' => 'closedby_version_name', + 'duedate' => 'due_date', + 'comments' => 'num_comments', + 'votes' => 'num_votes', + 'attachments'=> 'num_attachments', + 'dateclosed' => 'date_closed', + 'closedby' => 'closed_by', + 'commentedby'=> 'commented_by', + 'progress' => '', + 'os' => 'os_name', + 'private' => 'mark_private', + 'parent' => 'supertask_id', + 'estimatedeffort' => 'estimated_effort', + ); + + //must be an array , must contain elements and be alphanumeric (permitted "_") + if(!is_array($task) || empty($task) || preg_match('![^A-Za-z0-9_]!', $colname)) { + //run away.. + return ''; + } + $class= 'task_'.$colname; + + switch ($colname) { + case 'id': + $value = tpl_tasklink($task, $task['task_id']); + break; + case 'summary': + $value = tpl_tasklink($task, utf8_substr($task['item_summary'], 0, 55)); + if (utf8_strlen($task['item_summary']) > 55) { + $value .= '...'; + } + + if($task['tagids']!=''){ + #$tags=explode(',', $task['tags']); + $tagids=explode(',', $task['tagids']); + #$tagclass=explode(',', $task['tagclass']); + $tgs=''; + for($i=0;$i< count($tagids); $i++){ + $tgs.=tpl_tag($tagids[$i]); + } + $value.=$tgs; + } + break; + + case 'tasktype': + $value = htmlspecialchars($task['tasktype_name'], ENT_QUOTES, 'utf-8'); + $class.=' typ'.$task['task_type']; + break; + + case 'severity': + $value = $task['task_severity']==0 ? '' : $fs->severities[$task['task_severity']]; + $class.=' sev'.$task['task_severity']; + break; + + case 'priority': + $value = $task['task_priority']==0 ? '' : $fs->priorities[$task['task_priority']]; + $class.=' pri'.$task['task_priority']; + break; + + case 'attachments': + case 'comments': + case 'votes': + $value = $task[$indexes[$colname]]>0 ? $task[$indexes[$colname]]:''; + break; + + case 'lastedit': + case 'duedate': + case 'dateopened': + case 'dateclosed': + $value = formatDate($task[$indexes[$colname]]); + break; + + case 'status': + if ($task['is_closed']) { + $value = eL('closed'); + } else { + $value = htmlspecialchars($task[$indexes[$colname]], ENT_QUOTES, 'utf-8'); + } + $class.=' sta'.$task['item_status']; + break; + + case 'progress': + $value = tpl_img($page->get_image('percent-' . $task['percent_complete'], false), + $task['percent_complete'] . '%'); + break; + + case 'assignedto': + # group_concat-ed for mysql/pgsql + #$value = htmlspecialchars($task[$indexes[$colname]], ENT_QUOTES, 'utf-8'); + $value=''; + $anames=explode(',',$task[$indexes[$colname]]); + $aids=explode(',',$task['assignedids']); + $aimages=explode(',',$task['assigned_image']); + for($a=0;$a < count($anames);$a++){ + if($aids[$a]){ + # deactivated: avatars looks too ugly in the tasklist, user's name needs to be visible on a first look here, without needed mouse hovering.. + #if ($fs->prefs['enable_avatars']==1 && $aimages[$a]){ + # $value.=tpl_userlinkavatar($aids[$a],30); + #} else{ + $value.=tpl_userlink($aids[$a]); + #} + #$value.='<a href="'.$aids[$a].'">'.htmlspecialchars($anames[$a], ENT_QUOTES, 'utf-8').'</a>'; + } + } + + # fallback for DBs we haven't written sql string aggregation yet (currently with group_concat() mysql and array_agg() postgresql) + if( ('postgres' != $db->dblink->dataProvider) && ('mysql' != $db->dblink->dataProvider) && ($task['num_assigned'] > 1)) { + $value .= ', +' . ($task['num_assigned'] - 1); + } + break; + + case 'private': + $value = $task[$indexes[$colname]] ? L('yes') : L('no'); + break; + + case 'commentedby': + case 'openedby': + case 'editedby': + case 'closedby': + $value = ''; + # a bit expensive! tpl_userlinkavatar() an additional sql query for each new user in the output table + # at least tpl_userlink() uses a $cache array so query for repeated users + if ($task[$indexes[$colname]] > 0) { + # deactivated: avatars looks too ugly in the tasklist, user's name needs to be visible on a first look here, without needed mouse hovering.. + #if ($fs->prefs['enable_avatars']==1){ + # $value = tpl_userlinkavatar($task[$indexes[$colname]],30); + #} else{ + $value = tpl_userlink($task[$indexes[$colname]]); + #} + } + break; + + case 'parent': + $value = ''; + if ($task['supertask_id'] > 0) { + $value = tpl_tasklink($task, $task['supertask_id']); + } + break; + + case 'estimatedeffort': + $value = ''; + if ($user->perms('view_estimated_effort')) { + if ($task['estimated_effort'] > 0){ + $value = effort::secondsToString($task['estimated_effort'], $proj->prefs['hours_per_manday'], $proj->prefs['estimated_effort_format']); + } + } + break; + + case 'effort': + $value = ''; + if ($user->perms('view_current_effort_done')) { + if ($task['effort'] > 0){ + $value = effort::secondsToString($task['effort'], $proj->prefs['hours_per_manday'], $proj->prefs['current_effort_done_format']); + } + } + break; + + default: + $value = ''; + // $colname here is NOT column name in database but a name that can appear + // both in a projects visible fields and as a key in language translation + // file, which is also used to draw a localized heading. Column names in + // database customarily use _ t to separate words, translation file entries + // instead do not and can be also be quite different. If you do see an empty + // value when you expected something, check your usage, what visible fields + // in database actually constains, and maybe add a mapping from $colname to + // to the database column name to array $indexes at the beginning of this + // function. Note that inconsistencies between $colname, database column + // name, translation entry key and name in visible fields do occur sometimes + // during development phase. + if (array_key_exists($colname, $indexes)) { + $value = htmlspecialchars($task[$indexes[$colname]], ENT_QUOTES, 'utf-8'); + } + break; + } + return sprintf($format, $class, $value); +} + +$sort; +$orderby; + +/** + * + * comparison function used by export_task_list + * + */ +function do_cmp($a, $b) +{ + global $sort,$orderby; + + if ($a[ $orderby ] == $b[ $orderby ]) { return 0; } + + if ($sort == 'asc') + return ($a[ $orderby ] < $b[ $orderby ]) ? -1 : 1; + else + return ($a[ $orderby ] > $b[ $orderby ]) ? -1 : 1; +} + +/** +* workaround fputcsv() bug https://bugs.php.net/bug.php?id=43225 +*/ +function my_fputcsv($handle, $fields) +{ + $out = array(); + + foreach ($fields as $field) { + if (empty($field)) { + $out[] = ''; + } + elseif (preg_match('/^\d+(\.\d+)?$/', $field)) { + $out[] = $field; + } + else { + $out[] = '"' . preg_replace('/"/', '""', $field) . '"'; + } + } + + return fwrite($handle, implode(',', $out) . "\n"); +} + + +/** + * Export the tasks as a .csv file + * Currently only a fixed list of task fields + */ +function export_task_list() +{ + global $tasks, $fs, $user, $sort, $orderby, $proj; + + if (!is_array($tasks)){ + return; + } + + # TODO enforcing user permissions on allowed fields + # TODO Flyspray 1.1 or later: selected fields by user request, saved user settings, tasklist settings or project defined list which fields should appear in an export + # TODO Flyspray 1.1 or later: export in .ods open document spreadsheet, .xml .... + $indexes = array ( + 'id' => 'task_id', + 'project' => 'project_title', + 'tasktype' => 'task_type', + 'category' => 'category_name', + 'severity' => 'task_severity', + 'priority' => 'task_priority', + 'summary' => 'item_summary', + 'dateopened' => 'date_opened', + 'status' => 'status_name', + 'openedby' => 'opened_by_name', + 'assignedto' => 'assigned_to_name', + 'lastedit' => 'max_date', + 'reportedin' => 'product_version', + 'dueversion' => 'closedby_version', + 'duedate' => 'due_date', + 'comments' => 'num_comments', + 'votes' => 'num_votes', + 'attachments'=> 'num_attachments', + 'dateclosed' => 'date_closed', + 'progress' => 'percent_complete', + 'os' => 'os_name', + 'private' => 'mark_private', + 'supertask' => 'supertask_id', + 'detailed_desc'=>'detailed_desc', + ); + + + # we can put this info also in the filename ... + #$projectinfo = array('Project ', $tasks[0]['project_title'], date("H:i:s d-m-Y") ); + + // sort the tasks into the order selected by the user. Set + // global vars for use by sort comparison function + + $sort = Get::safe('sort','desc') == 'desc' ? 'desc' : 'asc'; + $field = Get::safe('order', 'id'); + + if ($field == '') $field = 'id'; + $orderby = $indexes[ $field ]; + + usort($tasks, "do_cmp"); + + $outfile = str_replace(' ', '_', $proj->prefs['project_title']).'_'.date("Y-m-d").'.csv'; + + #header('Content-Type: application/csv'); + header('Content-Type: text/csv'); + header('Content-Disposition: attachment; filename='.$outfile); + header('Content-Transfer-Encoding: text'); + header('Expires: 0'); + header('Cache-Control: must-revalidate'); + #header('Pragma: public'); + #header('Content-Length: '.strlen($result)); # unknown at this time.. + ob_clean(); + flush(); + + $output = fopen('php://output', 'w'); + $headings= array( + 'ID', + 'Category', + 'Task Type', + 'Severity', + 'Summary', + 'Status', + 'Progress', + 'date_opened', + 'date_closed', + 'due_date', + 'supertask_id' + ); + if($user->perms('view_estimated_effort') && $proj->id>0 && $proj->prefs['use_effort_tracking']){ + $headings[]='Estimated Effort'; + } + $headings[]='Description'; + //if($user->perms('view_current_effort_done') && $proj->id>0 && $proj->prefs['use_effort_tracking']){ $headings[]='Done Effort'; } + + #fputcsv($output, $headings); + my_fputcsv($output, $headings); # fixes 'SYLK' FS#2123 Excel problem + foreach ($tasks as $task) { + $row = array( + $task['task_id'], + $task['category_name'], + $task['task_type'], + $fs->severities[ $task['task_severity'] ], + $task['item_summary'], + $task['status_name'], + $task['percent_complete'], + $task['date_opened'], + $task['date_closed'], + $task['due_date'], + $task['supertask_id'] + ); + if( $user->perms('view_estimated_effort') && $proj->id>0 && $proj->prefs['use_effort_tracking']){ + $row[]=$task['estimated_effort']; + } + $row[]=$task['detailed_desc']; + //if( $user->perms('view_current_effort_done') && $proj->id>0 && $proj->prefs['use_effort_tracking']){ $row=$task['effort']; } + + my_fputcsv($output, $row); # fputcsv() is buggy + } + fclose($output); + exit(); +} + +// Javascript replacement +if (Get::val('toggleadvanced')) { + $advanced_search = intval(!Req::val('advancedsearch')); + Flyspray::setCookie('advancedsearch', $advanced_search, time()+60*60*24*30); + $_COOKIE['advancedsearch'] = $advanced_search; +} + +/** + * Update check + */ +if(Get::has('hideupdatemsg')) { + unset($_SESSION['latest_version']); +} else if ($conf['general']['update_check'] + && $user->perms('is_admin') + && $fs->prefs['last_update_check'] < time()-60*60*24*3) { + if (!isset($_SESSION['latest_version'])) { + $latest = Flyspray::remote_request('http://www.flyspray.org/version.txt', GET_CONTENTS); + # if for some silly reason we get an empty response, we use the actual version + $_SESSION['latest_version'] = empty($latest) ? $fs->version : $latest ; + $db->query('UPDATE {prefs} SET pref_value = ? WHERE pref_name = ?', array(time(), 'last_update_check')); + } +} +if (isset($_SESSION['latest_version']) && version_compare($fs->version, $_SESSION['latest_version'] , '<') ) { + $page->assign('updatemsg', true); +} + + +$page->setTitle($fs->prefs['page_title'] . $proj->prefs['project_title'] . ': ' . L('tasklist')); +$page->pushTpl('index.tpl'); + +?> diff --git a/scripts/langdiff.php b/scripts/langdiff.php new file mode 100644 index 0000000..f004b54 --- /dev/null +++ b/scripts/langdiff.php @@ -0,0 +1,192 @@ +<?php + +if(!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +# let also project managers allow translation of flyspray +if(!$user->perms('manage_project')) { + Flyspray::show_error(28); +} + +ob_start(); + +?> +<style type="text/css"> +pre { margin : 0; } +table{border-collapse:collapse;} +.progress_bar_container{height:20px;} +.progress_bar_container span:first-child{display:inline-block;margin-top:2px;z-index:101;color:#000;} +.overview{margin-left:auto;margin-right:auto;} +.overview td, .overview th{border:none;padding:0;} +a.button{padding:2px 10px 2px 10px;margin:2px;} +table th{text-align:center;} +table th, table td { + vertical-align:middle; + border: 1px solid #ccc; + padding: 2px; +} +tr:hover td, tr:hover th { background : #e0e0e0; } +</style> +<?php +require_once dirname(dirname(__FILE__)) . '/includes/fix.inc.php'; +/* +* Usage: Open this file like ?do=langdiff?lang=de in your browser. +* "de" represents your language code. +*/ +$lang = isset($_GET['lang']) ? $_GET['lang'] : 'en'; +if( preg_match('/[^a-zA-Z_]/', $lang)) { + die('Invalid language name.'); +} + +# reload en.php if flyspray did it before! +require('lang/en.php'); +// while the en.php and $lang.php both defines $language, the english one should be keept +$orig_language = $language; + +$translationfile = 'lang/'."$lang.php"; +if ($lang != 'en' && file_exists($translationfile)) { + # reload that file if flyspray did it before! + include($translationfile); + if( isset($_GET['sort']) && $_GET['sort']=='key'){ + ksort($orig_language); + }elseif( isset($_GET['sort']) && $_GET['sort']=='en'){ + asort($orig_language); + }elseif( isset($_GET['sort']) && $_GET['sort']==$_GET['lang']){ + # todo + }else{ + # show as it is in file en.php + } + + echo '<h1>Diff report for language ',$lang,'</h1>',"\n"; + echo '<h2>The following translation keys are missing in the translation:</h2>'; + echo '<table>'; + $i = 0; + foreach ($orig_language as $key => $val) { + if (!isset($translation[$key])) { + echo '<tr><th>',$key,'</th><td>'.htmlspecialchars($val).'</td></tr>',"\n"; + $i++; + } + + } + echo '</table>'; + if ( $i > 0 ){ + echo '<p>',$i,' out of ',sizeof($language),' keys to translate.</p>'; + } + echo '<h2>The following translation keys should be deleted from the translation:</h2>'; + echo '<table cellspacing="0">'; + $i = 0; + foreach ($translation as $key => $val) { + if ( !isset($orig_language[$key])) { + echo '<tr class="line',($i%2),'"><th>',$key,'</th><td><pre>\'',$val,'\'</pre></td></tr>',"\n"; + $i++; + } + } + echo '</table>'; + if ( $i > 0 ){ + echo '<p>'.$i.' entries can be removed from this translation.</p>'; + } else{ + echo '<p><i class="fa fa-check fa-2x"></i> None</p>'; + } + echo '<h2><a name="compare"></a>Direct comparision between english and '.htmlspecialchars($lang).'</h2>'; + echo '<table> + <colgroup></colgroup> + <thead><tr> + <th><a href="?do=langdiff&lang='.htmlspecialchars($lang).'&sort=key#compare" title="sort by translation key">translation key</th> + <th><a href="?do=langdiff&lang='.htmlspecialchars($lang).'&sort=en#compare" title="sort by english">en</a></th> + <th>'.htmlspecialchars($lang).'</th> + </tr> + </thead> + <tbody>'; + $i = 0; + foreach ($orig_language as $key => $val) { + if (!isset($translation[$key])) { + echo '<tr><th>',$key,'</th><td>'.htmlspecialchars($val).'</td><td></td></tr>'."\n"; + }else{ + echo ' + <tr> + <th>',$key,'</th><td>'.htmlspecialchars($val).'</td> + <td>'.htmlspecialchars($translation[$key]).'</td> + </tr>'."\n"; + } + $i++; + } + echo '</tbody></table>'; +} else { + # TODO show all existing translations overview and selection + # readdir + $english=$language; + $max=count($english); + $langfiles=array(); + $workfiles=array(); + if ($handle = opendir('lang')) { + $languages=array(); + while (false !== ($file = readdir($handle))) { + if ($file != "." + && $file != ".." + && $file!='.langdiff.php' + && $file!='.langedit.php' + && !(substr($file,-4)=='.bak') + && !(substr($file,-5)=='.safe') ) { + # if a .$lang.php.work file but no $lang.php exists yet + if( substr($file,-5)=='.work'){ + if(!is_file('lang/'.substr($file,1,-5)) ){ + $workfiles[]=$file; + } + } else{ + $langfiles[]=$file; + } + } + } + asort($langfiles); + asort($workfiles); + echo '<table class="overview"> + <thead><tr><th>'.L('file').'</th><th>'.L('progress').'</th><th> </th></tr></thead> + <tbody>'; + foreach($langfiles as $lang){ + unset($translation); + require('lang/'.$lang); # file $language variable + $i=0; $empty=0; + foreach ($orig_language as $key => $val) { + if (!isset($translation[$key])) { + $i++; + }else{ + if($val==''){ + $empty++; + } + } + } + $progress=floor(($max-$i)*100/$max*10)/10; + if($lang!='en.php'){ + echo ' +<tr> +<td><a href="?do=langdiff&lang='.substr($lang,0,-4).'">'.$lang.'</a></td> +<td><a href="?do=langdiff&lang='.substr($lang,0,-4).'" class="progress_bar_container"> +<span class="progress">'.$progress.' %</span> +<span style="width:'.$progress.'%" class="progress_bar"></span></a> +</td> +<td><a class="button" href="?do=langedit&lang='.substr($lang,0,-4).'">'.L('translate').' '.substr($lang,0,-4).'</a></td> +</tr>'; + }else{ + echo '<tr><td>en.php</td><td>is reference and fallback</td><td><a class="button" href="?do=langedit&lang='.substr($lang,0,-4).'">Translate '.substr($lang,0,-4).'</a></td></tr>'; + } + } + foreach($workfiles as $workfile){ + echo '<tr> + <td><a href="?do=langdiff&lang='.substr($workfile,1,-9).'">'.$workfile.'</a></td> + <td></td> + <td><a class="button" href="?do=langedit&lang='.substr($workfile,1,-9).'">'.L('translate').' '.substr($workfile,1,-9).'</a></td> + </tr>'; + } + closedir($handle); + echo '</tbody></table>'; + } +} + +$content = ob_get_contents(); +ob_end_clean(); + +$page->uses('content'); +$page->pushTpl('admin.translation.tpl'); + +?> diff --git a/scripts/langedit.php b/scripts/langedit.php new file mode 100644 index 0000000..20ba4a8 --- /dev/null +++ b/scripts/langedit.php @@ -0,0 +1,329 @@ + +<?php +/* langedit.php + * + * Translation tool for the Flyspray Bug Tracker System + * http://flyspray.org + * + * Author + * Lars-Erik Hoffsten + * larserik@softpoint.nu + * + * 2006-06-05 Version 1.0 + * Initial version + * 2006-06-05 Version 1.1 + * Using UTF-8 character encoding + * Handles all kinds of characters like line feed etc that need special escaping + * Hides backup files with leading '.' in filename + * Creates a work file for better safety + * New languages are easily created just by typing the new language code on the URL + * 2006-06-07 Version 1.2 + * Moved to the setup directory so that it wouldn't be left behind in the + * installation to be used by some one unauthorized + * mb_strlen() replaced by strlen(utf_decode()) because mb_* functions are not standard + * 2006-06-12 Version 1.3 + * Writes correct array name for english + * + * 2015-02-09 + * use flyspray theme, add button targeting translation overview for better workflow + * 2015-03-02 + * integration into flyspray user interface + * + * Usage: http://.../flyspray/?do=langedit&lang=sv + * "sv" represents your language code. + * + * !!! + * Note that this script rewrites the language file completely when saving. + * Anything else than the $translation array will be lost including any file comments. + * !!! + */ + +if(!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +# let also project managers allow translation of flyspray +if(!$user->perms('manage_project')) { + Flyspray::show_error(28); +} + +// Make it possible to reload page after updating language +// Don't want to send form data again if user reloads the page +ob_start(); +?> +<script language="javascript"> +// Indicate which texts are changed, called from input and textarea onchange +function set(id){ + var checkbox = document.getElementById('id_checkbox_' + id); + if(checkbox) + checkbox.checked = true; + var hidden = document.getElementById('id_hidden_' + id); + if(hidden) + hidden.disabled = false; + var conf = document.getElementById('id_confirm'); + if(conf) + conf.disabled = true; +} +</script> +<?php + +// Set current directory to where the language files are +chdir('lang'); + +$lang = isset($_GET['lang']) ? $_GET['lang']:false; +$fail = ''; +if(!$lang || !preg_match('/^[a-zA-Z0-9_]+$/', $lang)){ + $fail .= "Language code not supplied correctly<br>\n"; +} +if(!file_exists('en.php')) { + $fail .= "The english language file <code>en.php</code> is missing. Make sure this script is run from the same directory as the language files <code>.../flyspray/lang/</code><br>\n"; +} +if($fail) { + die($fail."<b>Usage:</b> <a href='?do=langedit&lang='><lang code></a> where <lang code> should be replaced by your language, e.g. <b>de</b> for German."); +} +// Read english language file in array $language (assumed to be UTF-8 encoded) +require('en.php'); +if(!is_array(@$language)){ + die("Invalid language file for english"); +} +$count = count($language); + +// Read the translation file in array $translation (assumed to be UTF-8 encoded) +$working_copy = false; +if(!file_exists($lang.'.php') && !file_exists('.'.$lang.'.php.work')) { + echo '<h3>A new language file will be created: <code>'.$lang.'.php</code></h2>'; +} else { + if($lang != 'en') { + if(file_exists('.'.$lang.'.php.work')) { + $working_copy = true; + include_once('.'.$lang.'.php.work'); // Read the translation array (work in progress) + } else{ + include($lang.'.php'); // Read the original translation array - maybe again, no _once here! + } + } else if(file_exists('.en.php.work')){ + $working_copy = true; + $tmp = $language; + include_once('.en.php.work'); // Read the language array (work in progress) + $translation = $language; // Edit the english language file + $language = $tmp; + } else{ + $translation = $language; // Edit the english language file + } + + if(!is_array(@$translation)){ + echo "<b>Warning: </b>the translation file does not contain the \$translation array, a new file will be created: <code>$lang.php</code>\n"; + } +} + +$limit = 30; +$begin = isset($_GET['begin']) ? (int)($_GET['begin'] / $limit) * $limit : 0; + +// Was show missing pressed? +$show_empty = (!isset($_POST['search']) && isset($_REQUEST['empty'])); // Either POST or URL +// Any text in search box? +if(!$show_empty && isset($_POST['search_for'])) { + $search = trim($_POST['search_for']); +} else if(!$show_empty && isset($_GET['search_for'])) { + $search = trim(urldecode($_GET['search_for'])); +} else { + $search = ""; +} +// Path to this file +$self = "{$_SERVER['SCRIPT_NAME']}?do=langedit&lang=$lang"; + +if(isset($_POST['confirm'])) { + // Make a backup + unlink(".$lang.php.bak"); + rename("$lang.php", ".$lang.php.bak"); + rename(".$lang.php.work", "$lang.php"); + // Reload page, so that form data won't get posted again on refresh + header("location: $self&begin=$begin" . ($search? "&search_for=".urlencode($search): "") . ($show_empty? "&empty=": "")); + exit; +} else if(isset($_POST['submit']) && isset($_POST['L'])) { + // Save button was pressed + update_language($lang, $_POST['L'], @$_POST['E']); + // Reload page, so that form data won't get posted again on refresh + header("location: $self&begin=$begin" . ($search? "&search_for=".urlencode($search): "") . ($show_empty? "&empty=": "")); + exit; +} + +// One form for all buttons and inputs +echo '<a class="button" href="?do=langdiff">Overview</a>'; +echo "<form action=\"$self&do=langedit&begin=$begin". ($show_empty? "&empty=": "") . "\" method=\"post\">\n"; +echo "<table cellspacing=0 cellpadding=1>\n<tr><td colspan=3>"; +// Make page links +for($p = 0; $p < $count; $p += $limit){ + if($p){ + echo " | "; + } + $bgn = $p+1; + $end = min($p+$limit, $count); + if($p != $begin || $search || $show_empty) { + echo "<a href=\"$self&begin=$bgn\">$bgn…$end</a>\n"; // Show all links when searching or display all missing strings + } else { + echo "<b>$bgn…$end</b>\n"; + } +} +?> +</td><td> +<input type="submit" name="submit" value="Save changes" title="Saves changes to a work file"> +<input type="submit" name="confirm" id="id_confirm" value="Confirm all changes"<?php echo !$working_copy ? ' disabled="disabled"': ''; ?> title="Confirm all changes and replace the original language file"> +<br> +<?php +if($working_copy) { + echo "Your changes are stored in <code>.$lang.php.work</code> until you press 'Confirm all changes'<br>"; +} +// Search +echo '<input type="text" name="search_for" value="'.Filters::noXSS($search).'"><input type="submit" name="search" value="Search">'; +// List empty +if($lang != 'en') { + echo '<input type="submit" name="empty" value="Show missing" title="Show all texts that have no translation">'; +} +?> +</td></tr> +<tr><th colspan=2>Key</th><th>English</th><th>Translation:<?php echo $lang; ?></th></tr> +<?php +$i = 0; // Counter to find offset +$j = 0; // Counter for background shading +foreach ($language as $key => $val){ + $trans = @$translation[$key]; + if((!$search && !$show_empty && $i >= $begin) || + ($search && (stristr($key, $search) || stristr($val, $search) || stristr($trans, $search))) || + ($show_empty && !$trans)){ + $bg = ($j++ & 1)? '#fff': '#eed'; + // Key + echo '<tr style="background-color:'.$bg.'" valign="top"><td align="right">'.($i+1).'</td><td><b>'.$key.'</b></td>'; + // English (underline leading and trailing spaces) + $space = '<b style="color:#c00;" title="Remember to include a space in the translation!">_</b>'; + echo '<td>'. (preg_match("/^[ \t]/",$val)? $space: "") . nl2br(htmlentities($val)). (preg_match("/[ \t]$/",$val)? $space: "") ."</td>\n"; + echo '<td align="right"><nobr>'; + echo '<input type="checkbox" disabled="disabled" id="id_checkbox_'.$key.'">'; + echo '<input type="hidden" disabled="disabled" id="id_hidden_'.$key.'" name="E['.$key.']">'; + // Count lines in both english and translation + $lines = 1 + max(preg_match_all("/\n/", $val, $matches), preg_match_all("/\n/", $trans, $matches)); + // Javascript call on some input events + $onchange = 'onchange="set(\''.$key.'\');" onkeypress="set(\''.$key.'\');"'; + // \ is displayed as \\ in edit fields to allow \n as line feed + $trans = str_replace("\\", "\\\\", $trans); + if($lines > 1 || strlen(utf8_decode($val)) > 60 || strlen(utf8_decode($trans)) > 60){ + // Format long texts for <textarea>, remove spaces after each new line + $trans = preg_replace("/\n[ \t]+|\\n/", "\n", htmlentities($trans, ENT_NOQUOTES, "UTF-8")); + echo '<textarea cols=79 rows='.max(4,$lines).' name="L['.$key.']" '.$onchange.'>'.$trans.'</textarea>'; + } else{ + // Format short texts for <input type=text> + $trans = str_replace(array("\n", "\""), array("\\n", """), $trans); + echo '<input class="edit" type="text" name="L['.$key.']" value="'.$trans.'" size=80 '.$onchange.'>'; + } + echo "</nobr></td></tr>\n"; + + if(--$limit == 0 && !$search && !$show_empty){ + break; + } + } + $i++; +} +?> +</table> +<hr> +<table width="100%"> +<tr> +<td>The language files are UTF-8 encoded, avoid manual editing if You are not sure that your editor supports UTF-8<br> +Syntax for <b>\</b> is <b>\\</b> and for line feed type <b>\\n</b> in single line edit fields</td> +<td style="text-align: right;"><i>langedit by <a href="mailto:larserik@softpoint.nu">larserik@softpoint.nu</a></i></td> +</tr> +</table> +<?php + +$content = ob_get_contents(); +ob_end_clean(); + +$page->uses('content'); +$page->pushTpl('admin.translation.tpl'); + +// Parse string for \n and \\ to be replaced by <lf> and \ +function parseNL($str) { + $pos = 0; + while(($pos = strpos($str, "\\", $pos)) !== false){ + switch(substr($str, $pos, 2)){ + case "\\n": + $str = substr_replace($str, "\n", $pos, 2); + break; + case "\\\\": + $str = substr_replace($str, "\\", $pos, 2); + break; + } + $pos++; + } + return $str; +} + +function update_language($lang, &$strings, $edit) { + global $language, $translation; + + if(!is_array($edit)) { + return; + } + // Form data contains UTF-8 encoded text + foreach($edit as $key => $dummy){ + if(@$strings[$key]) { + $translation[$key] = parseNL($strings[$key]); + } else { + unset($translation[$key]); + } + } + // Make a backup just in case! + if(!file_exists(".$lang.php.safe")){ + // Make one safe backup that will NOT be changed by this script + copy("$lang.php", ".$lang.php.safe"); + } + if(file_exists(".$lang.php.work")){ + // Then make ordinary backups + copy(".$lang.php.work", ".$lang.php.bak"); + } + // Write the translation array to file with UNIX style line endings + $file = fopen(".$lang.php.work", "w"); + // Write the UTF-8 BOM, Byte Order Marker + //fprintf($file, chr(0xef).chr(0xbb).chr(0xbf)); + // Header + fprintf($file, "<?php\n//\n" + ."// This file is auto generated with langedit.php\n" + ."// Characters are UTF-8 encoded\n" + ."// \n" + ."// Be careful when editing this file manually, some text editors\n" + ."// may convert text to UCS-2 or similar (16-bit) which is NOT\n" + ."// readable by the PHP parser\n" + ."// \n" + ."// Furthermore, nothing else than the language array is saved\n" + ."// when using the langedit.php editor!\n//\n"); + if($lang == 'en') { + fprintf($file, "\$language = array(\n"); + } else { + fprintf($file, "\$translation = array(\n"); + } + + // The following characters will be escaped in multiline strings + // in the following order: + // \ => \\ + // " => \" + // $ => \$ + // <lf> => \n + // <cr> are removed if any + $pattern = array("\\", "\"", "\$", "\n", "\r"); + $replace = array("\\\\", "\\\"", "\\\$", "\\n", ""); + // Write the array to the file, ordered as the english language file + foreach($language as $key => $val){ + $trans = @$translation[$key]; + if(!$trans) { + continue; + } + if(strstr($trans, "\n")) { // Use double quotes for multiline + fprintf($file, "%-26s=> \"%s\",\n", "'$key'", str_replace($pattern, $replace, $trans)); + } else { // Use single quotes for single lines, only \ and ' needs escaping + fprintf($file, "%-26s=> '%s',\n", "'$key'", str_replace(array("\\","'"), array("\\\\", "\\'"), $trans)); + } + } + fprintf($file, ");\n\n?".">\n"); // PHP end tag currupts some syntax color coders + fclose($file); +} + +?> diff --git a/scripts/lostpw.php b/scripts/lostpw.php new file mode 100644 index 0000000..ce2a78d --- /dev/null +++ b/scripts/lostpw.php @@ -0,0 +1,32 @@ +<?php + + /*********************************************************\ + | Deal with lost passwords | + | ~~~~~~~~~~~~~~~~~~~~~~~~ | + \*********************************************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +$page->setTitle($fs->prefs['page_title'] . L('lostpw')); + +if (!Req::has('magic_url') && $user->isAnon()) { + // Step One: user requests magic url + $page->pushTpl('lostpw.step1.tpl'); +} +elseif (Req::has('magic_url') && $user->isAnon()) { + # Step Two: user enters new password + # First as link from email (GET), form could be repeated as POST + # when user misrepeats the new password. so GET and POST possible here! + $check_magic = $db->query('SELECT * FROM {users} WHERE magic_url = ?', + array(Req::val('magic_url'))); + + if (!$db->countRows($check_magic)) { + Flyspray::show_error(12); + } + $page->pushTpl('lostpw.step2.tpl'); +} else { + Flyspray::redirect($baseurl); +} +?> diff --git a/scripts/myprofile.php b/scripts/myprofile.php new file mode 100644 index 0000000..bd9bd22 --- /dev/null +++ b/scripts/myprofile.php @@ -0,0 +1,41 @@ +<?php + + /*********************************************************\ + | User Profile Edition | + | ~~~~~~~~~~~~~~~~~~~~ | + \*********************************************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +if ($user->isAnon()) { + Flyspray::show_error(13); +} + +# maybe add some checks for output if a task or project or user changed permissions +# for example the user is moved from developer to basic +# or a task is changed to private modus +# or a task is closed now +# maybe add 'AND t.is_closed<>1' if we want only show votes of active tasks, that are taken for the votes limit. +# How can a user unvote such now unvisible tasks to get back under his voting limit for the project? +$votes=$db->query(' + SELECT v.*, t.project_id, t.item_summary, t.task_type, t.is_closed, p.project_title + FROM {votes} v + JOIN {tasks} t ON t.task_id=v.task_id + LEFT JOIN {projects} p ON p.project_id=t.project_id + WHERE user_id = ? + ORDER BY t.project_id, t.task_id', + $user->id +); +$votes=$db->fetchAllArray($votes); + +$page->assign('votes', $votes); +$page->assign('groups', Flyspray::listGroups()); +$page->assign('project_groups', Flyspray::listGroups($proj->id)); +$page->assign('theuser', $user); + +$page->setTitle($fs->prefs['page_title'] . L('editmydetails')); +$page->pushTpl('myprofile.tpl'); + +?> diff --git a/scripts/newmultitasks.php b/scripts/newmultitasks.php new file mode 100644 index 0000000..741ea81 --- /dev/null +++ b/scripts/newmultitasks.php @@ -0,0 +1,20 @@ +<?php + +/* +* Multiple Tasks Creation +*/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +if (!$user->can_open_task($proj) && !$user->perms('add_multiple_tasks')) { + Flyspray::show_error(15); +} + +$page->setTitle($fs->prefs['page_title'] . $proj->prefs['project_title'] . ': ' . L('newtask')); + +$page->assign('old_assigned', ''); +$page->pushTpl('newmultitasks.tpl'); + +?>
\ No newline at end of file diff --git a/scripts/newtask.php b/scripts/newtask.php new file mode 100644 index 0000000..8cd3dc2 --- /dev/null +++ b/scripts/newtask.php @@ -0,0 +1,53 @@ +<?php + + /********************************************************\ + | Task Creation | + | ~~~~~~~~~~~~~ | + \********************************************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +if (!$user->can_open_task($proj)) { + Flyspray::show_error(15); +} + +$page->setTitle($fs->prefs['page_title'] . $proj->prefs['project_title'] . ': ' . L('newtask')); + +$result = $db->query(' + SELECT u.user_id, u.user_name, u.real_name, g.group_id, g.group_name, g.project_id + FROM {users} u + JOIN {users_in_groups} uig ON u.user_id = uig.user_id + JOIN {groups} g ON g.group_id = uig.group_id + WHERE (g.show_as_assignees = 1 OR g.is_admin = 1) + AND (g.project_id = 0 OR g.project_id = ?) AND u.account_enabled = 1 + ORDER BY g.project_id ASC, g.group_name ASC, u.user_name ASC', $proj->id); + +$userlist = array(); +$userids=array(); +while ($row = $db->fetchRow($result)) { + if (!in_array($row['user_id'], $userids)){ + $userlist[$row['group_id']][] = array( + 0 => $row['user_id'], + 1 => sprintf('%s (%s)', $row['user_name'], $row['real_name']), + 2 => $row['project_id'], + 3 => $row['group_name'] + ); + $userids[]=$row['user_id']; + } else{ + # user is probably in a global group with assignee permission listed, so no need to show second time in a project group. + } +} + +$assignees = array(); +if (is_array(Post::val('rassigned_to'))) { + $assignees = Post::val('rassigned_to'); +} + +$page->assign('assignees', $assignees); +$page->assign('userlist', $userlist); +$page->assign('old_assigned', ''); +$page->pushTpl('newtask.tpl'); + +?> diff --git a/scripts/oauth.php b/scripts/oauth.php new file mode 100644 index 0000000..48dface --- /dev/null +++ b/scripts/oauth.php @@ -0,0 +1,201 @@ +<?php + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +$providers = array( + 'github' => function() use ($conf) { + if (empty($conf['oauth']['github_secret']) || + empty($conf['oauth']['github_id']) || + empty($conf['oauth']['github_redirect'])) { + + throw new Exception('Config error make sure the github_* variables are set.'); + } + return new GithubProvider(array( + 'clientId' => $conf['oauth']['github_id'], + 'clientSecret' => $conf['oauth']['github_secret'], + 'redirectUri' => $conf['oauth']['github_redirect'], + 'scopes' => array('user:email') + )); + }, + 'google' => function() use ($conf) { + if (empty($conf['oauth']['google_secret']) || + empty($conf['oauth']['google_id']) || + empty($conf['oauth']['google_redirect'])) { + + throw new Exception('Config error make sure the google_* variables are set.'); + } + return new League\OAuth2\Client\Provider\Google(array( + 'clientId' => $conf['oauth']['google_id'], + 'clientSecret' => $conf['oauth']['google_secret'], + 'redirectUri' => $conf['oauth']['google_redirect'], + 'scopes' => array('email', 'profile') + )); + }, + 'facebook' => function() use ($conf) { + if (empty($conf['oauth']['facebook_secret']) || + empty($conf['oauth']['facebook_id']) || + empty($conf['oauth']['facebook_redirect'])) { + + throw new Exception('Config error make sure the facebook_* variables are set.'); + } + return new League\OAuth2\Client\Provider\Facebook(array( + 'clientId' => $conf['oauth']['facebook_id'], + 'clientSecret' => $conf['oauth']['facebook_secret'], + 'redirectUri' => $conf['oauth']['facebook_redirect'], + )); + }, + 'microsoft' => function() use ($conf) { + if (empty($conf['oauth']['microsoft_secret']) || + empty($conf['oauth']['microsoft_id']) || + empty($conf['oauth']['microsoft_redirect'])) { + + throw new Exception('Config error make sure the microsoft_* variables are set.'); + } + return new League\OAuth2\Client\Provider\Microsoft(array( + 'clientId' => $conf['oauth']['microsoft_id'], + 'clientSecret' => $conf['oauth']['microsoft_secret'], + 'redirectUri' => $conf['oauth']['microsoft_redirect'], + )); + }, + 'instagram' => function() use ($conf) { + if (empty($conf['oauth']['instagram_secret']) || + empty($conf['oauth']['instagram_id']) || + empty($conf['oauth']['instagram_redirect'])) { + + throw new Exception('Config error make sure the instagram_* variables are set.'); + } + return new League\OAuth2\Client\Provider\Instagram(array( + 'clientId' => $conf['oauth']['instagram_id'], + 'clientSecret' => $conf['oauth']['instagram_secret'], + 'redirectUri' => $conf['oauth']['instagram_redirect'], + )); + }, + 'eventbrite' => function() use ($conf) { + if (empty($conf['oauth']['eventbrite_secret']) || + empty($conf['oauth']['eventbrite_id']) || + empty($conf['oauth']['eventbrite_redirect'])) { + + throw new Exception('Config error make sure the eventbrite_* variables are set.'); + } + return new League\OAuth2\Client\Provider\Eventbrite(array( + 'clientId' => $conf['oauth']['eventbrite_id'], + 'clientSecret' => $conf['oauth']['eventbrite_secret'], + 'redirectUri' => $conf['oauth']['eventbrite_redirect'], + )); + }, + 'linkedin' => function() use ($conf) { + if (empty($conf['oauth']['linkedin_secret']) || + empty($conf['oauth']['linkedin_id']) || + empty($conf['oauth']['linkedin_redirect'])) { + + throw new Exception('Config error make sure the linkedin_* variables are set.'); + } + return new League\OAuth2\Client\Provider\LinkedIn(array( + 'clientId' => $conf['oauth']['linkedin_id'], + 'clientSecret' => $conf['oauth']['linkedin_secret'], + 'redirectUri' => $conf['oauth']['linkedin_redirect'], + )); + }, + 'vkontakte' => function() use ($conf) { + if (empty($conf['oauth']['vkontakte_secret']) || + empty($conf['oauth']['vkontakte_id']) || + empty($conf['oauth']['vkontakte_redirect'])) { + + throw new Exception('Config error make sure the vkontakte_* variables are set.'); + } + return new League\OAuth2\Client\Provider\Vkontakte(array( + 'clientId' => $conf['oauth']['vkontakte_id'], + 'clientSecret' => $conf['oauth']['vkontakte_secret'], + 'redirectUri' => $conf['oauth']['vkontakte_redirect'], + )); + }, +); + +if (! isset($_SESSION['return_to'])) { + $_SESSION['return_to'] = base64_decode(Get::val('return_to', '')); + $_SESSION['return_to'] = $_SESSION['return_to'] ?: $baseurl; +} + +$provider = isset($_SESSION['oauth_provider']) ? $_SESSION['oauth_provider'] : 'none'; +$provider = strtolower(Get::val('provider', $provider)); +unset($_SESSION['oauth_provider']); + +$active_oauths = explode(' ', $fs->prefs['active_oauths']); +if (!in_array($provider, $active_oauths)) { + Flyspray::show_error(26); +} + +$obj = $providers[$provider](); + +if ( ! Get::has('code') && ! Post::has('username')) { + // get authorization code + header('Location: '.$obj->getAuthorizationUrl()); + exit; +} + +if (isset($_SESSION['oauth_token'])) { + $token = unserialize($_SESSION['oauth_token']); + unset($_SESSION['oauth_token']); +} else { + // Try to get an access token + try { + $token = $obj->getAccessToken('authorization_code', array('code' => $_GET['code'])); + } catch (\League\OAuth2\Client\Exception\IDPException $e) { + throw new Exception($e->getMessage()); + } +} + +$user_details = $obj->getUserDetails($token); +$uid = $user_details->uid; + +if (Post::has('username')) { + $username = Post::val('username'); +} else { + $username = $user_details->nickname; +} + +// First time logging in +if (! Flyspray::checkForOauthUser($uid, $provider)) { + if (! $user_details->email) { + Flyspray::show_error(27); + } + + $success = false; + + if ($username) { + $group_in = $fs->prefs['anon_group']; + $name = $user_details->name ?: $username; + $success = Backend::create_user($username, null, $name, '', $user_details->email, 0, 0, $group_in, 1, $uid, $provider); + } + + // username taken or not provided, ask for it + if (!$success) { + $_SESSION['oauth_token'] = serialize($token); + $_SESSION['oauth_provider'] = $provider; + $page->assign('provider', ucfirst($provider)); + $page->assign('username', $username); + $page->pushTpl('register.oauth.tpl'); + return; + } +} + +if (($user_id = Flyspray::checkLogin($user_details->email, null, 'oauth')) < 1) { + Flyspray::show_error(23); // account disabled +} + +$user = new User($user_id); + +// Set a couple of cookies +$passweirded = crypt($user->infos['user_pass'], $conf['general']['cookiesalt']); +Flyspray::setCookie('flyspray_userid', $user->id, 0,null,null,null,true); +Flyspray::setCookie('flyspray_passhash', $passweirded, 0,null,null,null,true); +$_SESSION['SUCCESS'] = L('loginsuccessful'); +$db->query("UPDATE {users} SET last_login = ? WHERE user_id=?", array(time(), $user->id)); + +$return_to = $_SESSION['return_to']; +unset($_SESSION['return_to']); + +Flyspray::redirect($return_to); +?> diff --git a/scripts/pm.php b/scripts/pm.php new file mode 100644 index 0000000..c9884e8 --- /dev/null +++ b/scripts/pm.php @@ -0,0 +1,61 @@ +<?php + + /********************************************************\ + | Project Managers Toolbox | + | ~~~~~~~~~~~~~~~~~~~~~~~~ | + | This script is for Project Managers to modify settings | + | for their project, including general permissions, | + | members, group permissions, and dropdown list items. | + \********************************************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +if (!$user->perms('manage_project') || !$proj->id) { + Flyspray::show_error(16); +} + +switch ($area = Req::val('area', 'prefs')) { + case 'pendingreq': + $sql = $db->query("SELECT * + FROM {admin_requests} ar + LEFT JOIN {tasks} t ON ar.task_id = t.task_id + LEFT JOIN {users} u ON ar.submitted_by = u.user_id + WHERE ar.project_id = ? AND resolved_by = 0 + ORDER BY ar.time_submitted ASC", array($proj->id)); + + $page->assign('pendings', $db->fetchAllArray($sql)); + + case 'prefs': + case 'groups': + $page->assign('globalgroups', Flyspray::listGroups(0)); # global user groups + $page->assign('groups', Flyspray::listGroups($proj->id)); # project specific user groups + case 'editgroup': + // yeah, utterly stupid, is changed in 1.0 already + if (Req::val('area') == 'editgroup') { + $group_details = Flyspray::getGroupDetails(Req::num('id')); + if (!$group_details || $group_details['project_id'] != $proj->id) { + Flyspray::show_error(L('groupnotexist')); + Flyspray::redirect(createURL('pm', 'groups', $proj->id)); + } + $page->uses('group_details'); + } + case 'tasktype': + case 'tag': + case 'resolution': + case 'os': + case 'version': + case 'cat': + case 'status': + case 'newgroup': + + $page->setTitle($fs->prefs['page_title'] . L('pmtoolbox')); + $page->pushTpl('pm.menu.tpl'); + $page->pushTpl('pm.'.$area.'.tpl'); + break; + + default: + Flyspray::show_error(17); +} +?> diff --git a/scripts/register.php b/scripts/register.php new file mode 100644 index 0000000..34b62d4 --- /dev/null +++ b/scripts/register.php @@ -0,0 +1,63 @@ +<?php + + /*********************************************************\ + | Register a new user (when confirmation codes is used) | + | ~~~~~~~~~~~~~~~~~~~ | + \*********************************************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +$page->setTitle($fs->prefs['page_title'] . L('registernewuser')); + +if (!$user->isAnon()) { + Flyspray::redirect($baseurl); +} + +if ($user->can_register()) { + // 32 is the length of the magic_url + if (Req::has('magic_url') && strlen(Req::val('magic_url')) == 32) { + // If the user came here from their notification link + $sql = $db->query('SELECT * FROM {registrations} WHERE magic_url = ?', + array(Get::val('magic_url'))); + + if (!$db->countRows($sql)) { + Flyspray::show_error(18); + } + + $page->pushTpl('register.magic.tpl'); + } else { + if($fs->prefs['captcha_securimage']){ + $captchaoptions = array( + 'input_name' => 'captcha_code', + 'show_image_url' => 'securimage.php', + 'show_refresh_button' => false, + 'show_audio_button' => false, + 'disable_flash_fallback' => true + ); + $captcha_securimage_html=Securimage::getCaptchaHtml($captchaoptions); + $page->assign('captcha_securimage_html', $captcha_securimage_html); + } + + $page->pushTpl('register.no-magic.tpl'); + } +} elseif ($user->can_self_register()) { + if($fs->prefs['captcha_securimage']){ + $captchaoptions = array( + 'input_name' => 'captcha_code', + 'show_image_url' => 'securimage.php', + 'show_refresh_button' => false, + 'show_audio_button' => false, + 'disable_flash_fallback' => true, + 'image_attributes' =>array('style'=>'') + ); + $captcha_securimage_html=Securimage::getCaptchaHtml($captchaoptions); + $page->assign('captcha_securimage_html', $captcha_securimage_html); + } + + $page->pushTpl('common.newuser.tpl'); +} else { + Flyspray::show_error(22); +} +?> diff --git a/scripts/reports.php b/scripts/reports.php new file mode 100644 index 0000000..7870291 --- /dev/null +++ b/scripts/reports.php @@ -0,0 +1,122 @@ +<?php + + /********************************************************\ + | Show various reports on tasks | + | ~~~~~~~~~~~~~~~~~~~~~~~~ | + \********************************************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +if (!$user->perms('view_reports')) { + Flyspray::redirect($baseurl); +} + +require_once(BASEDIR . '/includes/events.inc.php'); +$page->setTitle($fs->prefs['page_title'] . L('reports')); + +/**********************\ +* Event reports * +\**********************/ + +$events = array(1 => L('opened'), + 13 => L('reopened'), + 2 => L('closed'), + 3 => L('edited'), + 14 => L('assignmentchanged'), + 29 => L('events.useraddedtoassignees'), + 4 => L('commentadded'), + 5 => L('commentedited'), + 6 => L('commentdeleted'), + 7 => L('attachmentadded'), + 8 => L('attachmentdeleted'), + 11 => L('relatedadded'), + 12 => L('relateddeleted'), + 9 => L('notificationadded'), + 10 => L('notificationdeleted'), + 17 => L('reminderadded'), + 18 => L('reminderdeleted'), + 15 => L('addedasrelated'), + 16 => L('deletedasrelated'), + 19 => L('ownershiptaken'), + 20 => L('closerequestmade'), + 21 => L('reopenrequestmade'), + 22 => L('depadded'), + 23 => L('depaddedother'), + 24 => L('depremoved'), + 25 => L('depremovedother'), + 28 => L('pmreqdenied'), + 32 => L('subtaskadded'), + 33 => L('subtaskremoved'), + 34 => L('supertaskadded'), + 35 => L('supertaskremoved'), + ); + +// Should events 19, 20, 21, 29 be here instead? +$user_events = array(30 => L('created'), + 31 => L('deleted')); + +$page->assign('events', $events); +$page->assign('user_events', $user_events); +$page->assign('theuser', $user); + +$sort = strtoupper(Req::enum('sort', array('desc', 'asc'))); + +$where = array(); +$params = array(); +$orderby = ''; + +switch (Req::val('order')) { + case 'type': + $orderby = "h.event_type {$sort}, h.event_date {$sort}"; + break; + case 'user': + $orderby = "user_id {$sort}, h.event_date {$sort}"; + break; + case 'date': default: + $orderby = "h.event_date {$sort}, h.event_type {$sort}"; +} + +if( is_array(Req::val('events')) ){ + foreach (Req::val('events') as $eventtype) { + $where[] = 'h.event_type = ?'; + $params[] = $eventtype; + } + $where = '(' . implode(' OR ', $where) . ')'; + + if ($proj->id) { + $where = $where . 'AND (t.project_id = ? OR h.event_type IN(30, 31)) '; + $params[] = $proj->id; + } + + if ( Req::val('fromdate') || Req::val('todate')) { + $where .= ' AND '; + $fromdate = Req::val('fromdate'); + $todate = Req::val('todate'); + + if ($fromdate) { + $where .= ' h.event_date > ?'; + $params[] = Flyspray::strtotime($fromdate) + 0; + } + if ($todate && $fromdate) { + $where .= ' AND h.event_date < ?'; + $params[] = Flyspray::strtotime($todate) + 86400; + } else if ($todate) { + $where .= ' h.event_date < ?'; + $params[] = Flyspray::strtotime($todate) + 86400; + } + } + + $histories = $db->query("SELECT h.* + FROM {history} h + LEFT JOIN {tasks} t ON h.task_id = t.task_id + WHERE $where + ORDER BY $orderby", $params, Req::num('event_number', -1)); + $histories = $db->fetchAllArray($histories); +} + +$page->uses('histories', 'sort'); + +$page->pushTpl('reports.tpl'); +?> diff --git a/scripts/roadmap.php b/scripts/roadmap.php new file mode 100644 index 0000000..709a0a5 --- /dev/null +++ b/scripts/roadmap.php @@ -0,0 +1,84 @@ +<?php +/*********************************************************\ +| Show the roadmap | +| ~~~~~~~~~~~~~~~~~~~ | +\*********************************************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +if (!$proj->id) { + Flyspray::show_error(25); +} + +if ((!$user->isAnon() && !$user->perms('view_roadmap')) || ($user->isAnon() && $proj->prefs['others_viewroadmap'] !=1)) { + # better set redirect to false to avoid endless loops + Flyspray::show_error(28, false); +} else{ + + if($proj->prefs['use_effort_tracking']){ + require_once(BASEDIR . '/includes/class.effort.php'); + } + + + $page->setTitle($fs->prefs['page_title'] . L('roadmap')); + + // Get milestones + $milestones = $db->query('SELECT version_id, version_name + FROM {list_version} + WHERE (project_id = ? OR project_id=0) AND version_tense = 3 + ORDER BY list_position ASC', + array($proj->id)); + + $data = array(); + +while ($row = $db->fetchRow($milestones)) { + // Get all tasks related to a milestone + $all_tasks = $db->query('SELECT percent_complete, is_closed + FROM {tasks} + WHERE closedby_version = ? AND project_id = ?', + array($row['version_id'], $proj->id)); + $all_tasks = $db->fetchAllArray($all_tasks); + + $percent_complete = 0; + foreach($all_tasks as $task) { + if($task['is_closed']) { + $percent_complete += 100; + } else { + $percent_complete += $task['percent_complete']; + } + } + $percent_complete = round($percent_complete/max(count($all_tasks), 1)); + + $tasks = $db->query('SELECT task_id, item_summary, detailed_desc, item_status, task_severity, task_priority, task_type, mark_private, opened_by, content, task_token, t.project_id,estimated_effort + FROM {tasks} t + LEFT JOIN {cache} ca ON (t.task_id = ca.topic AND ca.type = \'rota\' AND t.last_edited_time <= ca.last_updated) + WHERE closedby_version = ? AND t.project_id = ? AND is_closed = 0', + array($row['version_id'], $proj->id)); + $tasks = $db->fetchAllArray($tasks); + + $count = count($tasks); + for ($i = 0; $i < $count; $i++) { + if (!$user->can_view_task($tasks[$i])) { + unset($tasks[$i]); + } + } + + $data[] = array('id' => $row['version_id'], 'open_tasks' => $tasks, 'percent_complete' => $percent_complete, + 'all_tasks' => $all_tasks, 'name' => $row['version_name']); +} # end while + + if (Get::val('txt')) { + $page = new FSTpl; + header('Content-Type: text/plain; charset=UTF-8'); + $page->uses('data', 'page'); + $page->display('roadmap.text.tpl'); + exit(); + } else { + $page->uses('data', 'page'); + $page->pushTpl('roadmap.tpl'); + } + +} # end if allowed roadmap view +?> diff --git a/scripts/toplevel.php b/scripts/toplevel.php new file mode 100644 index 0000000..f370cde --- /dev/null +++ b/scripts/toplevel.php @@ -0,0 +1,103 @@ +<?php +/***********************************\ +| Top level project overview | +\***********************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +if ($proj->id && $user->can_select_project($proj->prefs)) { + $projects = array( + 0 => array( + 'project_id' => $proj->id, + 'project_title' => $proj->prefs['project_title'], + 'project_is_active' => $proj->prefs['project_is_active'] + ) + ); +} else { + $projects = $fs->projects; + # anon users should not see details of a restricted project but anon tasks creation allowed + # but in /index.php we filter now by 'can_select_project', not 'can_view_project' anymore. + $projects= array_filter($projects, array($user, 'can_select_project')); +} + +if(count($projects)>0){ + + $most_wanted = array(); + $stats = array(); + $assigned_to_myself = array(); + $projprefs = array(); + + # Most wanted tasks for each project + foreach ($projects as $project) { + # means 'can view tasks' .. + if($user->can_view_project($project['project_id'])){ + $sql = $db->query('SELECT v.task_id, count(*) AS num_votes + FROM {votes} v + LEFT JOIN {tasks} t ON v.task_id = t.task_id AND t.project_id = ? + WHERE t.is_closed = 0 + GROUP BY v.task_id + ORDER BY num_votes DESC', + array($project['project_id']), 5 + ); + + if ($db->countRows($sql)) { + $most_wanted[$project['project_id']] = $db->fetchAllArray($sql); + } + } + } + + # Project stats + foreach ($projects as $project) { + $sql = $db->query('SELECT count(*) FROM {tasks} WHERE project_id = ?', array($project['project_id'])); + $stats[$project['project_id']]['all'] = $db->fetchOne($sql); + + $sql = $db->query('SELECT count(*) FROM {tasks} WHERE project_id = ? AND is_closed = 0', array($project['project_id'])); + $stats[$project['project_id']]['open'] = $db->fetchOne($sql); + + $sql = $db->query('SELECT avg(percent_complete) FROM {tasks} WHERE project_id = ? AND is_closed = 0', array($project['project_id'])); + $stats[$project['project_id']]['average_done'] = round($db->fetchOne($sql), 0); + + if ($proj->id) { + $prefs = $proj->prefs; + } else { + $currentproj = new Project($project['project_id']); + $prefs = $currentproj->prefs; + } + + $projprefs[$project['project_id']] = $prefs; + + if($user->perms('view_estimated_effort', $project['project_id']) ){ + if ($prefs['use_effort_tracking']) { + $sql = $db->query(' + SELECT t.task_id, t.estimated_effort + FROM {tasks} t + WHERE project_id = ? AND is_closed = 0', + array($project['project_id']) + ); + $stats[$project['project_id']]['tasks'] = $db->fetchAllArray($sql); + } + } + } + + # Assigned to myself + foreach ($projects as $project) { + $sql = $db->query(' + SELECT a.task_id + FROM {assigned} a + LEFT JOIN {tasks} t ON a.task_id = t.task_id AND t.project_id = ? + WHERE t.is_closed = 0 and a.user_id = ?', + array($project['project_id'], $user->id), 5 + ); + if ($db->countRows($sql)) { + $assigned_to_myself[$project['project_id']] = $db->fetchAllArray($sql); + } + } + $page->uses('most_wanted', 'stats', 'projects', 'assigned_to_myself', 'projprefs'); + $page->setTitle($fs->prefs['page_title'] . $proj->prefs['project_title'] . ': ' . L('toplevel')); + $page->pushTpl('toplevel.tpl'); +} else{ + # mmh what we want to show anon users with only the 'create anon task' permission enabled?... +} +?> diff --git a/scripts/user.php b/scripts/user.php new file mode 100644 index 0000000..9993d9a --- /dev/null +++ b/scripts/user.php @@ -0,0 +1,43 @@ +<?php + + /*********************************************************\ + | View a user's profile | + | ~~~~~~~~~~~~~~~~~~~~ | + \*********************************************************/ + +if (!defined('IN_FS')) { + die('Do not access this file directly.'); +} + +$page->assign('groups', Flyspray::listGroups()); + +if ($proj->id) { + $page->assign('project_groups', Flyspray::listGroups($proj->id)); +} + +$id = Flyspray::validUserId(Get::val('id', Get::val('uid'))); +if (!$id) { + $id = Flyspray::usernameToId(Get::val('user_name')); +} + +$theuser = new User($id); +if ($theuser->isAnon()) { + Flyspray::show_error(19); +} + +// Some possibly interesting information about the user +$sql = $db->query('SELECT count(*) FROM {comments} WHERE user_id = ?', array($theuser->id)); +$page->assign('comments', $db->fetchOne($sql)); + +$sql = $db->query('SELECT count(*) FROM {tasks} WHERE opened_by = ?', array($theuser->id)); +$page->assign('tasks', $db->fetchOne($sql)); + +$sql = $db->query('SELECT count(*) FROM {assigned} WHERE user_id = ?', array($theuser->id)); +$page->assign('assigned', $db->fetchOne($sql)); + +$page->assign('theuser', $theuser); + +$page->setTitle($fs->prefs['page_title'] . L('viewprofile')); +$page->pushTpl('profile.tpl'); + +?> |