summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/activity.php64
-rw-r--r--scripts/admin.php185
-rw-r--r--scripts/authenticate.php104
-rw-r--r--scripts/depends.php196
-rw-r--r--scripts/details.php768
-rw-r--r--scripts/editcomment.php28
-rw-r--r--scripts/index.php534
-rw-r--r--scripts/langdiff.php192
-rw-r--r--scripts/langedit.php329
-rw-r--r--scripts/lostpw.php32
-rw-r--r--scripts/myprofile.php41
-rw-r--r--scripts/newmultitasks.php20
-rw-r--r--scripts/newtask.php53
-rw-r--r--scripts/oauth.php201
-rw-r--r--scripts/pm.php61
-rw-r--r--scripts/register.php63
-rw-r--r--scripts/reports.php122
-rw-r--r--scripts/roadmap.php84
-rw-r--r--scripts/toplevel.php103
-rw-r--r--scripts/user.php43
20 files changed, 3223 insertions, 0 deletions
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 ? "&amp;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 .= '&nbsp;&nbsp;'.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).'&amp;sort=key#compare" title="sort by translation key">translation key</th>
+ <th><a href="?do=langdiff&lang='.htmlspecialchars($lang).'&amp;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='>&lt;lang code&gt;</a> where &lt;lang code&gt; 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&hellip;$end</a>\n"; // Show all links when searching or display all missing strings
+ } else {
+ echo "<b>$bgn&hellip;$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", "&quot;"), $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');
+
+?>