Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
authorAndreas Baumann <mail@andreasbaumann.cc>2020-02-01 09:05:48 +0100
committerAndreas Baumann <mail@andreasbaumann.cc>2020-02-01 09:05:48 +0100
commit6854cb3f4d8219cf1829e32122eb2502a916eae9 (patch)
tree350feb504587d932e02837a1442b059759927646 /includes
initial checkin
Diffstat (limited to 'includes')
-rw-r--r--includes/.htaccess1
-rw-r--r--includes/GithubProvider.php60
-rw-r--r--includes/class.backend.php1869
-rw-r--r--includes/class.csp.php106
-rw-r--r--includes/class.database.php434
-rw-r--r--includes/class.effort.php302
-rw-r--r--includes/class.flyspray.php1471
-rw-r--r--includes/class.gpc.php257
-rw-r--r--includes/class.jabber2.php943
-rw-r--r--includes/class.notify.php1114
-rw-r--r--includes/class.project.php474
-rw-r--r--includes/class.recaptcha.php33
-rw-r--r--includes/class.tpl.php1525
-rw-r--r--includes/class.user.php555
-rw-r--r--includes/constants.inc.php96
-rw-r--r--includes/events.inc.php308
-rw-r--r--includes/fix.inc.php205
-rw-r--r--includes/i18n.inc.php166
-rw-r--r--includes/modify.inc.php3053
-rw-r--r--includes/password_compat.php319
-rw-r--r--includes/utf8.inc.php118
21 files changed, 13409 insertions, 0 deletions
diff --git a/includes/.htaccess b/includes/.htaccess
new file mode 100644
index 0000000..31f17f9
--- /dev/null
+++ b/includes/.htaccess
@@ -0,0 +1 @@
+Deny From All
diff --git a/includes/GithubProvider.php b/includes/GithubProvider.php
new file mode 100644
index 0000000..5639383
--- /dev/null
+++ b/includes/GithubProvider.php
@@ -0,0 +1,60 @@
+<?php
+
+use League\OAuth2\Client\Provider\Github;
+use League\OAuth2\Client\Token\AccessToken as AccessToken;
+
+/**
+ * A workaround for fetching the users email address if the user does not have a
+ * public email address.
+ */
+class GithubProvider extends Github
+{
+ public function userDetails($response, AccessToken $token)
+ {
+ $user = parent::userDetails($response, $token);
+
+ // Fetch the primary email address
+ if (!$user->email) {
+ $emails = $this->fetchUserEmails($token);
+ $emails = json_decode($emails);
+ $email = null;
+
+ foreach ($emails as $email) {
+ if ($email->primary) {
+ $email = $email->email;
+ break;
+ }
+ }
+
+ $user->email = $email;
+ }
+
+ return $user;
+ }
+
+ protected function fetchUserEmails(AccessToken $token)
+ {
+ $url = "https://api.github.com/user/emails?access_token={$token}";
+
+ try {
+
+ $client = $this->getHttpClient();
+ $client->setBaseUrl($url);
+
+ if ($this->headers) {
+ $client->setDefaultOption('headers', $this->headers);
+ }
+
+ $request = $client->get()->send();
+ $response = $request->getBody();
+
+ } catch (BadResponseException $e) {
+ // @codeCoverageIgnoreStart
+ $raw_response = explode("\n", $e->getResponse());
+ throw new IDPException(end($raw_response));
+ // @codeCoverageIgnoreEnd
+ }
+
+ return $response;
+ }
+} \ No newline at end of file
diff --git a/includes/class.backend.php b/includes/class.backend.php
new file mode 100644
index 0000000..f440db9
--- /dev/null
+++ b/includes/class.backend.php
@@ -0,0 +1,1869 @@
+<?php
+/**
+ * Flyspray
+ *
+ * Backend class
+ *
+ * This script contains reusable functions we use to modify
+ * various things in the Flyspray database tables.
+ *
+ * @license http://opensource.org/licenses/lgpl-license.php Lesser GNU Public License
+ * @package flyspray
+ * @author Tony Collins, Florian Schmitz
+ */
+
+if (!defined('IN_FS')) {
+ die('Do not access this file directly.');
+}
+
+abstract class Backend
+{
+ /**
+ * Adds the user $user_id to the notifications list of $tasks
+ * @param integer $user_id
+ * @param array $tasks
+ * @param bool $do Force execution independent of user permissions
+ * @access public
+ * @return bool
+ * @version 1.0
+ */
+ public static function add_notification($user_id, $tasks, $do = false)
+ {
+ global $db, $user;
+
+ settype($tasks, 'array');
+
+ $user_id = Flyspray::validUserId($user_id);
+
+ if (!$user_id || !count($tasks)) {
+ return false;
+ }
+
+ $sql = $db->query(' SELECT *
+ FROM {tasks}
+ WHERE ' . substr(str_repeat(' task_id = ? OR ', count($tasks)), 0, -3),
+ $tasks);
+
+ while ($row = $db->fetchRow($sql)) {
+ // -> user adds himself
+ if ($user->id == $user_id) {
+ if (!$user->can_view_task($row) && !$do) {
+ continue;
+ }
+ // -> user is added by someone else
+ } else {
+ if (!$user->perms('manage_project', $row['project_id']) && !$do) {
+ continue;
+ }
+ }
+
+ $notif = $db->query('SELECT notify_id
+ FROM {notifications}
+ WHERE task_id = ? and user_id = ?',
+ array($row['task_id'], $user_id));
+
+ if (!$db->countRows($notif)) {
+ $db->query('INSERT INTO {notifications} (task_id, user_id)
+ VALUES (?,?)', array($row['task_id'], $user_id));
+ Flyspray::logEvent($row['task_id'], 9, $user_id);
+ }
+ }
+
+ return (bool) $db->countRows($sql);
+ }
+
+
+ /**
+ * Removes a user $user_id from the notifications list of $tasks
+ * @param integer $user_id
+ * @param array $tasks
+ * @access public
+ * @return void
+ * @version 1.0
+ */
+
+ public static function remove_notification($user_id, $tasks)
+ {
+ global $db, $user;
+
+ settype($tasks, 'array');
+
+ if (!count($tasks)) {
+ return;
+ }
+
+ $sql = $db->query(' SELECT *
+ FROM {tasks}
+ WHERE ' . substr(str_repeat(' task_id = ? OR ', count($tasks)), 0, -3),
+ $tasks);
+
+ while ($row = $db->fetchRow($sql)) {
+ // -> user removes himself
+ if ($user->id == $user_id) {
+ if (!$user->can_view_task($row)) {
+ continue;
+ }
+ // -> user is removed by someone else
+ } else {
+ if (!$user->perms('manage_project', $row['project_id'])) {
+ continue;
+ }
+ }
+
+ $db->query('DELETE FROM {notifications}
+ WHERE task_id = ? AND user_id = ?',
+ array($row['task_id'], $user_id));
+ if ($db->affectedRows()) {
+ Flyspray::logEvent($row['task_id'], 10, $user_id);
+ }
+ }
+ }
+
+
+ /**
+ * Assigns one or more $tasks only to a user $user_id
+ * @param integer $user_id
+ * @param array $tasks
+ * @access public
+ * @return void
+ * @version 1.0
+ */
+ public static function assign_to_me($user_id, $tasks)
+ {
+ global $db, $notify;
+
+ $user = $GLOBALS['user'];
+ if ($user_id != $user->id) {
+ $user = new User($user_id);
+ }
+
+ settype($tasks, 'array');
+ if (!count($tasks)) {
+ return;
+ }
+
+ $sql = $db->query(' SELECT *
+ FROM {tasks}
+ WHERE ' . substr(str_repeat(' task_id = ? OR ', count($tasks)), 0, -3),
+ $tasks);
+
+ while ($row = $db->fetchRow($sql)) {
+ if (!$user->can_take_ownership($row)) {
+ continue;
+ }
+
+ $db->query('DELETE FROM {assigned}
+ WHERE task_id = ?',
+ array($row['task_id']));
+
+ $db->query('INSERT INTO {assigned}
+ (task_id, user_id)
+ VALUES (?,?)',
+ array($row['task_id'], $user->id));
+
+ if ($db->affectedRows()) {
+ $current_proj = new Project($row['project_id']);
+ Flyspray::logEvent($row['task_id'], 19, $user->id, implode(' ', Flyspray::getAssignees($row['task_id'])));
+ $notify->create(NOTIFY_OWNERSHIP, $row['task_id'], null, null, NOTIFY_BOTH, $current_proj->prefs['lang_code']);
+ }
+
+ if ($row['item_status'] == STATUS_UNCONFIRMED || $row['item_status'] == STATUS_NEW) {
+ $db->query('UPDATE {tasks} SET item_status = 3 WHERE task_id = ?', array($row['task_id']));
+ Flyspray::logEvent($row['task_id'], 3, 3, 1, 'item_status');
+ }
+ }
+ }
+
+ /**
+ * Adds a user $user_id to the assignees of one or more $tasks
+ * @param integer $user_id
+ * @param array $tasks
+ * @param bool $do Force execution independent of user permissions
+ * @access public
+ * @return void
+ * @version 1.0
+ */
+ public static function add_to_assignees($user_id, $tasks, $do = false)
+ {
+ global $db, $notify;
+
+ settype($tasks, 'array');
+
+ $user = $GLOBALS['user'];
+ if ($user_id != $user->id) {
+ $user = new User($user_id);
+ }
+
+ settype($tasks, 'array');
+ if (!count($tasks)) {
+ return;
+ }
+
+ $sql = $db->query(' SELECT *
+ FROM {tasks}
+ WHERE ' . substr(str_repeat(' task_id = ? OR ', count($tasks)), 0, -3),
+ $tasks);
+
+ while ($row = $db->fetchRow($sql)) {
+ if (!$user->can_add_to_assignees($row) && !$do) {
+ continue;
+ }
+
+ $db->replace('{assigned}', array('user_id'=> $user->id, 'task_id'=> $row['task_id']), array('user_id','task_id'));
+
+ if ($db->affectedRows()) {
+ $current_proj = new Project($row['project_id']);
+ Flyspray::logEvent($row['task_id'], 29, $user->id, implode(' ', Flyspray::getAssignees($row['task_id'])));
+ $notify->create(NOTIFY_ADDED_ASSIGNEES, $row['task_id'], null, null, NOTIFY_BOTH, $current_proj->prefs['lang_code']);
+ }
+
+ if ($row['item_status'] == STATUS_UNCONFIRMED || $row['item_status'] == STATUS_NEW) {
+ $db->query('UPDATE {tasks} SET item_status = 3 WHERE task_id = ?', array($row['task_id']));
+ Flyspray::logEvent($row['task_id'], 3, 3, 1, 'item_status');
+ }
+ }
+ }
+
+ /**
+ * Adds a vote from $user_id to the task $task_id
+ * @param integer $user_id
+ * @param integer $task_id
+ * @access public
+ * @return bool
+ * @version 1.0
+ */
+ public static function add_vote($user_id, $task_id)
+ {
+ global $db;
+
+ $user = $GLOBALS['user'];
+ if ($user_id != $user->id) {
+ $user = new User($user_id);
+ }
+
+ $task = Flyspray::getTaskDetails($task_id);
+
+ if (!$task) {
+ return false;
+ }
+
+ if ($user->can_vote($task) > 0) {
+
+ if($db->query("INSERT INTO {votes} (user_id, task_id, date_time)
+ VALUES (?,?,?)", array($user->id, $task_id, time()))) {
+ // TODO: Log event in a later version.
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Removes a vote from $user_id to the task $task_id
+ * @param integer $user_id
+ * @param integer $task_id
+ * @access public
+ * @return bool
+ * @version 1.0
+ */
+ public static function remove_vote($user_id, $task_id)
+ {
+ global $db;
+
+ $user = $GLOBALS['user'];
+ if ($user_id != $user->id) {
+ $user = new User($user_id);
+ }
+
+ $task = Flyspray::getTaskDetails($task_id);
+
+ if (!$task) {
+ return false;
+ }
+
+ if ($user->can_vote($task) == -2) {
+
+ if($db->query("DELETE FROM {votes} WHERE user_id = ? and task_id = ?",
+ array($user->id, $task_id))) {
+ // TODO: Log event in a later version.
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Adds a comment to $task
+ * @param array $task
+ * @param string $comment_text
+ * @param integer $time for synchronisation with other functions
+ * @access public
+ * @return bool
+ * @version 1.0
+ */
+ public static function add_comment($task, $comment_text, $time = null)
+ {
+ global $conf, $db, $user, $notify, $proj;
+
+ if (!($user->perms('add_comments', $task['project_id']) && (!$task['is_closed'] || $user->perms('comment_closed', $task['project_id'])))) {
+ return false;
+ }
+
+ if($conf['general']['syntax_plugin'] != 'dokuwiki'){
+ $purifierconfig = HTMLPurifier_Config::createDefault();
+ $purifier = new HTMLPurifier($purifierconfig);
+ $comment_text = $purifier->purify($comment_text);
+ }
+
+ if (!is_string($comment_text) || !strlen($comment_text)) {
+ return false;
+ }
+
+ $time = !is_numeric($time) ? time() : $time ;
+
+ $db->query('INSERT INTO {comments}
+ (task_id, date_added, last_edited_time, user_id, comment_text)
+ VALUES ( ?, ?, ?, ?, ? )',
+ array($task['task_id'], $time, $time, $user->id, $comment_text));
+ $cid = $db->Insert_ID();
+ Backend::upload_links($task['task_id'], $cid);
+ Flyspray::logEvent($task['task_id'], 4, $cid);
+
+ if (Backend::upload_files($task['task_id'], $cid)) {
+ $notify->create(NOTIFY_COMMENT_ADDED, $task['task_id'], 'files', null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+ } else {
+ $notify->create(NOTIFY_COMMENT_ADDED, $task['task_id'], null, null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+ }
+
+
+ return true;
+ }
+
+ /**
+ * Upload files for a comment or a task
+ * @param integer $task_id
+ * @param integer $comment_id if it is 0, the files will be attached to the task itself
+ * @param string $source name of the file input
+ * @access public
+ * @return bool
+ * @version 1.0
+ */
+ public static function upload_files($task_id, $comment_id = 0, $source = 'userfile')
+ {
+ global $db, $notify, $conf, $user;
+
+ $task = Flyspray::getTaskDetails($task_id);
+
+ if (!$user->perms('create_attachments', $task['project_id'])) {
+ return false;
+ }
+
+ $res = false;
+
+ if (!isset($_FILES[$source]['error'])) {
+ return false;
+ }
+
+ foreach ($_FILES[$source]['error'] as $key => $error) {
+ if ($error != UPLOAD_ERR_OK) {
+ continue;
+ }
+
+
+ $fname = substr($task_id . '_' . md5(uniqid(mt_rand(), true)), 0, 30);
+ $path = BASEDIR .'/attachments/'. $fname ;
+
+ $tmp_name = $_FILES[$source]['tmp_name'][$key];
+
+ // Then move the uploaded file and remove exe permissions
+ if(!@move_uploaded_file($tmp_name, $path)) {
+ //upload failed. continue
+ continue;
+ }
+
+ @chmod($path, 0644);
+ $res = true;
+
+ // Use a different MIME type
+ $fileparts = explode( '.', $_FILES[$source]['name'][$key]);
+ $extension = end($fileparts);
+ if (isset($conf['attachments'][$extension])) {
+ $_FILES[$source]['type'][$key] = $conf['attachments'][$extension];
+ //actually, try really hard to get the real filetype, not what the browser reports.
+ } elseif($type = Flyspray::check_mime_type($path)) {
+ $_FILES[$source]['type'][$key] = $type;
+ }// we can try even more, however, far too much code is needed.
+
+ $db->query("INSERT INTO {attachments}
+ ( task_id, comment_id, file_name,
+ file_type, file_size, orig_name,
+ added_by, date_added)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+ array($task_id, $comment_id, $fname,
+ $_FILES[$source]['type'][$key],
+ $_FILES[$source]['size'][$key],
+ $_FILES[$source]['name'][$key],
+ $user->id, time()));
+ $attid = $db->insert_ID();
+ Flyspray::logEvent($task_id, 7, $attid, $_FILES[$source]['name'][$key]);
+ }
+
+ return $res;
+ }
+
+ public static function upload_links($task_id, $comment_id = 0, $source = 'userlink')
+ {
+ global $db, $user;
+
+ $task = Flyspray::getTaskDetails($task_id);
+
+ if (!$user->perms('create_attachments', $task['project_id'])) {
+ return false;
+ }
+
+ if (!isset($_POST[$source])) {
+ return false;
+ }
+
+ $res = false;
+ foreach($_POST[$source] as $text) {
+ $text = filter_var($text, FILTER_SANITIZE_URL);
+
+ if( preg_match( '/^\s*(javascript:|data:)/', $text)){
+ continue;
+ }
+
+ if(empty($text)) {
+ continue;
+ }
+
+ $res = true;
+
+ // Insert into database
+ $db->query("INSERT INTO {links} (task_id, comment_id, url, added_by, date_added) VALUES (?, ?, ?, ?, ?)",
+ array($task_id, $comment_id, $text, $user->id, time()));
+ // TODO: Log event in a later version.
+ }
+
+ return $res;
+ }
+
+ /**
+ * Delete one or more attachments of a task or comment
+ * @param array $attachments
+ * @access public
+ * @return void
+ * @version 1.0
+ */
+ public static function delete_files($attachments)
+ {
+ global $db, $user;
+
+ settype($attachments, 'array');
+ if (!count($attachments)) {
+ return;
+ }
+
+ $sql = $db->query(' SELECT t.*, a.*
+ FROM {attachments} a
+ LEFT JOIN {tasks} t ON t.task_id = a.task_id
+ WHERE ' . substr(str_repeat(' attachment_id = ? OR ', count($attachments)), 0, -3),
+ $attachments);
+
+ while ($task = $db->fetchRow($sql)) {
+ if (!$user->perms('delete_attachments', $task['project_id'])) {
+ continue;
+ }
+
+ $db->query('DELETE FROM {attachments} WHERE attachment_id = ?',
+ array($task['attachment_id']));
+ @unlink(BASEDIR . '/attachments/' . $task['file_name']);
+ Flyspray::logEvent($task['task_id'], 8, $task['orig_name']);
+ }
+ }
+
+ public static function delete_links($links)
+ {
+ global $db, $user;
+
+ settype($links, 'array');
+
+ if(!count($links)) {
+ return;
+ }
+
+ $sql = $db->query('SELECT t.*, l.* FROM {links} l LEFT JOIN {tasks} t ON t.task_id = l.task_id WHERE '.substr(str_repeat('link_id = ? OR ', count($links)), 0, -3), $links);
+
+ //Delete from database
+ while($task = $db->fetchRow($sql)) {
+ if (!$user->perms('delete_attachments', $task['project_id'])) {
+ continue;
+ }
+
+ $db->query('DELETE FROM {links} WHERE link_id = ?', array($task['link_id']));
+ // TODO: Log event in a later version.
+ }
+ }
+
+ /**
+ * Cleans a username (length, special chars, spaces)
+ * @param string $user_name
+ * @access public
+ * @return string
+ */
+ public static function clean_username($user_name)
+ {
+ // Limit length
+ $user_name = substr(trim($user_name), 0, 32);
+ // Remove doubled up spaces and control chars
+ $user_name = preg_replace('![\x00-\x1f\s]+!u', ' ', $user_name);
+ // Strip special chars
+ return utf8_keepalphanum($user_name);
+ }
+
+ public static function getAdminAddresses() {
+ global $db;
+
+ $emails = array();
+ $jabbers = array();
+ $onlines = array();
+
+ $sql = $db->query('SELECT DISTINCT u.user_id, u.email_address, u.jabber_id,
+ u.notify_online, u.notify_type, u.notify_own, u.lang_code
+ FROM {users} u
+ JOIN {users_in_groups} ug ON u.user_id = ug.user_id
+ JOIN {groups} g ON g.group_id = ug.group_id
+ WHERE g.is_admin = 1 AND u.account_enabled = 1');
+
+ Notifications::assignRecipients($db->fetchAllArray($sql), $emails, $jabbers, $onlines);
+
+ return array($emails, $jabbers, $onlines);
+ }
+
+ public static function getProjectManagerAddresses($project_id) {
+ global $db;
+
+ $emails = array();
+ $jabbers = array();
+ $onlines = array();
+
+ $sql = $db->query('SELECT DISTINCT u.user_id, u.email_address, u.jabber_id,
+ u.notify_online, u.notify_type, u.notify_own, u.lang_code
+ FROM {users} u
+ JOIN {users_in_groups} ug ON u.user_id = ug.user_id
+ JOIN {groups} g ON g.group_id = ug.group_id
+ WHERE g.manage_project = 1 AND g.project_id = ? AND u.account_enabled = 1',
+ array($project_id));
+
+ Notifications::assignRecipients($db->fetchAllArray($sql), $emails, $jabbers, $onlines);
+
+ return array($emails, $jabbers, $onlines);
+ }
+ /**
+ * Creates a new user
+ * @param string $user_name
+ * @param string $password
+ * @param string $real_name
+ * @param string $jabber_id
+ * @param string $email
+ * @param integer $notify_type
+ * @param integer $time_zone
+ * @param integer $group_in
+ * @access public
+ * @return bool false if username is already taken
+ * @version 1.0
+ * @notes This function does not have any permission checks (checked elsewhere)
+ */
+ public static function create_user($user_name, $password, $real_name, $jabber_id, $email, $notify_type, $time_zone, $group_in, $enabled, $oauth_uid = '', $oauth_provider = '', $profile_image = '')
+ {
+ global $fs, $db, $notify, $baseurl;
+
+ $user_name = Backend::clean_username($user_name);
+
+ // TODO Handle this whole create_user better concerning return false. Why did it fail?
+ # 'notassigned' and '-1' are possible filtervalues for advanced task search
+ if( empty($user_name) || ctype_digit($user_name) || $user_name == '-1' || $user_name=='notassigned' ) {
+ return false;
+ }
+
+ // Limit length
+ $real_name = substr(trim($real_name), 0, 100);
+ // Remove doubled up spaces and control chars
+ $real_name = preg_replace('![\x00-\x1f\s]+!u', ' ', $real_name);
+
+ # 'notassigned' and '-1' are possible filtervalues for advanced task search, lets avoid them
+ if( ctype_digit($real_name) || $real_name == '-1' || $real_name=='notassigned' ) {
+ return false;
+ }
+
+ // Check to see if the username is available
+ $sql = $db->query('SELECT COUNT(*) FROM {users} WHERE user_name = ?', array($user_name));
+
+ if ($db->fetchOne($sql)) {
+ return false;
+ }
+
+ $auto = false;
+ // Autogenerate a password
+ if (!$password) {
+ $auto = true;
+ $password = substr(md5(uniqid(mt_rand(), true)), 0, mt_rand(8, 12));
+ }
+
+ // Check the emails before inserting anything to database.
+ $emailList = explode(';',$email);
+ foreach ($emailList as $mail) { //Still need to do: check email
+ $count = $db->query("SELECT COUNT(*) FROM {user_emails} WHERE email_address = ?",array($mail));
+ $count = $db->fetchOne($count);
+ if ($count > 0) {
+ Flyspray::show_error("Email address has alredy been taken");
+ return false;
+ }
+ }
+
+ $db->query("INSERT INTO {users}
+ ( user_name, user_pass, real_name, jabber_id, profile_image, magic_url,
+ email_address, notify_type, account_enabled,
+ tasks_perpage, register_date, time_zone, dateformat,
+ dateformat_extended, oauth_uid, oauth_provider, lang_code)
+ VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, 25, ?, ?, ?, ?, ?, ?, ?)",
+ array($user_name, Flyspray::cryptPassword($password), $real_name, strtolower($jabber_id),
+ $profile_image, '', strtolower($email), $notify_type, $enabled, time(), $time_zone, '', '', $oauth_uid, $oauth_provider, $fs->prefs['lang_code']));
+
+ // Get this user's id for the record
+ $uid = Flyspray::userNameToId($user_name);
+
+ foreach ($emailList as $mail) {
+ if ($mail != '') {
+ $db->query("INSERT INTO {user_emails}(id,email_address,oauth_uid,oauth_provider) VALUES (?,?,?,?)",
+ array($uid,strtolower($mail),$oauth_uid, $oauth_provider));
+ }
+ }
+
+ // Now, create a new record in the users_in_groups table
+ $db->query('INSERT INTO {users_in_groups} (user_id, group_id)
+ VALUES (?, ?)', array($uid, $group_in));
+
+ Flyspray::logEvent(0, 30, serialize(Flyspray::getUserDetails($uid)));
+
+ $varnames = array('iwatch','atome','iopened');
+
+ $toserialize = array('string' => NULL,
+ 'type' => array (''),
+ 'sev' => array (''),
+ 'due' => array (''),
+ 'dev' => NULL,
+ 'cat' => array (''),
+ 'status' => array ('open'),
+ 'order' => NULL,
+ 'sort' => NULL,
+ 'percent' => array (''),
+ 'opened' => NULL,
+ 'search_in_comments' => NULL,
+ 'search_for_all' => NULL,
+ 'reported' => array (''),
+ 'only_primary' => NULL,
+ 'only_watched' => NULL);
+
+ foreach($varnames as $tmpname) {
+ if($tmpname == 'iwatch') {
+ $tmparr = array('only_watched' => '1');
+ } elseif ($tmpname == 'atome') {
+ $tmparr = array('dev'=> $uid);
+ } elseif($tmpname == 'iopened') {
+ $tmparr = array('opened'=> $uid);
+ }
+ $$tmpname = $tmparr + $toserialize;
+ }
+
+ // Now give him his default searches
+ $db->query('INSERT INTO {searches} (user_id, name, search_string, time)
+ VALUES (?, ?, ?, ?)',
+ array($uid, L('taskswatched'), serialize($iwatch), time()));
+ $db->query('INSERT INTO {searches} (user_id, name, search_string, time)
+ VALUES (?, ?, ?, ?)',
+ array($uid, L('assignedtome'), serialize($atome), time()));
+ $db->query('INSERT INTO {searches} (user_id, name, search_string, time)
+ VALUES (?, ?, ?, ?)',
+ array($uid, L('tasksireported'), serialize($iopened), time()));
+
+ if ($jabber_id) {
+ Notifications::jabberRequestAuth($jabber_id);
+ }
+
+ // Send a user his details (his username might be altered, password auto-generated)
+ // dont send notifications if the user logged in using oauth
+ if (!$oauth_provider) {
+ $recipients = self::getAdminAddresses();
+ $newuser = array();
+
+ // Add the right message here depending on $enabled.
+ if ($enabled === 0) {
+ $newuser[0][$email] = array('recipient' => $email, 'lang' => $fs->prefs['lang_code']);
+
+ } else {
+ $newuser[0][$email] = array('recipient' => $email, 'lang' => $fs->prefs['lang_code']);
+ }
+
+ // Notify the appropriate users
+ if ($fs->prefs['notify_registration']) {
+ $notify->create(NOTIFY_NEW_USER, null,
+ array($baseurl, $user_name, $real_name, $email, $jabber_id, $password, $auto),
+ $recipients, NOTIFY_EMAIL);
+ }
+ // And also the new user
+ $notify->create(NOTIFY_OWN_REGISTRATION, null,
+ array($baseurl, $user_name, $real_name, $email, $jabber_id, $password, $auto),
+ $newuser, NOTIFY_EMAIL);
+ }
+
+ // If the account is created as not enabled, no matter what any
+ // preferences might say or how the registration was made in first
+ // place, it MUST be first approved by an admin. And a small
+ // work-around: there's no field for email, so we use reason_given
+ // for that purpose.
+ if ($enabled === 0) {
+ Flyspray::adminRequest(3, 0, 0, $uid, $email);
+ }
+
+ return true;
+ }
+
+ /**
+ * Deletes a user
+ * @param integer $uid
+ * @access public
+ * @return bool
+ * @version 1.0
+ */
+ public static function delete_user($uid)
+ {
+ global $db, $user;
+
+ if (!$user->perms('is_admin')) {
+ return false;
+ }
+
+ $userDetails = Flyspray::getUserDetails($uid);
+
+ if (is_file(BASEDIR.'/avatars/'.$userDetails['profile_image'])) {
+ unlink(BASEDIR.'/avatars/'.$userDetails['profile_image']);
+ }
+
+ $tables = array('users', 'users_in_groups', 'searches', 'notifications', 'assigned', 'votes', 'effort');
+ # FIXME Deleting a users effort without asking when user is deleted may not be wanted in every situation.
+ # For example for billing a project and the deleted user worked for a project.
+ # The better solution is to just deactivate the user, but maybe there are cases a user MUSt be deleted from the database.
+ # Move that effort to an 'anonymous users' effort if the effort(s) was legal and should be measured for project(s)?
+ foreach ($tables as $table) {
+ if (!$db->query('DELETE FROM ' .'{' . $table .'}' . ' WHERE user_id = ?', array($uid))) {
+ return false;
+ }
+ }
+
+ if (!empty($userDetails['profile_image']) && is_file(BASEDIR.'/avatars/'.$userDetails['profile_image'])) {
+ unlink(BASEDIR.'/avatars/'.$userDetails['profile_image']);
+ }
+
+ $db->query('DELETE FROM {registrations} WHERE email_address = ?',
+ array($userDetails['email_address']));
+
+ $db->query('DELETE FROM {user_emails} WHERE id = ?',
+ array($uid));
+
+ $db->query('DELETE FROM {reminders} WHERE to_user_id = ? OR from_user_id = ?',
+ array($uid, $uid));
+
+ // for the unusual situuation that a user ID is re-used, make sure that the new user doesn't
+ // get permissions for a task automatically
+ $db->query('UPDATE {tasks} SET opened_by = 0 WHERE opened_by = ?', array($uid));
+
+ Flyspray::logEvent(0, 31, serialize($userDetails));
+
+ return true;
+ }
+
+
+ /**
+ * Deletes a project
+ * @param integer $pid
+ * @param integer $move_to to which project contents of the project are moved
+ * @access public
+ * @return bool
+ * @version 1.0
+ */
+ public static function delete_project($pid, $move_to = 0)
+ {
+ global $db, $user;
+
+ if (!$user->perms('manage_project', $pid)) {
+ return false;
+ }
+
+ // Delete all project's tasks related information
+ if (!$move_to) {
+ $task_ids = $db->query('SELECT task_id FROM {tasks} WHERE project_id = ' . intval($pid));
+ $task_ids = $db->fetchCol($task_ids);
+ // What was supposed to be in tables field_values, notification_threads
+ // and redundant, they do not exist in database?
+ $tables = array('admin_requests', 'assigned', 'attachments', 'comments',
+ 'dependencies', 'related', 'history',
+ 'notifications',
+ 'reminders', 'votes');
+ foreach ($tables as $table) {
+ if ($table == 'related') {
+ $stmt = $db->dblink->prepare('DELETE FROM ' . $db->dbprefix . $table . ' WHERE this_task = ? OR related_task = ? ');
+ } else {
+ $stmt = $db->dblink->prepare('DELETE FROM ' . $db->dbprefix . $table . ' WHERE task_id = ?');
+ }
+ foreach ($task_ids as $id) {
+ $db->dblink->execute($stmt, ($table == 'related') ? array($id, $id) : array($id));
+ }
+ }
+ }
+
+ // unset category of tasks because we don't move categories
+ if ($move_to) {
+ $db->query('UPDATE {tasks} SET product_category = 0 WHERE project_id = ?', array($pid));
+ }
+
+ $tables = array('list_category', 'list_os', 'list_resolution', 'list_tasktype',
+ 'list_status', 'list_version', 'admin_requests',
+ 'cache', 'projects', 'tasks');
+
+ foreach ($tables as $table) {
+ if ($move_to && $table !== 'projects' && $table !== 'list_category') {
+ // Having a unique index in most list_* tables prevents
+ // doing just a simple update, if the list item already
+ // exists in target project, so we have to update existing
+ // tasks to use the one in target project. Something similar
+ // should be done when moving a single task to another project.
+ // Consider making this a separate function that can be used
+ // for that purpose too, if possible.
+ if (strpos($table, 'list_') === 0) {
+ list($type, $name) = explode('_', $table);
+ $sql = $db->query('SELECT ' . $name . '_id, ' . $name . '_name
+ FROM {' . $table . '}
+ WHERE project_id = ?',
+ array($pid));
+ $rows = $db->fetchAllArray($sql);
+ foreach ($rows as $row) {
+ $sql = $db->query('SELECT ' . $name . '_id
+ FROM {' . $table . '}
+ WHERE project_id = ? AND '. $name . '_name = ?',
+ array($move_to, $row[$name .'_name']));
+ $new_id = $db->fetchOne($sql);
+ if ($new_id) {
+ switch ($name) {
+ case 'os';
+ $column = 'operating_system';
+ break;
+ case 'resolution';
+ $column = 'resolution_reason';
+ break;
+ case 'tasktype';
+ $column = 'task_type';
+ break;
+ case 'status';
+ $column = 'item_status';
+ break;
+ case 'version';
+ // Questionable what to do with this one. 1.0 could
+ // have been still future in the old project and
+ // already past in the new one...
+ $column = 'product_version';
+ break;
+ }
+ if (isset($column)) {
+ $db->query('UPDATE {tasks}
+ SET ' . $column . ' = ?
+ WHERE ' . $column . ' = ?',
+ array($new_id, $row[$name . '_id']));
+ $db->query('DELETE FROM {' . $table . '}
+ WHERE ' . $name . '_id = ?',
+ array($row[$name . '_id']));
+ }
+ }
+ }
+ }
+ $base_sql = 'UPDATE {' . $table . '} SET project_id = ?';
+ $sql_params = array($move_to, $pid);
+ } else {
+ $base_sql = 'DELETE FROM {' . $table . '}';
+ $sql_params = array($pid);
+ }
+
+ if (!$db->query($base_sql . ' WHERE project_id = ?', $sql_params)) {
+ return false;
+ }
+ }
+
+ // groups are only deleted, not moved (it is likely
+ // that the destination project already has all kinds
+ // of groups which are also used by the old project)
+ $sql = $db->query('SELECT group_id FROM {groups} WHERE project_id = ?', array($pid));
+ while ($row = $db->fetchRow($sql)) {
+ $db->query('DELETE FROM {users_in_groups} WHERE group_id = ?', array($row['group_id']));
+ }
+ $sql = $db->query('DELETE FROM {groups} WHERE project_id = ?', array($pid));
+
+ //we have enough reasons .. the process is OK.
+ return true;
+ }
+
+ /**
+ * Adds a reminder to a task
+ * @param integer $task_id
+ * @param string $message
+ * @param integer $how_often send a reminder every ~ seconds
+ * @param integer $start_time time when the reminder starts
+ * @param $user_id the user who is reminded. by default (null) all users assigned to the task are reminded.
+ * @access public
+ * @return bool
+ * @version 1.0
+ */
+ public static function add_reminder($task_id, $message, $how_often, $start_time, $user_id = null)
+ {
+ global $user, $db;
+ $task = Flyspray::getTaskDetails($task_id);
+
+ if (!$user->perms('manage_project', $task['project_id'])) {
+ return false;
+ }
+
+ if (is_null($user_id)) {
+ // Get all users assigned to a task
+ $user_id = Flyspray::getAssignees($task_id);
+ } else {
+ $user_id = array(Flyspray::validUserId($user_id));
+ if (!reset($user_id)) {
+ return false;
+ }
+ }
+
+ foreach ($user_id as $id) {
+ $sql = $db->replace('{reminders}',
+ array('task_id'=> $task_id, 'to_user_id'=> $id,
+ 'from_user_id' => $user->id, 'start_time' => $start_time,
+ 'how_often' => $how_often, 'reminder_message' => $message),
+ array('task_id', 'to_user_id', 'how_often', 'reminder_message'));
+ if(!$sql) {
+ // query has failed :(
+ return false;
+ }
+ }
+ // 2 = no record has found and was INSERT'ed correclty
+ if (isset($sql) && $sql == 2) {
+ Flyspray::logEvent($task_id, 17, $task_id);
+ }
+ return true;
+ }
+
+ /**
+ * Adds a new task
+ * @param array $args array containing all task properties. unknown properties will be ignored
+ * @access public
+ * @return integer the task ID on success
+ * @version 1.0
+ * @notes $args is POST data, bad..bad user..
+ */
+ public static function create_task($args)
+ {
+ global $conf, $db, $user, $proj;
+
+ if (!isset($args)) return 0;
+
+ // these are the POST variables that the user MUST send, if one of
+ // them is missing or if one of them is empty, then we have to abort
+ $requiredPostArgs = array('item_summary', 'project_id');//modify: made description not required
+ foreach ($requiredPostArgs as $required) {
+ if (empty($args[$required])) return 0;
+ }
+
+ $notify = new Notifications();
+ if ($proj->id != $args['project_id']) {
+ $proj = new Project($args['project_id']);
+ }
+
+ if (!$user->can_open_task($proj)) {
+ return 0;
+ }
+
+ // first populate map with default values
+ $sql_args = array(
+ 'project_id' => $proj->id,
+ 'date_opened' => time(),
+ 'last_edited_time' => time(),
+ 'opened_by' => intval($user->id),
+ 'percent_complete' => 0,
+ 'mark_private' => 0,
+ 'supertask_id' => 0,
+ 'closedby_version' => 0,
+ 'closure_comment' => '',
+ 'task_priority' => 2,
+ 'due_date' => 0,
+ 'anon_email' => '',
+ 'item_status'=> STATUS_UNCONFIRMED
+ );
+
+ // POST variables the user is ALLOWED to provide
+ $allowedPostArgs = array(
+ 'task_type', 'product_category', 'product_version',
+ 'operating_system', 'task_severity', 'estimated_effort',
+ 'supertask_id', 'item_summary', 'detailed_desc'
+ );
+ // these POST variables the user is only ALLOWED to provide if he got the permissions
+ if ($user->perms('modify_all_tasks')) {
+ $allowedPostArgs[] = 'closedby_version';
+ $allowedPostArgs[] = 'task_priority';
+ $allowedPostArgs[] = 'due_date';
+ $allowedPostArgs[] = 'item_status';
+ }
+ if ($user->perms('manage_project')) {
+ $allowedPostArgs[] = 'mark_private';
+ }
+ // now copy all over all POST variables the user is ALLOWED to provide
+ // (but only if they are not empty)
+ foreach ($allowedPostArgs as $allowed) {
+ if (!empty($args[$allowed])) {
+ $sql_args[$allowed] = $args[$allowed];
+ }
+ }
+
+ // Process the due_date
+ if ( isset($args['due_date']) && ($due_date = $args['due_date']) || ($due_date = 0) ) {
+ $due_date = Flyspray::strtotime($due_date);
+ }
+
+ $sql_params[] = 'mark_private';
+ $sql_values[] = intval($user->perms('manage_project') && isset($args['mark_private']) && $args['mark_private'] == '1');
+
+ $sql_params[] = 'due_date';
+ $sql_values[] = $due_date;
+
+ $sql_params[] = 'closure_comment';
+ $sql_values[] = '';
+
+ // Process estimated effort
+ $estimated_effort = 0;
+ if ($proj->prefs['use_effort_tracking'] && isset($sql_args['estimated_effort'])) {
+ if (($estimated_effort = effort::editStringToSeconds($sql_args['estimated_effort'], $proj->prefs['hours_per_manday'], $proj->prefs['estimated_effort_format'])) === FALSE) {
+ Flyspray::show_error(L('invalideffort'));
+ $estimated_effort = 0;
+ }
+ $sql_args['estimated_effort'] = $estimated_effort;
+ }
+
+ // Token for anonymous users
+ $token = '';
+ if ($user->isAnon()) {
+ if (empty($args['anon_email'])) {
+ return 0;
+ }
+ $token = md5(function_exists('openssl_random_pseudo_bytes') ?
+ openssl_random_pseudo_bytes(32) :
+ uniqid(mt_rand(), true));
+ $sql_args['task_token'] = $token;
+ $sql_args['anon_email'] = $args['anon_email'];
+ }
+
+ // ensure all variables are in correct format
+ if (!empty($sql_args['due_date'])) {
+ $sql_args['due_date'] = Flyspray::strtotime($sql_args['due_date']);
+ }
+ if (isset($sql_args['mark_private'])) {
+ $sql_args['mark_private'] = intval($sql_args['mark_private'] == '1');
+ }
+
+ # dokuwiki syntax plugin filters on output
+ if($conf['general']['syntax_plugin'] != 'dokuwiki' && isset($sql_args['detailed_desc']) ){
+ $purifierconfig = HTMLPurifier_Config::createDefault();
+ $purifier = new HTMLPurifier($purifierconfig);
+ $sql_args['detailed_desc'] = $purifier->purify($sql_args['detailed_desc']);
+ }
+
+ // split keys and values into two separate arrays
+ $sql_keys = array();
+ $sql_values = array();
+ foreach ($sql_args as $key => $value) {
+ $sql_keys[] = $key;
+ $sql_values[] = $value;
+ }
+
+ /*
+ * TODO: At least with PostgreSQL, this has caused the sequence to be
+ * out of sync with reality. Must be fixed in upgrade process. Check
+ * what's the situation with MySQL. (It's fine, it updates the value even
+ * if the column was manually adjusted. Remove this whole block later.)
+ $result = $db->query('SELECT MAX(task_id)+1
+ FROM {tasks}');
+ $task_id = $db->fetchOne($result);
+ $task_id = $task_id ? $task_id : 1;
+ */
+ //now, $task_id is always the first element of $sql_values
+ #array_unshift($sql_keys, 'task_id');
+ #array_unshift($sql_values, $task_id);
+
+ $sql_keys_string = join(', ', $sql_keys);
+ $sql_placeholder = $db->fill_placeholders($sql_values);
+
+ $result = $db->query("INSERT INTO {tasks}
+ ($sql_keys_string)
+ VALUES ($sql_placeholder)", $sql_values);
+ $task_id=$db->insert_ID();
+
+ Backend::upload_links($task_id);
+
+ // create tags
+ if (isset($args['tags'])) {
+ $tagList = explode(';', $args['tags']);
+ $tagList = array_map('strip_tags', $tagList);
+ $tagList = array_map('trim', $tagList);
+ $tagList = array_unique($tagList); # avoid duplicates for inputs like: "tag1;tag1" or "tag1; tag1<p></p>"
+ foreach ($tagList as $tag){
+ if ($tag == ''){
+ continue;
+ }
+
+ # old tag feature
+ #$result2 = $db->query("INSERT INTO {tags} (task_id, tag) VALUES (?,?)",array($task_id,$tag));
+
+ # new tag feature. let's do it in 2 steps, it is getting too complicated to make it cross database compatible, drawback is possible (rare) race condition (use transaction?)
+ $res=$db->query("SELECT tag_id FROM {list_tag} WHERE (project_id=0 OR project_id=?) AND tag_name LIKE ? ORDER BY project_id", array($proj->id,$tag) );
+ if($t=$db->fetchRow($res)){
+ $tag_id=$t['tag_id'];
+ } else{
+ if( $proj->prefs['freetagging']==1){
+ # add to taglist of the project
+ $db->query("INSERT INTO {list_tag} (project_id,tag_name) VALUES (?,?)", array($proj->id,$tag));
+ $tag_id=$db->insert_ID();
+ } else{
+ continue;
+ }
+ };
+ $db->query("INSERT INTO {task_tag}(task_id,tag_id) VALUES(?,?)", array($task_id, $tag_id) );
+ }
+ }
+
+ // Log the assignments and send notifications to the assignees
+ if (isset($args['rassigned_to']) && is_array($args['rassigned_to']))
+ {
+ // Convert assigned_to and store them in the 'assigned' table
+ foreach ($args['rassigned_to'] as $val)
+ {
+ $db->replace('{assigned}', array('user_id'=> $val, 'task_id'=> $task_id), array('user_id','task_id'));
+ }
+ // Log to task history
+ Flyspray::logEvent($task_id, 14, implode(' ', $args['rassigned_to']));
+
+ // Notify the new assignees what happened. This obviously won't happen if the task is now assigned to no-one.
+ $notify->create(NOTIFY_NEW_ASSIGNEE, $task_id, null, $notify->specificAddresses($args['rassigned_to']), NOTIFY_BOTH, $proj->prefs['lang_code']);
+ }
+
+ // Log that the task was opened
+ Flyspray::logEvent($task_id, 1);
+
+ $result = $db->query('SELECT *
+ FROM {list_category}
+ WHERE category_id = ?',
+ array($args['product_category']));
+ $cat_details = $db->fetchRow($result);
+
+ // We need to figure out who is the category owner for this task
+ if (!empty($cat_details['category_owner'])) {
+ $owner = $cat_details['category_owner'];
+ }
+ else {
+ // check parent categories
+ $result = $db->query('SELECT *
+ FROM {list_category}
+ WHERE lft < ? AND rgt > ? AND project_id = ?
+ ORDER BY lft DESC',
+ array($cat_details['lft'], $cat_details['rgt'], $cat_details['project_id']));
+ while ($row = $db->fetchRow($result)) {
+ // If there's a parent category owner, send to them
+ if (!empty($row['category_owner'])) {
+ $owner = $row['category_owner'];
+ break;
+ }
+ }
+ }
+
+ if (!isset($owner)) {
+ $owner = $proj->prefs['default_cat_owner'];
+ }
+
+ if ($owner) {
+ if ($proj->prefs['auto_assign'] && ($args['item_status'] == STATUS_UNCONFIRMED || $args['item_status'] == STATUS_NEW)) {
+ Backend::add_to_assignees($owner, $task_id, true);
+ }
+ Backend::add_notification($owner, $task_id, true);
+ }
+
+ // Reminder for due_date field
+ if (!empty($sql_args['due_date'])) {
+ Backend::add_reminder($task_id, L('defaultreminder') . "\n\n" . createURL('details', $task_id), 2*24*60*60, time());
+ }
+
+ // Create the Notification
+ if (Backend::upload_files($task_id)) {
+ $notify->create(NOTIFY_TASK_OPENED, $task_id, 'files', null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+ } else {
+ $notify->create(NOTIFY_TASK_OPENED, $task_id, null, null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+ }
+
+ // If the reporter wanted to be added to the notification list
+ if (isset($args['notifyme']) && $args['notifyme'] == '1' && $user->id != $owner) {
+ Backend::add_notification($user->id, $task_id, true);
+ }
+
+ if ($user->isAnon()) {
+ $anonuser = array();
+ $anonuser[$email] = array('recipient' => $args['anon_email'], 'lang' => $fs->prefs['lang_code']);
+ $recipients = array($anonuser);
+ $notify->create(NOTIFY_ANON_TASK, $task_id, $token,
+ $recipients, NOTIFY_EMAIL, $proj->prefs['lang_code']);
+ }
+
+ return array($task_id, $token);
+ }
+
+ /**
+ * Closes a task
+ * @param integer $task_id
+ * @param integer $reason
+ * @param string $comment
+ * @param bool $mark100
+ * @access public
+ * @return bool
+ * @version 1.0
+ */
+ public static function close_task($task_id, $reason, $comment, $mark100 = true)
+ {
+ global $db, $notify, $user, $proj;
+ $task = Flyspray::getTaskDetails($task_id);
+
+ if (!$user->can_close_task($task)) {
+ return false;
+ }
+
+ if ($task['is_closed']) {
+ return false;
+ }
+
+ $db->query('UPDATE {tasks}
+ SET date_closed = ?, closed_by = ?, closure_comment = ?,
+ is_closed = 1, resolution_reason = ?, last_edited_time = ?,
+ last_edited_by = ?
+ WHERE task_id = ?',
+ array(time(), $user->id, $comment, $reason, time(), $user->id, $task_id));
+
+ if ($mark100) {
+ $db->query('UPDATE {tasks} SET percent_complete = 100 WHERE task_id = ?',
+ array($task_id));
+
+ Flyspray::logEvent($task_id, 3, 100, $task['percent_complete'], 'percent_complete');
+ }
+
+ $notify->create(NOTIFY_TASK_CLOSED, $task_id, null, null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+ Flyspray::logEvent($task_id, 2, $reason, $comment);
+
+ // If there's an admin request related to this, close it
+ $db->query('UPDATE {admin_requests}
+ SET resolved_by = ?, time_resolved = ?
+ WHERE task_id = ? AND request_type = ?',
+ array($user->id, time(), $task_id, 1));
+
+ // duplicate
+ if ($reason == RESOLUTION_DUPLICATE) {
+ preg_match("/\b(?:FS#|bug )(\d+)\b/", $comment, $dupe_of);
+ if (count($dupe_of) >= 2) {
+ $existing = $db->query('SELECT * FROM {related} WHERE this_task = ? AND related_task = ? AND is_duplicate = 1',
+ array($task_id, $dupe_of[1]));
+
+ if ($existing && $db->countRows($existing) == 0) {
+ $db->query('INSERT INTO {related} (this_task, related_task, is_duplicate) VALUES(?, ?, 1)',
+ array($task_id, $dupe_of[1]));
+ }
+ Backend::add_vote($task['opened_by'], $dupe_of[1]);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns an array of tasks (respecting pagination) and an ID list (all tasks)
+ * @param array $args
+ * @param array $visible
+ * @param integer $offset
+ * @param integer $comment
+ * @param bool $perpage
+ * @access public
+ * @return array
+ * @version 1.0
+ */
+ public static function get_task_list($args, $visible, $offset = 0, $perpage = 20) {
+ global $fs, $proj, $db, $user, $conf;
+ /* build SQL statement {{{ */
+ // Original SQL courtesy of Lance Conry http://www.rhinosw.com/
+ $where = $sql_params = array();
+
+ // echo '<pre>' . print_r($visible, true) . '</pre>';
+ // echo '<pre>' . print_r($args, true) . '</pre>';
+ // PostgreSQL LIKE searches are by default case sensitive,
+ // so we use ILIKE instead. For other databases, in our case
+ // only MySQL/MariaDB, LIKE is good for our purposes.
+ $LIKEOP = 'LIKE';
+ if ($db->dblink->dataProvider == 'postgres') {
+ $LIKEOP = 'ILIKE';
+ }
+
+ $select = '';
+ $groupby = 't.task_id, ';
+ $cgroupbyarr = array();
+
+ // Joins absolutely needed for user viewing rights
+ $from = ' {tasks} t
+-- All tasks have a project!
+JOIN {projects} p ON t.project_id = p.project_id';
+
+ // Not needed for anonymous users
+ if (!$user->isAnon()) {
+$from .= ' -- Global group always exists
+JOIN ({groups} gpg
+ JOIN {users_in_groups} gpuig ON gpg.group_id = gpuig.group_id AND gpuig.user_id = ?
+) ON gpg.project_id = 0
+-- Project group might exist or not.
+LEFT JOIN ({groups} pg
+ JOIN {users_in_groups} puig ON pg.group_id = puig.group_id AND puig.user_id = ?
+) ON pg.project_id = t.project_id';
+ $sql_params[] = $user->id;
+ $sql_params[] = $user->id;
+ }
+
+ // Keep this always, could also used for showing assigned users for a task.
+ // Keeps the overall logic somewhat simpler.
+ $from .= ' LEFT JOIN {assigned} ass ON t.task_id = ass.task_id';
+ $from .= ' LEFT JOIN {task_tag} tt ON t.task_id = tt.task_id';
+ $cfrom = $from;
+
+ // Seems resution name really is needed...
+ $select .= 'lr.resolution_name, ';
+ $from .= ' LEFT JOIN {list_resolution} lr ON t.resolution_reason = lr.resolution_id ';
+ $groupby .= 'lr.resolution_name, ';
+
+ // Otherwise, only join tables which are really necessary to speed up the db-query
+ if (array_get($args, 'type') || in_array('tasktype', $visible)) {
+ $select .= ' lt.tasktype_name, ';
+ $from .= '
+LEFT JOIN {list_tasktype} lt ON t.task_type = lt.tasktype_id ';
+ $groupby .= ' lt.tasktype_id, ';
+ }
+
+ if (array_get($args, 'status') || in_array('status', $visible)) {
+ $select .= ' lst.status_name, ';
+ $from .= '
+LEFT JOIN {list_status} lst ON t.item_status = lst.status_id ';
+ $groupby .= ' lst.status_id, ';
+ }
+
+ if (array_get($args, 'cat') || in_array('category', $visible)) {
+ $select .= ' lc.category_name AS category_name, ';
+ $from .= '
+LEFT JOIN {list_category} lc ON t.product_category = lc.category_id ';
+ $groupby .= 'lc.category_id, ';
+ }
+
+ if (in_array('votes', $visible)) {
+ $select .= ' (SELECT COUNT(vot.vote_id) FROM {votes} vot WHERE vot.task_id = t.task_id) AS num_votes, ';
+ }
+
+ $maxdatesql = ' GREATEST(COALESCE((SELECT max(c.date_added) FROM {comments} c WHERE c.task_id = t.task_id), 0), t.date_opened, t.date_closed, t.last_edited_time) ';
+ $search_for_changes = in_array('lastedit', $visible) || array_get($args, 'changedto') || array_get($args, 'changedfrom');
+ if ($search_for_changes) {
+ $select .= ' GREATEST(COALESCE((SELECT max(c.date_added) FROM {comments} c WHERE c.task_id = t.task_id), 0), t.date_opened, t.date_closed, t.last_edited_time) AS max_date, ';
+ $cgroupbyarr[] = 't.task_id';
+ }
+
+ if (array_get($args, 'search_in_comments')) {
+ $from .= '
+LEFT JOIN {comments} c ON t.task_id = c.task_id ';
+ $cfrom .= '
+LEFT JOIN {comments} c ON t.task_id = c.task_id ';
+ $cgroupbyarr[] = 't.task_id';
+ }
+
+ if (in_array('comments', $visible)) {
+ $select .= ' (SELECT COUNT(cc.comment_id) FROM {comments} cc WHERE cc.task_id = t.task_id) AS num_comments, ';
+ }
+
+ if (in_array('reportedin', $visible)) {
+ $select .= ' lv.version_name AS product_version_name, ';
+ $from .= '
+LEFT JOIN {list_version} lv ON t.product_version = lv.version_id ';
+ $groupby .= 'lv.version_id, ';
+ }
+
+ if (array_get($args, 'opened') || in_array('openedby', $visible)) {
+ $select .= ' uo.real_name AS opened_by_name, ';
+ $from .= '
+LEFT JOIN {users} uo ON t.opened_by = uo.user_id ';
+ $groupby .= 'uo.user_id, ';
+ if (array_get($args, 'opened')) {
+ $cfrom .= '
+LEFT JOIN {users} uo ON t.opened_by = uo.user_id ';
+ }
+ }
+
+ if (array_get($args, 'closed')) {
+ $select .= ' uc.real_name AS closed_by_name, ';
+ $from .= '
+LEFT JOIN {users} uc ON t.closed_by = uc.user_id ';
+ $groupby .= 'uc.user_id, ';
+ $cfrom .= '
+LEFT JOIN {users} uc ON t.closed_by = uc.user_id ';
+ }
+
+ if (array_get($args, 'due') || in_array('dueversion', $visible)) {
+ $select .= ' lvc.version_name AS closedby_version_name, ';
+ $from .= '
+LEFT JOIN {list_version} lvc ON t.closedby_version = lvc.version_id ';
+ $groupby .= 'lvc.version_id, lvc.list_position, ';
+ }
+
+ if (in_array('os', $visible)) {
+ $select .= ' los.os_name AS os_name, ';
+ $from .= '
+LEFT JOIN {list_os} los ON t.operating_system = los.os_id ';
+ $groupby .= 'los.os_id, ';
+ }
+
+ if (in_array('attachments', $visible)) {
+ $select .= ' (SELECT COUNT(attc.attachment_id) FROM {attachments} attc WHERE attc.task_id = t.task_id) AS num_attachments, ';
+ }
+
+ if (array_get($args, 'has_attachment')) {
+ $where[] = 'EXISTS (SELECT 1 FROM {attachments} att WHERE t.task_id = att.task_id)';
+ }
+ # 20150213 currently without recursive subtasks!
+ if (in_array('effort', $visible)) {
+ $select .= ' (SELECT SUM(ef.effort) FROM {effort} ef WHERE t.task_id = ef.task_id) AS effort, ';
+ }
+
+ if (array_get($args, 'dev') || in_array('assignedto', $visible)) {
+ # not every db system has this feature out of box
+ if($conf['database']['dbtype']=='mysqli' || $conf['database']['dbtype']=='mysql'){
+ $select .= ' GROUP_CONCAT(DISTINCT u.user_name ORDER BY u.user_id) AS assigned_to_name, ';
+ $select .= ' GROUP_CONCAT(DISTINCT u.user_id ORDER BY u.user_id) AS assignedids, ';
+ $select .= ' GROUP_CONCAT(DISTINCT u.profile_image ORDER BY u.user_id) AS assigned_image, ';
+ } elseif( $conf['database']['dbtype']=='pgsql'){
+ $select .= " array_to_string(array_agg(u.user_name ORDER BY u.user_id), ',') AS assigned_to_name, ";
+ $select .= " array_to_string(array_agg(CAST(u.user_id as text) ORDER BY u.user_id), ',') AS assignedids, ";
+ $select .= " array_to_string(array_agg(u.profile_image ORDER BY u.user_id), ',') AS assigned_image, ";
+ } else{
+ $select .= ' MIN(u.user_name) AS assigned_to_name, ';
+ $select .= ' (SELECT COUNT(assc.user_id) FROM {assigned} assc WHERE assc.task_id = t.task_id) AS num_assigned, ';
+ }
+ // assigned table is now always included in join
+ $from .= '
+LEFT JOIN {users} u ON ass.user_id = u.user_id ';
+ $groupby .= 'ass.task_id, ';
+ if (array_get($args, 'dev')) {
+ $cfrom .= '
+LEFT JOIN {users} u ON ass.user_id = u.user_id ';
+ $cgroupbyarr[] = 't.task_id';
+ $cgroupbyarr[] = 'ass.task_id';
+ }
+ }
+
+ # not every db system has this feature out of box, it is not standard sql
+ if($conf['database']['dbtype']=='mysqli' || $conf['database']['dbtype']=='mysql'){
+ #$select .= ' GROUP_CONCAT(DISTINCT tg.tag_name ORDER BY tg.list_position) AS tags, ';
+ $select .= ' GROUP_CONCAT(DISTINCT tg.tag_id ORDER BY tg.list_position) AS tagids, ';
+ #$select .= ' GROUP_CONCAT(DISTINCT tg.class ORDER BY tg.list_position) AS tagclass, ';
+ } elseif($conf['database']['dbtype']=='pgsql'){
+ #$select .= " array_to_string(array_agg(tg.tag_name ORDER BY tg.list_position), ',') AS tags, ";
+ $select .= " array_to_string(array_agg(CAST(tg.tag_id as text) ORDER BY tg.list_position), ',') AS tagids, ";
+ #$select .= " array_to_string(array_agg(tg.class ORDER BY tg.list_position), ',') AS tagclass, ";
+ } else{
+ # unsupported groupconcat or we just do not know how write it for the other databasetypes in this section
+ #$select .= ' MIN(tg.tag_name) AS tags, ';
+ #$select .= ' (SELECT COUNT(tt.tag_id) FROM {task_tag} tt WHERE tt.task_id = t.task_id) AS tagnum, ';
+ $select .= ' MIN(tg.tag_id) AS tagids, ';
+ #$select .= " '' AS tagclass, ";
+ }
+ // task_tag join table is now always included in join
+ $from .= '
+LEFT JOIN {list_tag} tg ON tt.tag_id = tg.tag_id ';
+ $groupby .= 'tt.task_id, ';
+ $cfrom .= '
+LEFT JOIN {list_tag} tg ON tt.tag_id = tg.tag_id ';
+ $cgroupbyarr[] = 't.task_id';
+ $cgroupbyarr[] = 'tt.task_id';
+
+
+ # use preparsed task description cache for dokuwiki when possible
+ if($conf['general']['syntax_plugin']=='dokuwiki' && FLYSPRAY_USE_CACHE==true){
+ $select.=' MIN(cache.content) desccache, ';
+ $from.='
+LEFT JOIN {cache} cache ON t.task_id=cache.topic AND cache.type=\'task\' ';
+ } else {
+ $select .= 'NULL AS desccache, ';
+ }
+
+ if (array_get($args, 'only_primary')) {
+ $where[] = 'NOT EXISTS (SELECT 1 FROM {dependencies} dep WHERE dep.dep_task_id = t.task_id)';
+ }
+
+ # feature FS#1600
+ if (array_get($args, 'only_blocker')) {
+ $where[] = 'EXISTS (SELECT 1 FROM {dependencies} dep WHERE dep.dep_task_id = t.task_id)';
+ }
+
+ if (array_get($args, 'only_blocked')) {
+ $where[] = 'EXISTS (SELECT 1 FROM {dependencies} dep WHERE dep.task_id = t.task_id)';
+ }
+
+ # feature FS#1599
+ if (array_get($args, 'only_unblocked')) {
+ $where[] = 'NOT EXISTS (SELECT 1 FROM {dependencies} dep WHERE dep.task_id = t.task_id)';
+ }
+
+ if (array_get($args, 'hide_subtasks')) {
+ $where[] = 't.supertask_id = 0';
+ }
+
+ if (array_get($args, 'only_watched')) {
+ $where[] = 'EXISTS (SELECT 1 FROM {notifications} fsn WHERE t.task_id = fsn.task_id AND fsn.user_id = ?)';
+ $sql_params[] = $user->id;
+ }
+
+ if ($proj->id) {
+ $where[] = 't.project_id = ?';
+ $sql_params[] = $proj->id;
+ } else {
+ if (!$user->isAnon()) { // Anon-case handled later.
+ $allowed = array();
+ foreach($fs->projects as $p) {
+ $allowed[] = $p['project_id'];
+ }
+ if(count($allowed)>0){
+ $where[] = 't.project_id IN (' . implode(',', $allowed). ')';
+ }else{
+ $where[] = '0 = 1'; # always empty result
+ }
+ }
+ }
+
+ // process users viewing rights, if not anonymous
+ if (!$user->isAnon()) {
+ $where[] = '
+( -- Begin block where users viewing rights are checked.
+ -- Case everyone can see all project tasks anyway and task not private
+ (t.mark_private = 0 AND p.others_view = 1)
+ OR
+ -- Case admin or project manager, can see any task, even private
+ (gpg.is_admin = 1 OR gpg.manage_project = 1 OR pg.is_admin = 1 OR pg.manage_project = 1)
+ OR
+ -- Case allowed to see all tasks, but not private
+ ((gpg.view_tasks = 1 OR pg.view_tasks = 1) AND t.mark_private = 0)
+ OR
+ -- Case allowed to see own tasks (automatically covers private tasks also for this user!)
+ ((gpg.view_own_tasks = 1 OR pg.view_own_tasks = 1) AND (t.opened_by = ? OR ass.user_id = ?))
+ OR
+ -- Case task is private, but user either opened it or is an assignee
+ (t.mark_private = 1 AND (t.opened_by = ? OR ass.user_id = ?))
+ OR
+ -- Leave groups tasks as the last one to check. They are the only ones that actually need doing a subquery
+ -- for checking viewing rights. There\'s a chance that a previous check already matched and the subquery is
+ -- not executed at all. All this of course depending on how the database query optimizer actually chooses
+ -- to fetch the results and execute this query... At least it has been given the hint.
+
+ -- Case allowed to see groups tasks, all projects (NOTE: both global and project specific groups accepted here)
+ -- Strange... do not use OR here with user_id in EXISTS clause, seems to prevent using index with both mysql and
+ -- postgresql, query times go up a lot. So it\'ll be 2 different EXISTS OR\'ed together.
+ (gpg.view_groups_tasks = 1 AND t.mark_private = 0 AND (
+ EXISTS (SELECT 1 FROM {users_in_groups} WHERE (group_id = pg.group_id OR group_id = gpg.group_id) AND user_id = t.opened_by)
+ OR
+ EXISTS (SELECT 1 FROM {users_in_groups} WHERE (group_id = pg.group_id OR group_id = gpg.group_id) AND user_id = ass.user_id)
+ ))
+ OR
+ -- Case allowed to see groups tasks, current project. Only project group allowed here.
+ (pg.view_groups_tasks = 1 AND t.mark_private = 0 AND (
+ EXISTS (SELECT 1 FROM {users_in_groups} WHERE group_id = pg.group_id AND user_id = t.opened_by)
+ OR
+ EXISTS (SELECT 1 FROM {users_in_groups} WHERE group_id = pg.group_id AND user_id = ass.user_id)
+ ))
+) -- Rights have been checked
+';
+ $sql_params[] = $user->id;
+ $sql_params[] = $user->id;
+ $sql_params[] = $user->id;
+ $sql_params[] = $user->id;
+ }
+ /// process search-conditions {{{
+ $submits = array('type' => 'task_type', 'sev' => 'task_severity',
+ 'due' => 'closedby_version', 'reported' => 'product_version',
+ 'cat' => 'product_category', 'status' => 'item_status',
+ 'percent' => 'percent_complete', 'pri' => 'task_priority',
+ 'dev' => array('ass.user_id', 'u.user_name', 'u.real_name'),
+ 'opened' => array('opened_by', 'uo.user_name', 'uo.real_name'),
+ 'closed' => array('closed_by', 'uc.user_name', 'uc.real_name'));
+ foreach ($submits as $key => $db_key) {
+ $type = array_get($args, $key, ($key == 'status') ? 'open' : '');
+ settype($type, 'array');
+
+ if (in_array('', $type)) {
+ continue;
+ }
+
+ $temp = '';
+ $condition = '';
+ foreach ($type as $val) {
+ // add conditions for the status selection
+ if ($key == 'status' && $val == 'closed' && !in_array('open', $type)) {
+ $temp .= ' is_closed = 1 AND';
+ } elseif ($key == 'status' && !in_array('closed', $type)) {
+ $temp .= ' is_closed = 0 AND';
+ }
+ if (is_numeric($val) && !is_array($db_key) && !($key == 'status' && $val == 'closed')) {
+ $temp .= ' ' . $db_key . ' = ? OR';
+ $sql_params[] = $val;
+ } elseif (is_array($db_key)) {
+ if ($key == 'dev' && ($val == 'notassigned' || $val == '0' || $val == '-1')) {
+ $temp .= ' ass.user_id is NULL OR';
+ } else {
+ foreach ($db_key as $singleDBKey) {
+ if(ctype_digit($val) && strpos($singleDBKey, '_name') === false) {
+ $temp .= ' ' . $singleDBKey . ' = ? OR';
+ $sql_params[] = $val;
+ } elseif (!ctype_digit($val) && strpos($singleDBKey, '_name') !== false) {
+ $temp .= ' ' . $singleDBKey . " $LIKEOP ? OR";
+ $sql_params[] = '%' . $val . '%';
+ }
+ }
+ }
+ }
+
+ // Add the subcategories to the query
+ if ($key == 'cat') {
+ $result = $db->query('SELECT *
+ FROM {list_category}
+ WHERE category_id = ?', array($val));
+ $cat_details = $db->fetchRow($result);
+
+ $result = $db->query('SELECT *
+ FROM {list_category}
+ WHERE lft > ? AND rgt < ? AND project_id = ?', array($cat_details['lft'], $cat_details['rgt'], $cat_details['project_id']));
+ while ($row = $db->fetchRow($result)) {
+ $temp .= ' product_category = ? OR';
+ $sql_params[] = $row['category_id'];
+ }
+ }
+ }
+
+ if ($temp) {
+ $where[] = '(' . substr($temp, 0, -3) . ')'; # strip last ' OR' and 'AND'
+ }
+ }
+/// }}}
+
+ $order_keys = array(
+ 'id' => 't.task_id',
+ 'project' => 'project_title',
+ 'tasktype' => 'tasktype_name',
+ 'dateopened' => 'date_opened',
+ 'summary' => 'item_summary',
+ 'severity' => 'task_severity',
+ 'category' => 'lc.category_name',
+ 'status' => 'is_closed, item_status',
+ 'dueversion' => 'lvc.list_position',
+ 'duedate' => 'due_date',
+ 'progress' => 'percent_complete',
+ 'lastedit' => 'max_date',
+ 'priority' => 'task_priority',
+ 'openedby' => 'uo.real_name',
+ 'reportedin' => 't.product_version',
+ 'assignedto' => 'u.real_name',
+ 'dateclosed' => 't.date_closed',
+ 'os' => 'los.os_name',
+ 'votes' => 'num_votes',
+ 'attachments' => 'num_attachments',
+ 'comments' => 'num_comments',
+ 'private' => 'mark_private',
+ 'supertask' => 't.supertask_id',
+ );
+
+ // make sure that only columns can be sorted that are visible (and task severity, since it is always loaded)
+ $order_keys = array_intersect_key($order_keys, array_merge(array_flip($visible), array('severity' => 'task_severity')));
+
+ // Implementing setting "Default order by"
+ if (!array_key_exists('order', $args)) {
+ # now also for $proj->id=0 (allprojects)
+ $orderBy = $proj->prefs['sorting'][0]['field'];
+ $sort = $proj->prefs['sorting'][0]['dir'];
+ if (count($proj->prefs['sorting']) >1){
+ $orderBy2 =$proj->prefs['sorting'][1]['field'];
+ $sort2= $proj->prefs['sorting'][1]['dir'];
+ } else{
+ $orderBy2='severity';
+ $sort2='DESC';
+ }
+ } else {
+ $orderBy = $args['order'];
+ $sort = $args['sort'];
+ $orderBy2='severity';
+ $sort2='desc';
+ }
+
+ // TODO: Fix this! If something is already ordered by task_id, there's
+ // absolutely no use to even try to order by something else also.
+ $order_column[0] = $order_keys[Filters::enum(array_get($args, 'order', $orderBy), array_keys($order_keys))];
+ $order_column[1] = $order_keys[Filters::enum(array_get($args, 'order2', $orderBy2), array_keys($order_keys))];
+ $sortorder = sprintf('%s %s, %s %s, t.task_id ASC',
+ $order_column[0],
+ Filters::enum(array_get($args, 'sort', $sort), array('asc', 'desc')),
+ $order_column[1],
+ Filters::enum(array_get($args, 'sort2', $sort2), array('asc', 'desc'))
+ );
+
+ $having = array();
+ $dates = array('duedate' => 'due_date', 'changed' => $maxdatesql,
+ 'opened' => 'date_opened', 'closed' => 'date_closed');
+ foreach ($dates as $post => $db_key) {
+ $var = ($post == 'changed') ? 'having' : 'where';
+ if ($date = array_get($args, $post . 'from')) {
+ ${$var}[] = '(' . $db_key . ' >= ' . Flyspray::strtotime($date) . ')';
+ }
+ if ($date = array_get($args, $post . 'to')) {
+ ${$var}[] = '(' . $db_key . ' <= ' . Flyspray::strtotime($date) . ' AND ' . $db_key . ' > 0)';
+ }
+ }
+
+ if (array_get($args, 'string')) {
+ $words = explode(' ', strtr(array_get($args, 'string'), '()', ' '));
+ $comments = '';
+ $where_temp = array();
+
+ if (array_get($args, 'search_in_comments')) {
+ $comments .= " OR c.comment_text $LIKEOP ?";
+ }
+ if (array_get($args, 'search_in_details')) {
+ $comments .= " OR t.detailed_desc $LIKEOP ?";
+ }
+
+ foreach ($words as $word) {
+ $word=trim($word);
+ if($word==''){
+ continue;
+ }
+ $likeWord = '%' . str_replace('+', ' ', $word) . '%';
+ $where_temp[] = "(t.item_summary $LIKEOP ? OR t.task_id = ? $comments)";
+ array_push($sql_params, $likeWord, intval($word));
+ if (array_get($args, 'search_in_comments')) {
+ array_push($sql_params, $likeWord);
+ }
+ if (array_get($args, 'search_in_details')) {
+ array_push($sql_params, $likeWord);
+ }
+ }
+
+ if(count($where_temp)>0){
+ $where[] = '(' . implode((array_get($args, 'search_for_all') ? ' AND ' : ' OR '), $where_temp) . ')';
+ }
+ }
+
+ if ($user->isAnon()) {
+ $where[] = 't.mark_private = 0 AND p.others_view = 1';
+ if(array_key_exists('status', $args)){
+ if (in_array('closed', $args['status']) && !in_array('open', $args['status'])) {
+ $where[] = 't.is_closed = 1';
+ } elseif (in_array('open', $args['status']) && !in_array('closed', $args['status'])) {
+ $where[] = 't.is_closed = 0';
+ }
+ }
+ }
+
+ $where = (count($where)) ? 'WHERE ' . join(' AND ', $where) : '';
+
+ // Get the column names of table tasks for the group by statement
+ if (!strcasecmp($conf['database']['dbtype'], 'pgsql')) {
+ $groupby .= "p.project_title, p.project_is_active, ";
+ // Remove this after checking old PostgreSQL docs.
+ // 1 column from task table should be enough, after
+ // already grouping by task_id, there's no possibility
+ // to have anything more in that table to group by.
+ $groupby .= $db->getColumnNames('{tasks}', 't.task_id', 't.');
+ } else {
+ $groupby = 't.task_id';
+ }
+
+ $having = (count($having)) ? 'HAVING ' . join(' AND ', $having) : '';
+
+ // echo '<pre>' . print_r($args, true) . '</pre>';
+ // echo '<pre>' . print_r($cgroupbyarr, true) . '</pre>';
+ $cgroupby = count($cgroupbyarr) ? 'GROUP BY ' . implode(',', array_unique($cgroupbyarr)) : '';
+
+ $sqlcount = "SELECT COUNT(*) FROM (SELECT 1, t.task_id, t.date_opened, t.date_closed, t.last_edited_time
+ FROM $cfrom
+ $where
+ $cgroupby
+ $having) s";
+ $sqltext = "SELECT t.*, $select
+p.project_title, p.project_is_active
+FROM $from
+$where
+GROUP BY $groupby
+$having
+ORDER BY $sortorder";
+
+ // Very effective alternative with a little bit more work
+ // and if row_number() can be emulated in mysql. Idea:
+ // Move every join and other operation not needed in
+ // the inner clause to select rows to the outer query,
+ // and do the rest when we already know which rows
+ // are in the window to show. Got it to run constantly
+ // under 6000 ms.
+ /* Leave this for next version, don't have enough time for testing.
+ $sqlexperiment = "SELECT * FROM (
+SELECT row_number() OVER(ORDER BY task_id) AS rownum,
+t.*, $select p.project_title, p.project_is_active FROM $from
+$where
+GROUP BY $groupby
+$having
+ORDER BY $sortorder
+)
+t WHERE rownum BETWEEN $offset AND " . ($offset + $perpage);
+*/
+
+// echo '<pre>'.print_r($sql_params, true).'</pre>'; # for debugging
+// echo '<pre>'.$sqlcount.'</pre>'; # for debugging
+// echo '<pre>'.$sqltext.'</pre>'; # for debugging
+ $sql = $db->query($sqlcount, $sql_params);
+ $totalcount = $db->fetchOne($sql);
+
+# 20150313 peterdd: Do not override task_type with tasktype_name until we changed t.task_type to t.task_type_id! We need the id too.
+
+ $sql = $db->query($sqltext, $sql_params, $perpage, $offset);
+ // $sql = $db->query($sqlexperiment, $sql_params);
+ $tasks = $db->fetchAllArray($sql);
+ $id_list = array();
+ $limit = array_get($args, 'limit', -1);
+ $forbidden_tasks_count = 0;
+ foreach ($tasks as $key => $task) {
+ $id_list[] = $task['task_id'];
+ if (!$user->can_view_task($task)) {
+ unset($tasks[$key]);
+ $forbidden_tasks_count++;
+ }
+ }
+
+// Work on this is not finished until $forbidden_tasks_count is always zero.
+// echo "<pre>$offset : $perpage : $totalcount : $forbidden_tasks_count</pre>";
+ return array($tasks, $id_list, $totalcount, $forbidden_tasks_count);
+// # end alternative
+ }
+
+# end get_task_list
+} # end class
diff --git a/includes/class.csp.php b/includes/class.csp.php
new file mode 100644
index 0000000..ecf4541
--- /dev/null
+++ b/includes/class.csp.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ *
+ * A simple class for dynamic construction of Content-Security-Policy HTTP header string.
+ *
+ * This is just quick write to get the job for Flyspray done. May change completely!
+ *
+ * It does not check if the added rules are valid or make sense in the context and http request/response!
+ * So it is currently up to the code sections who use that class that the resulting csp string is correct.
+ *
+ * @license http://opensource.org/licenses/lgpl-license.php Lesser GNU Public License
+ * @author peterdd
+ *
+ * @see https://www.w3.org/TR/CSP2/
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
+ * @see https://caniuse.com/#search=csp
+ *
+ * Example: $csp=new ContentSecurityPolicy(); $csp->add('default-src', "'none'"); $csp->add('style-src', "'self'"); $csp->emit();
+ */
+class ContentSecurityPolicy {
+
+ #private $csp=array();
+ public $csp=array();
+
+ # for debugging just to track which extension/plugins wants to add csp-entries
+ #public $history=array();
+
+ function __construct(){
+ $this->csp=array();
+ }
+
+ /**
+ * get the constructed concatenated value string part for the http Content-Security-Header
+ *
+ * TODO: maybe some syntax checks and logical verification when building the string.
+ *
+ * MAYBE: add optional parameter to get only a part like $csp->get('img-src')
+ * Alternatively the user can just access the currently public $csp->csp['img-src'] to get that values as array.
+ *
+ */
+ public function get(){
+ $out = '';
+ foreach ( $this->csp as $key => $values ) {
+ $out .= $key.' '.implode(' ', $values).'; ';
+ }
+ $out = trim($out, '; ');
+ return $out;
+ }
+
+ /**
+ * adds a value to a csp type
+ *
+ * @param type
+ * @param value single values for a type
+ *
+ * examples:
+ * $csp->add('default-src', "'self'"); # surrounding double quotes "" used to pass the single quotes
+ * $csp->add('img-src', 'mycdn.example.com'); # single quoted string ok
+ */
+ public function add($type, $value){
+ if( isset($this->csp[$type]) ) {
+ if( !in_array( $value, $this->csp[$type] ) ) {
+ $this->csp[$type][] = $value;
+ }
+ } else {
+ $this->csp[$type] = array($value);
+ }
+ #$this->history[]=debug_backtrace()[1];
+ }
+
+ /**
+ * sends the Content-Security-Policy http headers
+ */
+ public function emit() {
+ $string=$this->get();
+ header('Content-Security-Policy: '.$string );
+ # some older web browsers used vendor prefixes before csp got w3c recommendation.
+ # maybe use useragent string to detect who should receive this outdated vendor csp strings.
+ # for IE 10-11
+ header('X-Content-Security-Policy: '.$string );
+ # for Chrome 15-24, Safari 5.1-6, ..
+ header('X-WebKit-CSP: '.$string );
+ }
+
+ /**
+ * Put the csp as meta-tags in the HTML-head section.
+ *
+ * Q: What is the benefit of adding csp as meta tags too?
+ *
+ * I don't know, maybe this way the csp persist if someone saves a page to his harddrive for instance or if bad web proxies rip off csp http headers?
+ * Do webbrowsers store the CSP-HTTP header to the HTML-head as metatags automatically if there is no related metatag in the original page? Mhh..
+ */
+ public function getMeta() {
+ $string=$this->get();
+ $out= '<meta http-equiv="Content-Security-Policy" content="'.$string.'">';
+ # enable if you think it is necessary for your customers.
+ # older web browsers used vendor prefixes before csp2 got a w3c recommendation standard..
+ # maybe use useragent string to detect who should receive this outdated vendor csp strings.
+ # for IE 10-11
+ $out.= '<meta http-equiv="X-Content-Security-Policy" content="'.$string.'">';
+ # for Chrome 15-24, Safari 5.1-6, ..
+ $out.= '<meta http-equiv="X-WebKit-CSP" content="'.$string.'">';
+ return $out;
+ }
+
+}
diff --git a/includes/class.database.php b/includes/class.database.php
new file mode 100644
index 0000000..652cc98
--- /dev/null
+++ b/includes/class.database.php
@@ -0,0 +1,434 @@
+<?php
+/**
+ * Flyspray
+ *
+ * Database class class
+ *
+ * This class is a wrapper for ADOdb functions.
+ *
+ * @license http://opensource.org/licenses/lgpl-license.php Lesser GNU Public License
+ * @package flyspray
+ * @author Tony Collins
+ * @author Cristian Rodriguez
+ */
+
+if (!defined('IN_FS')) {
+ die('Do not access this file directly.');
+}
+
+class Database
+{
+ /**
+ * Table prefix, usually flyspray_
+ * @var string
+ * @access private
+ */
+ public $dbprefix;
+
+ /**
+ * Cache for queries done by cached_query()
+ * @var array
+ * @access private
+ * @see cached_query();
+ */
+ private $cache = array();
+
+ /**
+ * dblink
+ * adodb handler object
+ * @var object
+ * @access public
+ */
+ public $dblink = null;
+
+ /**
+ * Open a connection to the database quickly
+ * @param array $conf connection data
+ * @return void
+ */
+ public function dbOpenFast($conf)
+ {
+ if(!is_array($conf) || extract($conf, EXTR_REFS|EXTR_SKIP) < 5) {
+
+ die( 'Flyspray was unable to connect to the database. '
+ .'Check your settings in flyspray.conf.php');
+ }
+
+ $this->dbOpen($dbhost, $dbuser, $dbpass, $dbname, $dbtype, isset($dbprefix) ? $dbprefix : '');
+ }
+
+ /**
+ * Open a connection to the database and set connection parameters
+ * @param string $dbhost hostname where the database server uses
+ * @param string $dbuser username to connect to the database
+ * @param string $dbpass password to connect to the database
+ * @param string $dbname
+ * @param string $dbtype database driver to use, currently : "mysql", "mysqli", "pgsql"
+ * "pdo_mysql" and "pdo_pgsql" experimental
+ * @param string $dbprefix database prefix.
+ */
+ public function dbOpen($dbhost = '', $dbuser = '', $dbpass = '', $dbname = '', $dbtype = '', $dbprefix = '')
+ {
+
+ $this->dbtype = $dbtype;
+ $this->dbprefix = $dbprefix;
+ $ADODB_COUNTRECS = false;
+
+ # 20160408 peterdd: hack to enable database socket usage with adodb-5.20.3
+ # For instance on german 1und1 managed linux servers, e.g. $dbhost='localhost:/tmp/mysql5.sock'
+ if( ($dbtype=='mysqli' || $dbtype='pdo_mysql') && 'localhost:/'==substr($dbhost,0,11) ){
+ $dbsocket=substr($dbhost,10);
+ $dbhost='localhost';
+ if($dbtype=='mysqli'){
+ ini_set('mysqli.default_socket', $dbsocket );
+ }else{
+ ini_set('pdo_mysql.default_socket',$dbsocket);
+ }
+ }
+
+ # adodb for pdo is a bit different then the others at the moment (adodb 5.20.4)
+ # see http://adodb.org/dokuwiki/doku.php?id=v5:database:pdo
+ if($this->dbtype=='pdo_mysql'){
+ $this->dblink = ADOnewConnection('pdo');
+ $dsnString= 'host='.$dbhost.';dbname='.$dbname.';charset=utf8mb4';
+ $this->dblink->connect('mysql:' . $dsnString, $dbuser, $dbpass);
+ }else{
+ $this->dblink = ADOnewConnection($this->dbtype);
+ $this->dblink->connect($dbhost, $dbuser, $dbpass, $dbname);
+ }
+
+ if ($this->dblink === false || (!empty($this->dbprefix) && !preg_match('/^[a-z][a-z0-9_]+$/i', $this->dbprefix))) {
+
+ die('Flyspray was unable to connect to the database. '
+ .'Check your settings in flyspray.conf.php');
+ }
+ $this->dblink->setFetchMode(ADODB_FETCH_BOTH);
+
+ if($dbtype=='mysqli'){
+ $sinfo=$this->dblink->serverInfo();
+ if(version_compare($sinfo['version'], '5.5.3')>=0 ){
+ $this->dblink->setCharSet('utf8mb4');
+ }else{
+ $this->dblink->setCharSet('utf8');
+ }
+ }else{
+ $this->dblink->setCharSet('utf8');
+ }
+
+ // enable debug if constant DEBUG_SQL is defined.
+ !defined('DEBUG_SQL') || $this->dblink->debug = true;
+
+ if($dbtype === 'mysql' || $dbtype === 'mysqli') {
+ $dbinfo = $this->dblink->serverInfo();
+ if(isset($dbinfo['version']) && version_compare($dbinfo['version'], '5.0.2', '>=')) {
+ $this->dblink->execute("SET SESSION SQL_MODE='TRADITIONAL'");
+ }
+ }
+ }
+
+ /**
+ * Closes the database connection
+ * @return void
+ */
+ public function dbClose()
+ {
+ $this->dblink->close();
+ }
+
+ /**
+ * insert_ID
+ *
+ * @access public
+ */
+ public function insert_ID()
+ {
+ return $this->dblink->insert_ID();
+ }
+
+ /**
+ * countRows
+ * Returns the number of rows in a result
+ * @param object $result
+ * @access public
+ * @return int
+ */
+ public function countRows($result)
+ {
+ return (int) $result->recordCount();
+ }
+
+ /**
+ * affectedRows
+ *
+ * @access public
+ * @return int
+ */
+ public function affectedRows()
+ {
+ return (int) $this->dblink->affected_Rows();
+ }
+
+ /**
+ * fetchRow
+ *
+ * @param $result
+ * @access public
+ * @return void
+ */
+
+ public function fetchRow($result)
+ {
+ return $result->fetchRow();
+ }
+
+ /**
+ * fetchCol
+ *
+ * @param $result
+ * @param int $col
+ * @access public
+ * @return void
+ */
+
+ public function fetchCol($result, $col=0)
+ {
+ $tab = array();
+ while ($tmp = $result->fetchRow()) {
+ $tab[] = $tmp[$col];
+ }
+ return $tab;
+ }
+
+ /**
+ * query
+ *
+ * @param mixed $sql
+ * @param mixed $inputarr
+ * @param mixed $numrows
+ * @param mixed $offset
+ * @access public
+ * @return void
+ */
+
+ public function query($sql, $inputarr = false, $numrows = -1, $offset = -1)
+ {
+ // auto add $dbprefix where we have {table}
+ $sql = $this->_add_prefix($sql);
+ // remove conversions for MySQL
+ if (strcasecmp($this->dbtype, 'pgsql') != 0) {
+ $sql = str_replace('::int', '', $sql);
+ $sql = str_replace('::text', '', $sql);
+ }
+
+ $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC;
+
+ if (($numrows >= 0 ) or ($offset >= 0 )) {
+ /* adodb drivers are inconsisent with the casting of $numrows and $offset so WE
+ * cast to integer here anyway */
+ $result = $this->dblink->selectLimit($sql, (int) $numrows, (int) $offset, $inputarr);
+ } else {
+ $result = $this->dblink->execute($sql, $inputarr);
+ }
+
+ if (!$result) {
+
+ if(function_exists("debug_backtrace") && defined('DEBUG_SQL')) {
+ echo "<pre style='text-align: left;'>";
+ var_dump(debug_backtrace());
+ echo "</pre>";
+ }
+
+ $query_params = '';
+
+ if(is_array($inputarr) && count($inputarr)) {
+ $query_params = implode(',', array_map(array('Filters','noXSS'), $inputarr));
+ }
+
+ die(sprintf("Query {%s} with params {%s} failed! (%s)",
+ Filters::noXSS($sql), $query_params, Filters::noXSS($this->dblink->errorMsg())));
+
+ }
+
+ return $result;
+ }
+
+ /**
+ * cached_query
+ *
+ * @param mixed $idx
+ * @param mixed $sql
+ * @param array $sqlargs
+ * @access public
+ * @return array
+ */
+ public function cached_query($idx, $sql, $sqlargs = array())
+ {
+ if (isset($this->cache[$idx])) {
+ return $this->cache[$idx];
+ }
+
+ $sql = $this->query($sql, $sqlargs);
+ return ($this->cache[$idx] = $this->fetchAllArray($sql));
+ }
+
+ /**
+ * fetchOne
+ *
+ * @param $result
+ * @access public
+ * @return array
+ */
+ public function fetchOne($result)
+ {
+ $row = $this->fetchRow($result);
+ return (isset($row[0]) ? $row[0] : '');
+ }
+
+ /**
+ * fetchAllArray
+ *
+ * @param $result
+ * @access public
+ * @return array
+ */
+ public function fetchAllArray($result)
+ {
+ return $result->getArray();
+ }
+
+ /**
+ * groupBy
+ *
+ * This groups a result by a single column the way
+ * MySQL would do it. Postgre doesn't like the queries MySQL needs.
+ *
+ * @param object $result
+ * @param string $column
+ * @access public
+ * @return array process the returned array with foreach ($return as $row) {}
+ */
+ public function groupBy($result, $column)
+ {
+ $rows = array();
+ while ($row = $this->fetchRow($result)) {
+ $rows[$row[$column]] = $row;
+ }
+ return array_values($rows);
+ }
+
+ /**
+ * getColumnNames
+ *
+ * @param mixed $table
+ * @param mixed $alt
+ * @param mixed $prefix
+ * @access public
+ * @return void
+ */
+
+ public function getColumnNames($table, $alt, $prefix)
+ {
+ global $conf;
+
+ if (strcasecmp($conf['database']['dbtype'], 'pgsql')) {
+ return $alt;
+ }
+
+ $table = $this->_add_prefix($table);
+ $fetched_columns = $this->query('SELECT column_name FROM information_schema.columns WHERE table_name = ?',
+ array(str_replace('"', '', $table)));
+ $fetched_columns = $this->fetchAllArray($fetched_columns);
+
+ foreach ($fetched_columns as $key => $value)
+ {
+ $col_names[$key] = $prefix . $value[0];
+ }
+
+ $groupby = implode(', ', $col_names);
+
+ return $groupby;
+ }
+
+ /**
+ * replace
+ *
+ * Try to update a record,
+ * and if the record is not found,
+ * an insert statement is generated and executed.
+ *
+ * @param string $table
+ * @param array $field
+ * @param array $keys
+ * @param bool $autoquote
+ * @access public
+ * @return integer 0 on error, 1 on update. 2 on insert
+ */
+ public function replace($table, $field, $keys, $autoquote = true)
+ {
+ $table = $this->_add_prefix($table);
+ return $this->dblink->replace($table, $field, $keys, $autoquote);
+ }
+
+ /**
+ * Adds the table prefix
+ * @param string $sql_data table name or sql query
+ * @return string sql with correct,quoted table prefix
+ * @access private
+ * @since 0.9.9
+ */
+ private function _add_prefix($sql_data)
+ {
+ return preg_replace('/{([\w\-]*?)}/', $this->quoteIdentifier($this->dbprefix . '\1'), $sql_data);
+ }
+
+ /**
+ * Helper method to quote an indentifier
+ * (table or field name) with the database specific quote
+ * @param string $ident table or field name to be quoted
+ * @return string
+ * @access public
+ * @since 0.9.9
+ */
+ public function quoteIdentifier($ident)
+ {
+ return (string) $this->dblink->nameQuote . $ident . $this->dblink->nameQuote ;
+ }
+
+ /**
+ * Quote a string in a safe way to be entered to the database
+ * (for the very few cases we don't use prepared statements)
+ *
+ * @param string $string string to be quoted
+ * @return string quoted string
+ * @access public
+ * @since 0.9.9
+ * @notes please use this little as possible, always prefer prepared statements
+ */
+ public function qstr($string)
+ {
+ return $this->dblink->qstr($string, false);
+ }
+
+ /**
+ * fill_placeholders
+ * a convenience function to fill sql query placeholders
+ * according to the number of columns to be used.
+ * @param array $cols
+ * @param integer $additional generate N additional placeholders
+ * @access public
+ * @return string comma separated "?" placeholders
+ * @static
+ */
+ public function fill_placeholders($cols, $additional=0)
+ {
+ if(is_array($cols) && count($cols) && is_int($additional)) {
+
+ return join(',', array_fill(0, (count($cols) + $additional), '?'));
+
+ } else {
+ //this is not an user error, is a programmer error.
+ trigger_error("incorrect data passed to fill_placeholders", E_USER_ERROR);
+ }
+ }
+ // End of Database Class
+}
diff --git a/includes/class.effort.php b/includes/class.effort.php
new file mode 100644
index 0000000..984feeb
--- /dev/null
+++ b/includes/class.effort.php
@@ -0,0 +1,302 @@
+<?php
+
+/**
+ * Class effort
+ *
+ * Task level Effort Tracking functionality.
+ */
+class effort
+{
+ const FORMAT_HOURS_COLON_MINUTES = 0; // Default value in database
+ const FORMAT_HOURS_SPACE_MINUTES = 1;
+ const FORMAT_HOURS_PLAIN = 2;
+ const FORMAT_HOURS_ONE_DECIMAL = 3;
+ const FORMAT_MINUTES = 4;
+ const FORMAT_DAYS_PLAIN = 5;
+ const FORMAT_DAYS_ONE_DECIMAL = 6;
+ const FORMAT_DAYS_PLAIN_HOURS_PLAIN = 7;
+ const FORMAT_DAYS_PLAIN_HOURS_ONE_DECIMAL = 8;
+ const FORMAT_DAYS_PLAIN_HOURS_COLON_MINUTES = 9;
+ const FORMAT_DAYS_PLAIN_HOURS_SPACE_MINUTES = 10;
+
+ private $_task_id;
+ private $_userId;
+ public $details;
+
+ /**
+ * Class Constructor: Requires the user id and task id as all effort is in context of the task.
+ *
+ * @param $task_id
+ * @param $user_id
+ */
+ public function __construct($task_id,$user_id)
+ {
+ $this->_task_id = $task_id;
+ $this->_userId = $user_id;
+ }
+
+ /**
+ * Manually add effort to the effort table for this issue / user.
+ *
+ * @param $effort_to_add int amount of effort in hh:mm to add to effort table.
+ */
+ public function addEffort($effort_to_add, $proj)
+ {
+ global $db;
+
+ # note: third parameter seem useless, not used by EditStringToSeconds().., maybe drop it..
+ $effort = self::editStringToSeconds($effort_to_add, $proj->prefs['hours_per_manday'], $proj->prefs['estimated_effort_format']);
+ if ($effort === FALSE) {
+ Flyspray::show_error(L('invalideffort'));
+ return false;
+ }
+
+ # quickfix to avoid useless table entries.
+ if($effort==0){
+ Flyspray::show_error(L('zeroeffort'));
+ return false;
+ } else{
+ $db->query('INSERT INTO {effort}
+ (task_id, date_added, user_id,start_timestamp,end_timestamp,effort)
+ VALUES ( ?, ?, ?, ?,?,? )',
+ array($this->_task_id, time(), $this->_userId,time(),time(),$effort)
+ );
+ return true;
+ }
+ }
+
+ /**
+ * Starts tracking effort for the current user against the current issue.
+ *
+ * @return bool Returns Success or Failure of the action.
+ */
+ public function startTracking()
+ {
+ global $db;
+
+ //check if the user is already tracking time against this task.
+ $result = $db->query('SELECT * FROM {effort} WHERE task_id ='.$this->_task_id.' AND user_id='.$this->_userId.' AND end_timestamp IS NULL;');
+ if($db->countRows($result)>0)
+ {
+ return false;
+ }
+ else
+ {
+ $db->query('INSERT INTO {effort}
+ (task_id, date_added, user_id,start_timestamp)
+ VALUES ( ?, ?, ?, ? )',
+ array ($this->_task_id, time(), $this->_userId,time()));
+
+ return true;
+ }
+ }
+
+ /**
+ * Stops tracking the current tracking request and then updates the actual hours field on the table, this
+ * is useful as both stops constant calculation from start/end timestamps and provides a quick aggregation
+ * method as we only need to deal with one field.
+ */
+ public function stopTracking()
+ {
+ global $db;
+
+ $time = time();
+
+
+ $sql = $db->query('SELECT start_timestamp FROM {effort} WHERE user_id='.$this->_userId.' AND task_id='.$this->_task_id.' AND end_timestamp IS NULL;');
+ $result = $db->fetchRow($sql);
+ $start_time = $result[0];
+ $seconds = $time - $start_time;
+
+ // Round to full minutes upwards.
+ $effort = ($seconds % 60 == 0 ? $seconds : floor($seconds / 60) * 60 + 60);
+
+ $sql = $db->query("UPDATE {effort} SET end_timestamp = ".$time.",effort = ".$effort." WHERE user_id=".$this->_userId." AND task_id=".$this->_task_id." AND end_timestamp IS NULL;");
+ }
+
+ /**
+ * Removes any outstanding tracking requests for this task for this user.
+ */
+ public function cancelTracking()
+ {
+ global $db;
+
+ # 2016-07-04: also remove invalid finished 0 effort entries that were accidently possible up to Flyspray 1.0-rc
+ $db->query('DELETE FROM {effort}
+ WHERE user_id='.$this->_userId.'
+ AND task_id='.$this->_task_id.'
+ AND (
+ end_timestamp IS NULL
+ OR (start_timestamp=end_timestamp AND effort=0)
+ );'
+ );
+ }
+
+ public function populateDetails()
+ {
+ global $db;
+
+ $this->details = $db->query('SELECT * FROM {effort} WHERE task_id ='.$this->_task_id.';');
+ }
+
+ public static function secondsToString($seconds, $factor, $format) {
+ if ($seconds == 0) {
+ return '';
+ }
+
+ $factor = ($factor == 0 ? 86400 : $factor);
+
+ switch ($format) {
+ case self::FORMAT_HOURS_COLON_MINUTES:
+ $seconds = ($seconds % 60 == 0 ? $seconds : floor($seconds / 60) * 60 + 60);
+ $hours = floor($seconds / 3600);
+ $minutes = floor(($seconds - ($hours * 3600)) / 60);
+ return sprintf('%01u:%02u', $hours, $minutes);
+ break;
+ case self::FORMAT_HOURS_SPACE_MINUTES:
+ $seconds = ($seconds % 60 == 0 ? $seconds : floor($seconds / 60) * 60 + 60);
+ $hours = floor($seconds / 3600);
+ $minutes = floor(($seconds - ($hours * 3600)) / 60);
+ if ($hours == 0) {
+ return sprintf('%u %s', $minutes, L('minuteabbrev'));
+ } else {
+ return sprintf('%u %s %u %s', $hours, L('hourabbrev'), $minutes, L('minuteabbrev'));
+ }
+ break;
+ case self::FORMAT_HOURS_PLAIN:
+ $hours = ceil($seconds / 3600);
+ return sprintf('%01u %s', $hours, ($hours == 1 ? L('hoursingular') : L('hourplural')));
+ break;
+ case self::FORMAT_HOURS_ONE_DECIMAL:
+ $hours = round(ceil($seconds * 10 / 3600) / 10, 1);
+ return sprintf('%01.1f %s', $hours, ($hours == 1 ? L('hoursingular') : L('hourplural')));
+ break;
+ case self::FORMAT_MINUTES:
+ $minutes = ceil($seconds / 60);
+ return sprintf('%01u %s', $minutes, L('minuteabbrev'));
+ break;
+ case self::FORMAT_DAYS_PLAIN:
+ $days = ceil($seconds / $factor);
+ return sprintf('%01u %s', $days, ($days == 1 ? L('manday') : L('mandays')));
+ break;
+ case self::FORMAT_DAYS_ONE_DECIMAL:
+ $days = round(ceil($seconds * 10 / $factor) / 10, 1);
+ return sprintf('%01.1f %s', $days, ($days == 1 ? L('manday') : L('mandays')));
+ break;
+ case self::FORMAT_DAYS_PLAIN_HOURS_PLAIN:
+ $days = floor($seconds / $factor);
+ $hours = ceil(($seconds - ($days * $factor)) / 3600);
+ if ($days == 0) {
+ return sprintf('%1u %s', $hours, L('hourabbrev'));
+ } else {
+ return sprintf('%u %s %1u %s', $days, L('mandayabbrev'), $hours, L('hourabbrev'));
+ }
+ break;
+ case self::FORMAT_DAYS_PLAIN_HOURS_ONE_DECIMAL:
+ $days = floor($seconds / $factor);
+ $hours = round(ceil(($seconds - ($days * $factor)) * 10 / 3600) / 10, 1);
+ if ($days == 0) {
+ return sprintf('%01.1f %s', $hours, L('hourabbrev'));
+ } else {
+ return sprintf('%u %s %01.1f %s', $days, L('mandayabbrev'), $hours, L('hourabbrev'));
+ }
+ break;
+ case self::FORMAT_DAYS_PLAIN_HOURS_COLON_MINUTES:
+ $seconds = ($seconds % 60 == 0 ? $seconds : floor($seconds / 60) * 60 + 60);
+ $days = floor($seconds / $factor);
+ $hours = floor(($seconds - ($days * $factor)) / 3600);
+ $minutes = floor(($seconds - (($days * $factor) + ($hours * 3600))) / 60);
+ if ($days == 0) {
+ return sprintf('%01u:%02u', $hours, $minutes);
+ } else {
+ return sprintf('%u %s %01u:%02u', $days, L('mandayabbrev'), $hours, $minutes);
+ }
+ break;
+ case self::FORMAT_DAYS_PLAIN_HOURS_SPACE_MINUTES:
+ $seconds = ($seconds % 60 == 0 ? $seconds : floor($seconds / 60) * 60 + 60);
+ $days = floor($seconds / $factor);
+ $hours = floor(($seconds - ($days * $factor)) / 3600);
+ $minutes = floor(($seconds - (($days * $factor) + ($hours * 3600))) / 60);
+ if ($days == 0) {
+ return sprintf('%u %s %u %s', $hours, L('hourabbrev'), $minutes, L('minuteabbrev'));
+ } else {
+ return sprintf('%u %s %u %s %u %s', $days, L('mandayabbrev'), $hours, L('hourabbrev'), $minutes, L('minuteabbrev'));
+ }
+ break;
+ default:
+ $seconds = ($seconds % 60 == 0 ? $seconds : floor($seconds / 60) * 60 + 60);
+ $hours = floor($seconds / 3600);
+ $minutes = floor(($seconds - ($hours * 3600)) / 60);
+ return sprintf('%01u:%02u', $hours, $minutes);
+ }
+ }
+
+ public static function secondsToEditString($seconds, $factor, $format) {
+ $factor = ($factor == 0 ? 86400 : $factor);
+
+ // Adjust seconds to be evenly dividable by 60, so
+ // 3595 -> 3600, floor can be safely used for minutes in formats
+ // and the result will be 1:00 instead of 0:60 (if ceil would be used).
+
+ $seconds = ($seconds % 60 == 0 ? $seconds : floor($seconds / 60) * 60 + 60);
+
+ switch ($format) {
+ case self::FORMAT_HOURS_COLON_MINUTES:
+ case self::FORMAT_HOURS_SPACE_MINUTES:
+ case self::FORMAT_HOURS_PLAIN:
+ case self::FORMAT_HOURS_ONE_DECIMAL:
+ case self::FORMAT_MINUTES:
+ $hours = floor($seconds / 3600);
+ $minutes = floor(($seconds - ($hours * 3600)) / 60);
+ return sprintf('%01u:%02u', $hours, $minutes);
+ break;
+ case self::FORMAT_DAYS_PLAIN:
+ case self::FORMAT_DAYS_ONE_DECIMAL:
+ case self::FORMAT_DAYS_PLAIN_HOURS_PLAIN:
+ case self::FORMAT_DAYS_PLAIN_HOURS_ONE_DECIMAL:
+ case self::FORMAT_DAYS_PLAIN_HOURS_COLON_MINUTES:
+ case self::FORMAT_DAYS_PLAIN_HOURS_SPACE_MINUTES:
+ $days = floor($seconds / $factor);
+ $hours = floor(($seconds - ($days * $factor)) / 3600);
+ $minutes = floor(($seconds - ($hours * 3600)) / 60);
+ if ($days == 0) {
+ return sprintf('%01u:%02u', $hours, $minutes);
+ } else {
+ return sprintf('%u %02u:%02u', $days, $hours, $minutes);
+ }
+ break;
+ default:
+ $hours = floor($seconds / 3600);
+ $minutes = floor(($seconds - (($days * $factor) + ($hours * 3600))) / 60);
+ return sprintf('%01u:%02u', $hours, $minutes);
+ }
+ }
+
+ public static function editStringToSeconds($string, $factor, $format) {
+ if (!isset($string) || empty($string)) {
+ return 0;
+ }
+
+ $factor = ($factor == 0 ? 86400 : $factor);
+
+ $matches = array();
+ if (preg_match('/^((\d+)\s)?(\d+)(:(\d{2}))?$/', $string, $matches) !== 1) {
+ return FALSE;
+ }
+
+ if (!isset($matches[2])) {
+ $matches[2] = 0;
+ }
+
+ if (!isset($matches[5])) {
+ $matches[5] = 0;
+ } else {
+ if ($matches[5] > 59) {
+ return FALSE;
+ }
+ }
+
+ $effort = ($matches[2] * $factor) + ($matches[3] * 3600) + ($matches[5] * 60);
+ return $effort;
+ }
+}
diff --git a/includes/class.flyspray.php b/includes/class.flyspray.php
new file mode 100644
index 0000000..1dc2352
--- /dev/null
+++ b/includes/class.flyspray.php
@@ -0,0 +1,1471 @@
+<?php
+/**
+ * Flyspray
+ *
+ * Flyspray class
+ *
+ * This script contains all the functions we use often in
+ * Flyspray to do miscellaneous things.
+ *
+ * @license http://opensource.org/licenses/lgpl-license.php Lesser GNU Public License
+ * @package flyspray
+ * @author Tony Collins
+ * @author Florian Schmitz
+ * @author Cristian Rodriguez
+ */
+
+class Flyspray
+{
+
+ /**
+ * Current Flyspray version. Change this for each release. Don't forget!
+ * @access public
+ * @var string
+ * For github development use e.g. '1.0-beta dev' ; Flyspray::base_version() currently splits on the ' ' ...
+ * For making github release use e.g. '1.0-beta' here.
+ * For online version check www.flyspray.org/version.txt use e.g. '1.0-beta'
+ * For making releases on github use github's recommended versioning e.g. 'v1.0-beta' --> release files are then named v1.0-beta.zip and v1.0-beta.tar.gz and unzips to a flyspray-1.0-beta/ directory.
+ * Well, looks like a mess but hopefully consolidate this in future. Maybe use version_compare() everywhere in future instead of an own invented Flyspray::base_version()
+ */
+ public $version = '1.0-rc9';
+
+ /**
+ * Flyspray preferences
+ * @access public
+ * @var array
+ */
+ public $prefs = array();
+
+ /**
+ * Max. file size for file uploads. 0 = no uploads allowed
+ * @access public
+ * @var integer
+ */
+ public $max_file_size = 0;
+
+ /**
+ * List of projects the user is allowed to view
+ * @access public
+ * @var array
+ */
+ public $projects = array();
+
+ /**
+ * List of severities. Loaded in i18n.inc.php
+ * @access public
+ * @var array
+ */
+ public $severities = array();
+
+ /**
+ * List of priorities. Loaded in i18n.inc.php
+ * @access public
+ * @var array
+ */
+ public $priorities = array();
+
+ /**
+ * Constructor, starts session, loads settings
+ * @access private
+ * @return void
+ * @version 1.0
+ */
+ public function __construct()
+ {
+ global $db;
+
+ $this->startSession();
+
+ $res = $db->query('SELECT pref_name, pref_value FROM {prefs}');
+
+ while ($row = $db->fetchRow($res)) {
+ $this->prefs[$row['pref_name']] = $row['pref_value'];
+ }
+
+ $this->setDefaultTimezone();
+
+ $sizes = array();
+ foreach (array(ini_get('memory_limit'), ini_get('post_max_size'), ini_get('upload_max_filesize')) as $val) {
+ if($val === '-1'){
+ // unlimited value in php configuration
+ $val = PHP_INT_MAX;
+ }
+ if (!$val || $val < 0) {
+ continue;
+ }
+
+ $last = strtolower($val{strlen($val)-1});
+ $val = trim($val, 'gGmMkK');
+ switch ($last) {
+ // The 'G' modifier is available since PHP 5.1.0
+ case 'g':
+ $val *= 1024;
+ case 'm':
+ $val *= 1024;
+ case 'k':
+ $val *= 1024;
+ }
+
+ $sizes[] = $val;
+ }
+ clearstatcache();
+ $this->max_file_size = (
+ (bool) ini_get('file_uploads')
+ && is_file(BASEDIR.DIRECTORY_SEPARATOR.'attachments'.DIRECTORY_SEPARATOR.'index.html')
+ && is_writable(BASEDIR.DIRECTORY_SEPARATOR.'attachments')
+ ) ? round((min($sizes)/1024/1024), 1) : 0;
+ }
+
+ protected function setDefaultTimezone()
+ {
+ $default_timezone = isset($this->prefs['default_timezone']) && !empty($this->prefs['default_timezone']) ? $this->prefs['default_timezone'] : 'UTC';
+ // set the default time zone - this will be redefined as we go
+ define('DEFAULT_TIMEZONE',$default_timezone);
+ date_default_timezone_set(DEFAULT_TIMEZONE);
+ }
+
+ public static function base_version($version)
+ {
+ if (strpos($version, ' ') === false) {
+ return $version;
+ }
+ return substr($version, 0, strpos($version, ' '));
+ }
+
+ public static function get_config_path($basedir = BASEDIR)
+ {
+ $cfile = $basedir . '/flyspray.conf.php';
+ if (is_readable($hostconfig = sprintf('%s/%s.conf.php', $basedir, $_SERVER['SERVER_NAME']))) {
+ $cfile = $hostconfig;
+ }
+ return $cfile;
+ }
+
+ /**
+ * Redirects the browser to the page in $url
+ * This function is based on PEAR HTTP class
+ * @param string $url
+ * @param bool $exit
+ * @param bool $rfc2616
+ * @license BSD
+ * @access public static
+ * @return bool
+ * @version 1.0
+ */
+ public static function redirect($url, $exit = true, $rfc2616 = true)
+ {
+
+ @ob_clean();
+
+ if (isset($_SESSION) && count($_SESSION)) {
+ session_write_close();
+ }
+
+ if (headers_sent()) {
+ die('Headers are already sent, this should not have happened. Please inform Flyspray developers.');
+ }
+
+ $url = Flyspray::absoluteURI($url);
+
+ if($_SERVER['REQUEST_METHOD']=='POST' && version_compare(PHP_VERSION, '5.4.0')>=0 ) {
+ http_response_code(303);
+ }
+ header('Location: '. $url);
+
+ if ($rfc2616 && isset($_SERVER['REQUEST_METHOD']) &&
+ $_SERVER['REQUEST_METHOD'] != 'HEAD') {
+ $url = htmlspecialchars($url, ENT_QUOTES, 'utf-8');
+ printf('%s to: <a href="%s">%s</a>.', eL('Redirect'), $url, $url);
+ }
+ if ($exit) {
+ exit;
+ }
+
+ return true;
+ }
+
+ /**
+ * Absolute URI (This function is part of PEAR::HTTP licensed under the BSD) {{{
+ *
+ * This function returns the absolute URI for the partial URL passed.
+ * The current scheme (HTTP/HTTPS), host server, port, current script
+ * location are used if necessary to resolve any relative URLs.
+ *
+ * Offsets potentially created by PATH_INFO are taken care of to resolve
+ * relative URLs to the current script.
+ *
+ * You can choose a new protocol while resolving the URI. This is
+ * particularly useful when redirecting a web browser using relative URIs
+ * and to switch from HTTP to HTTPS, or vice-versa, at the same time.
+ *
+ * @author Philippe Jausions <Philippe.Jausions@11abacus.com>
+ * @static
+ * @access public
+ * @return string The absolute URI.
+ * @param string $url Absolute or relative URI the redirect should go to.
+ * @param string $protocol Protocol to use when redirecting URIs.
+ * @param integer $port A new port number.
+ */
+ public static function absoluteURI($url = null, $protocol = null, $port = null)
+ {
+ // filter CR/LF
+ $url = str_replace(array("\r", "\n"), ' ', $url);
+
+ // Mess around with already absolute URIs
+ if (preg_match('!^([a-z0-9]+)://!i', $url)) {
+ if (empty($protocol) && empty($port)) {
+ return $url;
+ }
+ if (!empty($protocol)) {
+ $url = $protocol .':'. end($array = explode(':', $url, 2));
+ }
+ if (!empty($port)) {
+ $url = preg_replace('!^(([a-z0-9]+)://[^/:]+)(:[\d]+)?!i',
+ '\1:'. $port, $url);
+ }
+ return $url;
+ }
+
+ $host = 'localhost';
+ if (!empty($_SERVER['HTTP_HOST'])) {
+ list($host) = explode(':', $_SERVER['HTTP_HOST']);
+
+ if (strpos($_SERVER['HTTP_HOST'], ':') !== false && !isset($port)) {
+ $port = explode(':', $_SERVER['HTTP_HOST']);
+ }
+ } elseif (!empty($_SERVER['SERVER_NAME'])) {
+ list($host) = explode(':', $_SERVER['SERVER_NAME']);
+ }
+
+ if (empty($protocol)) {
+ if (isset($_SERVER['HTTPS']) && !strcasecmp($_SERVER['HTTPS'], 'on')) {
+ $protocol = 'https';
+ } else {
+ $protocol = 'http';
+ }
+ if (!isset($port) || $port != intval($port)) {
+ $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80;
+ }
+ }
+
+ if ($protocol == 'http' && $port == 80) {
+ unset($port);
+ }
+ if ($protocol == 'https' && $port == 443) {
+ unset($port);
+ }
+
+ $server = $protocol .'://'. $host . (isset($port) ? ':'. $port : '');
+
+
+ if (!strlen($url) || $url{0} == '?' || $url{0} == '#') {
+ $uri = isset($_SERVER['REQUEST_URI']) ?
+ $_SERVER['REQUEST_URI'] : $_SERVER['PHP_SELF'];
+ if ($url && $url{0} == '?' && false !== ($q = strpos($uri, '?'))) {
+ $url = substr($uri, 0, $q) . $url;
+ } else {
+ $url = $uri . $url;
+ }
+ }
+
+ if ($url{0} == '/') {
+ return $server . $url;
+ }
+
+ // Check for PATH_INFO
+ if (isset($_SERVER['PATH_INFO']) && strlen($_SERVER['PATH_INFO']) &&
+ $_SERVER['PHP_SELF'] != $_SERVER['PATH_INFO']) {
+ $path = dirname(substr($_SERVER['PHP_SELF'], 0, -strlen($_SERVER['PATH_INFO'])));
+ } else {
+ $path = dirname($_SERVER['PHP_SELF']);
+ }
+
+ if (substr($path = strtr($path, '\\', '/'), -1) != '/') {
+ $path .= '/';
+ }
+
+ return $server . $path . $url;
+ }
+
+ /**
+ * Test to see if user resubmitted a form.
+ * Checks only newtask and addcomment actions.
+ * @return bool true if user has submitted the same action within less than 6 hours, false otherwise
+ * @access public static
+ * @version 1.0
+ */
+ public static function requestDuplicated()
+ {
+ // garbage collection -- clean entries older than 6 hrs
+ $now = isset($_SERVER['REQUEST_TIME']) ? $_SERVER['REQUEST_TIME'] : time();
+ if (!empty($_SESSION['requests_hash'])) {
+ foreach ($_SESSION['requests_hash'] as $key => $val) {
+ if ($val < $now-6*60*60) {
+ unset($_SESSION['requests_hash'][$key]);
+ }
+ }
+ }
+
+ if (count($_POST)) {
+
+ if (preg_match('/^newtask.newtask|details.addcomment$/', Post::val('action', '')))
+ {
+ $currentrequest = md5(serialize($_POST));
+ if (!empty($_SESSION['requests_hash'][$currentrequest])) {
+ return true;
+ }
+ $_SESSION['requests_hash'][$currentrequest] = time();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets all information about a task (and caches information if wanted)
+ * @param integer $task_id
+ * @param bool $cache_enabled
+ * @access public static
+ * @return mixed an array with all taskdetails or false on failure
+ * @version 1.0
+ */
+ public static function getTaskDetails($task_id, $cache_enabled = false)
+ {
+ global $db, $fs;
+
+ static $cache = array();
+
+ if (isset($cache[$task_id]) && $cache_enabled) {
+ return $cache[$task_id];
+ }
+
+ //for some reason, task_id is not here
+ // run away immediately..
+ if(!is_numeric($task_id)) {
+ return false;
+ }
+
+ $get_details = $db->query('SELECT t.*, p.*,
+ c.category_name, c.category_owner, c.lft, c.rgt, c.project_id as cproj,
+ o.os_name,
+ r.resolution_name,
+ tt.tasktype_name,
+ vr.version_name AS reported_version_name,
+ vd.version_name AS due_in_version_name,
+ uo.real_name AS opened_by_name,
+ ue.real_name AS last_edited_by_name,
+ uc.real_name AS closed_by_name,
+ lst.status_name AS status_name
+ FROM {tasks} t
+ LEFT JOIN {projects} p ON t.project_id = p.project_id
+ LEFT JOIN {list_category} c ON t.product_category = c.category_id
+ LEFT JOIN {list_os} o ON t.operating_system = o.os_id
+ LEFT JOIN {list_resolution} r ON t.resolution_reason = r.resolution_id
+ LEFT JOIN {list_tasktype} tt ON t.task_type = tt.tasktype_id
+ LEFT JOIN {list_version} vr ON t.product_version = vr.version_id
+ LEFT JOIN {list_version} vd ON t.closedby_version = vd.version_id
+ LEFT JOIN {list_status} lst ON t.item_status = lst.status_id
+ LEFT JOIN {users} uo ON t.opened_by = uo.user_id
+ LEFT JOIN {users} ue ON t.last_edited_by = ue.user_id
+ LEFT JOIN {users} uc ON t.closed_by = uc.user_id
+ WHERE t.task_id = ?', array($task_id));
+
+ if (!$db->countRows($get_details)) {
+ return false;
+ }
+
+ if ($get_details = $db->fetchRow($get_details)) {
+ $get_details += array('severity_name' => $get_details['task_severity']==0 ? '' : $fs->severities[$get_details['task_severity']]);
+ $get_details += array('priority_name' => $get_details['task_priority']==0 ? '' : $fs->priorities[$get_details['task_priority']]);
+ }
+
+ $get_details['tags'] = Flyspray::getTags($task_id);
+
+ $get_details['assigned_to'] = $get_details['assigned_to_name'] = array();
+ if ($assignees = Flyspray::getAssignees($task_id, true)) {
+ $get_details['assigned_to'] = $assignees[0];
+ $get_details['assigned_to_name'] = $assignees[1];
+ }
+
+ /**
+ * prevent RAM growing array like creating 100000 tasks with Backend::create_task() in a loop (Tests)
+ * Costs maybe some SQL queries if getTaskDetails is called first without $cache_enabled
+ * and later with $cache_enabled within same request
+ */
+ if($cache_enabled){
+ $cache[$task_id] = $get_details;
+ }
+ return $get_details;
+ }
+
+ /**
+ * Returns a list of all projects
+ * @param bool $active_only show only active projects
+ * @access public static
+ * @return array
+ * @version 1.0
+ */
+ // FIXME: $active_only would not work since the templates are accessing the returned array implying to be sortyed by project id, which is aparently wrong and error prone ! Same applies to the case when a project was deleted, causing a shift in the project id sequence, hence -> severe bug!
+ # comment by peterdd 20151012: reenabled param active_only with false as default. I do not see a problem within current Flyspray version. But consider using $fs->projects when possible, saves this extra sql request.
+ public static function listProjects($active_only = false)
+ {
+ global $db;
+ $query = 'SELECT project_id, project_title, project_is_active FROM {projects}';
+
+ if ($active_only) {
+ $query .= ' WHERE project_is_active = 1';
+ }
+
+ $query .= ' ORDER BY project_is_active DESC, project_id DESC'; # active first, latest projects first for option groups and new projects are probably the most used.
+
+ $sql = $db->query($query);
+ return $db->fetchAllArray($sql);
+ }
+
+ /**
+ * Returns a list of all themes
+ * @access public static
+ * @return array
+ * @version 1.0
+ */
+ public static function listThemes()
+ {
+ $themes = array();
+ $dirname = dirname(dirname(__FILE__));
+ if ($handle = opendir($dirname . '/themes/')) {
+ while (false !== ($file = readdir($handle))) {
+ if (substr($file,0,1) != '.' && is_dir("$dirname/themes/$file")
+ && (is_file("$dirname/themes/$file/theme.css") || is_dir("$dirname/themes/$file/templates"))
+ ) {
+ $themes[] = $file;
+ }
+ }
+ closedir($handle);
+ }
+
+ sort($themes);
+ # always put the full default Flyspray theme first, [0] works as fallback in class Tpl->setTheme()
+ array_unshift($themes, 'CleanFS');
+ $themes = array_unique($themes);
+ return $themes;
+ }
+
+ /**
+ * Returns a list of global groups or a project's groups
+ * @param integer $proj_id
+ * @access public static
+ * @return array
+ * @version 1.0
+ */
+ public static function listGroups($proj_id = 0)
+ {
+ global $db;
+ $res = $db->query('SELECT g.*, COUNT(uig.user_id) AS users
+ FROM {groups} g
+ LEFT JOIN {users_in_groups} uig ON uig.group_id=g.group_id
+ WHERE project_id = ?
+ GROUP BY g.group_id
+ ORDER BY g.group_id ASC', array($proj_id));
+ return $db->fetchAllArray($res);
+ }
+
+ /**
+ * Returns a list of a all users
+ * @access public static
+ * @param array $opts optional filter which fields (or group of fields) are needed, more may be added later (sorting, where ..)
+ * @return array
+ * @version 1.0
+ */
+ public static function listUsers($opts=array())
+ {
+ global $db;
+
+ if( empty($opts) || !isset($opts['stats']) ){
+
+ $res = $db->query('SELECT account_enabled, user_id, user_name, real_name,
+ email_address, jabber_id, oauth_provider, oauth_uid,
+ notify_type, notify_own, notify_online,
+ tasks_perpage, lang_code, time_zone, dateformat, dateformat_extended,
+ register_date, login_attempts, lock_until,
+ profile_image, hide_my_email, last_login
+ FROM {users}
+ ORDER BY account_enabled DESC, user_name ASC');
+
+ } else {
+ # Well, this is a big and slow query, but the current solution I found.
+ # If you know a more elegant for calculating user stats from the different tables with one query let us know!
+ $res = $db->query('
+SELECT
+MIN(u.account_enabled) AS account_enabled,
+MIN(u.user_id) AS user_id,
+MIN(u.user_name) AS user_name,
+MIN(u.real_name) AS real_name,
+MIN(u.email_address) AS email_address,
+MIN(u.jabber_id) AS jabber_id,
+MIN(u.oauth_provider) AS oauth_provider,
+MIN(u.oauth_uid) AS oauth_uid,
+MIN(u.notify_type) AS notify_type,
+MIN(u.notify_own) AS notify_own,
+MIN(u.notify_online) AS notify_online,
+MIN(u.tasks_perpage) AS tasks_perpage,
+MIN(u.lang_code) AS lang_code,
+MIN(u.time_zone) AS time_zone,
+MIN(u.dateformat) AS dateformat,
+MIN(u.dateformat_extended) AS dateformat_extended,
+MIN(u.register_date) AS register_date,
+MIN(u.login_attempts) AS login_attempts,
+MIN(u.lock_until) AS lock_until,
+MIN(u.profile_image) AS profile_image,
+MIN(u.hide_my_email) AS hide_my_email,
+MIN(u.last_login) AS last_login,
+SUM(countopen) AS countopen,
+SUM(countclose) AS countclose,
+SUM(countlastedit) AS countlastedit,
+SUM(comments) AS countcomments,
+SUM(assigned) AS countassign,
+SUM(watching) AS countwatching,
+SUM(votes) AS countvotes
+FROM
+( SELECT u.account_enabled, u.user_id, u.user_name, u.real_name,
+ u.email_address, u.jabber_id, u.oauth_provider, u.oauth_uid,
+ u.notify_type, u.notify_own, u.notify_online,
+ u.tasks_perpage, u.lang_code, u.time_zone, u.dateformat, u.dateformat_extended,
+ u.register_date, u.login_attempts, u.lock_until,
+ u.profile_image, u.hide_my_email, u.last_login,
+ COUNT(topen.opened_by) AS countopen, 0 AS countclose, 0 AS countlastedit, 0 AS comments, 0 AS assigned, 0 AS watching, 0 AS votes
+ FROM {users} u
+ LEFT JOIN {tasks} topen ON topen.opened_by=u.user_id
+ GROUP BY u.user_id
+UNION
+ SELECT u.account_enabled, u.user_id, u.user_name, u.real_name,
+ u.email_address, u.jabber_id, u.oauth_provider, u.oauth_uid,
+ u.notify_type, u.notify_own, u.notify_online,
+ u.tasks_perpage, u.lang_code, u.time_zone, u.dateformat, u.dateformat_extended,
+ u.register_date, u.login_attempts, u.lock_until,
+ u.profile_image, u.hide_my_email, u.last_login,
+ 0, COUNT(tclose.closed_by) AS countclose, 0, 0, 0, 0, 0
+ FROM {users} u
+ LEFT JOIN {tasks} tclose ON tclose.closed_by=u.user_id
+ GROUP BY u.user_id
+UNION
+ SELECT u.account_enabled, u.user_id, u.user_name, u.real_name,
+ u.email_address, u.jabber_id, u.oauth_provider, u.oauth_uid,
+ u.notify_type, u.notify_own, u.notify_online,
+ u.tasks_perpage, u.lang_code, u.time_zone, u.dateformat, u.dateformat_extended,
+ u.register_date, u.login_attempts, u.lock_until,
+ u.profile_image, u.hide_my_email, u.last_login,
+ 0, 0, COUNT(tlast.last_edited_by) AS countlastedit, 0, 0, 0, 0
+ FROM {users} u
+ LEFT JOIN {tasks} tlast ON tlast.last_edited_by=u.user_id
+ GROUP BY u.user_id
+UNION
+ SELECT u.account_enabled, u.user_id, u.user_name, u.real_name,
+ u.email_address, u.jabber_id, u.oauth_provider, u.oauth_uid,
+ u.notify_type, u.notify_own, u.notify_online,
+ u.tasks_perpage, u.lang_code, u.time_zone, u.dateformat, u.dateformat_extended,
+ u.register_date, u.login_attempts, u.lock_until,
+ u.profile_image, u.hide_my_email, u.last_login,
+ 0, 0, 0, COUNT(c.user_id) AS comments, 0, 0, 0
+ FROM {users} u
+ LEFT JOIN {comments} c ON c.user_id=u.user_id
+ GROUP BY u.user_id
+ UNION
+ SELECT u.account_enabled, u.user_id, u.user_name, u.real_name,
+ u.email_address, u.jabber_id, u.oauth_provider, u.oauth_uid,
+ u.notify_type, u.notify_own, u.notify_online,
+ u.tasks_perpage, u.lang_code, u.time_zone, u.dateformat, u.dateformat_extended,
+ u.register_date, u.login_attempts, u.lock_until,
+ u.profile_image, u.hide_my_email, u.last_login,
+ 0, 0, 0, 0, COUNT(a.user_id) AS assigned, 0, 0
+ FROM {users} u
+ LEFT JOIN {assigned} a ON a.user_id=u.user_id
+ GROUP BY u.user_id
+UNION
+ SELECT u.account_enabled, u.user_id, u.user_name, u.real_name,
+ u.email_address, u.jabber_id, u.oauth_provider, u.oauth_uid,
+ u.notify_type, u.notify_own, u.notify_online,
+ u.tasks_perpage, u.lang_code, u.time_zone, u.dateformat, u.dateformat_extended,
+ u.register_date, u.login_attempts, u.lock_until,
+ u.profile_image, u.hide_my_email, u.last_login,
+ 0, 0, 0, 0, 0, COUNT(n.user_id) AS watching, 0
+ FROM {users} u
+ LEFT JOIN {notifications} n ON n.user_id=u.user_id
+ GROUP BY u.user_id
+UNION
+ SELECT u.account_enabled, u.user_id, u.user_name, u.real_name,
+ u.email_address, u.jabber_id, u.oauth_provider, u.oauth_uid,
+ u.notify_type, u.notify_own, u.notify_online,
+ u.tasks_perpage, u.lang_code, u.time_zone, u.dateformat, u.dateformat_extended,
+ u.register_date, u.login_attempts, u.lock_until,
+ u.profile_image, u.hide_my_email, u.last_login,
+ 0, 0, 0, 0, 0, 0, COUNT(v.user_id) AS votes
+ FROM {users} u
+ LEFT JOIN {votes} v ON v.user_id=u.user_id
+ GROUP BY u.user_id
+) u
+GROUP BY u.user_id
+ORDER BY MIN(u.account_enabled) DESC, MIN(u.user_name) ASC');
+ }
+
+ return $db->fetchAllArray($res);
+ }
+
+ /**
+ * Returns a list of installed languages
+ * @access public static
+ * @return array
+ * @version 1.0
+ */
+ public static function listLangs()
+ {
+ return str_replace('.php', '', array_map('basename', glob_compat(BASEDIR ."/lang/[a-zA-Z]*.php")));
+
+ }
+
+ /**
+ * Saves an event to the {history} db table
+ * @param integer $task_id
+ * @param integer $type
+ * @param string $newvalue
+ * @param string $oldvalue
+ * @param string $field
+ * @param integer $time for synchronisation with other functions
+ * @access public static
+ * @return void
+ * @version 1.0
+ */
+ public static function logEvent($task_id, $type, $newvalue = '', $oldvalue = '', $field = '', $time = null)
+ {
+ global $db, $user;
+
+ // This function creates entries in the history table. These are the event types:
+ // 0: Fields changed in a task
+ // 1: New task created
+ // 2: Task closed
+ // 3: Task edited (for backwards compatibility with events prior to the history system)
+ // 4: Comment added
+ // 5: Comment edited
+ // 6: Comment deleted
+ // 7: Attachment added
+ // 8: Attachment deleted
+ // 9: User added to notification list
+ // 10: User removed from notification list
+ // 11: Related task added to this task
+ // 12: Related task removed from this task
+ // 13: Task re-opened
+ // 14: Task assigned to user / re-assigned to different user / Unassigned
+ // 15: This task was added to another task's related list
+ // 16: This task was removed from another task's related list
+ // 17: Reminder added
+ // 18: Reminder deleted
+ // 19: User took ownership
+ // 20: Closure request made
+ // 21: Re-opening request made
+ // 22: Adding a new dependency
+ // 23: This task added as a dependency of another task
+ // 24: Removing a dependency
+ // 25: This task removed from another task's dependency list
+ // 26: Task was made private
+ // 27: Task was made public
+ // 28: PM request denied
+ // 29: User added to the list of assignees
+ // 30: New user registration
+ // 31: User deletion
+ // 32: Add new subtask
+ // 33: Remove Subtask
+ // 34: Add new parent
+ // 35: Remove parent
+
+ $query_params = array(intval($task_id), intval($user->id),
+ ((!is_numeric($time)) ? time() : $time),
+ $type, $field, $oldvalue, $newvalue);
+
+ if($db->query('INSERT INTO {history} (task_id, user_id, event_date, event_type, field_changed,
+ old_value, new_value) VALUES (?, ?, ?, ?, ?, ?, ?)', $query_params)) {
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Adds an admin or project manager request to the database
+ * @param integer $type 1: Task close, 2: Task re-open, 3: Pending user registration
+ * @param integer $project_id
+ * @param integer $task_id
+ * @param integer $submitter
+ * @param string $reason
+ * @access public static
+ * @return void
+ * @version 1.0
+ */
+ public static function adminRequest($type, $project_id, $task_id, $submitter, $reason)
+ {
+ global $db;
+ $db->query('INSERT INTO {admin_requests} (project_id, task_id, submitted_by, request_type, reason_given, time_submitted, deny_reason)
+ VALUES (?, ?, ?, ?, ?, ?, ?)',
+ array($project_id, $task_id, $submitter, $type, $reason, time(), ''));
+ }
+
+ /**
+ * Checks whether or not there is an admin request for a task
+ * @param integer $type 1: Task close, 2: Task re-open, 3: Pending user registration
+ * @param integer $task_id
+ * @access public static
+ * @return bool
+ * @version 1.0
+ */
+ public static function adminRequestCheck($type, $task_id)
+ {
+ global $db;
+
+ $check = $db->query("SELECT *
+ FROM {admin_requests}
+ WHERE request_type = ? AND task_id = ? AND resolved_by = 0",
+ array($type, $task_id));
+ return (bool)($db->countRows($check));
+ }
+
+ /**
+ * Gets all user details of a user
+ * @param integer $user_id
+ * @access public static
+ * @return array
+ * @version 1.0
+ */
+ public static function getUserDetails($user_id)
+ {
+ global $db;
+
+ // Get current user details. We need this to see if their account is enabled or disabled
+ $result = $db->query('SELECT * FROM {users} WHERE user_id = ?', array(intval($user_id)));
+ return $db->fetchRow($result);
+ }
+
+ /**
+ * Gets all information about a group
+ * @param integer $group_id
+ * @access public static
+ * @return array
+ * @version 1.0
+ */
+ public static function getGroupDetails($group_id)
+ {
+ global $db;
+ $sql = $db->query('SELECT * FROM {groups} WHERE group_id = ?', array($group_id));
+ return $db->fetchRow($sql);
+ }
+
+ /**
+ * Crypt a password with the method set in the configfile
+ * @param string $password
+ * @access public static
+ * @return string
+ * @version 1.0
+ */
+ public static function cryptPassword($password)
+ {
+ global $conf;
+
+ # during install e.g. not set
+ if(isset($conf['general']['passwdcrypt'])){
+ $pwcrypt = strtolower($conf['general']['passwdcrypt']);
+ }else{
+ $pwcrypt='';
+ }
+
+ # sha1, md5, sha512 are unsalted, hashing methods, not suited for storing passwords anymore.
+ # Use password_hash(), that adds random salt, customizable rounds and customizable hashing algorithms.
+ if ($pwcrypt == 'sha1') {
+ return sha1($password);
+ } elseif ($pwcrypt == 'md5') {
+ return md5($password);
+ } elseif ($pwcrypt == 'sha512') {
+ return hash('sha512', $password);
+ } elseif ($pwcrypt =='argon2i' && version_compare(PHP_VERSION,'7.2.0')>=0){
+ # php7.2+
+ return password_hash($password, PASSWORD_ARGON2I);
+ } else {
+ $bcryptoptions=array('cost'=>14);
+ return password_hash($password, PASSWORD_BCRYPT, $bcryptoptions);
+ }
+ }
+
+ /**
+ * Check if a user provided the right credentials
+ * @param string $username
+ * @param string $password
+ * @param string $method '', 'oauth', 'ldap', 'native'
+ * @access public static
+ * @return integer user_id on success, 0 if account or user is disabled, -1 if password is wrong
+ * @version 1.0
+ */
+ public static function checkLogin($username, $password, $method = 'native')
+ {
+ global $db;
+
+ $email_address = $username; //handle multiple email addresses
+ $temp = $db->query("SELECT id FROM {user_emails} WHERE email_address = ?",$email_address);
+ $user_id = $db->fetchRow($temp);
+ $user_id = $user_id["id"];
+
+ $result = $db->query("SELECT uig.*, g.group_open, u.account_enabled, u.user_pass,
+ lock_until, login_attempts
+ FROM {users_in_groups} uig
+ LEFT JOIN {groups} g ON uig.group_id = g.group_id
+ LEFT JOIN {users} u ON uig.user_id = u.user_id
+ WHERE u.user_id = ? OR u.user_name = ? AND g.project_id = ?
+ ORDER BY g.group_id ASC", array($user_id, $username, 0));
+
+ $auth_details = $db->fetchRow($result);
+
+ if($auth_details === false) {
+ return -2;
+ }
+ if(!$result || !count($auth_details)) {
+ return 0;
+ }
+
+ if ($auth_details['lock_until'] > 0 && $auth_details['lock_until'] < time()) {
+ $db->query('UPDATE {users} SET lock_until = 0, account_enabled = 1, login_attempts = 0
+ WHERE user_id = ?', array($auth_details['user_id']));
+ $auth_details['account_enabled'] = 1;
+ $_SESSION['was_locked'] = true;
+ }
+
+ // skip password check if the user is using oauth
+ if($method == 'oauth'){
+ $pwok = true;
+ } elseif( $method == 'ldap'){
+ $pwok = Flyspray::checkForLDAPUser($username, $password);
+ } else{
+ // encrypt the password with the method used in the db
+ if(substr($auth_details['user_pass'],0,1)!='$' && (
+ strlen($auth_details['user_pass'])==32
+ || strlen($auth_details['user_pass'])==40
+ || strlen($auth_details['user_pass'])==128
+ )){
+ # detecting (old) password stored with old unsalted hashing methods: md5,sha1,sha512
+ switch(strlen($auth_details['user_pass'])){
+ case 32:
+ $pwhash = md5($password);
+ break;
+ case 40:
+ $pwhash = sha1($password);
+ break;
+ case 128:
+ $pwhash = hash('sha512', $password);
+ break;
+ }
+ $pwok = hash_equals($auth_details['user_pass'], $pwhash);
+ }else{
+ #$pwhash = crypt($password, $auth_details['user_pass']); // user_pass contains algorithm, rounds, salt
+ $pwok = password_verify($password, $auth_details['user_pass']);
+ }
+ }
+
+ // Admin users cannot be disabled
+ if ($auth_details['group_id'] == 1 /* admin */ && $pwok) {
+ return $auth_details['user_id'];
+ }
+ if ($pwok && $auth_details['account_enabled'] == '1' && $auth_details['group_open'] == '1'){
+ return $auth_details['user_id'];
+ }
+
+ return ($auth_details['account_enabled'] && $auth_details['group_open']) ? 0 : -1;
+ }
+
+ static public function checkForOauthUser($uid, $provider)
+ {
+ global $db;
+
+ if(empty($uid) || empty($provider)) {
+ return false;
+ }
+
+ $sql = $db->query("SELECT id FROM {user_emails} WHERE oauth_uid = ? AND oauth_provider = ?",array($uid, $provider));
+
+ if ($db->fetchOne($sql)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * 20150320 just added from provided patch, untested!
+ */
+ public static function checkForLDAPUser($username, $password)
+ {
+ # TODO: add to admin settings area, maybe let user set the config at final installation step
+ $ldap_host = 'ldaphost';
+ $ldap_port = '389';
+ $ldap_version = '3';
+ $base_dn = 'OU=SBSUsers,OU=Users,OU=MyBusiness,DC=MyDomain,DC=local';
+ $ldap_search_user = 'ldapuser@mydomain.local';
+ $ldap_search_pass = "ldapuserpass";
+ $filter = "SAMAccountName=%USERNAME%"; // this is for AD - may be different with other setups
+ $username = $username;
+
+ if (strlen($password) == 0){ // LDAP will succeed binding with no password on AD (defaults to anon bind)
+ return false;
+ }
+
+ $rs = ldap_connect($ldap_host, $ldap_port);
+ @ldap_set_option($rs, LDAP_OPT_PROTOCOL_VERSION, $ldap_version);
+ @ldap_set_option($rs, LDAP_OPT_REFERRALS, 0);
+ $ldap_bind_dn = empty($ldap_search_user) ? NULL : $ldap_search_user;
+ $ldap_bind_pw = empty($ldap_search_pass) ? NULL : $ldap_search_pass;
+ if (!$bindok = @ldap_bind($rs, $ldap_bind_dn, $ldap_search_pass)){
+ // Uncomment for LDAP debugging
+ $error_msg = ldap_error($rs);
+ die("Couldn't bind using ".$ldap_bind_dn."@".$ldap_host.":".$ldap_port." Because:".$error_msg);
+ return false;
+ } else{
+ $filter_r = str_replace("%USERNAME%", $username, $filter);
+ $result = @ldap_search($rs, $base_dn, $filter_r);
+ if (!$result){ // ldap search returned nothing or error
+ return false;
+ }
+ $result_user = ldap_get_entries($rs, $result);
+ if ($result_user["count"] == 0){ // No users match the filter
+ return false;
+ }
+ $first_user = $result_user[0];
+ $ldap_user_dn = $first_user["dn"];
+ // Bind with the dn of the user that matched our filter (only one user should match sAMAccountName or uid etc..)
+ if (!$bind_user = @ldap_bind($rs, $ldap_user_dn, $password)){
+ $error_msg = ldap_error($rs);
+ die("Couldn't bind using ".$ldap_user_dn."@".$ldap_host.":".$ldap_port." Because:".$error_msg);
+ return false;
+ } else{
+ return true;
+ }
+ }
+ }
+
+ /**
+ * Sets a cookie, automatically setting the URL
+ * Now same params as PHP's builtin setcookie()
+ * @param string $name
+ * @param string $val
+ * @param integer $time
+ * @param string $path
+ * @param string $domain
+ * @param bool $secure
+ * @param bool $httponly
+ * @access public static
+ * @return bool
+ * @version 1.1
+ */
+ public static function setCookie($name, $val, $time = null, $path=null, $domain=null, $secure=false, $httponly=false)
+ {
+ global $conf;
+
+ if (null===$path){
+ $url = parse_url($GLOBALS['baseurl']);
+ }else{
+ $url['path']=$path;
+ }
+
+ if (!is_int($time)) {
+ $time = time()+60*60*24*30;
+ }
+ if(null===$domain){
+ $domain='';
+ }
+ if(null===$secure){
+ $secure = isset($conf['general']['securecookies']) ? $conf['general']['securecookies'] : false;
+ }
+ if((strlen($name) + strlen($val)) > 4096) {
+ //violation of the protocol
+ trigger_error("Flyspray sent a too big cookie, browsers will not handle it");
+ return false;
+ }
+
+ return setcookie($name, $val, $time, $url['path'],$domain,$secure,$httponly);
+ }
+
+ /**
+ * Starts the session
+ * @access public static
+ * @return void
+ * @version 1.0
+ * @notes smile intented
+ */
+ public static function startSession()
+ {
+ global $conf;
+ if (defined('IN_FEED') || php_sapi_name() === 'cli') {
+ return;
+ }
+
+ $url = parse_url($GLOBALS['baseurl']);
+ session_name('flyspray');
+ session_set_cookie_params(0,$url['path'],'', (isset($conf['general']['securecookies'])? $conf['general']['securecookies']:false), TRUE);
+ session_start();
+ if(!isset($_SESSION['csrftoken'])){
+ $_SESSION['csrftoken']=rand(); # lets start with one anti csrf token secret for the session and see if it's simplicity is good enough (I hope together with enforced Content Security Policies)
+ }
+ }
+
+ /**
+ * Compares two tasks and returns an array of differences
+ * @param array $old
+ * @param array $new
+ * @access public static
+ * @return array array('field', 'old', 'new')
+ * @version 1.0
+ */
+ public static function compare_tasks($old, $new)
+ {
+ $comp = array('priority_name', 'severity_name', 'status_name', 'assigned_to_name', 'due_in_version_name',
+ 'reported_version_name', 'tasktype_name', 'os_name', 'category_name',
+ 'due_date', 'percent_complete', 'item_summary', 'due_in_version_name',
+ 'detailed_desc', 'project_title', 'mark_private');
+
+ $changes = array();
+ foreach ($old as $key => $value)
+ {
+ if (!in_array($key, $comp) || ($key === 'due_date' && intval($old[$key]) === intval($new[$key]))) {
+ continue;
+ }
+
+ if($old[$key] != $new[$key]) {
+ switch ($key)
+ {
+ case 'due_date':
+ $new[$key] = formatDate($new[$key]);
+ $value = formatDate($value);
+ break;
+
+ case 'percent_complete':
+ $new[$key] .= '%';
+ $value .= '%';
+ break;
+
+ case 'mark_private':
+ $new[$key] = $new[$key] ? L('private') : L('public');
+ $value = $value ? L('private') : L('public');
+ break;
+ }
+ $changes[] = array($key, $value, $new[$key]);
+ }
+ }
+
+ return $changes;
+ }
+
+ /**
+ * Get all tags of a task
+ * @access public static
+ * @return array
+ * @version 1.0
+ */
+ public static function getTags($task_id)
+ {
+ global $db;
+ # pre FS1.0beta
+ #$sql = $db->query('SELECT * FROM {tags} WHERE task_id = ?', array($task_id));
+ # since FS1.0beta
+ $sql = $db->query('SELECT tg.tag_id, tg.tag_name AS tag, tg.class FROM {task_tag} tt
+ JOIN {list_tag} tg ON tg.tag_id=tt.tag_id
+ WHERE task_id = ?
+ ORDER BY list_position', array($task_id));
+ return $db->fetchAllArray($sql);
+ }
+
+ /**
+ * load all task tags into array
+ *
+ * Compared to listTags() of class project, this loads all tags in Flyspray database into a global array.
+ * Ideally called only once per http request, then using the array index for getting tag info.
+ *
+ * Used mainly for tasklist view to simplify get_task_list() sql query.
+ *
+ * @return array
+ */
+ public static function getAllTags()
+ {
+ global $db;
+ $at=array();
+ $res = $db->query('SELECT tag_id, project_id, list_position, tag_name, class, show_in_list FROM {list_tag}');
+ while ($t = $db->fetchRow($res)){
+ $at[$t['tag_id']]=array(
+ 'project_id'=>$t['project_id'],
+ 'list_position'=>$t['list_position'],
+ 'tag_name'=>$t['tag_name'],
+ 'class'=>$t['class'],
+ 'show_in_list'=>$t['show_in_list']
+ );
+ }
+ return $at;
+ }
+
+ /**
+ * Get a list of assignees for a task
+ * @param integer $task_id
+ * @param bool $name whether or not names of the assignees should be returned as well
+ * @access public static
+ * @return array
+ * @version 1.0
+ */
+ public static function getAssignees($task_id, $name = false)
+ {
+ global $db;
+
+ $sql = $db->query('SELECT u.real_name, u.user_id
+ FROM {users} u, {assigned} a
+ WHERE task_id = ? AND u.user_id = a.user_id',
+ array($task_id));
+
+ $assignees = array();
+ while ($row = $db->fetchRow($sql)) {
+ if ($name) {
+ $assignees[0][] = $row['user_id'];
+ $assignees[1][] = $row['real_name'];
+ } else {
+ $assignees[] = $row['user_id'];
+ }
+ }
+
+ return $assignees;
+ }
+
+ /**
+ * Explode string to the array of integers
+ * @param string $separator
+ * @param string $string
+ * @access public static
+ * @return array
+ * @version 1.0
+ */
+ public static function int_explode($separator, $string)
+ {
+ $ret = array();
+ foreach (explode($separator, $string) as $v)
+ {
+ if (ctype_digit($v)) {// $v is always string, this func returns false if $v == ''
+ $ret[] = intval($v); // convert to int
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Checks if a function is disabled
+ * @param string $func_name
+ * @access public static
+ * @return bool
+ * @version 1.0
+ */
+ public static function function_disabled($func_name)
+ {
+ $disabled_functions = explode(',', ini_get('disable_functions'));
+ return in_array($func_name, $disabled_functions);
+ }
+
+ /**
+ * Returns the key number of an array which contains an array like array($key => $value)
+ * For use with SQL result arrays
+ * returns 0 for first index, so take care if you want check when useing to check if a value exists, use ===
+ *
+ * @param string $key
+ * @param string $value
+ * @param array $array
+ * @access public static
+ * @return integer
+ * @version 1.0
+ */
+ public static function array_find($key, $value, $array)
+ {
+ foreach ($array as $num => $part) {
+ if (isset($part[$key]) && $part[$key] == $value) {
+ return $num;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Shows an error message
+ * @param string $error_message if it is an integer, an error message from the language file will be loaded
+ * @param bool $die enable/disable redirection (if outside the database modification script)
+ * @param string $advanced_info append a string to the error message
+ * @param string $url alternate redirection
+ * @access public static
+ * @return void
+ * @version 1.0
+ * @notes if a success and error happens on the same page, a mixed error message will be shown
+ * @todo is the if ($die) meant to be inside the else clause?
+ */
+ public static function show_error($error_message, $die = true, $advanced_info = null, $url = null)
+ {
+ global $modes, $baseurl;
+
+ if (!is_int($error_message)) {
+ // in modify.inc.php
+ $_SESSION['ERROR'] = $error_message;
+ } else {
+ $_SESSION['ERROR'] = L('error#') . $error_message . ': ' . L('error' . $error_message);
+ if (!is_null($advanced_info)) {
+ $_SESSION['ERROR'] .= ' ' . $advanced_info;
+ }
+ if ($die) {
+ Flyspray::redirect( (is_null($url) ? $baseurl : $url) );
+ }
+ }
+ }
+
+ /**
+ * Returns the user ID if valid, 0 otherwise
+ * @param int $id
+ * @access public static
+ * @return integer 0 if the user does not exist
+ * @version 1.0
+ */
+ public static function validUserId($id)
+ {
+ global $db;
+
+ $sql = $db->query('SELECT user_id FROM {users} WHERE user_id = ?', array(intval($id)));
+
+ return intval($db->fetchOne($sql));
+ }
+
+ /**
+ * Returns the ID of a user with $name
+ * @param string $name
+ * @access public static
+ * @return integer 0 if the user does not exist
+ * @version 1.0
+ */
+ public static function usernameToId($name)
+ {
+ global $db;
+
+ if(!is_string($name)){
+ return 0;
+ }
+
+ $sql = $db->query('SELECT user_id FROM {users} WHERE user_name = ?', array($name));
+
+ return intval($db->fetchOne($sql));
+ }
+
+ /**
+ * check_email
+ * checks if an email is valid
+ * @param string $email
+ * @access public
+ * @return bool
+ */
+ public static function check_email($email)
+ {
+ return is_string($email) && filter_var($email, FILTER_VALIDATE_EMAIL);
+ }
+
+ /**
+ * get_tmp_dir
+ * Based on PEAR System::tmpdir() by Tomas V.V.Cox.
+ * @access public
+ * @return void
+ */
+ public static function get_tmp_dir()
+ {
+ $return = '';
+
+ if (function_exists('sys_get_temp_dir')) {
+ $return = sys_get_temp_dir();
+ } elseif (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+ if ($var = isset($_ENV['TEMP']) ? $_ENV['TEMP'] : getenv('TEMP')) {
+ $return = $var;
+ } else
+ if ($var = isset($_ENV['TMP']) ? $_ENV['TMP'] : getenv('TMP')) {
+ $return = $var;
+ } else
+ if ($var = isset($_ENV['windir']) ? $_ENV['windir'] : getenv('windir')) {
+ $return = $var;
+ } else {
+ $return = getenv('SystemRoot') . '\temp';
+ }
+
+ } elseif ($var = isset($_ENV['TMPDIR']) ? $_ENV['TMPDIR'] : getenv('TMPDIR')) {
+ $return = $var;
+ } else {
+ $return = '/tmp';
+ }
+ // Now, the final check
+ if (@is_dir($return) && is_writable($return)) {
+ return rtrim($return, DIRECTORY_SEPARATOR);
+ // we have a problem at this stage.
+ } elseif(is_writable(ini_get('upload_tmp_dir'))) {
+ $return = ini_get('upload_tmp_dir');
+ } elseif(is_writable(ini_get('session.save_path'))) {
+ $return = ini_get('session.save_path');
+ }
+ return rtrim($return, DIRECTORY_SEPARATOR);
+ }
+
+ /**
+ * check_mime_type
+ *
+ * @param string $fname path to filename
+ * @access public
+ * @return string the mime type of the offended file.
+ * @notes DO NOT use this function for any security related
+ * task (i.e limiting file uploads by type)
+ * it wasn't designed for that purpose but to UI related tasks.
+ */
+ public static function check_mime_type($fname) {
+
+ $type = '';
+
+ if (extension_loaded('fileinfo') && class_exists('finfo')) {
+
+ $info = new finfo(FILEINFO_MIME);
+ $type = $info->file($fname);
+
+ } elseif(function_exists('mime_content_type')) {
+
+ $type = @mime_content_type($fname);
+ // I hope we don't have to...
+ } elseif(!FlySpray::function_disabled('exec') && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN'
+ && php_uname('s') !== 'SunOS') {
+
+ $type = @exec(sprintf('file -bi %s', escapeshellarg($fname)));
+
+ }
+ // if wasn't possible to determine , return empty string so
+ // we can use the browser reported mime-type (probably fake)
+ return trim($type);
+ }
+
+ /**
+ * Works like strtotime, but it considers the user's timezone
+ * @access public
+ * @param string $time
+ * @return integer
+ */
+ public static function strtotime($time)
+ {
+ global $user;
+
+ $time = strtotime($time);
+
+ if (!$user->isAnon()) {
+ $st = date('Z')/3600; // server GMT timezone
+ // Example: User is GMT+3, Server GMT-2.
+ // User enters 7:00. For the server it must be converted to 2:00 (done below)
+ $time += ($st - $user->infos['time_zone']) * 60 * 60;
+ // later it adds 5 hours to 2:00 for the user when the date is displayed.
+ }
+ //strtotime() may return false, making this method to return bool instead of int.
+ return $time ? $time : 0;
+ }
+
+ /**
+ * Writes content to a file using a lock.
+ * @access public
+ * @param string $filename location to write to
+ * @param string $content data to write
+ */
+ public static function write_lock($filename, $content)
+ {
+ if ($f = fopen($filename, 'wb')) {
+ if(flock($f, LOCK_EX)) {
+ fwrite($f, $content);
+ flock($f, LOCK_UN);
+ }
+ fclose($f);
+ }
+ }
+
+ /**
+ * file_get_contents replacement for remote files
+ * @access public
+ * @param string $url
+ * @param bool $get_contents whether or not to return file contents, use GET_CONTENTS for true
+ * @param integer $port
+ * @param string $connect manually choose server for connection
+ * @return string an empty string is not necessarily a failure
+ */
+ public static function remote_request($url, $get_contents = false, $port = 80, $connect = '', $host = null)
+ {
+ $url = parse_url($url);
+ if (!$connect) {
+ $connect = $url['host'];
+ }
+
+ if ($host) {
+ $url['host'] = $host;
+ }
+
+ $data = '';
+
+ if ($conn = @fsockopen($connect, $port, $errno, $errstr, 10)) {
+ $out = "GET {$url['path']} HTTP/1.0\r\n";
+ $out .= "Host: {$url['host']}\r\n";
+ $out .= "Connection: Close\r\n\r\n";
+
+ stream_set_timeout($conn, 5);
+ fwrite($conn, $out);
+
+ if ($get_contents) {
+ while (!feof($conn)) {
+ $data .= fgets($conn, 128);
+ }
+
+ $pos = strpos($data, "\r\n\r\n");
+
+ if ($pos !== false) {
+ //strip the http headers.
+ $data = substr($data, $pos + 2 * strlen("\r\n"));
+ }
+ }
+ fclose($conn);
+ }
+
+ return $data;
+ }
+
+ /**
+ * Returns an array containing all notification options the user is
+ * allowed to use.
+ * @access public
+ * @return array
+ */
+ public function getNotificationOptions($noneAllowed = true)
+ {
+ switch ($this->prefs['user_notify'])
+ {
+ case 0:
+ return array(0 => L('none'));
+ case 2:
+ return array(NOTIFY_EMAIL => L('email'));
+ case 3:
+ return array(NOTIFY_JABBER => L('jabber'));
+
+ }
+
+ $return = array(0 => L('none'),
+ NOTIFY_EMAIL => L('email'),
+ NOTIFY_JABBER => L('jabber'),
+ NOTIFY_BOTH => L('both'));
+ if (!$noneAllowed) {
+ unset($return[0]);
+ }
+
+ return $return;
+ }
+
+ public static function weedOutTasks($user, $tasks) {
+ $allowedtasks = array();
+ foreach ($tasks as $task) {
+ if ($user->can_view_task($task)) {
+ $allowedtasks[] = $task;
+ }
+ }
+ return $allowedtasks;
+ }
+}
diff --git a/includes/class.gpc.php b/includes/class.gpc.php
new file mode 100644
index 0000000..88235ef
--- /dev/null
+++ b/includes/class.gpc.php
@@ -0,0 +1,257 @@
+<?php
+// {{{ class Req
+/**
+ * Flyspray
+ *
+ * GPC classes
+ *
+ * This script contains classes for $_GET, $_REQUEST, $_POST and $_COOKIE
+ * to safely retrieve values. Example: Get::val('foo', 'bar') to get $_GET['foo'] or 'bar' if
+ * the key does not exist.
+ *
+ * @license http://opensource.org/licenses/lgpl-license.php Lesser GNU Public License
+ * @package flyspray
+ * @author Pierre Habouzit
+ */
+
+abstract class Req
+{
+ public static function has($key)
+ {
+ return isset($_REQUEST[$key]);
+ }
+
+ public static function val($key, $default = null)
+ {
+ return Req::has($key) ? $_REQUEST[$key] : $default;
+ }
+
+ //it will always return a number no matter what(null is 0)
+ public static function num($key, $default = null)
+ {
+ return Filters::num(Req::val($key, $default));
+ }
+
+ public static function enum($key, $options, $default = null)
+ {
+ return Filters::enum(Req::val($key, $default), $options);
+ }
+
+ //always a string (null is typed to an empty string)
+ public static function safe($key)
+ {
+ return Filters::noXSS(Req::val($key));
+ }
+
+ public static function isAlnum($key)
+ {
+ return Filters::isAlnum(Req::val($key));
+ }
+
+ /**
+ * Overwrites or sets a request value
+ */
+ public static function set($key, $value = null) {
+ $_REQUEST[$key] = $value;
+ }
+}
+
+ // }}}
+// {{{ class Post
+
+abstract class Post
+{
+ public static function has($key)
+ {
+ // XXX semantics is different for POST, as POST of '' values is never
+ // unintentionnal, whereas GET/COOKIE may have '' values for empty
+ // ones.
+ return isset($_POST[$key]);
+ }
+
+ public static function val($key, $default = null)
+ {
+ return Post::has($key) ? $_POST[$key] : $default;
+ }
+
+ //it will always return a number no matter what(null is 0)
+ public static function num($key, $default = null)
+ {
+ return Filters::num(Post::val($key, $default));
+ }
+
+ //always a string (null is typed to an empty string)
+ public static function safe($key)
+ {
+ return Filters::noXSS(Post::val($key));
+ }
+
+ public static function isAlnum($key)
+ {
+ return Filters::isAlnum(Post::val($key));
+ }
+}
+
+// }}}
+// {{{ class Get
+
+abstract class Get
+{
+ public static function has($key)
+ {
+ return isset($_GET[$key]) && $_GET[$key] !== '';
+ }
+
+ public static function val($key, $default = null)
+ {
+ return Get::has($key) ? $_GET[$key] : $default;
+ }
+
+ //it will always return a number no matter what(null is 0)
+ public static function num($key, $default = null)
+ {
+ return Filters::num(Get::val($key, $default));
+ }
+
+ //always a string (null is typed to an empty string)
+ public static function safe($key)
+ {
+ return Filters::noXSS(Get::val($key));
+ }
+
+ public static function enum($key, $options, $default = null)
+ {
+ return Filters::enum(Get::val($key, $default), $options);
+ }
+
+}
+
+// }}}
+//{{{ class Cookie
+
+abstract class Cookie
+{
+ public static function has($key)
+ {
+ return isset($_COOKIE[$key]) && $_COOKIE[$key] !== '';
+ }
+
+ public static function val($key, $default = null)
+ {
+ return Cookie::has($key) ? $_COOKIE[$key] : $default;
+ }
+}
+//}}}
+/**
+ * Class Filters
+ *
+ * This is a simple class for safe input validation
+ * no mixed stuff here, functions returns always the same type.
+ * @author Cristian Rodriguez R <judas.iscariote@flyspray.org>
+ * @license BSD
+ * @notes this intented to be used by Flyspray internals functions/methods
+ * please DO NOT use this in templates , if the code processing the input there
+ * is not safe, please fix the underlying problem.
+ */
+abstract class Filters {
+ /**
+ * give me a number only please?
+ * @param mixed $data
+ * @return int
+ * @access public static
+ * @notes changed before 0.9.9 to avoid strange results
+ * with arrays and objects
+ */
+ public static function num($data)
+ {
+ return intval($data); // no further checks here please
+ }
+
+ /**
+ * Give user input free from potentially mailicious html
+ * @param mixed $data
+ * @return string htmlspecialchar'ed
+ * @access public static
+ */
+ public static function noXSS($data)
+ {
+ if(empty($data) || is_numeric($data)) {
+ return $data;
+ } elseif(is_string($data)) {
+ return htmlspecialchars($data, ENT_QUOTES, 'utf-8');
+ }
+ return '';
+ }
+
+ /**
+ * Give user input free from potentially mailicious html and JS insertions
+ * @param mixed $data
+ * @return string
+ * @access public static
+ */
+ public static function noJsXSS($data)
+ {
+ if(empty($data) || is_numeric($data)) {
+ return $data;
+ } elseif(is_string($data)) {
+ return Filters::noXSS(preg_replace("/[\x01-\x1F\x7F]|\xC2[\x80-\x9F]/", "", addcslashes($data, "\t\"'\\")));
+ }
+ return '';
+ }
+
+ /**
+ * is $data alphanumeric eh ?
+ * @param string $data string value to check
+ * @return bool
+ * @access public static
+ * @notes unfortunately due to a bug in PHP < 5.1
+ * http://bugs.php.net/bug.php?id=30945 ctype_alnum
+ * returned true on empty string, that's the reason why
+ * we have to use strlen too.
+ *
+ * Be aware: $data MUST be an string, integers or any other
+ * type is evaluated to FALSE
+ */
+ public static function isAlnum($data)
+ {
+ return ctype_alnum($data) && strlen($data);
+ }
+
+ /**
+ * Checks if $data is a value of $options and returns the first element of
+ * $options if it is not (for input validation if all possible values are known)
+ * @param mixed $data
+ * @param array $options
+ * @return mixed
+ * @access public static
+ */
+ public static function enum($data, $options)
+ {
+ if (!in_array($data, $options) && isset($options[0])) {
+ return $options[0];
+ }
+
+ return $data;
+ }
+
+ public static function escapeqs($qs)
+ {
+ parse_str($qs, $clean_qs);
+ return http_build_query($clean_qs);
+ }
+}
+
+/**
+ * A basic function which works like the GPC classes above for any array
+ * @param array $array
+ * @param mixed $key
+ * @param mixed $default
+ * @return mixed
+ * @version 1.0
+ * @since 0.9.9
+ * @see Backend::get_task_list()
+ */
+function array_get(&$array, $key, $default = null)
+{
+ return (isset($array[$key])) ? $array[$key] : $default;
+}
diff --git a/includes/class.jabber2.php b/includes/class.jabber2.php
new file mode 100644
index 0000000..4617395
--- /dev/null
+++ b/includes/class.jabber2.php
@@ -0,0 +1,943 @@
+<?php
+/**
+ * Jabber class
+ *
+ * @version $Id$
+ * @copyright 2006 Flyspray.org
+ * @notes: This lib has been created due to the lack of any good and modern jabber class out there
+ * @author: Florian Schmitz (floele)
+ */
+
+define('SECURITY_NONE', 0);
+define('SECURITY_SSL', 1);
+define('SECURITY_TLS', 2);
+
+class Jabber
+{
+ public $connection = null;
+ public $session = array();
+ public $resource = 'class.jabber2.php';
+ public $log = array();
+ public $log_enabled = true;
+ public $timeout = 10;
+ public $user = '';
+ public $password = '';
+ public $server = '';
+ public $features = array();
+
+ public function __construct($login, $password, $security = SECURITY_NONE, $port = 5222, $host = '')
+ {
+ // Can we use Jabber at all?
+ // Note: Maybe replace with SimpleXML in the future
+ if (!extension_loaded('xml')) {
+ $this->log('Error: No XML functions available, Jabber functions can not operate.');
+ return false;
+ }
+
+ //bug in php 5.2.1 renders this stuff more or less useless.
+ if ((version_compare(phpversion(), '5.2.1', '>=') && version_compare(phpversion(), '5.2.3RC2', '<')) && $security != SECURITY_NONE) {
+ $this->log('Error: PHP ' . phpversion() . ' + SSL is incompatible with jabber, see http://bugs.php.net/41236');
+ return false;
+ }
+
+ if (!Jabber::check_jid($login)) {
+ $this->log('Error: Jabber ID is not valid: ' . $login);
+ return false;
+ }
+
+ // Extract data from user@server.org
+ list($username, $server) = explode('@', $login);
+
+ // Decide whether or not to use encryption
+ if ($security == SECURITY_SSL && !Jabber::can_use_ssl()) {
+ $this->log('Warning: SSL encryption is not supported (openssl required). Falling back to no encryption.');
+ $security = SECURITY_NONE;
+ }
+ if ($security == SECURITY_TLS && !Jabber::can_use_tls()) {
+ $this->log('Warning: TLS encryption is not supported (openssl and stream_socket_enable_crypto() required). Falling back to no encryption.');
+ $security = SECURITY_NONE;
+ }
+
+ $this->session['security'] = $security;
+ $this->server = $server;
+ $this->user = $username;
+ $this->password = $password;
+
+ if ($this->open_socket( ($host != '') ? $host : $server, $port, $security == SECURITY_SSL)) {
+ $this->send("<?xml version='1.0' encoding='UTF-8' ?" . ">\n");
+ $this->send("<stream:stream to='{$server}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>\n");
+ } else {
+ return false;
+ }
+ // Now we listen what the server has to say...and give appropriate responses
+ $this->response($this->listen());
+ }
+
+ /**
+ * Sets the resource which is used. No validation is done here, only escaping.
+ * @param string $$name
+ * @access public
+ */
+ public function setResource($name)
+ {
+ $this->resource = $name;
+ }
+
+ /**
+ * Send data to the Jabber server
+ * @param string $xml
+ * @access public
+ * @return bool
+ */
+ public function send($xml)
+ {
+ if ($this->connected()) {
+ $xml = trim($xml);
+ $this->log('SEND: '. $xml);
+ return fwrite($this->connection, $xml);
+ } else {
+ $this->log('Error: Could not send, connection lost (flood?).');
+ return false;
+ }
+ }
+
+ /**
+ * OpenSocket
+ * @param string $server host to connect to
+ * @param int $port port number
+ * @param bool $ssl use ssl or not
+ * @access public
+ * @return bool
+ */
+ public function open_socket($server, $port, $ssl = false)
+ {
+ if (function_exists("dns_get_record")) {
+ $record = dns_get_record("_xmpp-client._tcp.$server", DNS_SRV);
+ if (!empty($record)) {
+ $server = $record[0]['target'];
+ }
+ } else {
+ $this->log('Warning: dns_get_record function not found. gtalk will not work.');
+ }
+
+ $server = $ssl ? 'ssl://' . $server : $server;
+
+ if ($ssl) {
+ $this->session['ssl'] = true;
+ }
+
+ if ($this->connection = @fsockopen($server, $port, $errorno, $errorstr, $this->timeout)) {
+ socket_set_blocking($this->connection, 0);
+ socket_set_timeout($this->connection, 60);
+
+ return true;
+ }
+ // Apparently an error occured...
+ $this->log('Error: ' . $errorstr);
+ return false;
+ }
+
+ public function log($msg)
+ {
+ if ($this->log_enabled) {
+ $this->log[] = $msg;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Listens to the connection until it gets data or the timeout is reached.
+ * Thus, it should only be called if data is expected to be received.
+ * @access public
+ * @return mixed either false for timeout or an array with the received data
+ */
+ public function listen($timeout = 10, $wait = false)
+ {
+ if (!$this->connected()) {
+ return false;
+ }
+
+ // Wait for a response until timeout is reached
+ $start = time();
+ $data = '';
+
+ do {
+ $read = trim(fread($this->connection, 4096));
+ $data .= $read;
+ } while (time() <= $start + $timeout && !feof($this->connection) && ($wait || $data == '' || $read != ''
+ || (substr(rtrim($data), -1) != '>')));
+
+ if ($data != '') {
+ $this->log('RECV: '. $data);
+ return Jabber::xmlize($data);
+ } else {
+ $this->log('Timeout, no response from server.');
+ return false;
+ }
+ }
+
+ /**
+ * Initiates login (using data from contructor)
+ * @access public
+ * @return bool
+ */
+ public function login()
+ {
+ if (!count($this->features)) {
+ $this->log('Error: No feature information from server available.');
+ return false;
+ }
+
+ return $this->response($this->features);
+ }
+
+ /**
+ * Initiates account registration (based on data used for contructor)
+ * @access public
+ * @return bool
+ */
+ public function register()
+ {
+ if (!isset($this->session['id']) || isset($this->session['jid'])) {
+ $this->log('Error: Cannot initiate registration.');
+ return false;
+ }
+
+ $this->send("<iq type='get' id='reg_1'>
+ <query xmlns='jabber:iq:register'/>
+ </iq>");
+ return $this->response($this->listen());
+ }
+
+ /**
+ * Initiates account un-registration (based on data used for contructor)
+ * @access public
+ * @return bool
+ */
+ public function unregister()
+ {
+ if (!isset($this->session['id']) || !isset($this->session['jid'])) {
+ $this->log('Error: Cannot initiate un-registration.');
+ return false;
+ }
+
+ $this->send("<iq type='set' from='" . Jabber::jspecialchars($this->session['jid']) . "' id='unreg_1'>
+ <query xmlns='jabber:iq:register'>
+ <remove/>
+ </query>
+ </iq>");
+ return $this->response($this->listen(2)); // maybe we don't even get a response
+ }
+
+ /**
+ * Sets account presence. No additional info required (default is "online" status)
+ * @param $type dnd, away, chat, xa or nothing
+ * @param $message
+ * @param $unavailable set this to true if you want to become unavailable
+ * @access public
+ * @return bool
+ */
+ public function presence($type = '', $message = '', $unavailable = false)
+ {
+ if (!isset($this->session['jid'])) {
+ $this->log('Error: Cannot set presence at this point.');
+ return false;
+ }
+
+ if (in_array($type, array('dnd', 'away', 'chat', 'xa'))) {
+ $type = '<show>'. $type .'</show>';
+ } else {
+ $type = '';
+ }
+
+ $unavailable = ($unavailable) ? " type='unavailable'" : '';
+ $message = ($message) ? '<status>' . Jabber::jspecialchars($message) .'</status>' : '';
+
+ $this->session['sent_presence'] = !$unavailable;
+
+ return $this->send("<presence$unavailable>" .
+ $type .
+ $message .
+ '</presence>');
+ }
+
+ /**
+ * This handles all the different XML elements
+ * @param array $xml
+ * @access public
+ * @return bool
+ */
+ public function response($xml)
+ {
+ if (!is_array($xml) || !count($xml)) {
+ return false;
+ }
+
+ // did we get multiple elements? do one after another
+ // array('message' => ..., 'presence' => ...)
+ if (count($xml) > 1) {
+ foreach ($xml as $key => $value) {
+ $this->response(array($key => $value));
+ }
+ return;
+ } else
+ // or even multiple elements of the same type?
+ // array('message' => array(0 => ..., 1 => ...))
+ if (count(reset($xml)) > 1) {
+ foreach (reset($xml) as $value) {
+ $this->response(array(key($xml) => array(0 => $value)));
+ }
+ return;
+ }
+
+ switch (key($xml)) {
+ case 'stream:stream':
+ // Connection initialised (or after authentication). Not much to do here...
+ if (isset($xml['stream:stream'][0]['#']['stream:features'])) {
+ // we already got all info we need
+ $this->features = $xml['stream:stream'][0]['#'];
+ } else {
+ $this->features = $this->listen();
+ }
+ $second_time = isset($this->session['id']);
+ $this->session['id'] = $xml['stream:stream'][0]['@']['id'];
+ if ($second_time) {
+ // If we are here for the second time after TLS, we need to continue logging in
+ $this->login();
+ return;
+ }
+
+ // go on with authentication?
+ if (isset($this->features['stream:features'][0]['#']['bind'])) {
+ return $this->response($this->features);
+ }
+ break;
+
+ case 'stream:features':
+ // Resource binding after successful authentication
+ if (isset($this->session['authenticated'])) {
+ // session required?
+ $this->session['sess_required'] = isset($xml['stream:features'][0]['#']['session']);
+
+ $this->send("<iq type='set' id='bind_1'>
+ <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
+ <resource>" . Jabber::jspecialchars($this->resource) . "</resource>
+ </bind>
+ </iq>");
+ return $this->response($this->listen());
+ }
+ // Let's use TLS if SSL is not enabled and we can actually use it
+ if ($this->session['security'] == SECURITY_TLS && isset($xml['stream:features'][0]['#']['starttls'])) {
+ $this->log('Switching to TLS.');
+ $this->send("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>\n");
+ return $this->response($this->listen());
+ }
+ // Does the server support SASL authentication?
+
+ // I hope so, because we do (and no other method).
+ if (isset($xml['stream:features'][0]['#']['mechanisms'][0]['@']['xmlns']) &&
+ $xml['stream:features'][0]['#']['mechanisms'][0]['@']['xmlns'] == 'urn:ietf:params:xml:ns:xmpp-sasl') {
+ // Now decide on method
+ $methods = array();
+ foreach ($xml['stream:features'][0]['#']['mechanisms'][0]['#']['mechanism'] as $value) {
+ $methods[] = $value['#'];
+ }
+
+ // we prefer this one
+ if (in_array('DIGEST-MD5', $methods)) {
+ $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>");
+ // we don't want to use this (neither does the server usually) if no encryption is in place
+ # http://www.xmpp.org/extensions/attic/jep-0078-1.7.html
+ # The plaintext mechanism SHOULD NOT be used unless the underlying stream is encrypted (using SSL or TLS)
+ # and the client has verified that the server certificate is signed by a trusted certificate authority.
+ } else if (in_array('PLAIN', $methods) && (isset($this->session['ssl']) || isset($this->session['tls']))) {
+ $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>"
+ . base64_encode(chr(0) . $this->user . '@' . $this->server . chr(0) . $this->password) .
+ "</auth>");
+ } else if (in_array('ANONYMOUS', $methods)) {
+ $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>");
+ // not good...
+ } else {
+ $this->log('Error: No authentication method supported.');
+ $this->disconnect();
+ return false;
+ }
+ return $this->response($this->listen());
+
+ } else {
+ // ok, this is it. bye.
+ $this->log('Error: Server does not offer SASL authentication.');
+ $this->disconnect();
+ return false;
+ }
+ break;
+
+ case 'challenge':
+ // continue with authentication...a challenge literally -_-
+ $decoded = base64_decode($xml['challenge'][0]['#']);
+ $decoded = Jabber::parse_data($decoded);
+ if (!isset($decoded['digest-uri'])) {
+ $decoded['digest-uri'] = 'xmpp/'. $this->server;
+ }
+
+ // better generate a cnonce, maybe it's needed
+
+ $decoded['cnonce'] = base64_encode(md5(uniqid(mt_rand(), true)));
+
+ // second challenge?
+ if (isset($decoded['rspauth'])) {
+ $this->send("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>");
+ } else {
+ $response = array('username' => $this->user,
+ 'response' => $this->encrypt_password(array_merge($decoded, array('nc' => '00000001'))),
+ 'charset' => 'utf-8',
+ 'nc' => '00000001',
+ 'qop' => 'auth'); // the only option we support anyway
+
+ foreach (array('nonce', 'digest-uri', 'realm', 'cnonce') as $key) {
+ if (isset($decoded[$key])) {
+ $response[$key] = $decoded[$key];
+ }
+ }
+
+ $this->send("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" .
+ base64_encode(Jabber::implode_data($response))
+ . "</response>");
+ }
+
+ return $this->response($this->listen());
+
+ case 'failure':
+ $this->log('Error: Server sent "failure".');
+ $this->disconnect();
+ return false;
+
+ case 'proceed':
+ // continue switching to TLS
+ $meta = stream_get_meta_data($this->connection);
+ socket_set_blocking($this->connection, 1);
+ if (!stream_socket_enable_crypto($this->connection, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
+ $this->log('Error: TLS mode change failed.');
+ return false;
+ }
+ socket_set_blocking($this->connection, $meta['blocked']);
+ $this->session['tls'] = true;
+ // new stream
+ $this->send("<?xml version='1.0' encoding='UTF-8' ?" . ">\n");
+ $this->send("<stream:stream to='{$this->server}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>\n");
+
+ return $this->response($this->listen());
+
+ case 'success':
+ // Yay, authentication successful.
+ $this->send("<stream:stream to='{$this->server}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>\n");
+ $this->session['authenticated'] = true;
+ return $this->response($this->listen()); // we have to wait for another response
+
+ case 'iq':
+ // we are not interested in IQs we did not expect
+ if (!isset($xml['iq'][0]['@']['id'])) {
+ return false;
+ }
+ // multiple possibilities here
+ switch ($xml['iq'][0]['@']['id'])
+ {
+ case 'bind_1':
+ $this->session['jid'] = $xml['iq'][0]['#']['bind'][0]['#']['jid'][0]['#'];
+ // and (maybe) yet another request to be able to send messages *finally*
+ if ($this->session['sess_required']) {
+ $this->send("<iq to='{$this->server}'
+ type='set'
+ id='sess_1'>
+ <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>
+ </iq>");
+ return $this->response($this->listen());
+ }
+ return true;
+
+ case 'sess_1':
+ return true;
+
+ case 'reg_1':
+ $this->send("<iq type='set' id='reg_2'>
+ <query xmlns='jabber:iq:register'>
+ <username>" . Jabber::jspecialchars($this->user) . "</username>
+ <password>" . Jabber::jspecialchars($this->password) . "</password>
+ </query>
+ </iq>");
+ return $this->response($this->listen());
+
+ case 'reg_2':
+ // registration end
+ if (isset($xml['iq'][0]['#']['error'])) {
+ $this->log('Warning: Registration failed.');
+ return false;
+ }
+ return true;
+
+ case 'unreg_1':
+ return true;
+
+ default:
+ $this->log('Notice: Received unexpected IQ.');
+ return false;
+ }
+ break;
+
+ case 'message':
+ // we are only interested in content...
+ if (!isset($xml['message'][0]['#']['body'])) {
+ return false;
+ }
+
+ $message['body'] = $xml['message'][0]['#']['body'][0]['#'];
+ $message['from'] = $xml['message'][0]['@']['from'];
+ if (isset($xml['message'][0]['#']['subject'])) {
+ $message['subject'] = $xml['message'][0]['#']['subject'][0]['#'];
+ }
+ $this->session['messages'][] = $message;
+ break;
+
+ default:
+ // hm...don't know this response
+ $this->log('Notice: Unknown server response (' . key($xml) . ')');
+ return false;
+ }
+ }
+
+ public function send_message($to, $text, $subject = '', $type = 'normal')
+ {
+ if (!isset($this->session['jid'])) {
+ return false;
+ }
+
+ if (!in_array($type, array('chat', 'normal', 'error', 'groupchat', 'headline'))) {
+ $type = 'normal';
+ }
+
+ return $this->send("<message from='" . Jabber::jspecialchars($this->session['jid']) . "'
+ to='" . Jabber::jspecialchars($to) . "'
+ type='$type'
+ id='" . uniqid('msg') . "'>
+ <subject>" . Jabber::jspecialchars($subject) . "</subject>
+ <body>" . Jabber::jspecialchars($text) . "</body>
+ </message>");
+ }
+
+ public function get_messages($waitfor = 3)
+ {
+ if (!isset($this->session['sent_presence']) || !$this->session['sent_presence']) {
+ $this->presence();
+ }
+
+ if ($waitfor > 0) {
+ $this->response($this->listen($waitfor, $wait = true)); // let's see if any messages fly in
+ }
+
+ return isset($this->session['messages']) ? $this->session['messages'] : array();
+ }
+
+ public function connected()
+ {
+ return is_resource($this->connection) && !feof($this->connection);
+ }
+
+ public function disconnect()
+ {
+ if ($this->connected()) {
+ // disconnect gracefully
+ if (isset($this->session['sent_presence'])) {
+ $this->presence('', 'offline', $unavailable = true);
+ }
+ $this->send('</stream:stream>');
+ $this->session = array();
+ return fclose($this->connection);
+ }
+ return false;
+ }
+
+ public static function can_use_ssl()
+ {
+ return extension_loaded('openssl');
+ }
+
+ public static function can_use_tls()
+ {
+ return Jabber::can_use_ssl() && function_exists('stream_socket_enable_crypto');
+ }
+
+ /**
+ * Encrypts a password as in RFC 2831
+ * @param array $data Needs data from the client-server connection
+ * @access public
+ * @return string
+ */
+ public function encrypt_password($data)
+ {
+ // let's me think about <challenge> again...
+ foreach (array('realm', 'cnonce', 'digest-uri') as $key) {
+ if (!isset($data[$key])) {
+ $data[$key] = '';
+ }
+ }
+
+ $pack = md5($this->user . ':' . $data['realm'] . ':' . $this->password);
+ if (isset($data['authzid'])) {
+ $a1 = pack('H32', $pack) . sprintf(':%s:%s:%s', $data['nonce'], $data['cnonce'], $data['authzid']);
+ } else {
+ $a1 = pack('H32', $pack) . sprintf(':%s:%s', $data['nonce'], $data['cnonce']);
+ }
+
+ // should be: qop = auth
+ $a2 = 'AUTHENTICATE:'. $data['digest-uri'];
+
+ return md5(sprintf('%s:%s:%s:%s:%s:%s', md5($a1), $data['nonce'], $data['nc'], $data['cnonce'], $data['qop'], md5($a2)));
+ }
+
+ /**
+ * parse_data like a="b",c="d",...
+ * @param string $data
+ * @access public
+ * @return array a => b ...
+ */
+ public function parse_data($data)
+ {
+ // super basic, but should suffice
+ $data = explode(',', $data);
+ $pairs = array();
+ foreach ($data as $pair) {
+ $dd = strpos($pair, '=');
+ if ($dd) {
+ $pairs[substr($pair, 0, $dd)] = trim(substr($pair, $dd + 1), '"');
+ }
+ }
+ return $pairs;
+ }
+
+ /**
+ * opposite of Jabber::parse_data()
+ * @param array $data
+ * @access public
+ * @return string
+ */
+ public function implode_data($data)
+ {
+ $return = array();
+ foreach ($data as $key => $value) {
+ $return[] = $key . '="' . $value . '"';
+ }
+ return implode(',', $return);
+ }
+
+ /**
+ * Checks whether or not a Jabber ID is valid (FS#1131)
+ * @param string $jid
+ * @access public
+ * @return string
+ */
+ public function check_jid($jid)
+ {
+ $i = strpos($jid, '@');
+ if ($i === false) {
+ return false;
+ }
+
+ $username = substr($jid, 0, $i);
+ $realm = substr($jid, $i + 1);
+
+ if (strlen($username) == 0 || strlen($realm) < 3) {
+ return false;
+ }
+
+ $arr = explode('.', $realm);
+
+ if (count($arr) == 0) {
+ return false;
+ }
+
+ foreach ($arr as $part)
+ {
+ if (substr($part, 0, 1) == '-' || substr($part, -1, 1) == '-') {
+ return false;
+ }
+
+ if (preg_match("@^[a-zA-Z0-9-.]+$@", $part) == false) {
+ return false;
+ }
+ }
+
+ $b = array(array(0, 127), array(192, 223), array(224, 239),
+ array(240, 247), array(248, 251), array(252, 253));
+
+ // Prohibited Characters RFC3454 + RFC3920
+ $p = array(
+ // Table C.1.1
+ array(0x0020, 0x0020), // SPACE
+ // Table C.1.2
+ array(0x00A0, 0x00A0), // NO-BREAK SPACE
+ array(0x1680, 0x1680), // OGHAM SPACE MARK
+ array(0x2000, 0x2001), // EN QUAD
+ array(0x2001, 0x2001), // EM QUAD
+ array(0x2002, 0x2002), // EN SPACE
+ array(0x2003, 0x2003), // EM SPACE
+ array(0x2004, 0x2004), // THREE-PER-EM SPACE
+ array(0x2005, 0x2005), // FOUR-PER-EM SPACE
+ array(0x2006, 0x2006), // SIX-PER-EM SPACE
+ array(0x2007, 0x2007), // FIGURE SPACE
+ array(0x2008, 0x2008), // PUNCTUATION SPACE
+ array(0x2009, 0x2009), // THIN SPACE
+ array(0x200A, 0x200A), // HAIR SPACE
+ array(0x200B, 0x200B), // ZERO WIDTH SPACE
+ array(0x202F, 0x202F), // NARROW NO-BREAK SPACE
+ array(0x205F, 0x205F), // MEDIUM MATHEMATICAL SPACE
+ array(0x3000, 0x3000), // IDEOGRAPHIC SPACE
+ // Table C.2.1
+ array(0x0000, 0x001F), // [CONTROL CHARACTERS]
+ array(0x007F, 0x007F), // DELETE
+ // Table C.2.2
+ array(0x0080, 0x009F), // [CONTROL CHARACTERS]
+ array(0x06DD, 0x06DD), // ARABIC END OF AYAH
+ array(0x070F, 0x070F), // SYRIAC ABBREVIATION MARK
+ array(0x180E, 0x180E), // MONGOLIAN VOWEL SEPARATOR
+ array(0x200C, 0x200C), // ZERO WIDTH NON-JOINER
+ array(0x200D, 0x200D), // ZERO WIDTH JOINER
+ array(0x2028, 0x2028), // LINE SEPARATOR
+ array(0x2029, 0x2029), // PARAGRAPH SEPARATOR
+ array(0x2060, 0x2060), // WORD JOINER
+ array(0x2061, 0x2061), // FUNCTION APPLICATION
+ array(0x2062, 0x2062), // INVISIBLE TIMES
+ array(0x2063, 0x2063), // INVISIBLE SEPARATOR
+ array(0x206A, 0x206F), // [CONTROL CHARACTERS]
+ array(0xFEFF, 0xFEFF), // ZERO WIDTH NO-BREAK SPACE
+ array(0xFFF9, 0xFFFC), // [CONTROL CHARACTERS]
+ array(0x1D173, 0x1D17A), // [MUSICAL CONTROL CHARACTERS]
+ // Table C.3
+ array(0xE000, 0xF8FF), // [PRIVATE USE, PLANE 0]
+ array(0xF0000, 0xFFFFD), // [PRIVATE USE, PLANE 15]
+ array(0x100000, 0x10FFFD), // [PRIVATE USE, PLANE 16]
+ // Table C.4
+ array(0xFDD0, 0xFDEF), // [NONCHARACTER CODE POINTS]
+ array(0xFFFE, 0xFFFF), // [NONCHARACTER CODE POINTS]
+ array(0x1FFFE, 0x1FFFF), // [NONCHARACTER CODE POINTS]
+ array(0x2FFFE, 0x2FFFF), // [NONCHARACTER CODE POINTS]
+ array(0x3FFFE, 0x3FFFF), // [NONCHARACTER CODE POINTS]
+ array(0x4FFFE, 0x4FFFF), // [NONCHARACTER CODE POINTS]
+ array(0x5FFFE, 0x5FFFF), // [NONCHARACTER CODE POINTS]
+ array(0x6FFFE, 0x6FFFF), // [NONCHARACTER CODE POINTS]
+ array(0x7FFFE, 0x7FFFF), // [NONCHARACTER CODE POINTS]
+ array(0x8FFFE, 0x8FFFF), // [NONCHARACTER CODE POINTS]
+ array(0x9FFFE, 0x9FFFF), // [NONCHARACTER CODE POINTS]
+ array(0xAFFFE, 0xAFFFF), // [NONCHARACTER CODE POINTS]
+ array(0xBFFFE, 0xBFFFF), // [NONCHARACTER CODE POINTS]
+ array(0xCFFFE, 0xCFFFF), // [NONCHARACTER CODE POINTS]
+ array(0xDFFFE, 0xDFFFF), // [NONCHARACTER CODE POINTS]
+ array(0xEFFFE, 0xEFFFF), // [NONCHARACTER CODE POINTS]
+ array(0xFFFFE, 0xFFFFF), // [NONCHARACTER CODE POINTS]
+ array(0x10FFFE, 0x10FFFF), // [NONCHARACTER CODE POINTS]
+ // Table C.5
+ array(0xD800, 0xDFFF), // [SURROGATE CODES]
+ // Table C.6
+ array(0xFFF9, 0xFFF9), // INTERLINEAR ANNOTATION ANCHOR
+ array(0xFFFA, 0xFFFA), // INTERLINEAR ANNOTATION SEPARATOR
+ array(0xFFFB, 0xFFFB), // INTERLINEAR ANNOTATION TERMINATOR
+ array(0xFFFC, 0xFFFC), // OBJECT REPLACEMENT CHARACTER
+ array(0xFFFD, 0xFFFD), // REPLACEMENT CHARACTER
+ // Table C.7
+ array(0x2FF0, 0x2FFB), // [IDEOGRAPHIC DESCRIPTION CHARACTERS]
+ // Table C.8
+ array(0x0340, 0x0340), // COMBINING GRAVE TONE MARK
+ array(0x0341, 0x0341), // COMBINING ACUTE TONE MARK
+ array(0x200E, 0x200E), // LEFT-TO-RIGHT MARK
+ array(0x200F, 0x200F), // RIGHT-TO-LEFT MARK
+ array(0x202A, 0x202A), // LEFT-TO-RIGHT EMBEDDING
+ array(0x202B, 0x202B), // RIGHT-TO-LEFT EMBEDDING
+ array(0x202C, 0x202C), // POP DIRECTIONAL FORMATTING
+ array(0x202D, 0x202D), // LEFT-TO-RIGHT OVERRIDE
+ array(0x202E, 0x202E), // RIGHT-TO-LEFT OVERRIDE
+ array(0x206A, 0x206A), // INHIBIT SYMMETRIC SWAPPING
+ array(0x206B, 0x206B), // ACTIVATE SYMMETRIC SWAPPING
+ array(0x206C, 0x206C), // INHIBIT ARABIC FORM SHAPING
+ array(0x206D, 0x206D), // ACTIVATE ARABIC FORM SHAPING
+ array(0x206E, 0x206E), // NATIONAL DIGIT SHAPES
+ array(0x206F, 0x206F), // NOMINAL DIGIT SHAPES
+ // Table C.9
+ array(0xE0001, 0xE0001), // LANGUAGE TAG
+ array(0xE0020, 0xE007F), // [TAGGING CHARACTERS]
+ // RFC3920
+ array(0x22, 0x22), // "
+ array(0x26, 0x26), // &
+ array(0x27, 0x27), // '
+ array(0x2F, 0x2F), // /
+ array(0x3A, 0x3A), // :
+ array(0x3C, 0x3C), // <
+ array(0x3E, 0x3E), // >
+ array(0x40, 0x40) // @
+ );
+
+ $pos = 0;
+ $result = true;
+
+ while ($pos < strlen($username))
+ {
+ $len = 0;
+ $uni = 0;
+ for ($i = 0; $i <= 5; $i++)
+ {
+ if (ord($username[$pos]) >= $b[$i][0] && ord($username[$pos]) <= $b[$i][1])
+ {
+ $len = $i + 1;
+
+ $uni = (ord($username[$pos]) - $b[$i][0]) * pow(2, $i * 6);
+
+ for ($k = 1; $k < $len; $k++) {
+ $uni += (ord($username[$pos + $k]) - 128) * pow(2, ($i - $k) * 6);
+ }
+
+ break;
+ }
+ }
+
+ if ($len == 0) {
+ return false;
+ }
+
+ foreach ($p as $pval)
+ {
+ if ($uni >= $pval[0] && $uni <= $pval[1]) {
+ $result = false;
+ break 2;
+ }
+ }
+
+ $pos = $pos + $len;
+ }
+
+ return $result;
+ }
+
+ public static function jspecialchars($data)
+ {
+ return htmlspecialchars($data, ENT_QUOTES, 'utf-8');
+ }
+
+ // ======================================================================
+ // Third party code, taken from old jabber lib (the only usable code left)
+ // ======================================================================
+
+ // xmlize()
+ // (c) Hans Anderson / http://www.hansanderson.com/php/xml/
+
+ public static function xmlize($data, $WHITE=1, $encoding='UTF-8') {
+
+ $data = trim($data);
+ if (substr($data, 0, 5) != '<?xml') {
+ $data = '<root>'. $data . '</root>'; // mod
+ }
+ $vals = $array = array();
+ $parser = xml_parser_create($encoding);
+ xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
+ xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, $WHITE);
+ xml_parse_into_struct($parser, $data, $vals);
+ xml_parser_free($parser);
+
+ $i = 0;
+
+ $tagname = $vals[$i]['tag'];
+ if ( isset ($vals[$i]['attributes'] ) )
+ {
+ $array[$tagname][0]['@'] = $vals[$i]['attributes']; // mod
+ } else {
+ $array[$tagname][0]['@'] = array(); // mod
+ }
+
+ $array[$tagname][0]["#"] = Jabber::_xml_depth($vals, $i); // mod
+ if (substr($data, 0, 5) != '<?xml') {
+ $array = $array['root'][0]['#']; // mod
+ }
+
+ return $array;
+ }
+
+
+
+ // _xml_depth()
+ // (c) Hans Anderson / http://www.hansanderson.com/php/xml/
+
+ public static function _xml_depth($vals, &$i) {
+ $children = array();
+
+ if ( isset($vals[$i]['value']) )
+ {
+ array_push($children, $vals[$i]['value']);
+ }
+
+ while (++$i < count($vals)) {
+
+ switch ($vals[$i]['type']) {
+
+ case 'open':
+
+ if ( isset ( $vals[$i]['tag'] ) )
+ {
+ $tagname = $vals[$i]['tag'];
+ } else {
+ $tagname = '';
+ }
+
+ if ( isset ( $children[$tagname] ) )
+ {
+ $size = sizeof($children[$tagname]);
+ } else {
+ $size = 0;
+ }
+
+ if ( isset ( $vals[$i]['attributes'] ) ) {
+ $children[$tagname][$size]['@'] = $vals[$i]["attributes"];
+
+ }
+
+ $children[$tagname][$size]['#'] = Jabber::_xml_depth($vals, $i);
+
+ break;
+
+
+ case 'cdata':
+ array_push($children, $vals[$i]['value']);
+ break;
+
+ case 'complete':
+ $tagname = $vals[$i]['tag'];
+
+ if( isset ($children[$tagname]) )
+ {
+ $size = sizeof($children[$tagname]);
+ } else {
+ $size = 0;
+ }
+
+ if( isset ( $vals[$i]['value'] ) )
+ {
+ $children[$tagname][$size]["#"] = $vals[$i]['value'];
+ } else {
+ $children[$tagname][$size]["#"] = array();
+ }
+
+ if ( isset ($vals[$i]['attributes']) ) {
+ $children[$tagname][$size]['@']
+ = $vals[$i]['attributes'];
+ }
+
+ break;
+
+ case 'close':
+ return $children;
+ break;
+ }
+ }
+
+ return $children;
+ }
+}
+
diff --git a/includes/class.notify.php b/includes/class.notify.php
new file mode 100644
index 0000000..386158a
--- /dev/null
+++ b/includes/class.notify.php
@@ -0,0 +1,1114 @@
+<?php
+
+/*
+ ---------------------------------------------------
+ | This script contains the notification functions |
+ ---------------------------------------------------
+*/
+
+/**
+ * Notifications
+ *
+ * @package
+ * @version $Id$
+ * @copyright 2006 Flyspray.org
+ * @notes: This is a mess and should be replaced for 1.0
+ */
+
+class Notifications {
+
+ // {{{ Wrapper function for all others
+ function create($type, $task_id, $info = null, $to = null, $ntype = NOTIFY_BOTH, $proj_lang = null) {
+ global $fs;
+
+ if (is_null($to)) {
+ $to = $this->address($task_id, $type);
+ }
+
+ if (!is_array($to)) {
+ settype($to, 'array');
+ }
+
+ if (!count($to)) {
+ return false;
+ }
+
+ $languages = array();
+ $emails = array();
+ $jabbers = array();
+ $onlines = array();
+
+ if (isset($to[0])) {
+ foreach ($to[0] as $recipient) {
+ if (!empty($recipient['lang'])) {
+ $lang = $recipient['lang'];
+ } else if (!empty($proj_lang)) {
+ $lang = $proj_lang;
+ } else {
+ $lang = $fs->prefs['lang_code'];
+ }
+ $emails[$lang][] = $recipient['recipient'];
+ if (!in_array($lang, $languages)) {
+ $languages[] = $lang;
+ }
+ }
+ }
+
+ if (isset($to[1])) {
+ foreach ($to[1] as $recipient) {
+ if (!empty($recipient['lang'])) {
+ $lang = $recipient['lang'];
+ } else if (!empty($proj_lang)) {
+ $lang = $proj_lang;
+ } else {
+ $lang = $fs->prefs['lang_code'];
+ }
+ $jabbers[$lang][] = $recipient['recipient'];
+ if (!in_array($lang, $languages)) {
+ $languages[] = $lang;
+ }
+ }
+ }
+ /*
+ if (isset($to[2])) {
+ foreach ($to[2] as $recipient) {
+ $lang = $recipient['lang'];
+ if ($lang == 'j')
+ echo "<pre>Error 3!</pre>";
+ $onlines[$lang][] = $recipient['recipient'];
+ if (!in_array($lang, $languages)) {
+ $languages[] = $lang;
+ }
+ }
+ }
+ */
+
+ $result = true;
+ foreach ($languages as $lang) {
+ $msg = $this->generateMsg($type, $task_id, $info, $lang);
+ if (isset($emails[$lang]) && ($ntype == NOTIFY_EMAIL || $ntype == NOTIFY_BOTH)) {
+ if (!$this->sendEmail($emails[$lang], $msg[0], $msg[1], $task_id)) {
+ $result = false;
+ }
+ }
+
+ if (isset($jabbers[$lang]) && ($ntype == NOTIFY_JABBER || $ntype == NOTIFY_BOTH)) {
+ if (!$this->storeJabber($jabbers[$lang], $msg[0], $msg[1])) {
+ $result = false;
+ }
+ }
+
+ // Get rid of undefined offset 2 when notify type is explicitly set,
+ // in these cases caller really has not set offset 2. Track down the
+ // callers later.
+ /*
+ if (isset($onlines[$lang]) && ($ntype != NOTIFY_EMAIL && $ntype != NOTIFY_JABBER)) {
+ if (!$this->StoreOnline($onlines[$lang], $msg[2], $msg[3], $task_id)) {
+ $result = false;
+ }
+ }
+ */
+ }
+ return $result;
+
+ // End of Create() function
+ }
+
+ function storeOnline($to, $subject, $body, $online, $task_id = null) {
+ global $db, $fs;
+
+ if (!count($to)) {
+ return false;
+ }
+
+ $date = time();
+
+ // store notification in table
+ $db->query("INSERT INTO {notification_messages}
+ (message_subject, message_body, time_created)
+ VALUES (?, ?, ?)", array($online, '', $date)
+ );
+
+ // grab notification id
+ /*
+ $result = $db->query("SELECT message_id FROM {notification_messages}
+ WHERE time_created = ? ORDER BY message_id DESC", array($date), 1);
+
+ $row = $db->fetchRow($result);
+ $message_id = $row['message_id'];
+ */
+ $message_id = $db->insert_ID();
+ // If message could not be inserted for whatever reason...
+ if (!$message_id) {
+ return false;
+ }
+
+ // make sure every user is only added once
+ settype($to, 'array');
+ $to = array_unique($to);
+
+ foreach ($to as $jid) {
+ // store each recipient in table
+ $db->query("INSERT INTO {notification_recipients}
+ (notify_method, message_id, notify_address)
+ VALUES (?, ?, ?)", array('o', $message_id, $jid)
+ );
+ }
+
+ return true;
+ }
+
+ static function getUnreadNotifications() {
+ global $db, $fs, $user;
+
+ $notifications = $db->query('SELECT r.recipient_id, m.message_subject
+ FROM {notification_recipients} r
+ JOIN {notification_messages} m ON r.message_id = m.message_id
+ WHERE r.notify_method = ? AND notify_address = ?',
+ array('o', $user['user_id']));
+ return $db->fetchAllArray($notifications);
+ }
+
+ static function NotificationsHaveBeenRead($ids) {
+ global $db, $fs, $user;
+
+ $readones = join(",", array_map('intval', $ids));
+ if($readones !=''){
+ $db->query("
+ DELETE FROM {notification_recipients}
+ WHERE message_id IN ($readones)
+ AND notify_method = ?
+ AND notify_address = ?",
+ array('o', $user['user_id']
+ )
+ );
+ }
+ }
+
+ // {{{ Store Jabber messages for sending later
+ function storeJabber( $to, $subject, $body )
+ {
+ global $db, $fs;
+
+ if (empty($fs->prefs['jabber_server'])
+ || empty($fs->prefs['jabber_port'])
+ || empty($fs->prefs['jabber_username'])
+ || empty($fs->prefs['jabber_password'])) {
+ return false;
+ }
+
+ if (empty($to)) {
+ return false;
+ }
+
+ $date = time();
+
+ // store notification in table
+ $db->query("INSERT INTO {notification_messages}
+ (message_subject, message_body, time_created)
+ VALUES (?, ?, ?)",
+ array($subject, $body, $date)
+ );
+
+ // grab notification id
+ /*
+ $result = $db->query("SELECT message_id FROM {notification_messages}
+ WHERE time_created = ? ORDER BY message_id DESC",
+ array($date), 1);
+
+ $row = $db->fetchRow($result);
+ $message_id = $row['message_id'];
+ */
+ $message_id = $db->insert_ID();
+ // If message could not be inserted for whatever reason...
+ if (!$message_id) {
+ return false;
+ }
+
+ settype($to, 'array');
+
+ $duplicates = array();
+ foreach ($to as $jid) {
+ // make sure every recipient is only added once
+ if (in_array($jid, $duplicates)) {
+ continue;
+ }
+ $duplicates[] = $jid;
+ // store each recipient in table
+ $db->query("INSERT INTO {notification_recipients}
+ (notify_method, message_id, notify_address)
+ VALUES (?, ?, ?)",
+ array('j', $message_id, $jid)
+ );
+
+ }
+
+ return true;
+ } // }}}
+
+ static function jabberRequestAuth($email)
+ {
+ global $fs;
+
+ include_once BASEDIR . '/includes/class.jabber2.php';
+
+ if (empty($fs->prefs['jabber_server'])
+ || empty($fs->prefs['jabber_port'])
+ || empty($fs->prefs['jabber_username'])
+ || empty($fs->prefs['jabber_password'])) {
+ return false;
+ }
+
+ $JABBER = new Jabber($fs->prefs['jabber_username'] . '@' . $fs->prefs['jabber_server'],
+ $fs->prefs['jabber_password'],
+ $fs->prefs['jabber_ssl'],
+ $fs->prefs['jabber_port']);
+ $JABBER->login();
+ $JABBER->send("<presence to='" . Jabber::jspecialchars($email) . "' type='subscribe'/>");
+ $JABBER->disconnect();
+ }
+
+ // {{{ send Jabber messages that were stored earlier
+ function sendJabber()
+ {
+ global $db, $fs;
+
+ include_once BASEDIR . '/includes/class.jabber2.php';
+
+ if ( empty($fs->prefs['jabber_server'])
+ || empty($fs->prefs['jabber_port'])
+ || empty($fs->prefs['jabber_username'])
+ || empty($fs->prefs['jabber_password'])) {
+ return false;
+ }
+
+ // get listing of all pending jabber notifications
+ $result = $db->query("SELECT DISTINCT message_id
+ FROM {notification_recipients}
+ WHERE notify_method='j'");
+
+ if (!$db->countRows($result)) {
+ return false;
+ }
+
+ $JABBER = new Jabber($fs->prefs['jabber_username'] . '@' . $fs->prefs['jabber_server'],
+ $fs->prefs['jabber_password'],
+ $fs->prefs['jabber_ssl'],
+ $fs->prefs['jabber_port']);
+ $JABBER->login();
+
+ // we have notifications to process - connect
+ $JABBER->log("We have notifications to process...");
+ $JABBER->log("Starting Jabber session:");
+
+ $ids = array();
+
+ while ( $row = $db->fetchRow($result) ) {
+ $ids[] = $row['message_id'];
+ }
+
+ $desired = join(",", array_map('intval', $ids));
+ $JABBER->log("message ids to send = {" . $desired . "}");
+
+ // removed array usage as it's messing up the select
+ // I suspect this is due to the variable being comma separated
+ // Jamin W. Collins 20050328
+ $notifications = $db->query("
+ SELECT * FROM {notification_messages}
+ WHERE message_id IN ($desired)
+ ORDER BY time_created ASC"
+ );
+ $JABBER->log("number of notifications {" . $db->countRows($notifications) . "}");
+
+ // loop through notifications
+ while ( $notification = $db->fetchRow($notifications) ) {
+ $subject = $notification['message_subject'];
+ $body = $notification['message_body'];
+
+ $JABBER->log("Processing notification {" . $notification['message_id'] . "}");
+ $recipients = $db->query("
+ SELECT * FROM {notification_recipients}
+ WHERE message_id = ?
+ AND notify_method = 'j'",
+ array($notification['message_id'])
+ );
+
+ // loop through recipients
+ while ($recipient = $db->fetchRow($recipients) ) {
+ $jid = $recipient['notify_address'];
+ $JABBER->log("- attempting send to {" . $jid . "}");
+
+ // send notification
+ if ($JABBER->send_message($jid, $body, $subject, 'normal')) {
+ // delete entry from notification_recipients
+ $result = $db->query("DELETE FROM {notification_recipients}
+ WHERE message_id = ?
+ AND notify_method = 'j'
+ AND notify_address = ?",
+ array($notification['message_id'], $jid)
+ );
+ $JABBER->log("- notification sent");
+ } else {
+ $JABBER->log("- notification not sent");
+ }
+ }
+ // check to see if there are still recipients for this notification
+ $result = $db->query("SELECT * FROM {notification_recipients}
+ WHERE message_id = ?",
+ array($notification['message_id'])
+ );
+
+ if ( $db->countRows($result) == 0 ) {
+ $JABBER->log("No further recipients for message id {" . $notification['message_id'] . "}");
+ // remove notification no more recipients
+ $result = $db->query("DELETE FROM {notification_messages}
+ WHERE message_id = ?",
+ array($notification['message_id'])
+ );
+ $JABBER->log("- Notification deleted");
+ }
+ }
+
+ // disconnect from server
+ $JABBER->disconnect();
+ $JABBER->log("Disconnected from Jabber server");
+
+ return true;
+ } // }}}
+ // {{{ send email
+ function sendEmail($to, $subject, $body, $task_id = null)
+ {
+ global $fs, $proj, $user;
+
+ if (empty($to) || empty($to[0])) {
+ return;
+ }
+
+ // Do we want to use a remote mail server?
+ if (!empty($fs->prefs['smtp_server'])) {
+
+ // connection... SSL, TLS or none
+ if ($fs->prefs['email_tls']) {
+ $swiftconn = Swift_SmtpTransport::newInstance($fs->prefs['smtp_server'], 587, 'tls');
+ } else if ($fs->prefs['email_ssl']) {
+ $swiftconn = Swift_SmtpTransport::newInstance($fs->prefs['smtp_server'], 465, 'ssl');
+ } else {
+ $swiftconn = Swift_SmtpTransport::newInstance($fs->prefs['smtp_server']);
+ }
+
+ if ($fs->prefs['smtp_user']) {
+ $swiftconn->setUsername($fs->prefs['smtp_user']);
+ }
+
+ if ($fs->prefs['smtp_pass']){
+ $swiftconn->setPassword($fs->prefs['smtp_pass']);
+ }
+
+ if(defined('FS_SMTP_TIMEOUT')) {
+ $swiftconn->setTimeout(FS_SMTP_TIMEOUT);
+ }
+ // Use php's built-in mail() function
+ } else {
+ $swiftconn = Swift_MailTransport::newInstance();
+ }
+
+ // Make plaintext URLs into hyperlinks, but don't disturb existing ones!
+ $htmlbody = preg_replace("/(?<!\")(https?:\/\/)([a-zA-Z0-9\-.]+\.[a-zA-Z0-9\-]+([\/]([a-zA-Z0-9_\/\-.?&%=+#])*)*)/", '<a href="$1$2">$2</a>', $body);
+ $htmlbody = str_replace("\n","<br>", $htmlbody);
+
+ // Those constants used were introduced in 5.4.
+ if (version_compare(phpversion(), '5.4.0', '<')) {
+ $plainbody= html_entity_decode(strip_tags($body));
+ } else {
+ $plainbody= html_entity_decode(strip_tags($body), ENT_COMPAT | ENT_HTML401, 'utf-8');
+ }
+
+ $swift = Swift_Mailer::newInstance($swiftconn);
+
+ if(defined('FS_MAIL_LOGFILE')) {
+ $logger = new Swift_Plugins_Loggers_ArrayLogger();
+ $swift->registerPlugin(new Swift_Plugins_LoggerPlugin($logger));
+ }
+
+ $message = new Swift_Message($subject);
+ if (isset($fs->prefs['emailNoHTML']) && $fs->prefs['emailNoHTML'] == '1'){
+ $message->setBody($plainbody, 'text/plain');
+ }else{
+ $message->setBody($htmlbody, 'text/html');
+ $message->addPart($plainbody, 'text/plain');
+ }
+
+ $type = $message->getHeaders()->get('Content-Type');
+ $type->setParameter('charset', 'utf-8');
+
+ $message->getHeaders()->addTextHeader('Precedence', 'list');
+ $message->getHeaders()->addTextHeader('X-Mailer', 'Flyspray');
+
+ if ($proj->prefs['notify_reply']) {
+ $message->setReplyTo($proj->prefs['notify_reply']);
+ }
+
+ if (isset($task_id)) {
+ $hostdata = parse_url($GLOBALS['baseurl']);
+ $inreplyto = sprintf('<FS%d@%s>', $task_id, $hostdata['host']);
+ // see http://cr.yp.to/immhf/thread.html this does not seems to work though :(
+ $message->getHeaders()->addTextHeader('In-Reply-To', $inreplyto);
+ $message->getHeaders()->addTextHeader('References', $inreplyto);
+ }
+
+ // accepts string, array, or Swift_Address
+ if( is_array($to) && count($to)>1 ){
+ $message->setTo($fs->prefs['admin_email']);
+ $message->setBcc($to);
+ } else{
+ $message->setTo($to);
+ }
+ $message->setFrom(array($fs->prefs['admin_email'] => $proj->prefs['project_title']));
+ $swift->send($message);
+
+ if(defined('FS_MAIL_LOGFILE')) {
+ if(is_writable(dirname(FS_MAIL_LOGFILE))) {
+ if($fh = fopen(FS_MAIL_LOGFILE, 'ab')) {
+ fwrite($fh, $logger->dump());
+ fwrite($fh, php_uname());
+ fclose($fh);
+ }
+ }
+ }
+
+ return true;
+ } //}}}
+ // {{{ create a message for any occasion
+ function generateMsg($type, $task_id, $arg1 = '0', $lang) {
+ global $db, $fs, $user, $proj;
+
+ // Get the task details
+ $task_details = Flyspray::getTaskDetails($task_id);
+ if ($task_id) {
+ $proj = new Project($task_details['project_id']);
+ }
+
+ // Set the due date correctly
+ if ($task_details['due_date'] == '0') {
+ $due_date = tL('undecided', $lang);
+ } else {
+ $due_date = formatDate($task_details['due_date']);
+ }
+
+ // Set the due version correctly
+ if ($task_details['closedby_version'] == '0') {
+ $task_details['due_in_version_name'] = tL('undecided', $lang);
+ }
+
+ // Get the string of modification
+ $notify_type_msg = array(
+ 0 => tL('none'),
+ NOTIFY_TASK_OPENED => tL('taskopened', $lang),
+ NOTIFY_TASK_CHANGED => tL('pm.taskchanged', $lang),
+ NOTIFY_TASK_CLOSED => tL('taskclosed', $lang),
+ NOTIFY_TASK_REOPENED => tL('pm.taskreopened', $lang),
+ NOTIFY_DEP_ADDED => tL('pm.depadded', $lang),
+ NOTIFY_DEP_REMOVED => tL('pm.depremoved', $lang),
+ NOTIFY_COMMENT_ADDED => tL('commentadded', $lang),
+ NOTIFY_ATT_ADDED => tL('attachmentadded', $lang),
+ NOTIFY_REL_ADDED => tL('relatedadded', $lang),
+ NOTIFY_OWNERSHIP => tL('ownershiptaken', $lang),
+ NOTIFY_PM_REQUEST => tL('pmrequest', $lang),
+ NOTIFY_PM_DENY_REQUEST => tL('pmrequestdenied', $lang),
+ NOTIFY_NEW_ASSIGNEE => tL('newassignee', $lang),
+ NOTIFY_REV_DEP => tL('revdepadded', $lang),
+ NOTIFY_REV_DEP_REMOVED => tL('revdepaddedremoved', $lang),
+ NOTIFY_ADDED_ASSIGNEES => tL('assigneeadded', $lang),
+ );
+
+ // Generate the nofication message
+ if (isset($proj->prefs['notify_subject']) && !$proj->prefs['notify_subject']) {
+ $proj->prefs['notify_subject'] = '[%p][#%t] %s';
+ }
+ if (!isset($proj->prefs['notify_subject']) ||
+ $type == NOTIFY_CONFIRMATION ||
+ $type == NOTIFY_ANON_TASK ||
+ $type == NOTIFY_PW_CHANGE ||
+ $type == NOTIFY_NEW_USER ||
+ $type == NOTIFY_OWN_REGISTRATION) {
+ $subject = tL('notifyfromfs', $lang);
+ } else {
+ $subject = strtr($proj->prefs['notify_subject'], array('%p' => $proj->prefs['project_title'],
+ '%s' => $task_details['item_summary'],
+ '%t' => $task_id,
+ '%a' => $notify_type_msg[$type],
+ '%u' => $user->infos['user_name']));
+ }
+
+ $subject = strtr($subject, "\n", '');
+
+
+ /* -------------------------------
+ | List of notification types: |
+ | 1. Task opened |
+ | 2. Task details changed |
+ | 3. Task closed |
+ | 4. Task re-opened |
+ | 5. Dependency added |
+ | 6. Dependency removed |
+ | 7. Comment added |
+ | 8. Attachment added |
+ | 9. Related task added |
+ |10. Taken ownership |
+ |11. Confirmation code |
+ |12. PM request |
+ |13. PM denied request |
+ |14. New assignee |
+ |15. Reversed dep |
+ |16. Reversed dep removed |
+ |17. Added to assignees list |
+ |18. Anon-task opened |
+ |19. Password change |
+ |20. New user |
+ |21. User registration |
+ -------------------------------
+ */
+
+ $body = tL('donotreply', $lang) . "\n\n";
+ $online = '';
+
+ // {{{ New task opened
+ if ($type == NOTIFY_TASK_OPENED) {
+ $body .= tL('newtaskopened', $lang) . " \n\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ") \n\n";
+ $body .= tL('attachedtoproject', $lang) . ' - ' . $task_details['project_title'] . "\n";
+ $body .= tL('summary', $lang) . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('tasktype', $lang) . ' - ' . $task_details['tasktype_name'] . "\n";
+ $body .= tL('category', $lang) . ' - ' . $task_details['category_name'] . "\n";
+ $body .= tL('status', $lang) . ' - ' . $task_details['status_name'] . "\n";
+ $body .= tL('assignedto', $lang) . ' - ' . implode(', ', $task_details['assigned_to_name']) . "\n";
+ $body .= tL('operatingsystem', $lang) . ' - ' . $task_details['os_name'] . "\n";
+ $body .= tL('severity', $lang) . ' - ' . $task_details['severity_name'] . "\n";
+ $body .= tL('priority', $lang) . ' - ' . $task_details['priority_name'] . "\n";
+ $body .= tL('reportedversion', $lang) . ' - ' . $task_details['reported_version_name'] . "\n";
+ $body .= tL('dueinversion', $lang) . ' - ' . $task_details['due_in_version_name'] . "\n";
+ $body .= tL('duedate', $lang) . ' - ' . $due_date . "\n";
+ $body .= tL('details', $lang) . ' - ' . $task_details['detailed_desc'] . "\n\n";
+
+ if ($arg1 == 'files') {
+ $body .= tL('fileaddedtoo', $lang) . "\n\n";
+ $subject .= ' (' . tL('attachmentadded', $lang) . ')';
+ }
+
+ $body .= tL('moreinfo', $lang) . "\n";
+ $body .= createURL('details', $task_id);
+
+ $online .= tL('newtaskopened', $lang) . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ $online .= tL('attachedtoproject', $lang) . ' - ' . $task_details['project_title'] . ". ";
+ $online .= tL('summary', $lang) . ' - ' . $task_details['item_summary'];
+ } // }}}
+ // {{{ Task details changed
+ if ($type == NOTIFY_TASK_CHANGED) {
+ $translation = array('priority_name' => tL('priority', $lang),
+ 'severity_name' => tL('severity', $lang),
+ 'status_name' => tL('status', $lang),
+ 'assigned_to_name' => tL('assignedto', $lang),
+ 'due_in_version_name' => tL('dueinversion', $lang),
+ 'reported_version_name' => tL('reportedversion', $lang),
+ 'tasktype_name' => tL('tasktype', $lang),
+ 'os_name' => tL('operatingsystem', $lang),
+ 'category_name' => tL('category', $lang),
+ 'due_date' => tL('duedate', $lang),
+ 'percent_complete' => tL('percentcomplete', $lang),
+ 'mark_private' => tL('visibility', $lang),
+ 'item_summary' => tL('summary', $lang),
+ 'detailed_desc' => tL('taskedited', $lang),
+ 'project_title' => tL('attachedtoproject', $lang),
+ 'estimated_effort' => tL('estimatedeffort', $lang));
+
+ $body .= tL('taskchanged', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ': ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n";
+
+ $online .= tL('taskchanged', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'];
+
+ foreach ($arg1 as $change) {
+ if ($change[0] == 'assigned_to_name') {
+ $change[1] = implode(', ', $change[1]);
+ $change[2] = implode(', ', $change[2]);
+ }
+
+ if ($change[0] == 'detailed_desc') {
+ $body .= $translation[$change[0]] . ":\n-------\n" . $change[2] . "\n-------\n";
+ } else {
+ $body .= $translation[$change[0]] . ': ' . ( ($change[1]) ? $change[1] : '[-]' ) . ' -> ' . ( ($change[2]) ? $change[2] : '[-]' ) . "\n";
+ }
+ }
+ $body .= "\n" . tL('moreinfo', $lang) . "\n";
+ $body .= createURL('details', $task_id);
+ } // }}}
+ // {{{ Task closed
+ if ($type == NOTIFY_TASK_CLOSED) {
+ $body .= tL('notify.taskclosed', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n\n";
+ $body .= tL('reasonforclosing', $lang) . ' ' . $task_details['resolution_name'] . "\n";
+
+ if (!empty($task_details['closure_comment'])) {
+ $body .= tL('closurecomment', $lang) . ' ' . $task_details['closure_comment'] . "\n\n";
+ }
+
+ $body .= tL('moreinfo', $lang) . "\n";
+ $body .= createURL('details', $task_id);
+
+ $online .= tL('notify.taskclosed', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ Task re-opened
+ if ($type == NOTIFY_TASK_REOPENED) {
+ $body .= tL('notify.taskreopened', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n\n";
+ $body .= tL('moreinfo', $lang) . "\n";
+ $body .= createURL('details', $task_id);
+
+ $online .= tL('notify.taskreopened', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ Dependency added
+ if ($type == NOTIFY_DEP_ADDED) {
+ $depend_task = Flyspray::getTaskDetails($arg1);
+
+ $body .= tL('newdep', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n";
+ $body .= createURL('details', $task_id) . "\n\n\n";
+ $body .= tL('newdepis', $lang) . ':' . "\n\n";
+ $body .= 'FS#' . $depend_task['task_id'] . ' - ' . $depend_task['item_summary'] . "\n";
+ $body .= createURL('details', $depend_task['task_id']);
+
+ $online .= tL('newdep', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ Dependency removed
+ if ($type == NOTIFY_DEP_REMOVED) {
+ $depend_task = Flyspray::getTaskDetails($arg1);
+
+ $body .= tL('notify.depremoved', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n";
+ $body .= createURL('details', $task_id) . "\n\n\n";
+ $body .= tL('removeddepis', $lang) . ':' . "\n\n";
+ $body .= 'FS#' . $depend_task['task_id'] . ' - ' . $depend_task['item_summary'] . "\n";
+ $body .= createURL('details', $depend_task['task_id']);
+
+ $online .= tL('notify.depremoved', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ Comment added
+ if ($type == NOTIFY_COMMENT_ADDED) {
+ // Get the comment information
+ $result = $db->query("SELECT comment_id, comment_text
+ FROM {comments}
+ WHERE user_id = ?
+ AND task_id = ?
+ ORDER BY comment_id DESC", array($user->id, $task_id), '1');
+ $comment = $db->fetchRow($result);
+
+ $body .= tL('notify.commentadded', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n\n";
+ $body .= "----------\n";
+ $body .= $comment['comment_text'] . "\n";
+ $body .= "----------\n\n";
+
+ if ($arg1 == 'files') {
+ $body .= tL('fileaddedtoo', $lang) . "\n\n";
+ $subject .= ' (' . tL('attachmentadded', $lang) . ')';
+ }
+
+ $body .= tL('moreinfo', $lang) . "\n";
+ $body .= createURL('details', $task_id) . '#comment' . $comment['comment_id'];
+
+ $online .= tL('notify.commentadded', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ Attachment added
+ if ($type == NOTIFY_ATT_ADDED) {
+ $body .= tL('newattachment', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n\n";
+ $body .= tL('moreinfo', $lang) . "\n";
+ $body .= createURL('details', $task_id);
+
+ $online .= tL('newattachment', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ Related task added
+ if ($type == NOTIFY_REL_ADDED) {
+ $related_task = Flyspray::getTaskDetails($arg1);
+
+ $body .= tL('notify.relatedadded', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n";
+ $body .= createURL('details', $task_id) . "\n\n\n";
+ $body .= tL('relatedis', $lang) . ':' . "\n\n";
+ $body .= 'FS#' . $related_task['task_id'] . ' - ' . $related_task['item_summary'] . "\n";
+ $body .= createURL('details', $related_task['task_id']);
+
+ $online .= tL('notify.relatedadded', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ Ownership taken
+ if ($type == NOTIFY_OWNERSHIP) {
+ $body .= implode(', ', $task_details['assigned_to_name']) . ' ' . tL('takenownership', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n\n";
+ $body .= tL('moreinfo', $lang) . "\n";
+ $body .= createURL('details', $task_id);
+
+ $online .= implode(', ', $task_details['assigned_to_name']) . ' ' . tL('takenownership', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ".";
+ } // }}}
+ // {{{ Confirmation code
+ if ($type == NOTIFY_CONFIRMATION) {
+ $body .= tL('noticefrom', $lang) . " {$proj->prefs['project_title']}\n\n"
+ . tL('addressused', $lang) . "\n\n"
+ . " {$arg1[0]}index.php?do=register&magic_url={$arg1[1]} \n\n"
+ // In case that spaces in the username have been removed
+ . tL('username', $lang) . ': ' . $arg1[2] . "\n"
+ . tL('confirmcodeis', $lang) . " $arg1[3] \n\n";
+
+ $online = $body;
+ } // }}}
+ // {{{ Pending PM request
+ if ($type == NOTIFY_PM_REQUEST) {
+ $body .= tL('requiresaction', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho') . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n\n";
+ $body .= tL('moreinfo', $lang) . "\n";
+ $body .= createURL('details', $task_id);
+
+ $online .= tL('requiresaction', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ PM request denied
+ if ($type == NOTIFY_PM_DENY_REQUEST) {
+ $body .= tL('pmdeny', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n\n";
+ $body .= tL('denialreason', $lang) . ':' . "\n";
+ $body .= $arg1 . "\n\n";
+ $body .= tL('moreinfo', $lang) . "\n";
+ $body .= createURL('details', $task_id);
+
+ $online .= tL('pmdeny', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ New assignee
+ if ($type == NOTIFY_NEW_ASSIGNEE) {
+ $body .= tL('assignedtoyou', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n\n";
+ $body .= tL('moreinfo', $lang) . "\n";
+ $body .= createURL('details', $task_id);
+
+ $online .= tL('assignedtoyou', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ Reversed dep
+ if ($type == NOTIFY_REV_DEP) {
+ $depend_task = Flyspray::getTaskDetails($arg1);
+
+ $body .= tL('taskwatching', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n";
+ $body .= createURL('details', $task_id) . "\n\n\n";
+ $body .= tL('isdepfor', $lang) . ':' . "\n\n";
+ $body .= 'FS#' . $depend_task['task_id'] . ' - ' . $depend_task['item_summary'] . "\n";
+ $body .= createURL('details', $depend_task['task_id']);
+
+ $online .= tL('taskwatching', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ Reversed dep - removed
+ if ($type == NOTIFY_REV_DEP_REMOVED) {
+ $depend_task = Flyspray::getTaskDetails($arg1);
+
+ $body .= tL('taskwatching', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n";
+ $body .= createURL('details', $task_id) . "\n\n\n";
+ $body .= tL('isnodepfor', $lang) . ':' . "\n\n";
+ $body .= 'FS#' . $depend_task['task_id'] . ' - ' . $depend_task['item_summary'] . "\n";
+ $body .= createURL('details', $depend_task['task_id']);
+
+ $online .= tL('taskwatching', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ User added to assignees list
+ if ($type == NOTIFY_ADDED_ASSIGNEES) {
+ $body .= tL('useraddedtoassignees', $lang) . "\n\n";
+ $body .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . "\n";
+ $body .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . ")\n";
+ $body .= createURL('details', $task_id);
+
+ $online .= tL('useraddedtoassignees', $lang) . ". ";
+ $online .= 'FS#' . $task_id . ' - ' . $task_details['item_summary'] . ". ";
+ $online .= tL('userwho', $lang) . ' - ' . $user->infos['real_name'] . ' (' . $user->infos['user_name'] . "). ";
+ } // }}}
+ // {{{ Anon-task has been opened
+ if ($type == NOTIFY_ANON_TASK) {
+ $body .= tL('thankyouforbug', $lang) . "\n\n";
+ $body .= createURL('details', $task_id, null, array('task_token' => $arg1)) . "\n\n";
+
+ $online .= tL('thankyouforbug') . "";
+ } // }}}
+ // {{{ Password change
+ if ($type == NOTIFY_PW_CHANGE) {
+ $body = tL('magicurlmessage', $lang) . " \n"
+ . "{$arg1[0]}index.php?do=lostpw&magic_url=$arg1[1]\n\n"
+ . tL('messagefrom', $lang) . $arg1[0];
+ $online = $body;
+ } // } }}
+ // {{{ New user
+ if ($type == NOTIFY_NEW_USER) {
+ $body = tL('newuserregistered', $lang) . " \n\n"
+ . tL('username', $lang) . ': ' . $arg1[1] . "\n" .
+ tL('realname', $lang) . ': ' . $arg1[2] . "\n";
+ $online = $body;
+
+ if ($arg1[6]) {
+ $body .= tL('password', $lang) . ': ' . $arg1[5] . "\n";
+ }
+
+ $body .= tL('emailaddress', $lang) . ': ' . $arg1[3] . "\n";
+ $body .= tL('jabberid', $lang) . ':' . $arg1[4] . "\n\n";
+ $body .= tL('messagefrom', $lang) . $arg1[0];
+ } // }}}
+ // {{{ New user him/herself
+ if ($type == NOTIFY_OWN_REGISTRATION) {
+ $body = tL('youhaveregistered', $lang) . " \n\n"
+ . tL('username', $lang) . ': ' . $arg1[1] . "\n" .
+ tL('realname', $lang) . ': ' . $arg1[2] . "\n";
+ $online = $body;
+
+ if ($arg1[6]) {
+ $body .= tL('password', $lang) . ': ' . $arg1[5] . "\n";
+ }
+
+ $body .= tL('emailaddress', $lang) . ': ' . $arg1[3] . "\n";
+ $body .= tL('jabberid', $lang) . ':' . $arg1[4] . "\n\n";
+
+ // Add something here to tell the user whether the registration must
+ // first be accepted by Administrators or not. And if it had and was
+ // rejected, the reason. Check first what happening when requests are
+ // either denied or accepted.
+
+ $body .= tL('messagefrom', $lang) . $arg1[0];
+ } // }}}
+
+ $body .= "\n\n" . tL('disclaimer', $lang);
+ return array(Notifications::fixMsgData($subject), Notifications::fixMsgData($body), $online);
+ }
+
+// }}}
+
+ public static function assignRecipients($recipients, &$emails, &$jabbers, &$onlines, $ignoretype = false) {
+ global $db, $fs, $user;
+
+ if (!is_array($recipients)) {
+ return false;
+ }
+
+ foreach ($recipients as $recipient) {
+ if ($recipient['user_id'] == $user->id && !$user->infos['notify_own']) {
+ continue;
+ }
+
+ if (($fs->prefs['user_notify'] == '1' && ($recipient['notify_type'] == NOTIFY_EMAIL || $recipient['notify_type'] == NOTIFY_BOTH) ) || $fs->prefs['user_notify'] == '2' || $ignoretype) {
+ if (isset($recipient['email_address']) && !empty($recipient['email_address'])) {
+ $emails[$recipient['email_address']] = array('recipient' => $recipient['email_address'], 'lang' => $recipient['lang_code']);
+ }
+ }
+
+ if (($fs->prefs['user_notify'] == '1' && ($recipient['notify_type'] == NOTIFY_JABBER || $recipient['notify_type'] == NOTIFY_BOTH) ) || $fs->prefs['user_notify'] == '3' || $ignoretype) {
+ if (isset($recipient['jabber_id']) && !empty($recipient['jabber_id']) && $recipient['jabber_id']) {
+ $jabbers[$recipient['jabber_id']] = array('recipient' => $recipient['jabber_id'], 'lang' => $recipient['lang_code']);
+ }
+ }
+ /*
+ if ($fs->prefs['user_notify'] == '1' && $recipient['notify_online']) {
+ $onlines[$recipient['user_id']] = array('recipient' => $recipient['user_id'], 'lang' => $recipient['lang_code']);
+ }
+ */
+ }
+ }
+
+ // {{{ Create an address list for specific users
+ function specificAddresses($users, $ignoretype = false) {
+ global $db, $fs, $user;
+
+ $emails = array();
+ $jabbers = array();
+ $onlines = array();
+
+ if (!is_array($users)) {
+ settype($users, 'array');
+ }
+
+ if (count($users) < 1) {
+ return array();
+ }
+
+ $sql = $db->query('SELECT u.user_id, u.email_address, u.jabber_id,
+ u.notify_online, u.notify_type, u.notify_own, u.lang_code
+ FROM {users} u
+ WHERE' . substr(str_repeat(' user_id = ? OR ', count($users)), 0, -3), array_values($users));
+
+ self::assignRecipients($db->fetchAllArray($sql), $emails, $jabbers, $onlines, $ignoretype);
+
+ return array($emails, $jabbers, $onlines);
+ }
+
+// }}}
+
+ // {{{ Create a standard address list of users (assignees, notif tab and proj addresses)
+ function address($task_id, $type) {
+ global $db, $fs, $proj, $user;
+
+ $users = array();
+ $emails = array();
+ $jabbers = array();
+ $onlines = array();
+
+ $task_details = Flyspray::getTaskDetails($task_id);
+
+ // Get list of users from the notification tab
+ $get_users = $db->query('
+ SELECT * FROM {notifications} n
+ LEFT JOIN {users} u ON n.user_id = u.user_id
+ WHERE n.task_id = ?',
+ array($task_id)
+ );
+ self::assignRecipients($db->fetchAllArray($get_users), $emails, $jabbers, $onlines);
+
+ // Get list of assignees
+ $get_users = $db->query('
+ SELECT * FROM {assigned} a
+ LEFT JOIN {users} u ON a.user_id = u.user_id
+ WHERE a.task_id = ?',
+ array($task_id)
+ );
+ self::assignRecipients($db->fetchAllArray($get_users), $emails, $jabbers, $onlines);
+
+ // Now, we add the project contact addresses...
+ // ...but only if the task is public
+ if ($task_details['mark_private'] != '1'
+ && in_array($type, Flyspray::int_explode(' ', $proj->prefs['notify_types']))) {
+
+ // FIXME! Have to find users preferred language here too,
+ // must fetch from database. But the address could also be a mailing
+ // list address and user not exist in database, use fs->prefs in that case,
+
+ $proj_emails = preg_split('/[\s,;]+/', $proj->prefs['notify_email'], -1, PREG_SPLIT_NO_EMPTY);
+ $desired = implode("','", $proj_emails);
+ if($desired !=''){
+ $get_users = $db->query("
+ SELECT DISTINCT u.user_id, u.email_address, u.jabber_id,
+ u.notify_online, u.notify_type, u.notify_own, u.lang_code
+ FROM {users} u
+ WHERE u.email_address IN ('$desired')"
+ );
+
+ self::assignRecipients($db->fetchAllArray($get_users), $emails, $jabbers, $onlines);
+ }
+
+ $proj_jids = explode(',', $proj->prefs['notify_jabber']);
+ $desired = implode("','", $proj_jids);
+ if($desired!='') {
+ $get_users = $db->query("
+ SELECT DISTINCT u.user_id, u.email_address, u.jabber_id,
+ u.notify_online, u.notify_type, u.notify_own, u.lang_code
+ FROM {users} u
+ WHERE u.jabber_id IN ('$desired')"
+ );
+ self::assignRecipients($db->fetchAllArray($get_users), $emails, $jabbers, $onlines);
+ }
+
+ // Now, handle notification addresses that are not assigned to any user...
+ foreach ($proj_emails as $email) {
+ if (!array_key_exists($email, $emails)) {
+ $emails[$email] = array('recipient' => $email, 'lang' => $fs->prefs['lang_code']);
+ }
+ }
+
+ foreach ($proj_jids as $jabber) {
+ if (!array_key_exists($jabber, $jabbers)) {
+ $jabbers[$jabber] = array('recipient' => $jabber, 'lang' => $fs->prefs['lang_code']);
+ }
+ }
+ /*
+ echo "<pre>";
+ echo var_dump($proj_emails);
+ echo var_dump($proj_jids);
+ echo "</pre>";
+ */
+ // End of checking if a task is private
+ }
+ // Send back three arrays containing the notification addresses
+ return array($emails, $jabbers, $onlines);
+ }
+
+// }}}
+
+ // {{{ Fix the message data
+ /**
+ * fixMsgData
+ * a 0.9.9.x ONLY workaround for the "truncated email problem"
+ * based on code Henri Sivonen (http://hsivonen.iki.fi)
+ * @param mixed $data
+ * @access public
+ * @return void
+ */
+ function fixMsgData($data)
+ {
+ // at the first step, remove all NUL bytes
+ //users with broken databases encoding can give us this :(
+ $data = str_replace(chr(0), '', $data);
+
+ //then remove all invalid utf8 secuences
+ $UTF8_BAD =
+ '([\x00-\x7F]'. # ASCII (including control chars)
+ '|[\xC2-\xDF][\x80-\xBF]'. # non-overlong 2-byte
+ '|\xE0[\xA0-\xBF][\x80-\xBF]'. # excluding overlongs
+ '|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}'. # straight 3-byte
+ '|\xED[\x80-\x9F][\x80-\xBF]'. # excluding surrogates
+ '|\xF0[\x90-\xBF][\x80-\xBF]{2}'. # planes 1-3
+ '|[\xF1-\xF3][\x80-\xBF]{3}'. # planes 4-15
+ '|\xF4[\x80-\x8F][\x80-\xBF]{2}'. # plane 16
+ '|(.{1}))'; # invalid byte
+
+ $valid_data = '';
+
+ while (preg_match('/'.$UTF8_BAD.'/S', $data, $matches)) {
+ if ( !isset($matches[2])) {
+ $valid_data .= $matches[0];
+ } else {
+ $valid_data .= '?';
+ }
+ $data = substr($data, strlen($matches[0]));
+ }
+ return $valid_data;
+ } //}}}
+
+// End of Notify class
+}
diff --git a/includes/class.project.php b/includes/class.project.php
new file mode 100644
index 0000000..55a4231
--- /dev/null
+++ b/includes/class.project.php
@@ -0,0 +1,474 @@
+<?php
+
+class Project
+{
+ var $id = 0;
+ var $prefs = array();
+
+ function __construct($id)
+ {
+ global $db, $fs;
+
+ if (is_numeric($id)) {
+ $sql = $db->query("SELECT p.*, c.content AS pm_instructions, c.last_updated AS cache_update
+ FROM {projects} p
+ LEFT JOIN {cache} c ON c.topic = p.project_id AND c.type = 'msg'
+ WHERE p.project_id = ?", array($id));
+ if ($db->countRows($sql)) {
+ $this->prefs = $db->fetchRow($sql);
+ $this->id = (int) $this->prefs['project_id'];
+ $sortrules=explode(',', $this->prefs['default_order_by']);
+ foreach($sortrules as $rule){
+ $last_space=strrpos($rule, ' ');
+ if ($last_space === false){
+ # temporarly
+ $sorting[]=array('field'=>$rule, 'dir'=> $this->prefs['default_order_by_dir']);
+ # future - when column default_order_by_dir removed from project table:
+ #$sorting[]=array('field'=>$rule, 'dir'=>'desc');
+ }else{
+ $sorting[]=array(
+ 'field'=>trim(substr($rule, 0, $last_space)),
+ 'dir'=>trim(substr($rule, $last_space))
+ );
+ }
+ }
+ # using an extra name until default_order_by_dir completely removed
+ $this->prefs['sorting']=$sorting; # we can use this also for highlighting in template which columns are sorted by default in task list!
+
+ # For users with only the 'modify_own_tasks' permission within the project
+ # Currently hardcoded here to have it available for Flyspray1.0. May move to dbfield in future.
+ $this->prefs['basic_fields']=array(
+ 'item_summary',
+ 'detailed_desc',
+ 'task_type',
+ 'product_category',
+ 'operating_system',
+ 'task_severity',
+ 'percent_complete',
+ 'product_version',
+ 'estimated_effort'
+ );
+
+ return;
+ }
+ }
+
+ $this->id = 0;
+ $this->prefs['project_title'] = L('allprojects');
+ $this->prefs['feed_description'] = L('feedforall');
+ $this->prefs['theme_style'] = $fs->prefs['global_theme'];
+ $this->prefs['default_entry'] = $fs->prefs['default_entry'];
+ $this->prefs['lang_code'] = $fs->prefs['lang_code'];
+ $this->prefs['project_is_active'] = 1;
+ $this->prefs['others_view'] = 1;
+ $this->prefs['others_viewroadmap'] = 0;
+ $this->prefs['intro_message'] = '';
+ $this->prefs['anon_open'] = 0;
+ $this->prefs['feed_img_url'] = '';
+ $this->prefs['notify_reply'] = '';
+ $this->prefs['default_due_version'] = 'Undecided';
+ $this->prefs['disable_lostpw'] = 0;
+ $this->prefs['disable_changepw'] = 0;
+ $this->prefs['hours_per_manday'] = 0;
+ $this->prefs['estimated_effort_format'] = 0;
+ $this->prefs['current_effort_done_format'] = 0;
+ $this->prefs['custom_style']= $fs->prefs['custom_style'];
+
+ $sortrules=explode(',', $fs->prefs['default_order_by']);
+ foreach($sortrules as $rule){
+ $last_space=strrpos($rule, ' ');
+ if ($last_space === false){
+ # temporarly
+ $sorting[]=array('field'=>$rule, 'dir'=> $fs->prefs['default_order_by_dir']);
+ # future - when column default_order_by_dir removed from project table:
+ #$sorting[]=array('field'=>$rule, 'dir'=>'desc');
+ }else{
+ $sorting[]=array(
+ 'field'=>trim(substr($rule, 0, $last_space)),
+ 'dir'=>trim(substr($rule, $last_space))
+ );
+ }
+ }
+ # using an extra name until default_order_by_dir completely removed
+ $this->prefs['sorting']=$sorting;
+ }
+
+ # 20150219 peterdd: deprecated
+ function setCookie()
+ {
+ # 20150219 peterdd: unnecessary, setting and using a projectid-cookie makes parallel handling of 2 or more projects in different browser tabs impossible.
+ # instead, use form variables or variables from the url!
+ #Flyspray::setCookie('flyspray_project', $this->id);
+ }
+
+ /**
+ * private method
+ */
+ function _pm_list_sql($type, $join)
+ {
+ global $db;
+
+ // deny the possibility of shooting ourselves in the foot.
+ // although there is no risky usage atm, the api should never do unexpected things.
+ if(preg_match('![^A-Za-z0-9_]!', $type)) {
+ return '';
+ }
+ // Get the column names of list tables for the group by statement
+ $groupby = $db->getColumnNames('{list_' . $type . '}', 'l.' . $type . '_id', 'l.');
+
+ $join = 't.'.join(" = l.{$type}_id OR t.", $join)." = l.{$type}_id";
+
+ return "SELECT l.*, COUNT(t.task_id) AS used_in_tasks, COUNT(CASE t.is_closed WHEN 0 THEN 1 ELSE NULL END) AS opentasks, COUNT(CASE t.is_closed WHEN 1 THEN 1 ELSE NULL END) AS closedtasks
+ FROM {list_{$type}} l
+ LEFT JOIN {tasks} t ON ($join) AND (l.project_id=0 OR t.project_id = l.project_id)
+ WHERE l.project_id = ?
+ GROUP BY $groupby
+ ORDER BY list_position";
+ }
+
+ /**
+ * private method
+ *
+ * @param mixed $type
+ * @param mixed $where
+ * @access protected
+ * @return string
+ * @notes The $where parameter is dangerous, think twice what you pass there..
+ */
+ function _list_sql($type, $where = null)
+ {
+ // sanity check.
+ if(preg_match('![^A-Za-z0-9_]!', $type)) {
+ return '';
+ }
+
+ return "SELECT {$type}_id, {$type}_name
+ FROM {list_{$type}}
+ WHERE show_in_list = 1 AND ( project_id = ? OR project_id = 0 )
+ $where
+ ORDER BY list_position";
+ }
+
+ function listTaskTypes($pm = false)
+ {
+ global $db;
+ if ($pm) {
+ return $db->cached_query(
+ 'pm_task_types'.$this->id,
+ $this->_pm_list_sql('tasktype', array('task_type')),
+ array($this->id));
+ } else {
+ return $db->cached_query(
+ 'task_types'.$this->id, $this->_list_sql('tasktype'), array($this->id));
+ }
+ }
+
+ function listOs($pm = false)
+ {
+ global $db;
+ if ($pm) {
+ return $db->cached_query(
+ 'pm_os'.$this->id,
+ $this->_pm_list_sql('os', array('operating_system')),
+ array($this->id));
+ } else {
+ return $db->cached_query('os'.$this->id, $this->_list_sql('os'),
+ array($this->id));
+ }
+ }
+
+ function listVersions($pm = false, $tense = null, $reported_version = null)
+ {
+ global $db;
+
+ $params = array($this->id);
+
+ if (is_null($tense)) {
+ $where = '';
+ } else {
+ $where = 'AND version_tense = ?';
+ $params[] = $tense;
+ }
+
+ if ($pm) {
+ return $db->cached_query(
+ 'pm_version'.$this->id,
+ $this->_pm_list_sql('version', array('product_version', 'closedby_version')),
+ array($params[0]));
+ } elseif (is_null($reported_version)) {
+ return $db->cached_query(
+ 'version_'.$tense,
+ $this->_list_sql('version', $where),
+ $params);
+ } else {
+ $params[] = $reported_version;
+ return $db->cached_query(
+ 'version_'.$tense,
+ $this->_list_sql('version', $where . ' OR version_id = ?'),
+ $params);
+ }
+ }
+
+
+ function listCategories($project_id = null, $hide_hidden = true, $remove_root = true, $depth = true)
+ {
+ global $db, $conf;
+
+ // start with a empty arrays
+ $right = array();
+ $cats = array();
+ $g_cats = array();
+
+ // null = categories of current project + global project, int = categories of specific project
+ if (is_null($project_id)) {
+ $project_id = $this->id;
+ if ($this->id != 0) {
+ $g_cats = $this->listCategories(0);
+ }
+ }
+
+ // retrieve the left and right value of the root node
+ $result = $db->query("SELECT lft, rgt
+ FROM {list_category}
+ WHERE category_name = 'root' AND lft = 1 AND project_id = ?",
+ array($project_id));
+ $row = $db->fetchRow($result);
+
+ $groupby = $db->getColumnNames('{list_category}', 'c.category_id', 'c.');
+
+ // now, retrieve all descendants of the root node
+ $result = $db->query('SELECT c.category_id, c.category_name, c.*, count(t.task_id) AS used_in_tasks
+ FROM {list_category} c
+ LEFT JOIN {tasks} t ON (t.product_category = c.category_id)
+ WHERE c.project_id = ? AND lft BETWEEN ? AND ?
+ GROUP BY ' . $groupby . '
+ ORDER BY lft ASC',
+ array($project_id, intval($row['lft']), intval($row['rgt'])));
+
+ while ($row = $db->fetchRow($result)) {
+ if ($hide_hidden && !$row['show_in_list'] && $row['lft'] != 1) {
+ continue;
+ }
+
+ // check if we should remove a node from the stack
+ while (count($right) > 0 && $right[count($right)-1] < $row['rgt']) {
+ array_pop($right);
+ }
+ $cats[] = $row + array('depth' => count($right)-1);
+
+ // add this node to the stack
+ $right[] = $row['rgt'];
+ }
+
+ // Adjust output for select boxes
+ if ($depth) {
+ foreach ($cats as $key => $cat) {
+ if ($cat['depth'] > 0) {
+ $cats[$key]['category_name'] = str_repeat('...', $cat['depth']) . $cat['category_name'];
+ $cats[$key]['1'] = str_repeat('...', $cat['depth']) . $cat['1'];
+ }
+ }
+ }
+
+ if ($remove_root) {
+ unset($cats[0]);
+ }
+
+ return array_merge($cats, $g_cats);
+ }
+
+ function listResolutions($pm = false)
+ {
+ global $db;
+ if ($pm) {
+ return $db->cached_query(
+ 'pm_resolutions'.$this->id,
+ $this->_pm_list_sql('resolution', array('resolution_reason')),
+ array($this->id));
+ } else {
+ return $db->cached_query('resolution'.$this->id,
+ $this->_list_sql('resolution'), array($this->id));
+ }
+ }
+
+ function listTaskStatuses($pm = false)
+ {
+ global $db;
+ if ($pm) {
+ return $db->cached_query(
+ 'pm_statuses'.$this->id,
+ $this->_pm_list_sql('status', array('item_status')),
+ array($this->id));
+ } else {
+ return $db->cached_query('status'.$this->id,
+ $this->_list_sql('status'), array($this->id));
+ }
+ }
+
+ /* between FS0.9.9.7 to FS1.0alpha2 */
+ /*
+ function listTags($pm = false)
+ {
+ global $db;
+ if ($pm) {
+ $result= $db->query('SELECT tag AS tag_name, 1 AS list_position, 1 AS show_in_list, COUNT(*) AS used_in_tasks
+ FROM {tags} tg
+ JOIN {tasks} t ON t.task_id=tg.task_id
+ WHERE t.project_id=?
+ GROUP BY tag
+ ORDER BY tag', array($this->id));
+ } else {
+ $result= $db->query('SELECT tag AS tag_name, 1 AS list_position, 1 AS show_in_list, COUNT(*) AS used_in_tasks
+ FROM {tags}
+ GROUP BY tag
+ ORDER BY tag');
+ }
+
+ $tags=array();
+ while ($row = $db->fetchRow($result)) {
+ $tags[]=$row;
+ }
+ return $tags;
+ }
+ */
+ /* rewrite of tags feature, FS1.0beta1 */
+ function listTags($pm = false)
+ {
+ global $db;
+ if ($pm) {
+ $result= $db->query('SELECT tg.*, COUNT(tt.task_id) AS used_in_tasks
+ FROM {list_tag} tg
+ LEFT JOIN {task_tag} tt ON tt.tag_id=tg.tag_id
+ LEFT JOIN {tasks} t ON t.task_id=tt.task_id
+ WHERE tg.project_id=?
+ GROUP BY tg.tag_id
+ ORDER BY tg.list_position', array($this->id));
+ $tags=array();
+ while ($row = $db->fetchRow($result)) {
+ $tags[]=$row;
+ }
+ return $tags;
+ } else {
+ return $db->cached_query('tag'.$this->id, $this->_list_sql('tag'), array($this->id));
+ }
+ }
+
+ // This should really be moved to class Flyspray like some other ones too.
+ // Something todo for 1.1.
+ static function listUsersIn($group_id = null)
+ {
+ global $db;
+ return $db->cached_query(
+ 'users_in'.(is_null($group_id) ? $group_id : intval($group_id)),
+ "SELECT u.*
+ FROM {users} u
+ INNER JOIN {users_in_groups} uig ON u.user_id = uig.user_id
+ INNER JOIN {groups} g ON uig.group_id = g.group_id
+ WHERE g.group_id = ?
+ ORDER BY u.user_name ASC",
+ array($group_id));
+ }
+
+ function listAttachments($cid, $tid)
+ {
+ global $db;
+ return $db->cached_query(
+ 'attach_'.intval($cid),
+ "SELECT *
+ FROM {attachments}
+ WHERE comment_id = ? AND task_id = ?
+ ORDER BY attachment_id ASC",
+ array($cid, $tid));
+ }
+
+ function listLinks($cid, $tid)
+ {
+ global $db;
+ return $db->cached_query(
+ 'link_'.intval($cid),
+ "SELECT *
+ FROM {links}
+ WHERE comment_id = ? AND task_id = ?
+ ORDER BY link_id ASC",
+ array($cid, $tid));
+ }
+
+ function listTaskAttachments($tid)
+ {
+ global $db;
+ return $db->cached_query(
+ 'attach_'.intval($tid),
+ "SELECT * FROM {attachments}
+ WHERE task_id = ? AND comment_id = 0
+ ORDER BY attachment_id ASC",
+ array($tid)
+ );
+ }
+
+ function listTaskLinks($tid)
+ {
+ global $db;
+ return $db->cached_query(
+ 'link_'.intval($tid),
+ "SELECT * FROM {links}
+ WHERE task_id = ? AND comment_id = 0
+ ORDER BY link_id ASC",
+ array($tid));
+ }
+
+ /**
+ * Returns the activity by between dates for a project.
+ * @param date $startdate
+ * @param date $enddate
+ * @param integer $project_id
+ * @return array used to get the count
+ * @access public
+ */
+ static function getActivityProjectCount($startdate, $enddate, $project_id) {
+ global $db;
+ $result = $db->query('SELECT count(event_date) as val
+ FROM {history} h left join {tasks} t on t.task_id = h.task_id
+ WHERE t.project_id = ? AND event_date BETWEEN ? and ?',
+ array($project_id, $startdate, $enddate));
+
+ $result = $db->fetchCol($result);
+ return $result[0];
+ }
+
+ /**
+ * Returns the day activity by the date for a project.
+ * @param date $date
+ * @param integer $project_id
+ * @return array used to get the count
+ * @access public
+ */
+ static function getDayActivityByProject($date_start, $date_end, $project_id) {
+ global $db;
+ //NOTE: from_unixtime() on mysql, to_timestamp() on PostreSQL
+ $func = ('mysql' == $db->dblink->dataProvider) ? 'from_unixtime' : 'to_timestamp';
+
+ $result = $db->query("SELECT count(date({$func}(event_date))) as val, MIN(event_date) as event_date
+ FROM {history} h left join {tasks} t on t.task_id = h.task_id
+ WHERE t.project_id = ? AND event_date BETWEEN ? and ?
+ GROUP BY date({$func}(event_date)) ORDER BY event_date DESC",
+ array($project_id, $date_start, $date_end));
+
+ $date1 = new \DateTime("@$date_start");
+ $date2 = new \DateTime("@$date_end");
+ $days = $date1->diff($date2);
+ $days = $days->format('%a');
+ $results = array();
+
+ for ($i = $days; $i >0; $i--) {
+ $event_date = (string) strtotime("-{$i} day", $date_end);
+ $results[date('Y-m-d', $event_date)] = 0;
+ }
+
+ while ($row = $result->fetchRow()) {
+ $event_date = date('Y-m-d', $row['event_date']);
+ $results[$event_date] = (integer) $row['val'];
+ }
+
+ return array_values($results);
+ }
+}
diff --git a/includes/class.recaptcha.php b/includes/class.recaptcha.php
new file mode 100644
index 0000000..d998d43
--- /dev/null
+++ b/includes/class.recaptcha.php
@@ -0,0 +1,33 @@
+<?php
+/* quick solution
+* https://developers.google.com/recaptcha/docs/verify
+*/
+class recaptcha
+{
+
+ function verify(){
+ global $fs;
+
+ $url = 'https://www.google.com/recaptcha/api/siteverify';
+ $data = array(
+ 'secret' => $fs->prefs['captcha_recaptcha_secret'],
+ 'response' => $_POST['g-recaptcha-response']
+ );
+
+ $options = array(
+ 'http' => array (
+ 'method' => 'POST',
+ /* for php5.3, default enctype for http_build_query() was added with php5.4, http://php.net/manual/en/function.http-build-query.php */
+ 'header' => 'Content-type: application/x-www-form-urlencoded',
+ 'content' => http_build_query($data, '', '&')
+ )
+ );
+
+ $context = stream_context_create($options);
+ $verify = file_get_contents($url, false, $context);
+ $captcha_success=json_decode($verify);
+
+ return $captcha_success->success;
+ }
+
+} # end class
diff --git a/includes/class.tpl.php b/includes/class.tpl.php
new file mode 100644
index 0000000..24b1105
--- /dev/null
+++ b/includes/class.tpl.php
@@ -0,0 +1,1525 @@
+<?php
+
+if (!defined('IN_FS')) {
+ die('Do not access this file directly.');
+}
+
+class Tpl
+{
+ public $_uses = array();
+ public $_vars = array();
+ public $_theme = '';
+ public $_tpls = array();
+ public $_title = "";
+
+ public function uses()
+ {
+ $args = func_get_args();
+ $this->_uses = array_merge($this->_uses, $args);
+ }
+
+ public function assign($arg0 = null, $arg1 = null)
+ {
+ if (is_string($arg0)) {
+ $this->_vars[$arg0] = $arg1;
+ }elseif (is_array($arg0)) {
+ $this->_vars += $arg0;
+ }elseif (is_object($arg0)) {
+ $this->_vars += get_object_vars($arg0);
+ }
+ }
+
+ public function getTheme()
+ {
+ return $this->_theme;
+ }
+
+ public function setTheme($theme)
+ {
+ // Check available themes
+ $theme = trim($theme, '/');
+ $themes = Flyspray::listThemes();
+ if (in_array($theme, $themes)) {
+ $this->_theme = $theme.'/';
+ } else {
+ $this->_theme = $themes[0].'/';
+ }
+ }
+
+ public function setTitle($title)
+ {
+ $this->_title = $title;
+ }
+
+ public function themeUrl()
+ {
+ return sprintf('%sthemes/%s', $GLOBALS['baseurl'], $this->_theme);
+ }
+
+ public function pushTpl($_tpl)
+ {
+ $this->_tpls[] = $_tpl;
+ }
+
+ public function catch_start()
+ {
+ ob_start();
+ }
+
+ public function catch_end()
+ {
+ $this->_tpls[] = array(ob_get_contents());
+ ob_end_clean();
+ }
+
+ public function display($_tpl, $_arg0 = null, $_arg1 = null)
+ {
+ // if only plain text
+ if (is_array($_tpl) && count($tpl)) {
+ echo $_tpl[0];
+ return;
+ }
+
+ // variables part
+ if (!is_null($_arg0)) {
+ $this->assign($_arg0, $_arg1);
+ }
+
+ foreach ($this->_uses as $_var) {
+ global $$_var;
+ }
+
+ extract($this->_vars, EXTR_REFS|EXTR_SKIP);
+
+ if (is_readable(BASEDIR . '/themes/' . $this->_theme.'templates/'.$_tpl)) {
+ require BASEDIR . '/themes/' . $this->_theme.'templates/'.$_tpl;
+ } elseif (is_readable(BASEDIR . '/themes/CleanFS/templates/'.$_tpl)) {
+ # if a custom theme folder only contains a fraction of the .tpl files, use the template of the default full theme as fallback.
+ require BASEDIR . '/themes/CleanFS/templates/'.$_tpl;
+ } else {
+ # This is needed to catch times when there is no theme (for example setup pages, where BASEDIR is ../setup/ not ../)
+ require BASEDIR . "/templates/".$_tpl;
+ }
+ }
+
+ public function render()
+ {
+ while (count($this->_tpls)) {
+ $this->display(array_shift($this->_tpls));
+ }
+ }
+
+ public function fetch($tpl, $arg0 = null, $arg1 = null)
+ {
+ ob_start();
+ $this->display($tpl, $arg0, $arg1);
+ return ob_get_clean();
+ }
+}
+
+class FSTpl extends Tpl
+{
+ public $_uses = array('fs', 'conf', 'baseurl', 'language', 'proj', 'user');
+
+ public function get_image($name, $base = true)
+ {
+ global $proj, $baseurl;
+ $pathinfo = pathinfo($name);
+ $link = sprintf('themes/%s/', $proj->prefs['theme_style']);
+ if ($pathinfo['dirname'] != '.') {
+ $link .= $pathinfo['dirname'] . '/';
+ $name = $pathinfo['basename'];
+ }
+
+ $extensions = array('.png', '.gif', '.jpg', '.ico');
+
+ foreach ($extensions as $ext) {
+ if (is_file(BASEDIR . '/' . $link . $name . $ext)) {
+ return ($base) ? ($baseurl . $link . $name . $ext) : ($link . $name . $ext);
+ }
+ }
+ return '';
+ }
+
+}
+
+/**
+ * Draws the form start tag and the important anticsrftoken on 'post'-forms
+ *
+ * @param string action
+ * @param string name optional attribute of form tag
+ * @param string method optional request method, default 'post'
+ * @param string enctype optional enctype, default 'multipart/form-data'
+ * @param string attr optional attributes for the form tag, example: 'id="myformid" class="myextracssclass"'
+ *
+ * @return string
+ */
+function tpl_form($action, $name=null, $method=null, $enctype=null, $attr='')
+{
+ global $baseurl;
+
+ if (null === $method) {
+ $method='post';
+ }
+ if (null === $enctype) {
+ $enctype='multipart/form-data';
+ }
+
+ if(substr($action,0,4)!='http'){$action=$baseurl.$action;}
+ return '<form action="'.$action.'"'.($method=='get'?' method="get"':' method="post"').
+ ( $name!='' ? ' name="'.$name.'"':'').
+ ( ' enctype="'.$enctype.'"').
+ ( ' '.$attr).'>'.
+ ( $method=='post' ? '<input type="hidden" name="csrftoken" value="'.$_SESSION['csrftoken'].'" />':'');
+}
+
+/**
+ * Creates a link to a task
+ *
+ * @param array task with properties of a task. It also accepts a task_id, but that requires extra queries executed by this function.
+ * @param string text optional, by default the FS# + summary of task is used.
+ * @param bool strict check task permissions by the function too. Extra SQL queries if set true. default false.
+ * @param array attr extra attributes
+ * @param array title informations shown when hover over the link (title attribute of the HTML a-tag)
+ *
+ * @return string ready for html output
+ */
+function tpl_tasklink($task, $text = null, $strict = false, $attrs = array(), $title = array('status','summary','percent_complete'))
+{
+ global $user;
+
+ $params = array();
+
+ if (!is_array($task) || !isset($task['status_name'])) {
+ $td_id = (is_array($task) && isset($task['task_id'])) ? $task['task_id'] : $task;
+ $task = Flyspray::getTaskDetails($td_id, true);
+ }
+
+ if ($strict === true && (!is_object($user) || !$user->can_view_task($task))) {
+ return '';
+ }
+
+ if (is_object($user) && $user->can_view_task($task)) {
+ $summary = utf8_substr($task['item_summary'], 0, 64);
+ } else {
+ $summary = L('taskmadeprivate');
+ }
+
+ if (is_null($text)) {
+ $text = sprintf('FS#%d - %s', $task['task_id'], Filters::noXSS($summary));
+ } elseif(is_string($text)) {
+ $text = htmlspecialchars(utf8_substr($text, 0, 64), ENT_QUOTES, 'utf-8');
+ } else {
+ //we can't handle non-string stuff here.
+ return '';
+ }
+
+ if (!$task['task_id']) {
+ return $text;
+ }
+
+ $title_text = array();
+
+ foreach($title as $info)
+ {
+ switch($info)
+ {
+ case 'status':
+ if ($task['is_closed']) {
+ $title_text[] = $task['resolution_name'];
+ $attrs['class'] = 'closedtasklink';
+ } else {
+ $title_text[] = $task['status_name'];
+ }
+ break;
+
+ case 'summary':
+ $title_text[] = $summary;
+ break;
+
+ case 'assignedto':
+ if (isset($task['assigned_to_name']) ) {
+ if (is_array($task['assigned_to_name'])) {
+ $title_text[] = implode(', ', $task['assigned_to_name']);
+ } else {
+ $title_text[] = $task['assigned_to_name'];
+ }
+ }
+ break;
+
+ case 'percent_complete':
+ $title_text[] = $task['percent_complete'].'%';
+ break;
+
+ case 'category':
+ if ($task['product_category']) {
+ if (!isset($task['category_name'])) {
+ $task = Flyspray::getTaskDetails($task['task_id'], true);
+ }
+ $title_text[] = $task['category_name'];
+ }
+ break;
+
+ // ... more options if necessary
+ }
+ }
+
+ $title_text = implode(' | ', $title_text);
+
+ // to store search options
+ $params = $_GET;
+ unset($params['do'], $params['action'], $params['task_id'], $params['switch']);
+ if(isset($params['event_number'])){
+ # shorter links to tasks from report page
+ unset($params['events'], $params['event_number'], $params['fromdate'], $params['todate'], $params['submit']);
+ }
+
+ # We can unset the project param for shorter urls because flyspray knows project_id from current task data.
+ # Except we made a search from an 'all projects' view before, so the prev/next navigation on details page knows
+ # if it must search only in the project of current task or all projects the user is allowed to see tasks.
+ if(!isset($params['advancedsearch']) || (isset($params['project']) && $params['project']!=0) ){
+ unset($params['project']);
+ }
+
+ $url = htmlspecialchars(createURL('details', $task['task_id'], null, $params), ENT_QUOTES, 'utf-8');
+ $title_text = htmlspecialchars($title_text, ENT_QUOTES, 'utf-8');
+ $link = sprintf('<a href="%s" title="%s" %s>%s</a>',$url, $title_text, join_attrs($attrs), $text);
+
+ if ($task['is_closed']) {
+ $link = '<del>&#160;' . $link . '&#160;</del>';
+ }
+ return $link;
+}
+
+/*
+ * Creates a textlink to a user profile.
+ *
+ * For a link with user icon use tpl_userlinkavatar().
+ *
+ * @param int uid user_id from {users} db table
+ */
+function tpl_userlink($uid)
+{
+ global $db, $user;
+
+ static $cache = array();
+
+ if (is_array($uid)) {
+ list($uid, $uname, $rname) = $uid;
+ } elseif (empty($cache[$uid])) {
+ $sql = $db->query('SELECT user_name, real_name FROM {users} WHERE user_id = ?',
+ array(intval($uid)));
+ if ($sql && $db->countRows($sql)) {
+ list($uname, $rname) = $db->fetchRow($sql);
+ }
+ }
+
+ if (isset($uname)) {
+ #$url = createURL(($user->perms('is_admin')) ? 'edituser' : 'user', $uid);
+ # peterdd: I think it is better just to link to the user's page instead direct to the 'edit user' page also for admins.
+ # With more personalisation coming (personal todo list, charts, ..) in future to flyspray
+ # the user page itself is of increasing value. Instead show the 'edit user'-button on user's page.
+ $url = createURL('user', $uid);
+ $cache[$uid] = vsprintf('<a href="%s">%s</a>', array_map(array('Filters', 'noXSS'), array($url, $rname)));
+ } elseif (empty($cache[$uid])) {
+ $cache[$uid] = eL('anonymous');
+ }
+
+ return $cache[$uid];
+}
+
+/**
+* Builds the HTML string for displaying a gravatar image or an uploaded user image.
+* The string for a user and a size is cached per request.
+*
+* Class and style parameter should be avoided to make this function more effective for caching (less SQL queries)
+*
+* @param int uid the id of the user
+* @param int size in pixel for displaying. Should use global max_avatar_size pref setting by default.
+* @param string class optional, avoid calling with class parameter for better 'cacheability'
+* @param string style optional, avoid calling with style parameter for better 'cacheability'
+*/
+function tpl_userlinkavatar($uid, $size, $class='', $style='')
+{
+ global $db, $user, $baseurl, $fs;
+
+ static $avacache=array();
+
+ if( !($uid>0) ){
+ return '<i class="fa fa-user"></i>';
+ }
+
+ if($uid>0 && (empty($avacache[$uid]) || !isset($avacache[$uid][$size]))){
+ if (!isset($avacache[$uid]['uname'])) {
+ $sql = $db->query('SELECT user_name, real_name, email_address, profile_image FROM {users} WHERE user_id = ?', array(intval($uid)));
+ if ($sql && $db->countRows($sql)) {
+ list($uname, $rname, $email, $profile_image) = $db->fetchRow($sql);
+ } else {
+ return;
+ }
+ $avacache[$uid]['profile_image'] = $profile_image;
+ $avacache[$uid]['uname'] = $uname;
+ $avacache[$uid]['rname'] = $rname;
+ $avacache[$uid]['email'] = $email;
+ }
+
+ if (is_file(BASEDIR.'/avatars/'.$avacache[$uid]['profile_image'])) {
+ $image = '<img src="'.$baseurl.'avatars/'.$avacache[$uid]['profile_image'].'"/>';
+ } else {
+ if (isset($fs->prefs['gravatars']) && $fs->prefs['gravatars'] == 1) {
+ $email = md5(strtolower(trim($avacache[$uid]['email'])));
+ $default = 'mm';
+ $imgurl = '//www.gravatar.com/avatar/'.$email.'?d='.urlencode($default).'&s='.$size;
+ $image = '<img src="'.$imgurl.'"/>';
+ } else {
+ $image = '<i class="fa fa-user" style="font-size:'.$size.'px"></i>';
+ }
+ }
+ if (isset($avacache[$uid]['uname'])) {
+ #$url = createURL(($user->perms('is_admin')) ? 'edituser' : 'user', $uid);
+ # peterdd: I think it is better just to link to the user's page instead direct to the 'edit user' page also for admins.
+ # With more personalisation coming (personal todo list, charts, ..) in future to flyspray
+ # the user page itself is of increasing value. Instead show the 'edit user'-button on user's page.
+ $url = createURL('user', $uid);
+ $avacache[$uid][$size] = '<a'.($class!='' ? ' class="'.$class.'"':'').($style!='' ? ' style="'.$style.'"':'').' href="'.$url.'" title="'.Filters::noXSS($avacache[$uid]['rname']).'">'.$image.'</a>';
+ }
+ }
+ return $avacache[$uid][$size];
+}
+
+function tpl_fast_tasklink($arr)
+{
+ return tpl_tasklink($arr[1], $arr[0]);
+}
+
+/**
+ * Formats a task tag for HTML output based on a global $alltags array
+ *
+ * @param int id tag_id of {list_tag} db table
+ * @param bool showid set true if the tag_id is shown instead of the tag_name
+ *
+ * @return string ready for output
+ */
+function tpl_tag($id, $showid=false) {
+ global $alltags;
+
+ if(!is_array($alltags)) {
+ $alltags=Flyspray::getAllTags();
+ }
+
+ if(isset($alltags[$id])){
+ $out='<i class="tag t'.$id;
+ if( isset($alltags[$id]['class']) && preg_match('/^#([0-9a-f]{3}){1,2}$/i', $alltags[$id]['class']) ) {
+ $out.= '" style="background-color:'.$alltags[$id]['class'];
+ # max only calc once per tag per request
+ # assumes theme css of default tag font color is #000
+ if(!isset($alltags[$id]['fcolor'])){
+ $bg = hex2RGB($alltags[$id]['class']);
+ # from https://www.w3.org/TR/AERT/#color-contrast
+ $brightness=(299*$bg['r'] + 587*$bg['g'] + 114*$bg['b']) / 1000;
+ if($brightness<126){
+ $out.=';color:#fff';
+ $alltags[$id]['fcolor']='#fff';
+ }else{
+ $alltags[$id]['fcolor']='';
+ }
+ } else if( $alltags[$id]['fcolor']==='#fff'){
+ $out.=';color:#fff';
+ }
+ $out.='"';
+ } else {
+ $out.= (isset($alltags[$id]['class']) ? ' '.htmlspecialchars($alltags[$id]['class'], ENT_QUOTES, 'utf-8') : '').'"';
+ }
+ if($showid){
+ $out.='>'.$id;
+ } else{
+ $out.=' title="'.htmlspecialchars($alltags[$id]['tag_name'], ENT_QUOTES, 'utf-8').'">';
+ }
+
+ $out.='</i>';
+ return $out;
+ }
+}
+
+/**
+* Convert a hexa decimal color code to its RGB equivalent
+*
+* used by tpl_tag()
+*
+* @param string $hexstr (hexadecimal color value)
+* @param boolean $returnasstring (if set true, returns the value separated by the separator character. Otherwise returns associative array)
+* @param string $seperator (to separate RGB values. Applicable only if second parameter is true.)
+* @return array or string (depending on second parameter. Returns False if invalid hex color value)
+*
+* function is adapted from an exmaple on http://php.net/manual/de/function.hexdec.php
+*/
+function hex2RGB($hexstr, $returnasstring = false, $seperator = ',') {
+ $hexstr = preg_replace("/[^0-9A-Fa-f]/", '', $hexstr); // Gets a proper hex string
+ $rgb = array();
+ if (strlen($hexstr) == 6) { // if a proper hex code, convert using bitwise operation. No overhead... faster
+ $colorval = hexdec($hexstr);
+ $rgb['r'] = 0xFF & ($colorval >> 0x10);
+ $rgb['g'] = 0xFF & ($colorval >> 0x8);
+ $rgb['b'] = 0xFF & $colorval;
+ } elseif (strlen($hexstr) == 3) { // if shorthand notation, need some string manipulations
+ $rgb['r'] = hexdec(str_repeat(substr($hexstr, 0, 1), 2));
+ $rgb['g'] = hexdec(str_repeat(substr($hexstr, 1, 1), 2));
+ $rgb['b'] = hexdec(str_repeat(substr($hexstr, 2, 1), 2));
+ } else {
+ return false; // invalid hex color code
+ }
+ return $returnasstring ? implode($seperator, $rgb) : $rgb; // returns the rgb string or the associative array
+}
+
+/**
+ * joins an array of tag attributes together for output in a HTML tag.
+ *
+ * @param array attr
+ *
+ * @return string
+ */
+function join_attrs($attr = null) {
+ if (is_array($attr) && count($attr)) {
+ $arr = array();
+ foreach ($attr as $key=>$val) {
+ $arr[] = vsprintf('%s = "%s"', array_map(array('Filters', 'noXSS'), array($key, $val)));
+ }
+ return ' '.join(' ', $arr);
+ }
+ return '';
+}
+
+/**
+ * Datepicker
+ */
+function tpl_datepicker($name, $label = '', $value = 0) {
+ global $user, $page;
+
+ $date = '';
+
+ if ($value) {
+ if (!is_numeric($value)) {
+ $value = strtotime($value);
+ }
+
+ if (!$user->isAnon()) {
+ $st = date('Z')/3600; // server GMT timezone
+ $value += ($user->infos['time_zone'] - $st) * 60 * 60;
+ }
+
+ $date = date('Y-m-d', intval($value));
+
+ /* It must "look" as a date..
+ * XXX : do not blindly copy this code to validate other dates
+ * this is mostly a tongue-in-cheek validation
+ * 1. it will fail on 32 bit systems on dates < 1970
+ * 2. it will produce different results bewteen 32 and 64 bit systems for years < 1970
+ * 3. it will not work when year > 2038 on 32 bit systems (see http://en.wikipedia.org/wiki/Year_2038_problem)
+ *
+ * Fortunately tasks are never opened to be dated on 1970 and maybe our sons or the future flyspray
+ * coders may be willing to fix the 2038 issue ( in the strange case 32 bit systems are still used by that year) :-)
+ */
+
+ } elseif (Req::has($name) && strlen(Req::val($name))) {
+
+ //strtotime sadly returns -1 on faliure in php < 5.1 instead of false
+ $ts = strtotime(Req::val($name));
+
+ foreach (array('m','d','Y') as $period) {
+ //checkdate only accepts arguments of type integer
+ $$period = intval(date($period, $ts));
+ }
+ // $ts has to be > 0 to get around php behavior change
+ // false is casted to 0 by the ZE
+ $date = ($ts > 0 && checkdate($m, $d, $Y)) ? Req::val($name) : '';
+ }
+
+
+ $subPage = new FSTpl;
+ $subPage->setTheme($page->getTheme());
+ $subPage->assign('name', $name);
+ $subPage->assign('date', $date);
+ $subPage->assign('label', $label);
+ $subPage->assign('dateformat', '%Y-%m-%d');
+ $subPage->display('common.datepicker.tpl');
+}
+
+/**
+ * user selector
+ */
+function tpl_userselect($name, $value = null, $id = '', $attrs = array()) {
+ global $db, $user, $proj;
+
+ if (!$id) {
+ $id = $name;
+ }
+
+ if ($value && ctype_digit($value)) {
+ $sql = $db->query('SELECT user_name FROM {users} WHERE user_id = ?', array($value));
+ $value = $db->fetchOne($sql);
+ }
+
+ if (!$value) {
+ $value = '';
+ }
+
+
+ $page = new FSTpl;
+ $page->setTheme($proj->prefs['theme_style']);
+ $page->assign('name', $name);
+ $page->assign('id', $id);
+ $page->assign('value', $value);
+ $page->assign('attrs', $attrs);
+ $page->display('common.userselect.tpl');
+}
+
+/**
+ * Creates the options for a date format select
+ *
+ * @selected The format that should by selected by default
+ * @return html formatted options for a select tag
+**/
+function tpl_date_formats($selected, $detailed = false)
+{
+ $time = time();
+
+ # TODO: rewrite using 'return tpl_select(...)'
+ if (!$detailed) {
+ $dateFormats = array(
+ '%d.%m.%Y' => strftime('%d.%m.%Y', $time).' (DD.MM.YYYY)', # popular in many european countries
+ '%d/%m/%Y' => strftime('%d/%m/%Y', $time).' (DD/MM/YYYY)', # popular in Greek
+ '%m/%d/%Y' => strftime('%m/%d/%Y', $time).' (MM/DD/YYYY)', # popular in USA
+
+ '%d.%m.%y' => strftime('%d.%m.%y', $time),
+
+ '%Y.%m.%d' => strftime('%Y.%m.%d', $time),
+ '%y.%m.%d' => strftime('%y.%m.%d', $time),
+
+ '%d-%m-%Y' => strftime('%d-%m-%Y', $time),
+ '%d-%m-%y' => strftime('%d-%m-%y', $time),
+
+ '%Y-%m-%d' => strftime('%Y-%m-%d', $time).' (YYYY-MM-DD, ISO 8601)',
+ '%y-%m-%d' => strftime('%y-%m-%d', $time),
+
+ '%d %b %Y' => strftime('%d %b %Y', $time),
+ '%d %B %Y' => strftime('%d %B %Y', $time),
+
+ '%b %d %Y' => strftime('%b %d %Y', $time),
+ '%B %d %Y' => strftime('%B %d %Y', $time),
+ );
+ }
+ else {
+ # TODO: maybe use optgroups for tpl_select() to separate 24h and 12h (am/pm) formats
+ $dateFormats = array(
+ '%d.%m.%Y %H:%M' => strftime('%d.%m.%Y %H:%M', $time),
+ '%d.%m.%y %H:%M' => strftime('%d.%m.%y %H:%M', $time),
+
+ '%d.%m.%Y %I:%M %p' => strftime('%d.%m.%Y %I:%M %p', $time),
+ '%d.%m.%y %I:%M %p' => strftime('%d.%m.%y %I:%M %p', $time),
+
+ '%Y.%m.%d %H:%M' => strftime('%Y.%m.%d %H:%M', $time),
+ '%y.%m.%d %H:%M' => strftime('%y.%m.%d %H:%M', $time),
+
+ '%Y.%m.%d %I:%M %p' => strftime('%Y.%m.%d %I:%M %p', $time),
+ '%y.%m.%d %I:%M %p' => strftime('%y.%m.%d %I:%M %p', $time),
+
+ '%d-%m-%Y %H:%M' => strftime('%d-%m-%Y %H:%M', $time),
+ '%d-%m-%y %H:%M' => strftime('%d-%m-%y %H:%M', $time),
+
+ '%d-%m-%Y %I:%M %p' => strftime('%d-%m-%Y %I:%M %p', $time),
+ '%d-%m-%y %I:%M %p' => strftime('%d-%m-%y %I:%M %p', $time),
+
+ '%Y-%m-%d %H:%M' => strftime('%Y-%m-%d %H:%M', $time),
+ '%y-%m-%d %H:%M' => strftime('%y-%m-%d %H:%M', $time),
+
+ '%Y-%m-%d %I:%M %p' => strftime('%Y-%m-%d %I:%M %p', $time),
+ '%y-%m-%d %I:%M %p' => strftime('%y-%m-%d %I:%M %p', $time),
+
+ '%d %b %Y %H:%M' => strftime('%d %b %Y %H:%M', $time),
+ '%d %B %Y %H:%M' => strftime('%d %B %Y %H:%M', $time),
+
+ '%d %b %Y %I:%M %p' => strftime('%d %b %Y %I:%M %p', $time),
+ '%d %B %Y %I:%M %p' => strftime('%d %B %Y %I:%M %p', $time),
+
+ '%b %d %Y %H:%M' => strftime('%b %d %Y %H:%M', $time),
+ '%B %d %Y %H:%M' => strftime('%B %d %Y %H:%M', $time),
+
+ '%b %d %Y %I:%M %p' => strftime('%b %d %Y %I:%M %p', $time),
+ '%B %d %Y %I:%M %p' => strftime('%B %d %Y %I:%M %p', $time),
+ );
+ }
+
+ return tpl_options($dateFormats, $selected);
+}
+
+
+/**
+ * Options for a <select>
+ *
+ * FIXME peterdd: This function is currently often called by templates with just
+ * results from sqltablequeries like select * from tablex,
+ * so data[0] and data[1] of each row works only by table structure convention as wished.
+ * not by names, lack of a generic optgroup feature, css-id, css-classes, disabled option.
+ * Maybe rewrite a as tpl_select() ..
+ *
+ * @options array of values
+ * For optgroups, the values should be presorted by the optgroups
+ * example:
+ * $options=array(
+ * array(3,'project3',1), # active project group
+ * array(2,'project2',1),
+ * array(5,'project5',0) # inactive project optgroup
+ * ); tpl_options($options, 2)
+*/
+function tpl_options($options, $selected = null, $labelIsValue = false, $attr = null, $remove = null)
+{
+ $html = '';
+
+ // force $selected to be an array.
+ // this allows multi-selects to have multiple selected options.
+ $selected = is_array($selected) ? $selected : (array) $selected;
+ $options = is_array($options) ? $options : (array) $options;
+
+ $lastoptgroup=0;
+ $optgroup=0;
+ $ingroup=false;
+ foreach ($options as $idx=>$data) {
+ if (is_array($data)) {
+ $value = $data[0];
+ $label = $data[1];
+ # just a temp hack, we currently use optgroups only for project dropdown...
+ $optgroup=array_key_exists('project_is_active',$data) ? $data['project_is_active'] : 0;
+ if (array_key_exists('project_title', $data) && $optgroup!=$lastoptgroup) {
+ if ($ingroup) {
+ $html.='</optgroup>';
+ }
+ $html.='<optgroup'.($optgroup==0 ? ' label="'.L('inactive').'"' : '' ).'>';
+ $ingroup=true;
+ }
+ } else{
+ $value=$idx;
+ $label=$data;
+ }
+ $label = htmlspecialchars($label, ENT_QUOTES, 'utf-8');
+ $value = $labelIsValue ? $label : htmlspecialchars($value, ENT_QUOTES, 'utf-8');
+
+ if ($value === $remove) {
+ continue;
+ }
+
+ $html .= '<option value="'.$value.'"';
+ if (in_array($value, $selected)) {
+ $html .= ' selected="selected"';
+ }
+ $html .= ($attr ? join_attrs($attr): '') . '>' . $label . '</option>';
+ $lastoptgroup=$optgroup;
+ }
+
+ if ($ingroup) {
+ $html.='</optgroup>';
+ }
+
+ if (!$html) {
+ $html .= '<option value="0">---</option>';
+ }
+
+ return $html;
+}
+
+
+/**
+ * Builds a complete HTML-select with select options.
+ *
+ * Supports free choosable attributes for select, options and optgroup tags. optgroups can also be nested.
+ *
+ * @author peterdd
+ *
+ * @param array key-values pairs and can be nested
+ *
+ * @return string the complete html-select
+ *
+ * @since 1.0.0-beta3
+ *
+ * @example
+ * example output of print_r($array) to see the structure of the param $array
+ * Array
+ (
+ [name] => varname // required if you want submit it with a form to the server
+ [attr] => Array // optional
+ (
+ [id] => selid // optional
+ [class] => selclass1 // optional
+ )
+ [options] => Array // optional, but without doesn't make much sense ;-)
+ (
+ [0] => Array // at least one would be useful
+ (
+ [value] => opt1val // recommended
+ [label] => opt1label // recommended
+ [disabled] => 1 // optional
+ [selected] => 1 // optional
+ [attr] => Array // optional
+ (
+ [id] => opt1id // optional
+ [class] => optclass1 // optional
+ )
+ )
+ [1] => Array
+ (
+ [optgroup] => 1 // this tells the function that now comes an optgroup
+ [label] => optgrouplabel // optional
+ [attr] => Array // optional
+ (
+ [id] => optgroupid1 // optional
+ [class] => optgroupclass1 // optional
+ )
+ [options] => Array
+ // ... nested options and optgroups can follow here....
+ )
+ // ... and so on
+ )
+ )
+ */
+function tpl_select($select=array()){
+
+ if(isset($select['name'])){
+ $name=' name="'.$select['name'].'"';
+ }else{
+ $name='';
+ }
+
+ $attrjoin='';
+ if(isset($select['attr'])){
+ foreach($select['attr'] as $key=>$val){
+ $attrjoin.=' '.$key.'="'.htmlspecialchars($val, ENT_QUOTES, 'utf-8').'"';
+ }
+ }
+ $html='<select'.$name.$attrjoin.'>';
+ if(isset($select['options'])){
+ $html.=tpl_selectoptions($select['options']);
+ }
+ $html.="\n".'</select>';
+ return $html;
+}
+
+/**
+ * called by tpl_select()
+ *
+ * @author peterdd
+ *
+ * @param array key-values pairs and can be nested
+ *
+ * @return string option- and optgroup-tags as one string
+ *
+ * @since 1.0.0-beta3
+ *
+ * called recursively by itself
+ * Can also be called alone from template if the templates writes the wrapping select-tags.
+ *
+ * @example see [options]-array of example of tpl_select()
+ */
+function tpl_selectoptions($options=array(), $level=0){
+ $html='';
+ # such deep nesting is too weired - probably an endless loop lets
+ # return before something bad happens
+ if( $level>10){
+ return;
+ }
+ #print_r($options);
+ #print_r($level);
+ foreach($options as $o){
+ if(isset($o['optgroup'])){
+ # we have an optgroup
+ $html.="\n".str_repeat("\t",$level).'<optgroup label="'.$o['label'].'"';
+ if(isset($o['attr'])){
+ foreach($o['attr'] as $key=>$val){
+ $html.=' '.$key.'="'.htmlspecialchars($val, ENT_QUOTES, 'utf-8').'"';
+ }
+ }
+ $html.='>';
+ # may contain options and suboptgroups..
+ $html.=tpl_selectoptions($o['options'], $level+1);
+ $html.="\n".str_repeat("\t",$level).'</optgroup>';
+ } else{
+ # we have a simple option
+ $html.="\n".str_repeat("\t",$level).'<option value="'.htmlspecialchars($o['value'], ENT_QUOTES, 'utf-8').'"';
+ if(isset($o['disabled'])){
+ $html.=' disabled="disabled"'; # xhtml compatible
+ }
+ if(isset($o['selected'])){
+ $html.=' selected="selected"'; # xhtml compatible
+ }
+ if(isset($o['attr'])){
+ foreach($o['attr'] as $key=>$val){
+ $html.=' '.$key.'="'.htmlspecialchars($val, ENT_QUOTES, 'utf-8').'"';
+ }
+ }
+ $html.='>'.htmlspecialchars($o['label'], ENT_QUOTES, 'utf-8').'</option>';
+ }
+ }
+
+ return $html;
+}
+
+
+/**
+ * Creates a double select.
+ *
+ * Elements of arrays $options and $selected can be moved between eachother. The $selected list can also be sorted.
+ *
+ * @param string name
+ * @param array options
+ * @param array selected
+ * @param bool labelisvalue
+ * @param bool updown
+ */
+function tpl_double_select($name, $options, $selected = null, $labelisvalue = false, $updown = true)
+{
+ static $_id = 0;
+ static $tpl = null;
+
+ if (!$tpl) {
+ global $proj;
+
+ // poor man's cache
+ $tpl = new FSTpl();
+ $tpl->setTheme($proj->prefs['theme_style']);
+ }
+
+ settype($selected, 'array');
+ settype($options, 'array');
+
+ $tpl->assign('id', '_task_id_'.($_id++));
+ $tpl->assign('name', $name);
+ $tpl->assign('selected', $selected);
+ $tpl->assign('updown', $updown);
+
+ $html = $tpl->fetch('common.dualselect.tpl');
+
+ $selectedones = array();
+
+ $opt1 = '';
+ foreach ($options as $value => $label) {
+ if (is_array($label) && count($label) >= 2) {
+ $value = $label[0];
+ $label = $label[1];
+ }
+ if ($labelisvalue) {
+ $value = $label;
+ }
+ if (in_array($value, $selected)) {
+ $selectedones[$value] = $label;
+ continue;
+ }
+ $label = htmlspecialchars($label, ENT_QUOTES, 'utf-8');
+ $value = htmlspecialchars($value, ENT_QUOTES, 'utf-8');
+
+ $opt1 .= sprintf('<option title="%2$s" value="%1$s">%2$s</option>', $value, $label);
+ }
+
+ $opt2 = '';
+ foreach ($selected as $value) {
+ if (!isset($selectedones[$value])) {
+ continue;
+ }
+ $label = htmlspecialchars($selectedones[$value], ENT_QUOTES, 'utf-8');
+ $value = htmlspecialchars($value, ENT_QUOTES, 'utf-8');
+
+ $opt2 .= sprintf('<option title="%2$s" value="%1$s">%2$s</option>', $value, $label);
+ }
+
+ return sprintf($html, $opt1, $opt2);
+}
+
+/**
+ * Creates a HTML checkbox
+ *
+ * @param string name
+ * @param bool checked
+ * @param string id id attribute of the checkbox HTML element
+ * @param string value
+ * @param array attr tag attributes
+ *
+ * @return string for ready for HTML output
+ */
+function tpl_checkbox($name, $checked = false, $id = null, $value = 1, $attr = null)
+{
+ $name = htmlspecialchars($name, ENT_QUOTES, 'utf-8');
+ $value = htmlspecialchars($value, ENT_QUOTES, 'utf-8');
+ $html = sprintf('<input type="checkbox" name="%s" value="%s" ', $name, $value);
+ if (is_string($id)) {
+ $html .= sprintf('id="%s" ', Filters::noXSS($id));
+ }
+ if ($checked == true) {
+ $html .= 'checked="checked" ';
+ }
+ // do not call join_attrs if $attr is null or nothing..
+ return ($attr ? $html. join_attrs($attr) : $html) . '/>';
+}
+
+/**
+ * Image display
+ */
+function tpl_img($src, $alt = '')
+{
+ global $baseurl;
+ if (is_file(BASEDIR .'/'.$src)) {
+ return sprintf('<img src="%s%s" alt="%s" />', $baseurl, Filters::noXSS($src), Filters::noXSS($alt));
+ }
+ return Filters::noXSS($alt);
+}
+
+// Text formatting
+//format has been already checked in constants.inc.php
+if(isset($conf['general']['syntax_plugin'])) {
+
+ $path_to_plugin = BASEDIR . '/plugins/' . $conf['general']['syntax_plugin'] . '/' . $conf['general']['syntax_plugin'] . '_formattext.inc.php';
+
+ if (is_readable($path_to_plugin)) {
+ include($path_to_plugin);
+ }
+}
+
+class TextFormatter
+{
+ public static function get_javascript()
+ {
+ global $conf;
+
+ $path_to_plugin = sprintf('%s/plugins/%s', BASEDIR, $conf['general']['syntax_plugin']);
+ $return = array();
+
+ if (!is_readable($path_to_plugin)) {
+ return $return;
+ }
+
+ $d = dir($path_to_plugin);
+ while (false !== ($entry = $d->read())) {
+ if (substr($entry, -3) == '.js') {
+ $return[] = $conf['general']['syntax_plugin'] . '/' . $entry;
+ }
+ }
+
+ return $return;
+ }
+
+ public static function render($text, $type = null, $id = null, $instructions = null)
+ {
+ global $conf;
+
+ $methods = get_class_methods($conf['general']['syntax_plugin'] . '_TextFormatter');
+ $methods = is_array($methods) ? $methods : array();
+
+ if (in_array('render', $methods)) {
+ return call_user_func(array($conf['general']['syntax_plugin'] . '_TextFormatter', 'render'),
+ $text, $type, $id, $instructions);
+ } else {
+ $text=strip_tags($text, '<br><br/><p><h2><h3><h4><h5><h5><h6><blockquote><a><img><u><b><strong><s><ins><del><ul><ol><li><table><caption><tr><col><colgroup><td><th><thead><tfoot><tbody><code>');
+ if ( $conf['general']['syntax_plugin']
+ && $conf['general']['syntax_plugin'] != 'none'
+ && $conf['general']['syntax_plugin'] != 'html') {
+ $text='Unsupported output plugin '.$conf['general']['syntax_plugin'].'!'
+ .'<br/>Couldn\'t call '.$conf['general']['syntax_plugin'].'_TextFormatter::render()'
+ .'<br/>Temporarily handled like it is HTML until fixed.<br/>'
+ .$text;
+ }
+
+ //TODO: Remove Redundant Code once tested completely
+ //Author: Steve Tredinnick
+ //Have removed this as creating additional </br> lines even though <p> is already dealing with it
+ //possibly an conversion from Dokuwiki syntax to html issue, left in in case anyone has issues and needs to comment out
+ //$text = ' ' . nl2br($text) . ' ';
+
+ // Change FS#123 into hyperlinks to tasks
+ return preg_replace_callback("/\b(?:FS#|bug )(\d+)\b/", 'tpl_fast_tasklink', trim($text));
+ }
+ }
+
+ public static function textarea($name, $rows, $cols, $attrs = null, $content = null)
+ {
+ global $conf;
+
+ if (@in_array('textarea', get_class_methods($conf['general']['syntax_plugin'] . '_TextFormatter'))) {
+ return call_user_func(array($conf['general']['syntax_plugin'] . '_TextFormatter', 'textarea'),
+ $name, $rows, $cols, $attrs, $content);
+ }
+
+ $name = htmlspecialchars($name, ENT_QUOTES, 'utf-8');
+ $return = sprintf('<textarea name="%s" cols="%d" rows="%d"', $name, $cols, $rows);
+ if (is_array($attrs) && count($attrs)) {
+ $return .= join_attrs($attrs);
+ }
+ $return .= '>';
+ if (is_string($content) && strlen($content)) {
+ $return .= htmlspecialchars($content, ENT_QUOTES, 'utf-8');
+ }
+ $return .= '</textarea>';
+
+ # Activate CkEditor on textareas
+ if($conf['general']['syntax_plugin']=='html'){
+ $return .= "
+<script>
+ CKEDITOR.replace( '".$name."', { entities: true, entities_latin: false, entities_processNumerical: false } );
+</script>";
+ }
+
+ return $return;
+ }
+}
+
+/**
+ * Format Date
+ *
+ * Questionable if this function belongs in this class. Usages also elsewhere and not UI-related.
+ */
+function formatDate($timestamp, $extended = false, $default = '')
+{
+ global $db, $conf, $user, $fs;
+
+ setlocale(LC_ALL, str_replace('-', '_', L('locale')) . '.utf8');
+
+ if (!$timestamp) {
+ return $default;
+ }
+
+ $dateformat = '';
+ $format_id = $extended ? 'dateformat_extended' : 'dateformat';
+ $st = date('Z')/3600; // server GMT timezone
+
+ if (!$user->isAnon()) {
+ $dateformat = $user->infos[$format_id];
+ $timestamp += ($user->infos['time_zone'] - $st) * 60 * 60;
+ $st = $user->infos['time_zone'];
+ }
+
+ if (!$dateformat) {
+ $dateformat = $fs->prefs[$format_id];
+ }
+
+ if (!$dateformat) {
+ $dateformat = $extended ? '%A, %d %B %Y, %H:%M %GMT' : '%Y-%m-%d';
+ }
+
+ $zone = L('GMT') . (($st == 0) ? ' ' : (($st > 0) ? '+' . $st : $st));
+ $dateformat = str_replace('%GMT', $zone, $dateformat);
+ //it returned utf-8 encoded by the system
+ return strftime(Filters::noXSS($dateformat), (int) $timestamp);
+}
+
+/**
+ * Draw permissions table
+ */
+function tpl_draw_perms($perms)
+{
+ global $proj;
+
+ $perm_fields = array(
+ 'is_admin',
+ 'manage_project',
+ 'view_tasks',
+ 'view_groups_tasks',
+ 'view_own_tasks',
+ 'open_new_tasks',
+ 'add_multiple_tasks',
+ 'modify_own_tasks',
+ 'modify_all_tasks',
+ 'create_attachments',
+ 'delete_attachments',
+ 'assign_to_self',
+ 'assign_others_to_self',
+ 'edit_assignments',
+ 'close_own_tasks',
+ 'close_other_tasks',
+ 'view_roadmap',
+ 'view_history',
+ 'view_reports',
+ 'add_votes',
+ 'view_comments',
+ 'add_comments',
+ 'edit_comments',
+ 'edit_own_comments',
+ 'delete_comments',
+ 'view_estimated_effort',
+ 'view_current_effort_done',
+ 'track_effort'
+ );
+
+ $yesno = array(
+ '<td class="bad fa fa-ban" title="'.eL('no').'"></td>',
+ '<td class="good fa fa-check" title="'.eL('yes').'"></td>'
+ );
+
+ # 20150307 peterdd: This a temporary hack
+ $i=0;
+ $html='';
+ $projpermnames='';
+
+ foreach ($perms as $projperm){
+ $html .= '<table class="perms"><thead><tr><th>'.($i==0? 'global' : L('project').' '.$i).'</th>'.($i==0? '<th>'.L('permissions').'</th>' : '').'</tr></thead><tbody>';
+ foreach ($projperm as $key => $val) {
+ if (!is_numeric($key) && in_array($key, $perm_fields)) {
+ $html .= '<tr>';
+ $html .= $yesno[ ($val || $perms[0]['is_admin']) ];
+ $html .= $i==0 ? '<th>'.eL(str_replace('_','',$key)).'</th>' : '';
+ $html .= '</tr>';
+
+ # all projects have same permnames
+ $projpermnames .= $i==1 ? '<tr><td>'.eL(str_replace('_','',$key)).'</td></tr>' : '';
+ }
+ }
+ $html.= '</tbody></table>';
+ $i++;
+ }
+ $html.='<table class="perms"><thead><th>'.L('permissions').'</th></thead><tbody>'.$projpermnames.'</tbody></table>';
+ $html.='<style>.perms tr{height:30px;}</style>';
+ # end 20150307
+ return $html;
+}
+
+/**
+ * Highlights searchqueries in HTML code
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ */
+function html_hilight($html,$query){
+ //split at common delimiters
+ $queries = preg_split ('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>]+/',$query,-1,PREG_SPLIT_NO_EMPTY);
+ foreach ($queries as $q){
+ $q = preg_quote($q,'/');
+ $html = preg_replace_callback("/((<[^>]*)|$q)/i",'html_hilight_callback',$html);
+ }
+ return $html;
+}
+
+/**
+ * Callback used by html_hilight()
+ *
+ * @author Harry Fuecks <hfuecks@gmail.com>
+ */
+function html_hilight_callback($m) {
+ $hlight = unslash($m[0]);
+ if ( !isset($m[2])) {
+ $hlight = '<span class="search_hit">'.$hlight.'</span>';
+ }
+ return $hlight;
+}
+
+/**
+ * XHTML compatible output of disabled attribute
+ *
+ * @param bool if something that PHP sees as true or false
+ */
+function tpl_disableif ($if)
+{
+ if ($if) {
+ return 'disabled="disabled"';
+ }
+}
+
+/**
+ * Generates links for Flyspray
+ *
+ * Create an URL based upon address-rewriting preferences
+ *
+ */
+function createURL($type, $arg1 = null, $arg2 = null, $arg3 = array())
+{
+ global $baseurl, $conf, $fs;
+
+ $url = $baseurl;
+
+ // If we do want address rewriting
+ if ($fs->prefs['url_rewriting']) {
+ switch ($type) {
+ case 'depends':
+ $return = $url . 'task/' . $arg1 . '/' . $type;
+ break;
+ case 'details':
+ $return = $url . 'task/' . $arg1;
+ break;
+ case 'edittask':
+ $return = $url . 'task/' . $arg1 . '/edit';
+ break;
+ case 'pm':
+ $return = $url . 'pm/proj' . $arg2 . '/' . $arg1;
+ break;
+
+ case 'admin':
+ case 'edituser':
+ case 'user':
+ $return = $url . $type . '/' . $arg1;
+ break;
+
+ case 'project':
+ $return = $url . 'proj' . $arg1;
+ break;
+
+ case 'reports':
+ case 'roadmap':
+ case 'toplevel':
+ case 'gantt':
+ case 'index':
+ $return = $url.$type.'/proj'.$arg1;
+ break;
+
+ case 'newtask':
+ case 'newmultitasks':
+ $return = $url . $type . '/proj' . $arg1 . ($arg2 ? '/supertask' . $arg2 : '');
+ break;
+
+ case 'editgroup':
+ $return = $url . $arg2 . '/' . $type . '/' . $arg1;
+ break;
+
+ case 'logout':
+ case 'lostpw':
+ case 'myprofile':
+ case 'register':
+ $return = $url . $type;
+ break;
+
+ case 'mytasks':
+ $return = $url.'proj'.$arg1.'/dev'.$arg2;
+ break;
+ case 'tasklist':
+ # see also .htaccess for the mapping
+ if($arg1>0 && $fs->projects[$arg1]['default_entry']=='index'){
+ $return = $url.'proj'.$arg1;
+ }else{
+ $return = $url.$type.'/proj'.$arg1;
+ }
+
+ break;
+ default:
+ $return = $baseurl . 'index.php';
+ break;
+ }
+ } else {
+ if ($type == 'edittask') {
+ $url .= 'index.php?do=details';
+ } else {
+ $url .= 'index.php?do=' . $type;
+ }
+
+ switch ($type) {
+ case 'admin':
+ $return = $url . '&area=' . $arg1;
+ break;
+ case 'edittask':
+ $return = $url . '&task_id=' . $arg1 . '&edit=yep';
+ break;
+ case 'pm':
+ $return = $url . '&area=' . $arg1 . '&project=' . $arg2;
+ break;
+ case 'user':
+ $return = $baseurl . 'index.php?do=user&area=users&id=' . $arg1;
+ break;
+ case 'edituser':
+ $return = $baseurl . 'index.php?do=admin&area=users&user_id=' . $arg1;
+ break;
+ case 'logout':
+ $return = $baseurl . 'index.php?do=authenticate&logout=1';
+ break;
+
+ case 'details':
+ case 'depends':
+ $return = $url . '&task_id=' . $arg1;
+ break;
+
+ case 'project':
+ $return = $baseurl . 'index.php?project=' . $arg1;
+ break;
+
+ case 'reports':
+ case 'roadmap':
+ case 'toplevel':
+ case 'gantt':
+ case 'index':
+ case 'tasklist':
+ $return = $url . '&project=' . $arg1;
+ break;
+
+ case 'newtask':
+ case 'newmultitasks':
+ $return = $url . '&project=' . $arg1 . ($arg2 ? '&supertask=' . $arg2 : '');
+ break;
+
+ case 'editgroup':
+ $return = $baseurl . 'index.php?do=' . $arg2 . '&area=editgroup&id=' . $arg1;
+ break;
+
+ case 'lostpw':
+ case 'myprofile':
+ case 'register':
+ $return = $url;
+ break;
+
+ case 'mytasks':
+ $return = $baseurl.'index.php?do=index&project='.$arg1.'&dev='.$arg2;
+ break;
+
+ default:
+ $return = $baseurl . 'index.php';
+ break;
+ }
+ }
+
+ $url = new Url($return);
+ if( !is_null($arg3) && count($arg3) ) {
+ $url->addvars($arg3);
+ }
+ return $url->get();
+}
+
+/**
+ * Page numbering
+ *
+ * Thanks to Nathan Fritz for this. http://www.netflint.net/
+ */
+function pagenums($pagenum, $perpage, $totalcount)
+{
+ global $proj;
+ $pagenum = intval($pagenum);
+ $perpage = intval($perpage);
+ $totalcount = intval($totalcount);
+
+ // Just in case $perpage is something weird, like 0, fix it here:
+ if ($perpage < 1) {
+ $perpage = $totalcount > 0 ? $totalcount : 1;
+ }
+ $pages = ceil($totalcount / $perpage);
+ $output = sprintf(eL('page'), $pagenum, $pages);
+
+ if ( $totalcount / $perpage > 1 ) {
+ $params=$_GET;
+ # unset unneeded params for shorter urls
+ unset($params['do']);
+ unset($params['project']);
+ unset($params['switch']);
+ $output .= '<span class="pagenums DoNotPrint">';
+
+ $start = max(1, $pagenum - 4 + min(2, $pages - $pagenum));
+ $finish = min($start + 4, $pages);
+
+ if ($start > 1) {
+ $url = Filters::noXSS(createURL('tasklist', $proj->id, null, array_merge($params, array('pagenum' => 1))));
+ $output .= sprintf('<a href="%s">&lt;&lt;%s </a>', $url, eL('first'));
+ }
+ if ($pagenum > 1) {
+ $url = Filters::noXSS(createURL('tasklist', $proj->id, null, array_merge($params, array('pagenum' => $pagenum - 1))));
+ $output .= sprintf('<a id="previous" accesskey="p" href="%s">&lt; %s</a> - ', $url, eL('previous'));
+ }
+
+ for ($pagelink = $start; $pagelink <= $finish; $pagelink++) {
+ if ($pagelink != $start) {
+ $output .= ' - ';
+ }
+
+ if ($pagelink == $pagenum) {
+ $output .= sprintf('<strong>%d</strong>', $pagelink);
+ } else {
+ $url = Filters::noXSS(createURL('tasklist', $proj->id, null, array_merge($params, array('pagenum' => $pagelink))));
+ $output .= sprintf('<a href="%s">%d</a>', $url, $pagelink);
+ }
+ }
+
+ if ($pagenum < $pages) {
+ $url = Filters::noXSS(createURL('tasklist', $proj->id, null, array_merge($params, array('pagenum' => $pagenum + 1))));
+ $output .= sprintf(' - <a id="next" accesskey="n" href="%s">%s &gt;</a>', $url, eL('next'));
+ }
+ if ($finish < $pages) {
+ $url = Filters::noXSS(createURL('tasklist', $proj->id, null, array_merge($params, array('pagenum' => $pages))));
+ $output .= sprintf('<a href="%s"> %s &gt;&gt;</a>', $url, eL('last'));
+ }
+ $output .= '</span>';
+ }
+
+ return $output;
+}
+
+class Url {
+ public $url = '';
+ public $parsed;
+
+ public function __construct($url = '') {
+ $this->url = $url;
+ $this->parsed = parse_url($this->url);
+ }
+
+ public function seturl($url) {
+ $this->url = $url;
+ $this->parsed = parse_url($this->url);
+ }
+
+ public function getinfo($type = null) {
+ if (is_null($type)) {
+ return $this->parsed;
+ } elseif (isset($this->parsed[$type])) {
+ return $this->parsed[$type];
+ } else {
+ return '';
+ }
+ }
+
+ public function setinfo($type, $value) {
+ $this->parsed[$type] = $value;
+ }
+
+ public function addfrom($method = 'get', $vars = array()) {
+ $append = '';
+ foreach($vars as $key) {
+ $append .= http_build_query( (($method == 'get') ? Get::val($key) : Post::val($key)) ) . '&';
+ }
+ $append = substr($append, 0, -1);
+
+ $separator = ini_get('arg_separator.output');
+ if (strlen($separator) != 0) {
+ $append = str_replace($separator, '&', $append);
+ }
+
+ if ($this->getinfo('query')) {
+ $this->parsed['query'] .= '&' . $append;
+ } else {
+ $this->parsed['query'] = $append;
+ }
+ }
+
+ public function addvars($vars = array()) {
+ $append = http_build_query($vars);
+
+ $separator = ini_get('arg_separator.output');
+ if (strlen($separator) != 0) {
+ $append = str_replace($separator, '&', $append);
+ }
+
+ if ($this->getinfo('query')) {
+ $this->parsed['query'] .= '&' . $append;
+ } else {
+ $this->parsed['query'] = $append;
+ }
+ }
+
+ public function get($fullpath = true) {
+ $return = '';
+ if ($fullpath) {
+ $return .= $this->getinfo('scheme') . '://' . $this->getinfo('host');
+
+ if ($this->getinfo('port')) {
+ $return .= ':' . $this->getinfo('port');
+ }
+ }
+
+ $return .= $this->getinfo('path');
+
+ if ($this->getinfo('query')) {
+ $return .= '?' . $this->getinfo('query');
+ }
+
+ if ($this->getinfo('fragment')) {
+ $return .= '#' . $this->getinfo('fragment');
+ }
+
+ return $return;
+ }
+}
diff --git a/includes/class.user.php b/includes/class.user.php
new file mode 100644
index 0000000..f79ad04
--- /dev/null
+++ b/includes/class.user.php
@@ -0,0 +1,555 @@
+<?php
+
+class User
+{
+ public $id = -1;
+ public $perms = array();
+ public $infos = array();
+ public $searches = array();
+ public $search_keys = array('project', 'string', 'type', 'sev', 'pri', 'due', 'dev', 'cat', 'status', 'percent', 'changedfrom', 'closedfrom',
+ 'opened', 'closed', 'search_in_comments', 'search_in_details', 'search_for_all', 'reported', 'only_primary', 'only_watched', 'closedto',
+ 'changedto', 'duedatefrom', 'duedateto', 'openedfrom', 'openedto', 'has_attachment');
+
+ public function __construct($uid = 0)
+ {
+ global $db;
+
+ if ($uid > 0) {
+ $sql = $db->query('SELECT *, g.group_id AS global_group, uig.record_id AS global_record_id
+ FROM {users} u, {users_in_groups} uig, {groups} g
+ WHERE u.user_id = ? AND uig.user_id = ? AND g.project_id = 0
+ AND uig.group_id = g.group_id',
+ array($uid, $uid));
+ }
+
+ if ($uid > 0 && $db->countRows($sql) == 1) {
+ $this->infos = $db->fetchRow($sql);
+ $this->id = intval($uid);
+ } else {
+ $this->infos['real_name'] = L('anonuser');
+ $this->infos['user_name'] = '';
+ }
+
+ $this->get_perms();
+ }
+
+ /**
+ * save_search
+ *
+ * @param string $do
+ * @access public
+ * @return void
+ * @notes FIXME: must return something, should not merge _GET and _REQUEST with other stuff.
+ */
+ public function save_search($do = 'index')
+ {
+ global $db;
+
+ if($this->isAnon()) {
+ return;
+ }
+ // Only logged in users get to use the 'last search' functionality
+
+ if ($do == 'index') {
+ if (Post::val('search_name')) {
+ $arr = array();
+ foreach ($this->search_keys as $key) {
+ $arr[$key] = Post::val($key, ($key == 'status') ? 'open' : null);
+ }
+ foreach (array('order', 'sort', 'order2', 'sort2') as $key) {
+ if (Post::val($key)) {
+ $arr[$key] = Post::val($key);
+ }
+ }
+
+ $fields = array(
+ 'search_string'=> serialize($arr),
+ 'time'=> time(),
+ 'user_id'=> $this->id ,
+ 'name'=> Post::val('search_name')
+ );
+ $keys = array('name','user_id');
+ $db->replace('{searches}', $fields, $keys);
+ }
+ }
+
+ $sql = $db->query('SELECT * FROM {searches} WHERE user_id = ? ORDER BY name ASC', array($this->id));
+ $this->searches = $db->fetchAllArray($sql);
+ }
+
+ public function perms($name, $project = null) {
+ if (is_null($project)) {
+ global $proj;
+ $project = $proj->id;
+ }
+
+ if (isset($this->perms[$project][$name])) {
+ return $this->perms[$project][$name];
+ } else {
+ return 0;
+ }
+ }
+
+ public function get_perms()
+ {
+ global $db, $fs;
+
+ $fields = array('is_admin', 'manage_project', 'view_tasks', 'edit_own_comments',
+ 'open_new_tasks', 'modify_own_tasks', 'modify_all_tasks',
+ 'view_comments', 'add_comments', 'edit_comments', 'edit_assignments',
+ 'delete_comments', 'create_attachments',
+ 'delete_attachments', 'view_history', 'close_own_tasks',
+ 'close_other_tasks', 'assign_to_self', 'assign_others_to_self',
+ 'add_to_assignees', 'view_reports', 'add_votes', 'group_open','view_estimated_effort',
+ 'track_effort', 'view_current_effort_done', 'add_multiple_tasks', 'view_roadmap',
+ 'view_own_tasks', 'view_groups_tasks');
+
+ $this->perms = array(0 => array());
+ // Get project settings which are important for permissions
+ # php7.2 compatible variant without create_function(), instead use a SQL UNION to fill a fake global-project with project_id=0
+ $sql = $db->query('
+ SELECT project_id, others_view, project_is_active, anon_open, comment_closed
+ FROM {projects}
+ UNION
+ SELECT 0,1,1,1,1');
+ while ($row = $db->fetchRow($sql)) {
+ $this->perms[$row['project_id']] = $row;
+ }
+
+ if (!$this->isAnon()) {
+ // Get the global group permissions for the current user
+ $sql = $db->query("SELECT ".join(', ', $fields).", g.project_id, uig.record_id,
+ g.group_open, g.group_id AS project_group
+ FROM {groups} g
+ LEFT JOIN {users_in_groups} uig ON g.group_id = uig.group_id
+ LEFT JOIN {projects} p ON g.project_id = p.project_id
+ WHERE uig.user_id = ?
+ ORDER BY g.project_id, g.group_id ASC",
+ array($this->id));
+
+ while ($row = $db->fetchRow($sql)) {
+ if (!isset($this->perms[$row['project_id']])) {
+ // should not happen, so clean up the DB
+ $db->query('DELETE FROM {users_in_groups} WHERE record_id = ?', array($row['record_id']));
+ continue;
+ }
+
+ $this->perms[$row['project_id']] = array_merge($this->perms[$row['project_id']], $row);
+ }
+
+ // Set missing permissions and attachments
+ foreach ($this->perms as $proj_id => $value) {
+ foreach ($fields as $key) {
+ if ($key == 'project_group') {
+ continue;
+ }
+
+ $this->perms[$proj_id][$key] = max($this->perms[0]['is_admin'], @$this->perms[$proj_id][$key], $this->perms[0][$key]);
+ if ($proj_id && $key != 'is_admin') {
+ $this->perms[$proj_id][$key] = max(@$this->perms[$proj_id]['manage_project'], $this->perms[$proj_id][$key]);
+ }
+ }
+
+ // nobody can upload files if uploads are disabled at the system level..
+ if (!$fs->max_file_size || !is_writable(BASEDIR .'/attachments')) {
+ $this->perms[$proj_id]['create_attachments'] = 0;
+ }
+ }
+ }
+ }
+
+ public function check_account_ok()
+ {
+ global $conf, $baseurl;
+ // Anon users are always OK
+ if ($this->isAnon()) {
+ return;
+ }
+ $saltedpass = crypt($this->infos['user_pass'], $conf['general']['cookiesalt']);
+
+ if (Cookie::val('flyspray_passhash') !== $saltedpass || !$this->infos['account_enabled']
+ || !$this->perms('group_open', 0))
+ {
+ $this->logout();
+ Flyspray::redirect($baseurl);
+ }
+ }
+
+ public function isAnon()
+ {
+ return $this->id < 0;
+ }
+
+ /* }}} */
+ /* permission related {{{ */
+
+ public function can_edit_comment($comment)
+ {
+ return $this->perms('edit_comments')
+ || ($comment['user_id'] == $this->id && $this->perms('edit_own_comments'));
+ }
+
+ public function can_view_project($proj)
+ {
+ if (is_array($proj) && isset($proj['project_id'])) {
+ $proj = $proj['project_id'];
+ }
+
+ return ($this->perms('view_tasks', $proj) || $this->perms('view_groups_tasks', $proj) || $this->perms('view_own_tasks', $proj))
+ || ($this->perms('project_is_active', $proj)
+ && ($this->perms('others_view', $proj) || $this->perms('project_group', $proj)));
+ }
+
+ /* can_select_project() is similiar to can_view_project(), but
+ * allows anonymous users/guests to select this project if the project allows anon task creation,
+ * but all other stuff is restricted.
+ */
+ public function can_select_project($proj)
+ {
+ if (is_array($proj) && isset($proj['project_id'])) {
+ $proj = $proj['project_id'];
+ }
+
+ return (
+ $this->perms('view_tasks', $proj)
+ || $this->perms('view_groups_tasks', $proj)
+ || $this->perms('view_own_tasks', $proj)
+ )
+ ||
+ (
+ $this->perms('project_is_active', $proj)
+ && (
+ $this->perms('others_view', $proj)
+ || $this->perms('project_group', $proj)
+ || $this->perms('anon_open', $proj)
+ )
+ );
+ }
+
+ public function can_view_task($task)
+ {
+ if ($task['task_token'] && Get::val('task_token') == $task['task_token']) {
+ return true;
+ }
+
+ // Split into several separate tests so I can keep track on whats happening.
+
+ // Project managers and admins allowed always.
+ if ($this->perms('manage_project', $task['project_id'])
+ || $this->perms('is_admin', $task['project_id'])) {
+ return true;
+ }
+
+ // Allow if "allow anyone to view this project" is checked
+ // and task is not private.
+ if ($this->perms('others_view', $task['project_id']) && !$task['mark_private']) {
+ return true;
+ }
+
+ if ($this->isAnon()) {
+ // Following checks need identified user.
+ return false;
+ }
+
+ // Non-private task
+ if (!$task['mark_private']) {
+ // Can view tasks, always allow
+ if ($this->perms('view_tasks', $task['project_id'])) {
+ return true;
+ }
+ // User can view only own tasks
+ if ($this->perms('view_own_tasks', $task['project_id'])
+ && !$this->perms('view_groups_tasks', $task['project_id'])) {
+ if ($task['opened_by'] == $this->id) {
+ return true;
+ }
+ if (in_array($this->id, Flyspray::getAssignees($task['task_id']))) {
+ return true;
+ }
+ // No use to continue further.
+ return false;
+ }
+ // Ok, user *must* have view_groups_tasks permission,
+ // but do the check anyway just in case... there might
+ // appear more in the future.
+ if ($this->perms('view_groups_tasks', $task['project_id'])) {
+ // Two first checks the same as with view_own_tasks permission.
+ if ($task['opened_by'] == $this->id) {
+ return true;
+ }
+ // Fetch only once, could be needed three times.
+ $assignees = Flyspray::getAssignees($task['task_id']);
+ if (in_array($this->id, $assignees)) {
+ return true;
+ }
+
+ // Must fetch other persons in the group now. Find out
+ // how to detect the right group for project and the
+ // other persons in it. Funny, found it in $perms.
+ $group = $this->perms('project_group', $task['project_id']);
+ $others = Project::listUsersIn($group);
+
+ foreach ($others as $other) {
+ if ($other['user_id'] == $task['opened_by']) {
+ return true;
+ }
+ if (in_array($other['user_id'], $assignees)) {
+ return true;
+ }
+ }
+
+ // Check the global group next. Note that for users in that group to be included,
+ // the has to be specified at global group level. So even if our permission system
+ // works by OR'ing the permissions together, who is actually considered to be in
+ // in the same group now depends on whether this permission has been given on global
+ // or project level.
+ if ($this->perms('view_groups_tasks', 0)) {
+ $group = $this->perms('project_group', 0);
+ $others = Project::listUsersIn($group);
+
+ foreach ($others as $other) {
+ if ($other['user_id'] == $task['opened_by']) {
+ return true;
+ }
+ if (in_array($other['user_id'], $assignees)) {
+ return true;
+ }
+ }
+ }
+
+ // No use to continue further.
+ return false;
+ }
+ }
+
+ // Private task, user must be either assigned to the task
+ // or have opened it.
+ if ($task['mark_private']) {
+ if ($task['opened_by'] == $this->id) {
+ return true;
+ }
+ if (in_array($this->id, Flyspray::getAssignees($task['task_id']))) {
+ return true;
+ }
+ // No use to continue further.
+ return false;
+ }
+
+ // Could not find any permission for viewing the task.
+ return false;
+ }
+
+ public function can_edit_task($task)
+ {
+ return !$task['is_closed'] && (
+ $this->perms('modify_all_tasks', $task['project_id']) ||
+ ($this->id == $task['opened_by'] && $this->perms('modify_own_tasks', $task['project_id'])) ||
+ in_array($this->id, Flyspray::getAssignees($task['task_id']))
+ );
+ }
+
+ public function can_take_ownership($task)
+ {
+ $assignees = Flyspray::getAssignees($task['task_id']);
+
+ return ($this->perms('assign_to_self', $task['project_id']) && empty($assignees))
+ || ($this->perms('assign_others_to_self', $task['project_id']) && !in_array($this->id, $assignees));
+ }
+
+ public function can_add_to_assignees($task)
+ {
+ return ($this->perms('add_to_assignees', $task['project_id']) && !in_array($this->id, Flyspray::getAssignees($task['task_id'])));
+ }
+
+ public function can_close_task($task)
+ {
+ return ($this->perms('close_own_tasks', $task['project_id']) && in_array($this->id, $task['assigned_to']))
+ || $this->perms('close_other_tasks', $task['project_id']);
+ }
+
+ public function can_set_task_parent($task)
+ {
+ return !$task['is_closed'] && (
+ $this->perms('modify_all_tasks', $task['project_id']) ||
+ in_array($this->id, Flyspray::getAssignees($task['task_id']))
+ );
+ }
+
+ public function can_associate_task($task)
+ {
+ return !$task['is_closed'] && (
+ $this->perms('modify_all_tasks', $task['project_id']) ||
+ in_array($this->id, Flyspray::getAssignees($task['task_id']))
+ );
+ }
+
+ public function can_add_task_dependency($task)
+ {
+ return !$task['is_closed'] && (
+ $this->perms('modify_all_tasks', $task['project_id']) ||
+ in_array($this->id, Flyspray::getAssignees($task['task_id']))
+ );
+ }
+
+//admin approve user registration
+ public function need_admin_approval()
+ {
+ global $fs;
+ return $this->isAnon() && $fs->prefs['need_approval'] && $fs->prefs['anon_reg'];
+ }
+
+ public function get_group_id()
+ {
+ }
+
+ /**
+ * tests if current configuration allows a guest user to register - without email verification code
+ */
+ public function can_self_register()
+ {
+ global $fs;
+ return $this->isAnon() && $fs->prefs['anon_reg'] && !$fs->prefs['only_oauth_reg'] && !$fs->prefs['spam_proof'] ;
+ }
+
+ /**
+ * tests if current configuration allows a guest user to register - with email verification code
+ */
+ public function can_register()
+ {
+ global $fs;
+ return $this->isAnon() && $fs->prefs['anon_reg'] && !$fs->prefs['only_oauth_reg'] && $fs->prefs['spam_proof'] && !$fs->prefs['need_approval'] ;
+ }
+
+ public function can_open_task($proj)
+ {
+ return $proj->id && ($this->perms('manage_project') ||
+ $this->perms('project_is_active', $proj->id) && ($this->perms('open_new_tasks') || $this->perms('anon_open', $proj->id)));
+ }
+
+ public function can_change_private($task)
+ {
+ return !$task['is_closed'] && ($this->perms('manage_project', $task['project_id']) || in_array($this->id, Flyspray::getAssignees($task['task_id'])));
+ }
+
+ public function can_vote($task)
+ {
+ global $db, $fs;
+
+ if (!$this->perms('add_votes', $task['project_id'])) {
+ return -1;
+ }
+
+ // Check that the user hasn't already voted this task
+ $check = $db->query('SELECT vote_id
+ FROM {votes}
+ WHERE user_id = ? AND task_id = ?',
+ array($this->id, $task['task_id']));
+ if ($db->countRows($check)) {
+ return -2;
+ }
+
+ /* FS 1.0alpha daily vote limit
+ // Check that the user hasn't voted more than allowed today
+ $check = $db->query('SELECT vote_id
+ FROM {votes}
+ WHERE user_id = ? AND date_time > ?',
+ array($this->id, time() - 86400));
+ if ($db->countRows($check) >= $fs->prefs['max_vote_per_day']) {
+ return -3;
+ }
+ */
+
+ /* FS 1.0beta2 max votes per user per project limit */
+ $check = $db->query('
+ SELECT COUNT(v.vote_id)
+ FROM {votes} v
+ JOIN {tasks} t ON t.task_id=v.task_id
+ WHERE user_id = ?
+ AND t.project_id = ?
+ AND t.is_closed <>1',
+ array($this->id, $task['project_id'])
+ );
+ if ($db->countRows($check) >= $fs->prefs['votes_per_project']) {
+ return -4;
+ }
+
+
+ return 1;
+ }
+
+ public function logout()
+ {
+ // Set cookie expiry time to the past, thus removing them
+ Flyspray::setcookie('flyspray_userid', '', time()-60);
+ Flyspray::setcookie('flyspray_passhash', '', time()-60);
+ Flyspray::setcookie('flyspray_project', '', time()-60);
+ if (Cookie::has(session_name())) {
+ Flyspray::setcookie(session_name(), '', time()-60);
+ }
+
+ // Unset all of the session variables.
+ $_SESSION = array();
+ session_destroy();
+
+ return !$this->isAnon();
+ }
+
+ /**
+ * Returns the activity by between dates for a project and user.
+ * @param date $startdate
+ * @param date $enddate
+ * @param integer $project_id
+ * @param integer $userid
+ * @return array used to get the count
+ * @access public
+ */
+ static function getActivityUserCount($startdate, $enddate, $project_id, $userid) {
+ global $db;
+ $result = $db->query('SELECT count(event_date) as val
+ FROM {history} h left join {tasks} t on t.task_id = h.task_id
+ WHERE t.project_id = ? AND h.user_id = ? AND event_date BETWEEN ? AND ?',
+ array($project_id, $userid, $startdate, $enddate));
+ $result = $db->fetchCol($result);
+ return $result[0];
+ }
+
+ /**
+ * Returns the day activity by the date for a project and user.
+ * @param date $date
+ * @param integer $project_id
+ * @param integer $userid
+ * @return array used to get the count
+ * @access public
+ */
+ static function getDayActivityByUser($date_start, $date_end, $project_id, $userid) {
+ global $db;
+ //NOTE: from_unixtime() on mysql, to_timestamp() on PostreSQL
+ $func = ('mysql' == $db->dblink->dataProvider) ? 'from_unixtime' : 'to_timestamp';
+
+ $result = $db->query("SELECT count(date({$func}(event_date))) as val, MIN(event_date) as event_date
+ FROM {history} h left join {tasks} t on t.task_id = h.task_id
+ WHERE t.project_id = ? AND h.user_id = ? AND event_date BETWEEN ? AND ?
+ GROUP BY date({$func}(event_date)) ORDER BY event_date DESC",
+ array($project_id, $userid, $date_start, $date_end));
+
+ $date1 = new \DateTime("@$date_start");
+ $date2 = new \DateTime("@$date_end");
+ $days = $date1->diff($date2);
+ $days = $days->format('%a');
+ $results = array();
+
+ for ($i = $days; $i > 0; $i--) {
+ $event_date = (string) strtotime("-{$i} day", $date_end);
+ $results[date('Y-m-d', $event_date)] = 0;
+ }
+
+ while ($row = $result->fetchRow()) {
+ $event_date = date('Y-m-d', $row['event_date']);
+ $results[$event_date] = (integer) $row['val'];
+ }
+
+ return array_values($results);
+ }
+
+ /* }}} */
+}
diff --git a/includes/constants.inc.php b/includes/constants.inc.php
new file mode 100644
index 0000000..b3c3171
--- /dev/null
+++ b/includes/constants.inc.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * Basic constants/variables required for flyspray operation
+ *
+ * @notes be a real paranoid here.
+ * @version $Id$
+ */
+
+define('BASEDIR', dirname(dirname(__FILE__)));
+
+// Change this line if you move flyspray.conf.php elsewhere
+$conf = @parse_ini_file(Flyspray::get_config_path(), true);
+
+// $baseurl
+// htmlspecialchars because PHP_SELF is user submitted data, and can be used as an XSS vector.
+if (isset($conf['general']['force_baseurl']) && $conf['general']['force_baseurl'] != '') {
+ $baseurl = $conf['general']['force_baseurl'];
+} else {
+ if (!isset($webdir)) {
+ $webdir = dirname(htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'utf-8'));
+ if (!$webdir) {
+ $webdir = dirname($_SERVER['SCRIPT_NAME']);
+ }
+ if(substr($webdir, -13) == '/js/callbacks'){
+ $webdir = dirname(dirname($webdir));
+ } elseif (substr($webdir, -9) == 'index.php') {
+ $webdir = dirname($webdir);
+ }
+ }
+
+ $baseurl = rtrim(Flyspray::absoluteURI($webdir),'/\\') . '/' ;
+}
+
+if(isset($conf['general']['syntax_plugin']) && preg_match('/^[a-z0-9_]+$/iD', $conf['general']['syntax_plugin'])) {
+
+$path_to_plugin = sprintf('%s/plugins/%s/%s_constants.inc.php', BASEDIR, $conf['general']['syntax_plugin'], $conf['general']['syntax_plugin']);
+
+ if (is_readable($path_to_plugin)) {
+ include($path_to_plugin);
+ }
+}
+
+define('NOTIFY_TASK_OPENED', 1);
+define('NOTIFY_TASK_CHANGED', 2);
+define('NOTIFY_TASK_CLOSED', 3);
+define('NOTIFY_TASK_REOPENED', 4);
+define('NOTIFY_DEP_ADDED', 5);
+define('NOTIFY_DEP_REMOVED', 6);
+define('NOTIFY_COMMENT_ADDED', 7);
+define('NOTIFY_ATT_ADDED', 8);
+define('NOTIFY_REL_ADDED', 9);
+define('NOTIFY_OWNERSHIP', 10);
+define('NOTIFY_CONFIRMATION', 11);
+define('NOTIFY_PM_REQUEST', 12);
+define('NOTIFY_PM_DENY_REQUEST', 13);
+define('NOTIFY_NEW_ASSIGNEE', 14);
+define('NOTIFY_REV_DEP', 15);
+define('NOTIFY_REV_DEP_REMOVED', 16);
+define('NOTIFY_ADDED_ASSIGNEES', 17);
+define('NOTIFY_ANON_TASK', 18);
+define('NOTIFY_PW_CHANGE', 19);
+define('NOTIFY_NEW_USER', 20);
+define('NOTIFY_OWN_REGISTRATION',21);
+
+define('NOTIFY_EMAIL', 1);
+define('NOTIFY_JABBER', 2);
+define('NOTIFY_BOTH', 3);
+
+define('STATUS_UNCONFIRMED', 1);
+define('STATUS_NEW', 2);
+define('STATUS_ASSIGNED', 3);
+
+define('GET_CONTENTS', true);
+
+# resolution_id with special meaning and protection, always 6 (Flyspray history)
+define('RESOLUTION_DUPLICATE', 6);
+
+// Others
+define('MIN_PW_LENGTH', 5);
+define('LOGIN_ATTEMPTS', 5);
+
+# 201508: webdot currently used not anymore in flyspray. Graphs can be done in future with svg or canvas elements.
+define('FLYSPRAY_WEBDOT', 'http://webdot.flyspray.org/');
+define('FS_DOMAIN_HASH', md5($_SERVER['SERVER_NAME'] . BASEDIR));
+define('FS_CACHE_DIR', Flyspray::get_tmp_dir() . DIRECTORY_SEPARATOR . FS_DOMAIN_HASH);
+
+is_dir(FS_CACHE_DIR) || @mkdir(FS_CACHE_DIR, 0700);
+
+// developers or advanced users only
+//define('DEBUG_SQL',true);
+
+# 201508: Currently without usage! Was once used in file fsjabber.php (not in src anymore), but not within class.jabber2.php.
+//define('JABBER_DEBUG', true);
+//define('JABBER_DEBUG_FILE', BASEDIR . '/logs/jabberlog.txt');
+
+//define('FS_MAIL_LOGFILE', BASEDIR . '/logs/maillog.txt');
diff --git a/includes/events.inc.php b/includes/events.inc.php
new file mode 100644
index 0000000..ec9f2a7
--- /dev/null
+++ b/includes/events.inc.php
@@ -0,0 +1,308 @@
+<?php
+
+// XXX be aware: make sure you quote correctly using qstr()
+// the variables used in the $where parameter, since statement is
+// executed AS IS.
+
+function get_events($task_id, $where = '')
+{
+ global $db;
+ return $db->query("SELECT h.*,
+ tt1.tasktype_name AS task_type1,
+ tt2.tasktype_name AS task_type2,
+ los1.os_name AS operating_system1,
+ los2.os_name AS operating_system2,
+ lc1.category_name AS product_category1,
+ lc2.category_name AS product_category2,
+ p1.project_title AS project_id1,
+ p2.project_title AS project_id2,
+ lv1.version_name AS product_version1,
+ lv2.version_name AS product_version2,
+ ls1.status_name AS item_status1,
+ ls2.status_name AS item_status2,
+ lr.resolution_name,
+ c.date_added AS c_date_added,
+ c.user_id AS c_user_id,
+ att.orig_name
+
+ FROM {history} h
+
+ LEFT JOIN {list_tasktype} tt1 ON tt1.tasktype_id::text = h.old_value AND h.field_changed='task_type'
+ LEFT JOIN {list_tasktype} tt2 ON tt2.tasktype_id::text = h.new_value AND h.field_changed='task_type'
+
+ LEFT JOIN {list_os} los1 ON los1.os_id::text = h.old_value AND h.field_changed='operating_system'
+ LEFT JOIN {list_os} los2 ON los2.os_id::text = h.new_value AND h.field_changed='operating_system'
+
+ LEFT JOIN {list_category} lc1 ON lc1.category_id::text = h.old_value AND h.field_changed='product_category'
+ LEFT JOIN {list_category} lc2 ON lc2.category_id::text = h.new_value AND h.field_changed='product_category'
+
+ LEFT JOIN {list_status} ls1 ON ls1.status_id::text = h.old_value AND h.field_changed='item_status'
+ LEFT JOIN {list_status} ls2 ON ls2.status_id::text = h.new_value AND h.field_changed='item_status'
+
+ LEFT JOIN {list_resolution} lr ON lr.resolution_id::text = h.new_value AND h.event_type = 2
+
+ LEFT JOIN {projects} p1 ON p1.project_id::text = h.old_value AND h.field_changed='project_id'
+ LEFT JOIN {projects} p2 ON p2.project_id::text = h.new_value AND h.field_changed='project_id'
+
+ LEFT JOIN {comments} c ON c.comment_id::text = h.field_changed AND h.event_type = 5
+
+ LEFT JOIN {attachments} att ON att.attachment_id::text = h.new_value AND h.event_type = 7
+
+ LEFT JOIN {list_version} lv1 ON lv1.version_id::text = h.old_value
+ AND (h.field_changed='product_version' OR h.field_changed='closedby_version')
+ LEFT JOIN {list_version} lv2 ON lv2.version_id::text = h.new_value
+ AND (h.field_changed='product_version' OR h.field_changed='closedby_version')
+
+ WHERE h.task_id = ? $where
+ ORDER BY event_date ASC, history_id ASC, event_type ASC", array($task_id));
+}
+
+/**
+ * XXX: A mess,remove my in 1.0. No time for that, sorry.
+ */
+function event_description($history) {
+ $return = '';
+ global $fs, $baseurl, $details, $proj;
+
+ $translate = array('item_summary' => 'summary', 'project_id' => 'attachedtoproject',
+ 'task_type' => 'tasktype', 'product_category' => 'category', 'item_status' => 'status',
+ 'task_priority' => 'priority', 'operating_system' => 'operatingsystem', 'task_severity' => 'severity',
+ 'product_version' => 'reportedversion', 'mark_private' => 'visibility',
+ 'estimated_effort' => 'estimatedeffort');
+ // if somehing gets double escaped, add it here.
+ $noescape = array('new_value', 'old_value');
+
+ foreach($history as $key=> $value) {
+ if(!in_array($key, $noescape)) {
+ $history[$key] = Filters::noXSS($value);
+ }
+ }
+
+ $new_value = $history['new_value'];
+ $old_value = $history['old_value'];
+
+ switch($history['event_type']) {
+ case '3': //Field changed
+ if (!$new_value && !$old_value) {
+ $return .= eL('taskedited');
+ break;
+ }
+
+ $field = $history['field_changed'];
+ switch ($field) {
+ case 'item_summary':
+ case 'project_id':
+ case 'task_type':
+ case 'product_category':
+ case 'item_status':
+ case 'task_priority':
+ case 'operating_system':
+ case 'task_severity':
+ case 'product_version':
+ if($field == 'task_priority') {
+ $old_value = $fs->priorities[$old_value];
+ $new_value = $fs->priorities[$new_value];
+ } elseif($field == 'task_severity') {
+ $old_value = $fs->severities[$old_value];
+ $new_value = $fs->severities[$new_value];
+ } elseif($field == 'item_summary') {
+ $old_value = Filters::noXSS($old_value);
+ $new_value = Filters::noXSS($new_value);
+ } else {
+ $old_value = $history[$field . '1'];
+ $new_value = $history[$field . '2'];
+ }
+ $field = eL($translate[$field]);
+ break;
+ case 'closedby_version':
+ $field = eL('dueinversion');
+ $old_value = ($old_value == '0') ? eL('undecided') : $history['product_version1'];
+ $new_value = ($new_value == '0') ? eL('undecided') : $history['product_version2'];
+ break;
+ case 'due_date':
+ $field = eL('duedate');
+ $old_value = formatDate($old_value, false, eL('undecided'));
+ $new_value = formatDate($new_value, false, eL('undecided'));
+ break;
+ case 'percent_complete':
+ $field = eL('percentcomplete');
+ $old_value .= '%';
+ $new_value .= '%';
+ break;
+ case 'mark_private':
+ $field = eL($translate[$field]);
+ if ($old_value == 1) {
+ $old_value = eL('private');
+ } else {
+ $old_value = eL('public');
+ }
+ if ($new_value == 1) {
+ $new_value = eL('private');
+ } else {
+ $new_value = eL('public');
+ }
+ break;
+ case 'detailed_desc':
+ $field = "<a href=\"javascript:getHistory('{$history['task_id']}', '$baseurl', 'history', '{$history['history_id']}');showTabById('history', true);\">" . eL('details') . '</a>';
+ if (!empty($details)) {
+ $details_previous = TextFormatter::render($old_value);
+ $details_new = TextFormatter::render($new_value);
+ }
+ $old_value = '';
+ $new_value = '';
+ break;
+ case 'estimated_effort':
+ $field = eL($translate[$field]);
+ $old_value = effort::secondsToString($old_value, $proj->prefs['hours_per_manday'], $proj->prefs['estimated_effort_format']);
+ $new_value = effort::secondsToString($new_value, $proj->prefs['hours_per_manday'], $proj->prefs['estimated_effort_format']);;
+ break;
+ }
+ $return .= eL('fieldchanged').": {$field}";
+ if ($old_value || $new_value) {
+ $return .= " ({$old_value} &rarr; {$new_value})";
+ }
+ break;
+ case '1': //Task opened
+ $return .= eL('taskopened');
+ break;
+ case '2': //Task closed
+ $return .= eL('taskclosed');
+ $return .= " ({$history['resolution_name']}";
+ if (!empty($old_value)) {
+ $return .= ': ' . TextFormatter::render($old_value, true);
+ }
+ $return .= ')';
+ break;
+ case '4': //Comment added
+ $return .= '<a href="#comments">' . eL('commentadded') . '</a>';
+ break;
+ case '5': //Comment edited
+ $return .= "<a href=\"javascript:getHistory('{$history['task_id']}', '$baseurl', 'history', '{$history['history_id']}');\">".eL('commentedited')."</a>";
+ if ($history['c_date_added']) {
+ $return .= " (".eL('commentby').' ' . tpl_userlink($history['c_user_id']) . " - " . formatDate($history['c_date_added'], true) . ")";
+ }
+ if ($details) {
+ $details_previous = TextFormatter::render($old_value);
+ $details_new = TextFormatter::render($new_value);
+ }
+ break;
+ case '6': //Comment deleted
+ $return .= "<a href=\"javascript:getHistory('{$history['task_id']}', '$baseurl', 'history', '{$history['history_id']}');\">".eL('commentdeleted')."</a>";
+ if ($new_value != '' && $history['field_changed'] != '') {
+ $return .= " (". eL('commentby'). ' ' . tpl_userlink($new_value) . " - " . formatDate($history['field_changed'], true) . ")";
+ }
+ if (!empty($details)) {
+ $details_previous = TextFormatter::render($old_value);
+ $details_new = '';
+ }
+ break;
+ case '7': //Attachment added
+ $return .= eL('attachmentadded');
+ if ($history['orig_name']) {
+ $return .= ": <a href=\"{$baseurl}?getfile=" . intval($new_value) . '">' . "{$history['orig_name']}</a>";
+ } else if ($history['old_value']) {
+ $return .= ': ' . $history['old_value'];
+ }
+ break;
+ case '8': //Attachment deleted
+ $return .= eL('attachmentdeleted') . ': ' . Filters::noXSS($new_value);
+ break;
+ case '9': //Notification added
+ $return .= eL('notificationadded') . ': ' . tpl_userlink($new_value);
+ break;
+ case '10': //Notification deleted
+ $return .= eL('notificationdeleted') . ': ' . tpl_userlink($new_value);
+ break;
+ case '11': //Related task added
+ $return .= eL('relatedadded') . ': ' . tpl_tasklink($new_value);
+ break;
+ case '12': //Related task deleted
+ $return .= eL('relateddeleted') . ': ' . tpl_tasklink($new_value);
+ break;
+ case '13': //Task reopened
+ $return .= eL('taskreopened');
+ break;
+ case '14': //Task assigned
+ if (empty($old_value)) {
+ $users = explode(' ', trim($new_value));
+ $users = array_map('tpl_userlink', $users);
+ $return .= eL('taskassigned').' ';
+ $return .= implode(', ', $users);
+ } elseif (empty($new_value)) {
+ $return .= eL('assignmentremoved');
+ } else {
+ $users = explode(' ', trim($new_value));
+ $users = array_map('tpl_userlink', $users);
+ $return .= eL('taskreassigned').' ';
+ $return .= implode(', ', $users);
+ }
+ break;
+ // Mentioned in docs, not used anywhere. Will implement if suitable
+ // translations already exist, otherwise leave to 1.1. (Found translations)
+ case '15': // This task was added to another task's related list
+ $return .= eL('addedasrelated') . ': ' . tpl_tasklink($new_value);
+ break;
+ case '16': // This task was removed from another task's related list
+ $return .= eL('deletedasrelated') . ': ' . tpl_tasklink($new_value);
+ break;
+ case '17': //Reminder added
+ $return .= eL('reminderadded') . ': ' . tpl_userlink($new_value);
+ break;
+ case '18': //Reminder deleted
+ $return .= eL('reminderdeleted') . ': ' . tpl_userlink($new_value);
+ break;
+ case '19': //User took ownership
+ $return .= eL('ownershiptaken') . ': ' . tpl_userlink($new_value);
+ break;
+ case '20': //User requested task closure
+ $return .= eL('closerequestmade') . ' - ' . $new_value;
+ break;
+ case '21': //User requested task
+ $return .= eL('reopenrequestmade') . ' - ' . $new_value;
+ break;
+ case '22': // Dependency added
+ $return .= eL('depadded') . ': ' . tpl_tasklink($new_value);
+ break;
+ case '23': // Dependency added to other task
+ $return .= eL('depaddedother') . ': ' . tpl_tasklink($new_value);
+ break;
+ case '24': // Dependency removed
+ $return .= eL('depremoved') . ': ' . tpl_tasklink($new_value);
+ break;
+ case '25': // Dependency removed from other task
+ $return .= eL('depremovedother') . ': ' . tpl_tasklink($new_value);
+ break;
+ // 26 and 27 replaced by 0 (mark_private)
+ case '28': // PM request denied
+ $return .= eL('pmreqdenied') . ' - ' . $new_value;
+ break;
+ case '29': // User added to assignees list
+ $return .= eL('addedtoassignees');
+ break;
+ case '30': // user created
+ $return .= eL('usercreated');
+ break;
+ case '31': // user deleted
+ $return .= eL('userdeleted');
+ break;
+ case '32': // Subtask added
+ $return .= eL('subtaskadded') . ' ' . tpl_tasklink($new_value);
+ break;
+ case '33': // Subtask removed
+ $return .= eL('subtaskremoved') . ' ' . tpl_tasklink($new_value);
+ break;
+ case '34': // supertask added
+ $return .= eL('supertaskadded') . ' ' . tpl_tasklink($new_value);
+ break;
+ case '35': // supertask removed
+ $return .= eL('supertaskremoved') . ' ' . tpl_tasklink($new_value);
+ break;
+ }
+
+ if (isset($details_previous)) $GLOBALS['details_previous'] = $details_previous;
+ if (isset($details_new)) $GLOBALS['details_new'] = $details_new;
+
+ return $return;
+}
+
+?>
diff --git a/includes/fix.inc.php b/includes/fix.inc.php
new file mode 100644
index 0000000..d5d27b1
--- /dev/null
+++ b/includes/fix.inc.php
@@ -0,0 +1,205 @@
+<?php
+
+/*
+ * This file is meant to add every hack that is needed to fix default PHP
+ * behaviours, and to ensure that our PHP env will be able to run flyspray
+ * correctly.
+ *
+ */
+ini_set('display_errors', 0);
+
+// html errors will mess the layout
+ini_set('html_errors', 0);
+
+//error_reporting(E_ALL);
+if(version_compare(PHP_VERSION, '7.2.0') >= 0) {
+ # temporary for php7.2+ (2017-11-30)
+ # not all parts of Flyspray and 3rd party libs like ADODB 5.20.9 not yet 'since-php7.2-deprecated'-ready
+ error_reporting(E_ALL & ~E_STRICT & ~E_DEPRECATED);
+}else{
+ error_reporting(E_ALL & ~E_STRICT);
+}
+// our default charset
+
+ini_set('default_charset','utf-8');
+
+// This to stop PHP being retarded and using the '&' char for session id delimiters
+ini_set('arg_separator.output','&amp;');
+
+// no transparent session id improperly configured servers
+if (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '/setup/') === false) // Skip installer, as it starts the session before calling fix.inc.php causing a warning as this can't be used when a session is already active
+ ini_set('session.use_trans_sid', 0);
+
+//see http://php.net/manual/en/ref.session.php#ini.session.use-only-cookies
+ini_set('session.use_only_cookies',1);
+
+//no session auto start
+ini_set('session.auto_start',0);
+
+/*this stops most cookie attacks via XSS at the interpreter level
+* see http://msdn.microsoft.com/workshop/author/dhtml/httponly_cookies.asp
+* supported by IE 6 SP1, Safari, Konqueror, Opera, silently ignored by others
+* ( sadly, including firefox) available since PHP 5.2.0
+ */
+
+ini_set('session.cookie_httponly',1);
+
+// use stronger entropy in sessions whenever possible
+ini_set('session.entropy_file', '/dev/urandom');
+ini_set('session.entropy_length', 16);
+// use sha-1 for sessions
+ini_set('session.hash_function',1);
+
+ini_set('auto_detect_line_endings', 0);
+
+# for using stronger blowfish hashing functions also with php5.3 < yourphpversion < php5.5
+# minimal php5.3.8 recommended, see https://github.com/ircmaxell/password_compat
+if(!function_exists('password_hash')){
+ require_once dirname(__FILE__).'/password_compat.php';
+}
+
+# for php < php.5.6
+if(!function_exists('hash_equals')) {
+ function hash_equals($str1, $str2) {
+ if(strlen($str1) != strlen($str2)) {
+ return false;
+ } else {
+ $res = $str1 ^ $str2;
+ $ret = 0;
+ for($i = strlen($res) - 1; $i >= 0; $i--) $ret |= ord($res[$i]);
+ return !$ret;
+ }
+ }
+}
+
+
+ini_set('include_path', join( PATH_SEPARATOR, array(
+ dirname(__FILE__) . '/external' ,
+ ini_get('include_path'))));
+
+
+if(count($_GET)) {
+ foreach ($_GET as $key => $value) {
+ if(is_array($value))
+ $_GET[$key] = filter_input(INPUT_GET, $key, FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);
+ else
+ $_GET[$key] = filter_input(INPUT_GET, $key, FILTER_UNSAFE_RAW);
+ }
+}
+if(count($_POST)) {
+ foreach ($_POST as $key => $value) {
+ if(is_array($value))
+ $_POST[$key] = filter_input(INPUT_POST, $key, FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);
+ else
+ $_POST[$key] = filter_input(INPUT_POST, $key, FILTER_UNSAFE_RAW);
+ }
+}
+if(count($_COOKIE)) {
+ foreach ($_COOKIE as $key => $value) {
+ if(is_array($value))
+ $_COOKIE[$key] = filter_input(INPUT_COOKIE, $key, FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);
+ else
+ $_COOKIE[$key] = filter_input(INPUT_COOKIE, $key, FILTER_UNSAFE_RAW);
+ }
+}
+if(isset($_SESSION) && is_array($_SESSION) && count($_SESSION)) {
+ foreach ($_SESSION as $key => $value) {
+ if(is_array($value))
+ $_SESSION[$key] = filter_input(INPUT_SESSION, $key, FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);
+ else
+ $_SESSION[$key] = filter_input(INPUT_SESSION, $key, FILTER_UNSAFE_RAW);
+ }
+}
+
+
+// This is for retarded Windows servers not having REQUEST_URI
+
+if (!isset($_SERVER['REQUEST_URI']))
+{
+ if (isset($_SERVER['SCRIPT_NAME'])) {
+ $_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'];
+ }
+ else {
+ // this is tained now.
+ $_SERVER['REQUEST_URI'] = $_SERVER['PHP_SELF'];
+ }
+
+ if (isset($_SERVER['QUERY_STRING'])) {
+ $_SERVER['REQUEST_URI'] .= '?'.$_SERVER['QUERY_STRING'];
+ }
+}
+
+if (!isset($_SERVER['QUERY_STRING']))
+{
+ $_SERVER['QUERY_STRING'] = '';
+}
+
+
+/**
+ * Replace glob() since this function is apparently
+ * disabled for no apparent reason ("security") on some systems
+ *
+ * @see glob()
+ * @require PHP 4.3.0 (fnmatch)
+ * @todo is this still required?
+ */
+function glob_compat($pattern, $flags = 0) {
+
+ $split = explode('/', $pattern);
+ $match = array_pop($split);
+ $path = implode('/', $split);
+ if (($dir = opendir($path)) !== false) {
+ $glob = array();
+ while (($file = readdir($dir)) !== false) {
+ if (fnmatch($match, $file)) {
+ if (is_dir("$path/$file") || !($flags & GLOB_ONLYDIR)) {
+ if ($flags & GLOB_MARK) $file .= '/';
+ $glob[] = $file;
+ }
+ }
+ }
+ closedir($dir);
+ if (!($flags & GLOB_NOSORT)) sort($glob);
+ return $glob;
+ }
+ return false;
+}
+
+// now for all those borked PHP installations...
+// TODO still required. Enabled by default since 4.2
+if (!function_exists('ctype_alnum')) {
+ function ctype_alnum($text) {
+ return is_string($text) && preg_match('/^[a-z0-9]+$/iD', $text);
+ }
+}
+if (!function_exists('ctype_digit')) {
+ function ctype_digit($text) {
+ return is_string($text) && preg_match('/^[0-9]+$/iD', $text);
+ }
+}
+
+if(!isset($_SERVER['SERVER_NAME']) && php_sapi_name() === 'cli') {
+ $_SERVER['SERVER_NAME'] = php_uname('n');
+}
+
+//for reasons outside flsypray, the PHP core may throw Exceptions in PHP5
+// for a good example see this article
+// http://ilia.ws/archives/107-Another-unserialize-abuse.html
+
+function flyspray_exception_handler($exception) {
+ // Sometimes it helps removing temporary comments from the following three lines.
+ // echo "<pre>";
+ // var_dump(debug_backtrace());
+ // echo "</pre>";
+ die("Completely unexpected exception: " .
+ htmlspecialchars($exception->getMessage(),ENT_QUOTES, 'utf-8') . "<br/>" .
+ "This should <strong> never </strong> happend, please inform Flyspray Developers");
+
+}
+
+set_exception_handler('flyspray_exception_handler');
+
+
+// We don't need session IDs in URLs
+output_reset_rewrite_vars();
+
diff --git a/includes/i18n.inc.php b/includes/i18n.inc.php
new file mode 100644
index 0000000..47d6852
--- /dev/null
+++ b/includes/i18n.inc.php
@@ -0,0 +1,166 @@
+<?php
+
+if (!defined('IN_FS')) {
+ die('Do not access this file directly.');
+}
+
+require_once BASEDIR . '/lang/en.php';
+FlySprayI18N::init('en', $language);
+FlySprayI18N::setDefault($language);
+
+class FlySprayI18N {
+ private static $translations = array();
+
+ public static function init($lang, $translation) {
+ self::$translations[$lang] = $translation;
+ }
+
+ public static function setDefault($translation) {
+ self::$translations['default'] = $translation;
+ }
+
+ public static function L($key, $lang = null) {
+ if (!isset($lang) || empty($lang) || !is_string($lang)) {
+ $lang = 'default';
+ }
+ if ($lang != 'default' && $lang != 'en' && !array_key_exists($lang, self::$translations)) {
+ // echo "<pre>Only once here for $lang!</pre>";
+ $language = BASEDIR . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR . $lang . '.php';
+ if (is_readable($language)) {
+ if ((@require $language) !== FALSE) {
+ // echo "<pre>Loaded: $lang!</pre>";
+ self::$translations[$lang] = $translation;
+ }
+ else {
+ $lang = 'default';
+ }
+ }
+ else {
+ $lang = 'default';
+ }
+ }
+ if (empty($key)) {
+ return '';
+ }
+ if (isset(self::$translations[$lang][$key])) {
+ // echo "<pre>Case 1: $lang!</pre>";
+ return self::$translations[$lang][$key];
+ }
+ if (isset(self::$translations['default'][$key])) {
+ // echo "<pre>Case 2: $lang!</pre>";
+ return self::$translations['default'][$key];
+ }
+ if (isset(self::$translations['en'][$key])) {
+ // echo "<pre>Case 3: $lang!</pre>";
+ return self::$translations['en'][$key];
+ }
+ // echo "<pre>Case 4: $lang!". var_dump(self::$translations['en']) ."</pre>";
+ return "[[$key]]";
+ }
+}
+/**
+ * get the language string $key
+ * return string
+ */
+
+function L($key){
+ global $language;
+ if (empty($key)) {
+ return '';
+ }
+ if (isset($language[$key])) {
+ return $language[$key];
+ }
+ return "[[$key]]";
+}
+
+/**
+ * get the language string $key in $lang
+ * or current default language if $lang
+ * is not given.
+ * return string
+ */
+
+function tL($key, $lang = null) {
+ return FlySprayI18N::L($key, $lang);
+}
+/**
+ * html escaped variant of the previous
+ * return $string
+ */
+function eL($key){
+ return htmlspecialchars(L($key), ENT_QUOTES, 'utf-8');
+}
+
+function load_translations(){
+ global $proj, $language, $user, $fs;
+ # Load translations
+ # if no valid lang_code, return english
+ # valid == a-z and "_" case insensitive
+
+ if (isset($user) && array_key_exists('lang_code', $user->infos)){
+ $lang_code=$user->infos['lang_code'];
+ }
+
+ # 20150211 add language preferences detection of visitors
+ # locale_accept_from_http() not available on every hosting, so we must parse it self.
+ # TODO ..and we can loop later through $langs until we find a matching translation file
+ if((!isset($lang_code) || $lang_code=='' || $lang_code=='browser') && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])){
+ foreach( explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $lang) {
+ # taken from a php.net comment
+ $pattern = '/^(?P<primarytag>[a-zA-Z]{2,8})'.
+ '(?:-(?P<subtag>[a-zA-Z]{2,8}))?(?:(?:;q=)'.
+ '(?P<quantifier>\d\.\d))?$/';
+ $splits = array();
+ if (preg_match($pattern, $lang, $splits)) {
+ $langs[]=$splits;
+ }
+ }
+ # TODO maybe sort $langs-array by quantifiers, but for most browsers it should be ok, because they sent it in right order.
+ if(isset($langs)){
+ $lang_code=$langs[0]['primarytag'];
+ if(isset($langs[0]['subtag'])){
+ $lang_code.='_'.$langs[0]['subtag']; # '_' for our language files, '-' in HTTP_ACCEPT_LANGUAGE
+ }
+ }
+ }
+
+ if(!isset($lang_code) || $lang_code=='' || $lang_code=='project'){
+ if($proj->prefs['lang_code']){
+ $lang_code = $proj->prefs['lang_code'];
+ }else{
+ $lang_code = 'en';
+ }
+ }
+
+ if (!preg_match('/^[a-z_]+$/iD', $lang_code)) {
+ $lang_code ='en';
+ }
+
+ $lang_code = strtolower($lang_code);
+ $translation = BASEDIR.'/lang/'.$lang_code.'.php';
+ if ($lang_code != 'en' && is_readable($translation)) {
+ include_once($translation);
+ $language = is_array($translation) ? array_merge($language, $translation) : $language;
+ FlySprayI18N::init($lang_code, $language);
+ }elseif( 'en'!=substr($lang_code, 0, strpos($lang_code, '_')) && is_readable(BASEDIR.'/lang/'.(substr($lang_code, 0, strpos($lang_code, '_'))).'.php') ){
+ # fallback 'de_AT' to 'de', but not for 'en_US'
+ $translation=BASEDIR.'/lang/'.(substr($lang_code, 0, strpos($lang_code, '_'))).'.php';
+ include_once($translation);
+ $language = is_array($translation) ? array_merge($language, $translation) : $language;
+ }
+
+ FlySprayI18N::setDefault($language);
+ // correctly translate title since language not set when initialising the project
+ if (isset($proj) && !$proj->id) {
+ $proj->prefs['project_title'] = L('allprojects');
+ $proj->prefs['feed_description'] = L('feedforall');
+ }
+
+ for ($i = 6; $i >= 1; $i--) {
+ $fs->priorities[$i] = L('priority' . $i);
+ }
+ for ($i = 5; $i >= 1; $i--) {
+ $fs->severities[$i] = L('severity' . $i);
+ }
+}
diff --git a/includes/modify.inc.php b/includes/modify.inc.php
new file mode 100644
index 0000000..004a5a8
--- /dev/null
+++ b/includes/modify.inc.php
@@ -0,0 +1,3053 @@
+<?php
+
+/**
+ * Database Modifications
+ * @version $Id$
+ */
+
+if (!defined('IN_FS')) {
+ die('Do not access this file directly.');
+}
+
+$notify = new Notifications;
+
+$lt = Post::isAlnum('list_type') ? Post::val('list_type') : '';
+$list_table_name = null;
+$list_column_name = null;
+$list_id = null;
+
+if (strlen($lt)) {
+ $list_table_name = '{list_'.$lt .'}';
+ $list_column_name = $lt . '_name';
+ $list_id = $lt . '_id';
+}
+
+function Post_to0($key) { return Post::val($key, 0); }
+
+function resizeImage($file, $max_x, $max_y, $forcePng = false)
+{
+ if ($max_x <= 0 || $max_y <= 0) {
+ $max_x = 5;
+ $max_y = 5;
+ }
+
+ $src = BASEDIR.'/avatars/'.$file;
+
+ list($width, $height, $type) = getImageSize($src);
+
+ $scale = min($max_x / $width, $max_y / $height);
+ $newWidth = $width * $scale;
+ $newHeight = $height * $scale;
+
+ $img = imagecreatefromstring(file_get_contents($src));
+ $black = imagecolorallocate($img, 0, 0, 0);
+ $resizedImage = imageCreateTrueColor($newWidth, $newHeight);
+ imagecolortransparent($resizedImage, $black);
+ imageCopyResampled($resizedImage, $img, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
+ imageDestroy($img);
+ unlink($src);
+
+ if (!$forcePng) {
+ switch ($type) {
+ case IMAGETYPE_JPEG:
+ imageJpeg($resizedImage, BASEDIR.'/avatars/'.$file);
+ break;
+ case IMAGETYPE_GIF:
+ imageGif($resizedImage, BASEDIR.'/avatars/'.$file);
+ break;
+ case IMAGETYPE_PNG:
+ imagePng($resizedImage, BASEDIR.'/avatars/'.$file);
+ break;
+ default:
+ imagePng($resizedImage, BASEDIR.'/avatars/'.$file);
+ break;
+ }
+ }
+ else {
+ imagePng($resizedImage, BASEDIR.'/avatars/'.$file.'.png');
+ }
+
+ return;
+}
+
+if (Req::num('task_id')) {
+ $task = Flyspray::getTaskDetails(Req::num('task_id'));
+}
+
+if(isset($_SESSION)) {
+ unset($_SESSION['SUCCESS'], $_SESSION['ERROR'], $_SESSION['ERRORS']);
+}
+
+switch ($action = Req::val('action'))
+{
+ // ##################
+ // Adding a new task
+ // ##################
+ case 'newtask.newtask':
+
+ $newtaskerrors=array();
+
+ if (!Post::val('item_summary') || trim(Post::val('item_summary')) == '') { // description not required anymore
+ $newtaskerrors['summaryrequired']=1;
+ }
+
+ if ($user->isAnon() && !filter_var(Post::val('anon_email'), FILTER_VALIDATE_EMAIL)) {
+ $newtaskerrors['invalidemail']=1;
+ }
+
+ if (count($newtaskerrors)>0){
+ $_SESSION['ERRORS']=$newtaskerrors;
+ $_SESSION['ERROR']=L('invalidnewtask');
+ break;
+ }
+
+ list($task_id, $token) = Backend::create_task($_POST);
+ // Status and redirect
+ if ($task_id) {
+ $_SESSION['SUCCESS'] = L('newtaskadded');
+
+ if ($user->isAnon()) {
+ Flyspray::redirect(createURL('details', $task_id, null, array('task_token' => $token)));
+ } else {
+ Flyspray::redirect(createURL('details', $task_id));
+ }
+ } else {
+ Flyspray::show_error(L('databasemodfailed'));
+ break;
+ }
+ break;
+
+ // ##################
+ // Adding multiple new tasks
+ // ##################
+ case 'newmultitasks.newmultitasks':
+ if(!isset($_POST['item_summary'])) {
+ #Flyspray::show_error(L('summaryanddetails'));
+ Flyspray::show_error(L('summaryrequired'));
+ break;
+ }
+ $flag = true;
+ foreach($_POST['item_summary'] as $summary) {
+ if(!$summary || trim($summary) == "") {
+ $flag = false;
+ break;
+ }
+ }
+ $i = 0;
+ foreach($_POST['detailed_desc'] as $detail) {
+ if($detail){
+ # only for ckeditor/html, not for dokuwiki (or other syntax plugins in future)
+ if ($conf['general']['syntax_plugin'] != 'dokuwiki') {
+ $_POST['detailed_desc'][$i] = "<p>" . $detail . "</p>";
+ }
+ }
+ $i++;
+ }
+ if(!$flag) {
+ #Flyspray::show_error(L('summaryanddetails'));
+ Flyspray::show_error(L('summaryrequired'));
+ break;
+ }
+
+ $flag = true;
+ $length = count($_POST['detailed_desc']);
+ for($i = 0; $i < $length; $i++) {
+ $ticket = array();
+ foreach($_POST as $key => $value) {
+ if($key == "assigned_to") {
+ $sql = $db->query("SELECT user_id FROM {users} WHERE user_name = ? or real_name = ?", array($value[$i], $value[$i]));
+ $ticket["rassigned_to"] = array(intval($db->fetchOne($sql)));
+ continue;
+ }
+ if(is_array($value))
+ $ticket[$key] = $value[$i];
+ else
+ $ticket[$key] = $value;
+ }
+ list($task_id, $token) = Backend::create_task($ticket);
+ if (!$task_id) {
+ $flag = false;
+ break;
+ }
+ }
+
+ if(!$flag) {
+ Flyspray::show_error(L('databasemodfailed'));
+ break;
+ }
+
+ $_SESSION['SUCCESS'] = L('newtaskadded');
+ Flyspray::redirect(createURL('index', $proj->id));
+ break;
+
+ // ##################
+ // Modifying an existing task
+ // ##################
+ case 'details.update':
+ if (!$user->can_edit_task($task)) {
+ Flyspray::show_error(L('nopermission')); # TODO create a better error message
+ break;
+ }
+
+ $errors=array();
+
+ # TODO add checks who should be able to move a task, modify_all_tasks perm should not be enough and the target project perms are required too.
+ # - User has project manager permission in source project AND in target project: Allowed to move task
+ # - User has project manager permission in source project, but NOT in target project: Can send request to PUSH task to target project. A user with project manager permission of target project can accept the PUSH request.
+ # - User has NO project manager permission in source project, but in target project: Can send request to PULL task to target project. A user with project manager permission of source project can accept the PULL request.
+ # - User has calculated can_edit_task permission in source project AND (at least) newtask perm in target project: Can send a request to move task (similiar to 'close task please'-request) with the target project id, sure.
+
+ $move=0;
+ if($task['project_id'] != Post::val('project_id')) {
+ $toproject=new Project(Post::val('project_id'));
+ if($user->perms('modify_all_tasks', $toproject->id)){
+ $move=1;
+ } else{
+ $errors['invalidtargetproject']=1;
+ }
+ }
+
+ if($move==1){
+ # Check that a task is not moved to a different project than its
+ # possible parent or subtasks. Note that even closed tasks are
+ # included in the result, a task can be always reopened later.
+ $result = $db->query('
+ SELECT parent.task_id, parent.project_id FROM {tasks} p
+ JOIN {tasks} parent ON parent.task_id = p.supertask_id
+ WHERE p.task_id = ?
+ AND parent.project_id <> ?',
+ array( $task['task_id'], Post::val('project_id') )
+ );
+ $parentcheck = $db->fetchRow($result);
+ if ($parentcheck && $parentcheck['task_id']) {
+ if ($parentcheck['project_id'] != Post::val('project_id')) {
+ $errors['denymovehasparent']=L('denymovehasparent');
+ }
+ }
+
+ $result = $db->query('
+ SELECT sub.task_id, sub.project_id FROM {tasks} p
+ JOIN {tasks} sub ON p.task_id = sub.supertask_id
+ WHERE p.task_id = ?
+ AND sub.project_id <> ?',
+ array( $task['task_id'], Post::val('project_id') )
+ );
+ $subcheck = $db->fetchRow($result);
+
+ # if there are any subtasks, check that the project is not changed
+ if ($subcheck && $subcheck['task_id']) {
+ $errors['denymovehassub']=L('denymovehassub');
+ }
+ }
+
+ # summary form input fields, so user get notified what needs to be done right to be accepted
+ if (!Post::val('item_summary')) {
+ # description can be empty now
+ #Flyspray::show_error(L('summaryanddetails'));
+ #Flyspray::show_error(L('summaryrequired'));
+ $errors['summaryrequired']=L('summaryrequired');
+ }
+
+ # ids of severity and priority are (probably!) intentional fixed in Flyspray.
+ if( isset($_POST['task_severity']) && (!is_numeric(Post::val('task_severity')) || Post::val('task_severity')>5 || Post::val('task_severity')<0 ) ){
+ $errors['invalidseverity']=1;
+ }
+
+ # peterdd:temp fix to allow priority 6 again
+ # But I think about 1-5 valid (and 0 for unset) only in future to harmonize
+ # with other trackers/taskplaner software and for severity-priority graphs like
+ # https://en.wikipedia.org/wiki/Time_management#The_Eisenhower_Method
+ if( isset($_POST['task_priority']) && (!is_numeric(Post::val('task_priority')) || Post::val('task_priority')>6 || Post::val('task_priority')<0 ) ){
+ $errors['invalidpriority']=1;
+ }
+
+ if( isset($_POST['percent_complete']) && (!is_numeric(Post::val('percent_complete')) || Post::val('percent_complete')>100 || Post::val('percent_complete')<0 ) ){
+ $errors['invalidprogress']=1;
+ }
+
+ # Description for the following list values here when moving a task to a different project:
+ # - Do we use the old invalid values? (current behavior until 1.0-beta2, invalid id-values in database can be set, can result in php-'notices' or values arent shown on pages)
+ # - Or set to default value of the new project? And inform the user to adjust the task properties in the new project?
+ # - Or create a new tasktype for the new project, but:
+ # - Has the user the permission to create a new tasktype for the new project?
+ # - similiar named tasktypes exists?
+ #
+ # Maybe let's go with 2 steps when in this situation:
+ # When user want move task to other project, a second page shows the form again but:
+ # dropdown list forms show - maybe divided as optiongroups - :
+ # -global list values ()
+ # -current project list values
+ # -target project list values
+ # -option to create a new option based on current project value (if the user has the permission for the target project!)
+ # -option to set to default value in target project or unset value
+ # Also consider that not all list dropdown field may be shown to the user because of project settings (visible_fields)!
+
+
+ # which $proj should we use here? $proj object is set in header.php by a request param before modify.inc.php is loaded, so it can differ from $task['project_id']!
+ if($move==1){
+ $statusarray=$toproject->listTaskStatuses();
+ } else{
+ $statusarray=$proj->listTaskStatuses();
+ }
+
+ # FIXME what if we move to different project, but the status_id is defined for the old project only (not global)?
+ # FIXME what if we move to different project and item_status selection is deactivated/not shown in edit task page?
+ if( isset($_POST['item_status']) && (!is_numeric(Post::val('item_status')) || false===Flyspray::array_find('status_id', Post::val('item_status'), $statusarray) ) ){
+ $errors['invalidstatus']=1;
+ }
+
+ if($move==1){
+ $typearray=$toproject->listTaskTypes();
+ } else{
+ $typearray=$proj->listTaskTypes();
+ }
+
+ # FIXME what if we move to different project, but tasktype_id is defined for the old project only (not global)?
+ # FIXME what if we move to different project and task_type selection is deactiveated/not shown in edit task page?
+ if( isset($_POST['task_type']) && (!is_numeric(Post::val('task_type')) || false===Flyspray::array_find('tasktype_id', Post::val('task_type'), $typearray) ) ){
+ $errors['invalidtasktype']=1;
+ }
+
+ # FIXME what if we move to different project and reportedver selection is deactivated/not shown in edit task page?
+ # FIXME what if we move to different project and reportedver is deactivated/not shown in edit task page?
+ # FIXME what if we move to different project and closedby_version selection is deactivated/not shown in edit task page?
+ # FIXME what if we move to different project and closedby_version is deactivated/not shown in edit task page?
+ if($move==1){
+ $versionarray=$toproject->listVersions();
+ } else{
+ $versionarray=$proj->listVersions();
+ }
+ if( isset($_POST['reportedver']) && (!is_numeric(Post::val('reportedver')) || ( $_POST['reportedver']!=='0' && false===Flyspray::array_find('version_id', Post::val('reportedver'), $versionarray)) ) ){
+ $errors['invalidreportedversion']=1;
+ }
+ if( isset($_POST['closedby_version']) && (!is_numeric(Post::val('closedby_version')) || ( $_POST['closedby_version']!=='0' && false===Flyspray::array_find('version_id', Post::val('closedby_version'), $versionarray)) ) ){
+ $errors['invaliddueversion']=1;
+ }
+
+ # FIXME what if we move to different project, but category_id is defined for the old project only (not global)?
+ # FIXME what if we move to different project and category selection is deactivated/not shown in edit task page?
+ if($move==1){
+ $catarray=$toproject->listCategories();
+ } else{
+ $catarray=$proj->listCategories();
+ }
+ if( isset($_POST['product_category']) && (!is_numeric(Post::val('product_category')) || false===Flyspray::array_find('category_id', Post::val('product_category'), $catarray) ) ){
+ $errors['invalidcategory']=1;
+ }
+
+ # FIXME what if we move to different project, but os_id is defined for the old project only (not global)?
+ # FIXME what if we move to different project and operating_system selection is deactivated/not shown in edit task page?
+ if($move==1){
+ $osarray=$toproject->listOs();
+ } else{
+ $osarray=$proj->listOs();
+ }
+ if( isset($_POST['operating_system']) && (!is_numeric(Post::val('operating_system')) || ( $_POST['operating_system']!=='0' && false===Flyspray::array_find('os_id', Post::val('operating_system'), $osarray)) ) ){
+ $errors['invalidos']=1;
+ }
+
+ if ($due_date = Post::val('due_date', 0)) {
+ $due_date = Flyspray::strtotime(Post::val('due_date'));
+ }
+
+ $estimated_effort = 0;
+ if (($estimated_effort = effort::editStringToSeconds(Post::val('estimated_effort'), $proj->prefs['hours_per_manday'], $proj->prefs['estimated_effort_format'])) === FALSE) {
+ $errors['invalideffort']=1;
+ }
+
+ $time = time();
+
+ $result = $db->query('SELECT * from {tasks} WHERE task_id = ?', array($task['task_id']));
+ $defaults = $db->fetchRow($result);
+
+ if (!Post::has('due_date')) {
+ $due_date = $defaults['due_date'];
+ }
+
+ if (!Post::has('estimated_effort')) {
+ $estimated_effort = $defaults['estimated_effort'];
+ }
+
+
+ if(count($errors)>0){
+ # some invalid input by the user. Do not save the input and in the details-edit-template show the user where in the form the invalid values are.
+ $_SESSION['ERRORS']=$errors; # $_SESSION['ERROR'] is very limited, holds only one string and often just overwritten
+ $_SESSION['ERROR']=L('invalidinput');
+ # pro and contra http 303 redirect here:
+ # - good: browser back button works, browser history.
+ # - bad: form inputs of user not preserved (at the moment). Annoying if user wrote a long description and then the form submit gets denied because of other reasons.
+ #Flyspray::redirect(createURL('edittask', $task['task_id']));
+ break;
+ }
+
+ # FIXME/TODO: If a user has only 'edit own task edit' permission and task remains in the same project,
+ # but there are not all fields visible/editable so the browser do not send that values with the form,
+ # the sql update query should not touch that fields. And it should not overwrite the field with the default value in this case.
+ # So this update query should be build dynamic (And for the future: when 'custom fields' are implemented ..)
+ # Alternative: Read task field values before update query.
+ # And we should check too what task fields the 'edit own task only'-user is allowed to change.
+ # (E.g ignore form fields the user is not allowed to change. Currently hardcoded in template..)
+
+/*
+ # Dynamic creation of the UPDATE query required
+ # First step: Settings which task fields can be changed by 'permission level': Based on situation found in FS 1.0-rc1 'status quo' in backend::create_task() and CleanFS/templates/template details.edit.tpl
+ #$basicfields[]=array('item_summary','detailed_desc', 'task_type', 'product_category', 'operating_system', 'task_severity', 'percent_complete', 'product_version', 'estimated_effort'); # modify_own_tasks, anon_open
+ $basicfields=$proj->prefs['basic_fields'];
+
+ # peterdd: just saved a bit work in progress for future dynamic sql update string
+ $sqlup='';
+ foreach($basicfields as $bf){
+ $sqlup.=' '.$bf.' = ?,';
+ $sqlparam[]= Post::val($bf, $oldvals[$bf]);
+ }
+ $sqlup.=' last_edited_by = ?,';
+ $sqlparam[]= $user->id;
+ $sqlup.=' last_edited_time = ?,';
+ $sqlparam[]= $time;
+
+ $devfields[]=array('task_priority', 'due_date', 'item_status', 'closedby_version'); # modify_all_tasks
+ $managerfields[]=array('project_id','mark_private'); # manage_project
+ #$customfields[]=array(); # Flyspray 1.? future: perms depend of each custom field setting in a project..
+
+ $sqlparam[]=$task['task_id'];
+ $sqlupdate='UPDATE {tasks} SET '.$sqlup.' WHERE task_id = ?';
+
+ echo '<pre>';print_r($sqlupdate);print_r($sqlparam);die();
+ $db->query($sqlupdate, $sqlparam);
+*/
+
+ $detailed_desc = Post::val('detailed_desc', $defaults['detailed_desc']);
+
+ # dokuwiki syntax plugin filters on output
+ if($conf['general']['syntax_plugin'] != 'dokuwiki'){
+ $purifierconfig = HTMLPurifier_Config::createDefault();
+ $purifier = new HTMLPurifier($purifierconfig);
+ $detailed_desc = $purifier->purify($detailed_desc);
+ }
+
+ $db->query('UPDATE {tasks}
+ SET
+ project_id = ?,
+ task_type = ?,
+ item_summary = ?,
+ detailed_desc = ?,
+ item_status = ?,
+ mark_private = ?,
+ product_category = ?,
+ closedby_version = ?,
+ operating_system = ?,
+ task_severity = ?,
+ task_priority = ?,
+ last_edited_by = ?,
+ last_edited_time = ?,
+ due_date = ?,
+ percent_complete = ?,
+ product_version = ?,
+ estimated_effort = ?
+ WHERE task_id = ?',
+ array(
+ Post::val('project_id', $defaults['project_id']),
+ Post::val('task_type', $defaults['task_type']),
+ Post::val('item_summary', $defaults['item_summary']),
+ $detailed_desc,
+ Post::val('item_status', $defaults['item_status']),
+ intval($user->can_change_private($task) && Post::val('mark_private', $defaults['mark_private'])),
+ Post::val('product_category', $defaults['product_category']),
+ Post::val('closedby_version', $defaults['closedby_version']),
+ Post::val('operating_system', $defaults['operating_system']),
+ Post::val('task_severity', $defaults['task_severity']),
+ Post::val('task_priority', $defaults['task_priority']),
+ intval($user->id), $time, intval($due_date),
+ Post::val('percent_complete', $defaults['percent_complete']),
+ Post::val('reportedver', $defaults['product_version']),
+ intval($estimated_effort),
+ $task['task_id']
+ )
+ );
+
+ // Update the list of users assigned this task
+ $assignees = (array) Post::val('rassigned_to');
+ $assignees_changed = count(array_diff($task['assigned_to'], $assignees)) + count(array_diff($assignees, $task['assigned_to']));
+ if ($user->perms('edit_assignments') && $assignees_changed) {
+
+ // Delete the current assignees for this task
+ $db->query('DELETE FROM {assigned}
+ WHERE task_id = ?',
+ array($task['task_id']));
+
+ // Convert assigned_to and store them in the 'assigned' table
+ foreach ((array) Post::val('rassigned_to') as $key => $val)
+ {
+ $db->replace('{assigned}', array('user_id'=> $val, 'task_id'=> $task['task_id']), array('user_id','task_id'));
+ }
+ }
+
+ # FIXME what if we move to different project, but tag(s) is/are defined for the old project only (not global)?
+ # FIXME what if we move to different project and tag input field is deactivated/not shown in edit task page?
+ # - Create new tag(s) in target project if user has permission to create new tags but what with the users who have not the permission?
+ # update tags
+ $tagList = explode(';', Post::val('tags'));
+ $tagList = array_map('strip_tags', $tagList);
+ $tagList = array_map('trim', $tagList);
+ $tagList = array_unique($tagList); # avoid duplicates for inputs like: "tag1;tag1" or "tag1; tag1<p></p>"
+ $storedtags=array();
+ foreach($task['tags'] as $temptag){
+ $storedtags[]=$temptag['tag'];
+ }
+ $tags_changed = count(array_diff($storedtags, $tagList)) + count(array_diff($tagList, $storedtags));
+
+ if($tags_changed){
+ // Delete the current assigned tags for this task
+ $db->query('DELETE FROM {task_tag} WHERE task_id = ?', array($task['task_id']));
+ foreach ($tagList as $tag){
+ if ($tag == ''){
+ continue;
+ }
+ # size of {list_tag}.tag_name, see flyspray-install.xml
+ if(mb_strlen($tag) > 40){
+ # report that softerror
+ $errors['tagtoolong']=1;
+ continue;
+ }
+
+ $res=$db->query("SELECT tag_id FROM {list_tag} WHERE (project_id=0 OR project_id=?) AND tag_name LIKE ? ORDER BY project_id", array($proj->id,$tag) );
+ if($t=$db->fetchRow($res)){
+ $tag_id=$t['tag_id'];
+ } else{
+ if( $proj->prefs['freetagging']==1){
+ # add to taglist of the project
+ $db->query("INSERT INTO {list_tag} (project_id,tag_name) VALUES (?,?)", array($proj->id,$tag));
+ $tag_id=$db->insert_ID();
+ } else{
+ continue;
+ }
+ };
+ $db->query("INSERT INTO {task_tag}(task_id,tag_id) VALUES(?,?)", array($task['task_id'], $tag_id) );
+ }
+ }
+
+ // Get the details of the task we just updated
+ // To generate the changed-task message
+ $new_details_full = Flyspray::getTaskDetails($task['task_id']);
+ // Not very nice...maybe combine compare_tasks() and logEvent() ?
+ $result = $db->query("SELECT * FROM {tasks} WHERE task_id = ?",
+ array($task['task_id']));
+ $new_details = $db->fetchRow($result);
+
+ foreach ($new_details as $key => $val) {
+ if (strstr($key, 'last_edited_') || $key == 'assigned_to' || is_numeric($key)) {
+ continue;
+ }
+
+ if ($val != $task[$key]) {
+ // Log the changed fields in the task history
+ Flyspray::logEvent($task['task_id'], 3, $val, $task[$key], $key, $time);
+ }
+ }
+
+ $changes = Flyspray::compare_tasks($task, $new_details_full);
+ if (count($changes) > 0) {
+ $notify->create(NOTIFY_TASK_CHANGED, $task['task_id'], $changes, null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+ }
+
+ if ($assignees_changed) {
+ // Log to task history
+ Flyspray::logEvent($task['task_id'], 14, implode(' ', $assignees), implode(' ', $task['assigned_to']), '', $time);
+
+ // Notify the new assignees what happened. This obviously won't happen if the task is now assigned to no-one.
+ if (count($assignees)) {
+ $new_assignees = array_diff($task['assigned_to'], $assignees);
+ // Remove current user from notification list
+ if (!$user->infos['notify_own']) {
+ $new_assignees = array_filter($new_assignees, function($u) use($user) { return $user->id != $u; } );
+ }
+ if(count($new_assignees)) {
+ $notify->create(NOTIFY_NEW_ASSIGNEE, $task['task_id'], null, $notify->specificAddresses($new_assignees), NOTIFY_BOTH, $proj->prefs['lang_code']);
+ }
+ }
+ }
+
+ Backend::add_comment($task, Post::val('comment_text'), $time);
+ Backend::delete_files(Post::val('delete_att'));
+ Backend::upload_files($task['task_id'], '0', 'usertaskfile');
+ Backend::delete_links(Post::val('delete_link'));
+ Backend::upload_links($task['task_id'], '0', 'userlink');
+
+ $_SESSION['SUCCESS'] = L('taskupdated');
+ # report minor/soft errors too that does not hindered saving task
+ if(count($errors)>0){
+ $_SESSION['ERRORS']=$errors;
+ }
+ Flyspray::redirect(createURL('details', $task['task_id']));
+ break;
+
+ // ##################
+ // closing a task
+ // ##################
+ case 'details.close':
+ if (!$user->can_close_task($task)) {
+ break;
+ }
+
+ if ($task['is_closed']) {
+ break;
+ }
+
+ if (!Post::val('resolution_reason')) {
+ Flyspray::show_error(L('noclosereason'));
+ break;
+ }
+
+ Backend::close_task($task['task_id'], Post::val('resolution_reason'), Post::val('closure_comment', ''), Post::val('mark100', false));
+
+ $_SESSION['SUCCESS'] = L('taskclosedmsg');
+ # FIXME there are several pages using this form, details and pendingreq at least
+ #Flyspray::redirect(createURL('details', $task['task_id']));
+ break;
+
+ case 'details.associatesubtask':
+ if ( $task['task_id'] == Post::num('associate_subtask_id')) {
+ Flyspray::show_error(L('selfsupertasknotallowed'));
+ break;
+ }
+ $sql = $db->query('SELECT supertask_id, project_id FROM {tasks} WHERE task_id = ?',
+ array(Post::num('associate_subtask_id')));
+
+ $suptask = $db->fetchRow($sql);
+
+ // check to see if the subtask exists.
+ if (!$suptask) {
+ Flyspray::show_error(L('subtasknotexist'));
+ break;
+ }
+
+ // if the user has not the permission to view all tasks, check if the task
+ // is in tasks allowed to see, otherwise tell that the task does not exist.
+ if (!$user->perms('view_tasks')) {
+ $taskcheck = Flyspray::getTaskDetails(Post::num('associate_subtask_id'));
+ if (!$user->can_view_task($taskcheck)) {
+ Flyspray::show_error(L('subtasknotexist'));
+ break;
+ }
+ }
+
+ // check to see if associated subtask is already the parent of this task
+ if ($suptask['supertask_id'] == Post::num('associate_subtask_id')) {
+ Flyspray::show_error(L('subtaskisparent'));
+ break;
+ }
+
+ // check to see if associated subtask already has a parent task
+ if ($suptask['supertask_id']) {
+ Flyspray::show_error(L('subtaskalreadyhasparent'));
+ break;
+ }
+
+ // check to see that both tasks belong to the same project
+ if ($task['project_id'] != $suptask['project_id']) {
+ Flyspray::show_error(L('musthavesameproject'));
+ break;
+ }
+
+ //associate the subtask
+ $db->query('UPDATE {tasks} SET supertask_id=? WHERE task_id=?',array( $task['task_id'], Post::num('associate_subtask_id')));
+ Flyspray::logEvent($task['task_id'], 32, Post::num('associate_subtask_id'));
+ Flyspray::logEvent(Post::num('associate_subtask_id'), 34, $task['task_id']);
+
+ $_SESSION['SUCCESS'] = sprintf( L('associatedsubtask'), Post::num('associate_subtask_id') );
+ break;
+
+
+ case 'reopen':
+ // ##################
+ // re-opening an task
+ // ##################
+ if (!$user->can_close_task($task)) {
+ break;
+ }
+
+ // Get last %
+ $old_percent = $db->query("SELECT old_value, new_value
+ FROM {history}
+ WHERE field_changed = 'percent_complete'
+ AND task_id = ? AND old_value != '100'
+ ORDER BY event_date DESC
+ LIMIT 1",
+ array($task['task_id']));
+ $old_percent = $db->fetchRow($old_percent);
+
+ $db->query("UPDATE {tasks}
+ SET resolution_reason = 0, closure_comment = '', date_closed = 0,
+ last_edited_time = ?, last_edited_by = ?, is_closed = 0, percent_complete = ?
+ WHERE task_id = ?",
+ array(time(), $user->id, intval($old_percent['old_value']), $task['task_id']));
+
+ Flyspray::logEvent($task['task_id'], 3, $old_percent['old_value'], $old_percent['new_value'], 'percent_complete');
+
+ $notify->create(NOTIFY_TASK_REOPENED, $task['task_id'], null, null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+
+ // add comment of PM request to comment page if accepted
+ $sql = $db->query('SELECT * FROM {admin_requests} WHERE task_id = ? AND request_type = ? AND resolved_by = 0',
+ array($task['task_id'], 2));
+ $request = $db->fetchRow($sql);
+ if ($request) {
+ $db->query('INSERT INTO {comments}
+ (task_id, date_added, last_edited_time, user_id, comment_text)
+ VALUES ( ?, ?, ?, ?, ? )',
+ array($task['task_id'], time(), time(), $request['submitted_by'], $request['reason_given']));
+ // delete existing PM request
+ $db->query('UPDATE {admin_requests}
+ SET resolved_by = ?, time_resolved = ?
+ WHERE request_id = ?',
+ array($user->id, time(), $request['request_id']));
+ }
+
+ Flyspray::logEvent($task['task_id'], 13);
+
+ $_SESSION['SUCCESS'] = L('taskreopenedmsg');
+ # FIXME there are several pages using this form, details and pendingreq at least
+ #Flyspray::redirect(createURL('details', $task['task_id']));
+ break;
+
+ // ##################
+ // adding a comment
+ // ##################
+ case 'details.addcomment':
+ if (!Backend::add_comment($task, Post::val('comment_text'))) {
+ Flyspray::show_error(L('nocommententered'));
+ break;
+ }
+
+ if (Post::val('notifyme') == '1') {
+ // If the user wanted to watch this task for changes
+ Backend::add_notification($user->id, $task['task_id']);
+ }
+
+ $_SESSION['SUCCESS'] = L('commentaddedmsg');
+ Flyspray::redirect(createURL('details', $task['task_id']));
+ break;
+
+ // ##################
+ // Tracking
+ // ##################
+ case 'details.efforttracking':
+
+ require_once BASEDIR . '/includes/class.effort.php';
+ $effort = new effort($task['task_id'],$user->id);
+
+
+ if(Post::val('start_tracking')){
+ if($effort->startTracking())
+ {
+ $_SESSION['SUCCESS'] = L('efforttrackingstarted');
+ }
+ else
+ {
+ $_SESSION['ERROR'] = L('efforttrackingnotstarted');
+ }
+ }
+
+ if(Post::val('stop_tracking')){
+ $effort->stopTracking();
+ $_SESSION['SUCCESS'] = L('efforttrackingstopped');
+ }
+
+ if(Post::val('cancel_tracking')){
+ $effort->cancelTracking();
+ $_SESSION['SUCCESS'] = L('efforttrackingcancelled');
+ }
+
+ if(Post::val('manual_effort')){
+ if($effort->addEffort(Post::val('effort_to_add'), $proj)){
+ $_SESSION['SUCCESS'] = L('efforttrackingadded');
+ }
+ }
+
+ Flyspray::redirect(createURL('details', $task['task_id']).'#effort');
+ break;
+
+ // ##################
+ // sending a new user a confirmation code
+ // ##################
+ case 'register.sendcode':
+ if (!$user->can_register()) {
+ break;
+ }
+
+ $captchaerrors=array();
+ if($fs->prefs['captcha_securimage']){
+ $image = new Securimage();
+ if( !Post::isAlnum('captcha_code') || !$image->check(Post::val('captcha_code'))) {
+ $captchaerrors['invalidsecurimage']=1;
+ }
+ }
+
+ if($fs->prefs['captcha_recaptcha']){
+ require_once('class.recaptcha.php');
+ if( !recaptcha::verify()) {
+ $captchaerrors['invalidrecaptcha']=1;
+ }
+ }
+
+ if(count($captchaerrors)){
+ $_SESSION['ERRORS']=$captchaerrors;
+ Flyspray::show_error(L('captchaerror'));
+ break;
+ }
+
+ if (!Post::val('user_name') || !Post::val('real_name')
+ || !Post::val('email_address'))
+ {
+ // If the form wasn't filled out correctly, show an error
+ Flyspray::show_error(L('registererror'));
+ break;
+ }
+
+ if ($fs->prefs['repeat_emailaddress'] && Post::val('email_address') != Post::val('verify_email_address'))
+ {
+ Flyspray::show_error(L('emailverificationwrong'));
+ break;
+ }
+
+ $email = strtolower(Post::val('email_address'));
+ $jabber_id = strtolower(Post::val('jabber_id'));
+
+ //email is mandatory
+ if (!$email || !Flyspray::check_email($email)) {
+ Flyspray::show_error(L('novalidemail'));
+ break;
+ }
+ //jabber_id is optional
+ if ($jabber_id && !Jabber::check_jid($jabber_id)) {
+ Flyspray::show_error(L('novalidjabber'));
+ break;
+ }
+
+ $user_name = Backend::clean_username(Post::val('user_name'));
+
+ // Limit length
+ $real_name = substr(trim(Post::val('real_name')), 0, 100);
+ // Remove doubled up spaces and control chars
+ $real_name = preg_replace('![\x00-\x1f\s]+!u', ' ', $real_name);
+
+ if (!$user_name || empty($user_name) || !$real_name) {
+ Flyspray::show_error(L('entervalidusername'));
+ break;
+ }
+
+ // Delete registration codes older than 24 hours
+ $yesterday = time() - 86400;
+ $db->query('DELETE FROM {registrations} WHERE reg_time < ?', array($yesterday));
+
+ $sql = $db->query('SELECT COUNT(*) FROM {users} u, {registrations} r
+ WHERE u.user_name = ? OR r.user_name = ?',
+ array($user_name, $user_name));
+ if ($db->fetchOne($sql)) {
+ Flyspray::show_error(L('usernametaken'));
+ break;
+ }
+
+ $sql = $db->query("SELECT COUNT(*) FROM {users} WHERE
+ jabber_id = ? AND jabber_id != ''
+ OR email_address = ? AND email_address != ''",
+ array($jabber_id, $email));
+ if ($db->fetchOne($sql)) {
+ Flyspray::show_error(L('emailtaken'));
+ break;
+ }
+
+ // Generate a random bunch of numbers for the confirmation code and the confirmation url
+
+ foreach(array('randval','magic_url') as $genrandom) {
+
+ $$genrandom = md5(function_exists('openssl_random_pseudo_bytes') ?
+ openssl_random_pseudo_bytes(32) :
+ uniqid(mt_rand(), true));
+ }
+
+ $confirm_code = substr($randval, 0, 20);
+
+ // echo "<pre>Am I here?</pre>";
+ // send the email first
+ $userconfirmation = array();
+ $userconfirmation[$email] = array('recipient' => $email, 'lang' => $fs->prefs['lang_code']);
+ $recipients = array($userconfirmation);
+ if($notify->create(NOTIFY_CONFIRMATION, null, array($baseurl, $magic_url, $user_name, $confirm_code),
+ $recipients,
+ NOTIFY_EMAIL)) {
+
+ //email sent succefully, now update the database.
+ $reg_values = array(time(), $confirm_code, $user_name, $real_name,
+ $email, $jabber_id,
+ Post::num('notify_type'), $magic_url, Post::num('time_zone'));
+ // Insert everything into the database
+ $query = $db->query("INSERT INTO {registrations}
+ ( reg_time, confirm_code, user_name, real_name,
+ email_address, jabber_id, notify_type,
+ magic_url, time_zone )
+ VALUES ( " . $db->fill_placeholders($reg_values) . ' )', $reg_values);
+
+ if ($query) {
+ $_SESSION['SUCCESS'] = L('codesent');
+ Flyspray::redirect($baseurl);
+ }
+
+ } else {
+ Flyspray::show_error(L('codenotsent'));
+ break;
+ }
+
+ break;
+
+ // ##################
+ // new user self-registration with a confirmation code
+ // ##################
+ case 'register.registeruser':
+ if (!$user->can_register()) {
+ break;
+ }
+
+ if (!Post::val('user_pass') || !Post::val('confirmation_code')) {
+ Flyspray::show_error(L('formnotcomplete'));
+ break;
+ }
+
+ if (strlen(Post::val('user_pass')) < MIN_PW_LENGTH) {
+ Flyspray::show_error(L('passwordtoosmall'));
+ break;
+ }
+
+ if ( $fs->prefs['repeat_password'] && Post::val('user_pass') != Post::val('user_pass2')) {
+ Flyspray::show_error(L('nomatchpass'));
+ break;
+ }
+
+ // Check that the user entered the right confirmation code
+ $sql = $db->query("SELECT * FROM {registrations} WHERE magic_url = ?",
+ array(Post::val('magic_url')));
+ $reg_details = $db->fetchRow($sql);
+
+ if ($reg_details['confirm_code'] != trim(Post::val('confirmation_code'))) {
+ Flyspray::show_error(L('confirmwrong'));
+ break;
+ }
+
+ $profile_image = 'profile_image';
+ $image_path = '';
+
+ if(isset($_FILES[$profile_image])) {
+ if(!empty($_FILES[$profile_image]['name'])) {
+ $allowed = array('jpg', 'jpeg', 'gif', 'png');
+
+ $image_name = $_FILES[$profile_image]['name'];
+ $explode = explode('.', $image_name);
+ $image_extn = strtolower(end($explode));
+ $image_temp = $_FILES[$profile_image]['tmp_name'];
+
+ if(in_array($image_extn, $allowed)) {
+ $avatar_name = substr(md5(time()), 0, 10).'.'.$image_extn;
+ $image_path = BASEDIR.'/avatars/'.$avatar_name;
+ move_uploaded_file($image_temp, $image_path);
+ resizeImage($avatar_name, $fs->prefs['max_avatar_size'], $fs->prefs['max_avatar_size']);
+ } else {
+ Flyspray::show_error(L('incorrectfiletype'));
+ break;
+ }
+ }
+ }
+
+ $enabled = 1;
+ if (!Backend::create_user($reg_details['user_name'],
+ Post::val('user_pass'),
+ $reg_details['real_name'],
+ $reg_details['jabber_id'],
+ $reg_details['email_address'],
+ $reg_details['notify_type'], $reg_details['time_zone'], $fs->prefs['anon_group'], $enabled ,'', '', $image_path)) {
+ Flyspray::show_error(L('usernametaken'));
+ break;
+ }
+
+ $db->query('DELETE FROM {registrations} WHERE magic_url = ? AND confirm_code = ?',
+ array(Post::val('magic_url'), Post::val('confirmation_code')));
+
+
+ $_SESSION['SUCCESS'] = L('accountcreated');
+ // If everything is ok, add here a notify to both administrators and the user.
+ // Otherwise, explain what wen wrong.
+
+ define('NO_DO', true);
+ break;
+
+ // ##################
+ // new user self-registration without a confirmation code
+ // ##################
+ case 'register.newuser':
+ case 'admin.newuser':
+ if (!($user->perms('is_admin') || $user->can_self_register())) {
+ break;
+ }
+
+ $captchaerrors=array();
+ if( !($user->perms('is_admin')) && $fs->prefs['captcha_securimage']) {
+ $image = new Securimage();
+ if( !Post::isAlnum('captcha_code') || !$image->check(Post::val('captcha_code'))) {
+ $captchaerrors['invalidsecurimage']=1;
+ }
+ }
+
+ if( !($user->perms('is_admin')) && $fs->prefs['captcha_recaptcha']){
+ require_once('class.recaptcha.php');
+ if( !recaptcha::verify()) {
+ $captchaerrors['invalidrecaptcha']=1;
+ }
+ }
+
+ # if both captchatypes are configured, maybe show the user which one or both failed.
+ if(count($captchaerrors)){
+ $_SESSION['ERRORS']=$captchaerrors;
+ Flyspray::show_error(L('captchaerror'));
+ break;
+ }
+
+ if (!Post::val('user_name') || !Post::val('real_name') || !Post::val('email_address'))
+ {
+ // If the form wasn't filled out correctly, show an error
+ Flyspray::show_error(L('registererror'));
+ break;
+ }
+
+ // Check email format
+ if (!Post::val('email_address') || !Flyspray::check_email(Post::val('email_address')))
+ {
+ Flyspray::show_error(L('novalidemail'));
+ break;
+ }
+
+ if ( $fs->prefs['repeat_emailaddress'] && Post::val('email_address') != Post::val('verify_email_address'))
+ {
+ Flyspray::show_error(L('emailverificationwrong'));
+ break;
+ }
+
+ if (strlen(Post::val('user_pass')) && (strlen(Post::val('user_pass')) < MIN_PW_LENGTH)) {
+ Flyspray::show_error(L('passwordtoosmall'));
+ break;
+ }
+
+ if ( $fs->prefs['repeat_password'] && Post::val('user_pass') != Post::val('user_pass2')) {
+ Flyspray::show_error(L('nomatchpass'));
+ break;
+ }
+
+ if ($user->perms('is_admin')) {
+ $group_in = Post::val('group_in');
+ } else {
+ $group_in = $fs->prefs['anon_group'];
+ }
+
+ if(!$user->perms('is_admin')) {
+
+ $sql = $db->query("SELECT COUNT(*) FROM {users} WHERE
+ jabber_id = ? AND jabber_id != ''
+ OR email_address = ? AND email_address != ''",
+ array(Post::val('jabber_id'), Post::val('email_address')));
+
+ if ($db->fetchOne($sql)) {
+ Flyspray::show_error(L('emailtaken'));
+ break;
+ }
+ }
+
+ $enabled = 1;
+ if($user->need_admin_approval()) $enabled = 0;
+
+ $profile_image = 'profile_image';
+ $image_path = '';
+
+ if(isset($_FILES[$profile_image])) {
+ if(!empty($_FILES[$profile_image]['name'])) {
+ $allowed = array('jpg', 'jpeg', 'gif', 'png');
+
+ $image_name = $_FILES[$profile_image]['name'];
+ $explode = explode('.', $image_name);
+ $image_extn = strtolower(end($explode));
+ $image_temp = $_FILES[$profile_image]['tmp_name'];
+
+ if(in_array($image_extn, $allowed)) {
+ $avatar_name = substr(md5(time()), 0, 10).'.'.$image_extn;
+ $image_path = BASEDIR.'/avatars/'.$avatar_name;
+ move_uploaded_file($image_temp, $image_path);
+ resizeImage($avatar_name, $fs->prefs['max_avatar_size'], $fs->prefs['max_avatar_size']);
+ } else {
+ Flyspray::show_error(L('incorrectfiletype'));
+ break;
+ }
+ }
+ }
+
+ if (!Backend::create_user(Post::val('user_name'), Post::val('user_pass'),
+ Post::val('real_name'), Post::val('jabber_id'),
+ Post::val('email_address'), Post::num('notify_type'),
+ Post::num('time_zone'), $group_in, $enabled, '', '', $image_path)) {
+ Flyspray::show_error(L('usernametaken'));
+ break;
+ }
+
+ $_SESSION['SUCCESS'] = L('newusercreated');
+
+ if (!$user->perms('is_admin')) {
+ define('NO_DO', true);
+ }
+ break;
+
+
+ // ##################
+ // Admin based bulk registration of users
+ // ##################
+ case 'register.newuserbulk':
+ case 'admin.newuserbulk':
+ if (!($user->perms('is_admin')))
+ break;
+
+ $group_in = Post::val('group_in');
+ $error = '';
+ $success = '';
+ $noUsers = true;
+
+ // For each user in post, add them
+ for ($i = 0 ; $i < 10 ; $i++)
+ {
+ $user_name = Post::val('user_name' . $i);
+ $real_name = Post::val('real_name' . $i);
+ $email_address = Post::val('email_address' . $i);
+
+
+ if( $user_name == '' || $real_name == '' || $email_address == '')
+ continue;
+ else
+ $noUsers = false;
+
+ $enabled = 1;
+
+ // Avoid dups
+ $sql = $db->query("SELECT COUNT(*) FROM {users} WHERE email_address = ?",
+ array($email_address));
+
+ if ($db->fetchOne($sql))
+ {
+ $error .= "\n" . L('emailtakenbulk') . ": $email_address\n";
+ continue;
+ }
+
+ if (!Backend::create_user($user_name, Post::val('user_pass'),
+ $real_name, '',
+ $email_address,
+ Post::num('notify_type'),
+ Post::num('time_zone'), $group_in, $enabled, '', '', ''))
+ {
+ $error .= "\n" . L('usernametakenbulk') .": $user_name\n";
+ continue;
+ }
+ else
+ $success .= ' '.$user_name.' ';
+ }
+
+ if ($error != '')
+ Flyspray::show_error($error);
+ else if ( $noUsers == true)
+ Flyspray::show_error(L('nouserstoadd'));
+ else
+ {
+ $_SESSION['SUCCESS'] = L('created').$success;
+ if (!$user->perms('is_admin')) {
+ define('NO_DO', true);
+ }
+ }
+ break;
+
+
+ // ##################
+ // Bulk User Edit Form
+ // ##################
+ case 'admin.editallusers':
+
+ if (!($user->perms('is_admin'))) {
+ break;
+ }
+
+ $userids = Post::val('checkedUsers');
+
+ if(!is_array($userids)){
+ break;
+ }
+
+ $users=array();
+
+ foreach ($userids as $uid) {
+ if( ctype_digit($uid) ) {
+ if( $user->id == $uid ){
+ Flyspray::show_error(L('nosuicide'));
+ } else{
+ $users[]=$uid;
+ }
+ } else{
+ Flyspray::show_error(L('invalidinput'));
+ break 2;
+ }
+ }
+
+ if (count($users) == 0){
+ Flyspray::show_error(L('nouserselected'));
+ break;
+ }
+
+ // Make array of users to modify
+ $ids = "(" . $users[0];
+ for ($i = 1 ; $i < count($users) ; $i++)
+ {
+ $ids .= ", " . $users[$i];
+ }
+ $ids .= ")";
+
+ // Grab the action
+ if (isset($_POST['enable']))
+ {
+ $sql = $db->query("UPDATE {users} SET account_enabled = 1 WHERE user_id IN $ids");
+ }
+ else if (isset($_POST['disable']))
+ {
+ $sql = $db->query("UPDATE {users} SET account_enabled = 0 WHERE user_id IN $ids");
+ }
+ else if (isset($_POST['delete']))
+ {
+ //$sql = $db->query("DELETE FROM {users} WHERE user_id IN $ids");
+ foreach ($users as $uid) {
+ Backend::delete_user($uid);
+ }
+ }
+
+ // Show success message and exit
+ $_SESSION['SUCCESS'] = L('usersupdated');
+ break;
+
+
+
+ // ##################
+ // adding a new group
+ // ##################
+ case 'pm.newgroup':
+ case 'admin.newgroup':
+ if (!$user->perms('manage_project')) {
+ break;
+ }
+
+ if (!Post::val('group_name')) {
+ Flyspray::show_error(L('groupanddesc'));
+ break;
+ } else {
+ // Check to see if the group name is available
+ $sql = $db->query("SELECT COUNT(*)
+ FROM {groups}
+ WHERE group_name = ? AND project_id = ?",
+ array(Post::val('group_name'), $proj->id));
+
+ if ($db->fetchOne($sql)) {
+ Flyspray::show_error(L('groupnametaken'));
+ break;
+ } else {
+ $cols = array('group_name', 'group_desc', 'manage_project', 'edit_own_comments',
+ 'view_tasks', 'open_new_tasks', 'modify_own_tasks', 'add_votes',
+ 'modify_all_tasks', 'view_comments', 'add_comments', 'edit_assignments',
+ 'edit_comments', 'delete_comments', 'create_attachments',
+ 'delete_attachments', 'view_history', 'close_own_tasks',
+ 'close_other_tasks', 'assign_to_self', 'show_as_assignees',
+ 'assign_others_to_self', 'add_to_assignees', 'view_reports', 'group_open',
+ 'view_estimated_effort', 'track_effort', 'view_current_effort_done',
+ 'add_multiple_tasks', 'view_roadmap', 'view_own_tasks', 'view_groups_tasks');
+
+ $params = array_map('Post_to0',$cols);
+ array_unshift($params, $proj->id);
+
+ $db->query("INSERT INTO {groups} (project_id, ". join(',', $cols).")
+ VALUES (". $db->fill_placeholders($cols, 1) . ')', $params);
+
+ $_SESSION['SUCCESS'] = L('newgroupadded');
+ }
+ }
+
+ break;
+
+ // ##################
+ // Update the global application preferences
+ // ##################
+ case 'globaloptions':
+ if (!$user->perms('is_admin')) {
+ break;
+ }
+
+ $errors=array();
+
+ $settings = array('jabber_server', 'jabber_port', 'jabber_username', 'notify_registration',
+ 'jabber_password', 'anon_group', 'user_notify', 'admin_email', 'email_ssl', 'email_tls',
+ 'lang_code', 'gravatars', 'hide_emails', 'spam_proof', 'default_project', 'default_entry',
+ 'dateformat','dateformat_extended',
+ 'jabber_ssl', 'anon_reg', 'global_theme', 'smtp_server', 'page_title',
+ 'smtp_user', 'smtp_pass', 'funky_urls', 'reminder_daemon','cache_feeds', 'intro_message',
+ 'disable_lostpw','disable_changepw','days_before_alert', 'emailNoHTML', 'need_approval', 'pages_welcome_msg',
+ 'active_oauths', 'only_oauth_reg', 'enable_avatars', 'max_avatar_size', 'default_order_by',
+ 'max_vote_per_day', 'votes_per_project', 'url_rewriting',
+ 'custom_style', 'general_integration', 'footer_integration',
+ 'repeat_password', 'repeat_emailaddress', 'massops');
+
+ if(!isset($fs->prefs['massops'])){
+ $db->query("INSERT INTO {prefs} (pref_name,pref_value) VALUES('massops',0)");
+ }
+
+ # candid for a plugin, so separate them for the future.
+ $settings[]='captcha_securimage';
+ if(!isset($fs->prefs['captcha_securimage'])){
+ $db->query("INSERT INTO {prefs} (pref_name,pref_value) VALUES('captcha_securimage',0)");
+ }
+
+ # candid for a plugin
+ $settings[]='captcha_recaptcha';
+ $settings[]='captcha_recaptcha_sitekey';
+ $settings[]='captcha_recaptcha_secret';
+ if(!isset($fs->prefs['captcha_recaptcha'])){
+ $db->query("INSERT INTO {prefs} (pref_name,pref_value) VALUES('captcha_recaptcha',0),('captcha_recaptcha_sitekey',''),('captcha_recaptcha_secret','')");
+ }
+
+ if(Post::val('need_approval') == '1' && Post::val('spam_proof')){
+ unset($_POST['spam_proof']); // if self register request admin to approve, disable spam_proof
+ // if you think different, modify functions in class.user.php directing different regiser tpl
+ }
+ if (Post::val('url_rewriting') == '1' && !$fs->prefs['url_rewriting']) {
+ # Setenv can't be used to set the env variable in .htaccess, because apache module setenv is often disabled on hostings and brings server error 500.
+ # First check if htaccess is turned on
+ #if (!array_key_exists('HTTP_HTACCESS_ENABLED', $_SERVER)) {
+ # Flyspray::show_error(L('enablehtaccess'));
+ # break;
+ #}
+
+ # Make sure mod_rewrite is enabled by checking a env var defined as HTTP_MOD_REWRITE in the .htaccess .
+ # It is possible to be converted to REDIRECT_HTTP_MOD_REWRITE . It's sound weired, but that's the case here.
+ if ( !array_key_exists('HTTP_MOD_REWRITE', $_SERVER) && !array_key_exists('REDIRECT_HTTP_MOD_REWRITE' , $_SERVER) ) {
+ #print_r($_SERVER);die();
+ Flyspray::show_error(L('nomodrewrite'));
+ break;
+ }
+ }
+
+ if( substr(Post::val('custom_style'), -4) != '.css'){
+ $_POST['custom_style']='';
+ }
+
+ # TODO validation
+ if( Post::val('default_order_by2') !='' && Post::val('default_order_by2') !='n'){
+ $_POST['default_order_by']=$_POST['default_order_by'].' '.$_POST['default_order_by_dir'].', '.$_POST['default_order_by2'].' '.$_POST['default_order_by_dir2'];
+ } else{
+ $_POST['default_order_by']=$_POST['default_order_by'].' '.$_POST['default_order_by_dir'];
+ }
+
+
+ foreach ($settings as $setting) {
+ $db->query('UPDATE {prefs} SET pref_value = ? WHERE pref_name = ?',
+ array(Post::val($setting, 0), $setting));
+ // Update prefs for following scripts
+ $fs->prefs[$setting] = Post::val($setting, 0);
+ }
+
+ // Process the list of groups into a format we can store
+ $viscols = trim(Post::val('visible_columns'));
+ $db->query("UPDATE {prefs} SET pref_value = ?
+ WHERE pref_name = 'visible_columns'",
+ array($viscols));
+ $fs->prefs['visible_columns'] = $viscols;
+
+ $visfields = trim(Post::val('visible_fields'));
+ $db->query("UPDATE {prefs} SET pref_value = ?
+ WHERE pref_name = 'visible_fields'",
+ array($visfields));
+ $fs->prefs['visible_fields'] = $visfields;
+
+ //save logo
+ if($_FILES['logo']['error'] == 0){
+ if( in_array(exif_imagetype($_FILES['logo']['tmp_name']), array(IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG)) ) {
+ $logofilename=strtolower(basename($_FILES['logo']['name']));
+ $logoexplode = explode('.', $logofilename);
+ $logoextension = strtolower(end($logoexplode));
+ $allowedextensions = array('gif', 'jpg', 'jpeg', 'png');
+
+ if(in_array($logoextension, $allowedextensions)){
+ move_uploaded_file($_FILES['logo']['tmp_name'], './' . $logofilename);
+ $sql = $db->query("SELECT * FROM {prefs} WHERE pref_name='logo'");
+ if(!$db->fetchOne($sql)){
+ $db->query("INSERT INTO {prefs} (pref_name) VALUES('logo')");
+ }
+ $db->query("UPDATE {prefs} SET pref_value = ? WHERE pref_name='logo'", $logofilename);
+ } else{
+ $errors['invalidfileextension']=1;
+ }
+ }
+ }
+ //saved logo
+
+ $_SESSION['SUCCESS'] = L('optionssaved');
+ if(count($errors)>0){
+ $_SESSION['ERRORS']=$errors;
+ }
+
+ break;
+
+ // ##################
+ // adding a new project
+ // ##################
+ case 'admin.newproject':
+ if (!$user->perms('is_admin')) {
+ break;
+ }
+
+ if (!Post::val('project_title')) {
+ Flyspray::show_error(L('emptytitle'));
+ break;
+ }
+
+ $viscols = $fs->prefs['visible_columns']
+ ? $fs->prefs['visible_columns']
+ : 'id tasktype priority severity summary status dueversion progress';
+
+ $visfields = $fs->prefs['visible_fields']
+ ? $fs->prefs['visible_fields']
+ : 'id tasktype priority severity summary status dueversion progress';
+
+
+ $db->query('INSERT INTO {projects}
+ ( project_title, theme_style, intro_message,
+ others_view, others_viewroadmap, anon_open, project_is_active,
+ visible_columns, visible_fields, lang_code, notify_email, notify_jabber, disp_intro)
+ VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?)',
+ array(Post::val('project_title'), Post::val('theme_style'),
+ Post::val('intro_message'), Post::num('others_view', 0), Post::num('others_viewroadmap', 0),
+ Post::num('anon_open', 0), $viscols, $visfields,
+ Post::val('lang_code', 'en'), '', '',
+ Post::num('disp_intro')
+ ));
+
+ // $sql = $db->query('SELECT project_id FROM {projects} ORDER BY project_id DESC', false, 1);
+ // $pid = $db->fetchOne($sql);
+ $pid = $db->insert_ID();
+
+ $cols = array( 'manage_project', 'view_tasks', 'open_new_tasks',
+ 'modify_own_tasks', 'modify_all_tasks', 'view_comments',
+ 'add_comments', 'edit_comments', 'delete_comments', 'show_as_assignees',
+ 'create_attachments', 'delete_attachments', 'view_history', 'add_votes',
+ 'close_own_tasks', 'close_other_tasks', 'assign_to_self', 'edit_own_comments',
+ 'assign_others_to_self', 'add_to_assignees', 'view_reports', 'group_open',
+ 'view_estimated_effort', 'view_current_effort_done', 'track_effort',
+ 'add_multiple_tasks', 'view_roadmap', 'view_own_tasks', 'view_groups_tasks',
+ 'edit_assignments');
+ $args = array_fill(0, count($cols), '1');
+ array_unshift($args, 'Project Managers',
+ 'Permission to do anything related to this project.',
+ intval($pid));
+
+ $db->query("INSERT INTO {groups}
+ ( group_name, group_desc, project_id,
+ ".join(',', $cols).")
+ VALUES ( ". $db->fill_placeholders($cols, 3) .")", $args);
+
+ $db->query("INSERT INTO {list_category}
+ ( project_id, category_name,
+ show_in_list, category_owner, lft, rgt)
+ VALUES ( ?, ?, 1, 0, 1, 4)", array($pid, 'root'));
+
+ $db->query("INSERT INTO {list_category}
+ ( project_id, category_name,
+ show_in_list, category_owner, lft, rgt )
+ VALUES ( ?, ?, 1, 0, 2, 3)", array($pid, 'Backend / Core'));
+
+ $db->query("INSERT INTO {list_os}
+ ( project_id, os_name, list_position, show_in_list )
+ VALUES (?, ?, 1, 1)", array($pid, 'All'));
+
+ $db->query("INSERT INTO {list_version}
+ ( project_id, version_name, list_position,
+ show_in_list, version_tense )
+ VALUES (?, ?, 1, 1, 2)", array($pid, '1.0'));
+
+ $_SESSION['SUCCESS'] = L('projectcreated');
+ Flyspray::redirect(createURL('pm', 'prefs', $pid));
+ break;
+
+ // ##################
+ // updating project preferences
+ // ##################
+ case 'pm.updateproject':
+ if (!$user->perms('manage_project')) {
+ break;
+ }
+
+ if (Post::val('delete_project')) {
+ if (Backend::delete_project($proj->id, Post::val('move_to'))) {
+ $_SESSION['SUCCESS'] = L('projectdeleted');
+ } else {
+ $_SESSION['ERROR'] = L('projectnotdeleted');
+ }
+
+ if (Post::val('move_to')) {
+ Flyspray::redirect(createURL('pm', 'prefs', Post::val('move_to')));
+ } else {
+ Flyspray::redirect($baseurl);
+ }
+ }
+
+ if (!Post::val('project_title')) {
+ Flyspray::show_error(L('emptytitle'));
+ break;
+ }
+
+ $cols = array( 'project_title', 'theme_style', 'lang_code', 'default_task', 'default_entry',
+ 'intro_message', 'notify_email', 'notify_jabber', 'notify_subject', 'notify_reply',
+ 'feed_description', 'feed_img_url','default_due_version','use_effort_tracking',
+ 'pages_intro_msg', 'estimated_effort_format', 'current_effort_done_format');
+ $args = array_map('Post_to0', $cols);
+ $cols = array_merge($cols, $ints = array('project_is_active', 'others_view', 'others_viewroadmap', 'anon_open', 'comment_closed', 'auto_assign', 'freetagging'));
+ $args = array_merge($args, array_map(array('Post', 'num'), $ints));
+ $cols[] = 'notify_types';
+ $args[] = implode(' ', (array) Post::val('notify_types'));
+ $cols[] = 'last_updated';
+ $args[] = time();
+ $cols[] = 'disp_intro';
+ $args[] = Post::num('disp_intro');
+ $cols[] = 'default_cat_owner';
+ $args[] = Flyspray::UserNameToId(Post::val('default_cat_owner'));
+ $cols[] = 'custom_style';
+ $args[] = Post::val('custom_style');
+
+ // Convert to seconds.
+ if (Post::val('hours_per_manday')) {
+ $args[] = effort::editStringToSeconds(Post::val('hours_per_manday'), $proj->prefs['hours_per_manday'], $proj->prefs['estimated_effort_format']);
+ $cols[] = 'hours_per_manday';
+ }
+
+ # TODO validation
+ if( Post::val('default_order_by2') !=''){
+ $_POST['default_order_by']=$_POST['default_order_by'].' '.$_POST['default_order_by_dir'].', '.$_POST['default_order_by2'].' '.$_POST['default_order_by_dir2'];
+ } else{
+ $_POST['default_order_by']=$_POST['default_order_by'].' '.$_POST['default_order_by_dir'];
+ }
+ $cols[]='default_order_by';
+ $args[]= $_POST['default_order_by'];
+
+ $args[] = $proj->id;
+
+ $update = $db->query("UPDATE {projects}
+ SET ".join('=?, ', $cols)."=?
+ WHERE project_id = ?", $args);
+
+ $update = $db->query('UPDATE {projects} SET visible_columns = ? WHERE project_id = ?',
+ array(trim(Post::val('visible_columns')), $proj->id));
+
+ $update = $db->query('UPDATE {projects} SET visible_fields = ? WHERE project_id = ?',
+ array(trim(Post::val('visible_fields')), $proj->id));
+
+ // Update project prefs for following scripts
+ $proj = new Project($proj->id);
+ $_SESSION['SUCCESS'] = L('projectupdated');
+ Flyspray::redirect(createURL('pm', 'prefs', $proj->id));
+ break;
+
+ // ##################
+ // modifying user details/profile
+ // ##################
+ case 'admin.edituser':
+ case 'myprofile.edituser':
+ if (Post::val('delete_user')) {
+ // There probably is a bug here somewhere but I just can't find it just now.
+ // Anyway, I get the message also when just editing my details.
+ if ($user->id == (int)Post::val('user_id') && $user->perms('is_admin')) {
+ Flyspray::show_error(L('nosuicide'));
+ break;
+ }
+ else {
+ // check that he is not the last user
+ $sql = $db->query('SELECT count(*) FROM {users}');
+ if ($db->fetchOne($sql) > 1) {
+ Backend::delete_user(Post::val('user_id'));
+ $_SESSION['SUCCESS'] = L('userdeleted');
+ Flyspray::redirect(createURL('admin', 'groups'));
+ } else {
+ Flyspray::show_error(L('lastuser'));
+ break;
+ }
+ }
+ }
+
+ if (!Post::val('onlypmgroup')):
+ if ($user->perms('is_admin') || $user->id == Post::val('user_id')): // only admin or user himself can change
+
+ if (!Post::val('real_name') || (!Post::val('email_address') && !Post::val('jabber_id'))) {
+ Flyspray::show_error(L('realandnotify'));
+ break;
+ }
+
+ // Check email format
+ if (!Post::val('email_address') || !Flyspray::check_email(Post::val('email_address')))
+ {
+ Flyspray::show_error(L('novalidemail'));
+ break;
+ }
+
+ # current CleanFS template skips oldpass input requirement for admin accounts: if someone is able to catch an admin session he could simply create another admin acc for example.
+ #if ( (!$user->perms('is_admin') || $user->id == Post::val('user_id')) && !Post::val('oldpass')
+ if ( !$user->perms('is_admin') && !Post::val('oldpass') && (Post::val('changepass') || Post::val('confirmpass')) ) {
+ Flyspray::show_error(L('nooldpass'));
+ break;
+ }
+
+ if ($user->infos['oauth_uid'] && Post::val('changepass')) {
+ Flyspray::show_error(sprintf(L('oauthreqpass'), ucfirst($uesr->infos['oauth_provider'])));
+ break;
+ }
+
+ if (Post::val('changepass')) {
+ if ($fs->prefs['repeat_password'] && Post::val('changepass') != Post::val('confirmpass')) {
+ Flyspray::show_error(L('passnomatch'));
+ break;
+ }
+ if (Post::val('oldpass')) {
+ $sql = $db->query('SELECT user_pass FROM {users} WHERE user_id = ?', array(Post::val('user_id')));
+ $oldpass = $db->fetchRow($sql);
+
+ $pwtest=false;
+ if(strlen($oldpass['user_pass'])==32){
+ $pwtest=hash_equals($oldpass['user_pass'], md5(Post::val('oldpass')));
+ }elseif(strlen($oldpass['user_pass'])==40){
+ $pwtest=hash_equals($oldpass['user_pass'], sha1(Post::val('oldpass')));
+ }elseif(strlen($oldpass['user_pass'])==128){
+ $pwtest=hash_equals($oldpass['user_pass'], hash('sha512',Post::val('oldpass')));
+ }else{
+ $pwtest=password_verify(Post::val('oldpass'), $oldpass['user_pass']);
+ }
+
+ if (!$pwtest){
+ Flyspray::show_error(L('oldpasswrong'));
+ break;
+ }
+ }
+ $new_hash = Flyspray::cryptPassword(Post::val('changepass'));
+ $db->query('UPDATE {users} SET user_pass = ? WHERE user_id = ?',
+ array($new_hash, Post::val('user_id')));
+
+ // If the user is changing their password, better update their cookie hash
+ if ($user->id == Post::val('user_id')) {
+ Flyspray::setCookie('flyspray_passhash',
+ crypt($new_hash, $conf['general']['cookiesalt']), time()+3600*24*30,null,null,null,true);
+ }
+ }
+ $jabId = Post::val('jabber_id');
+ if (!empty($jabId) && Post::val('old_jabber_id') != $jabId) {
+ Notifications::JabberRequestAuth(Post::val('jabber_id'));
+ }
+
+ $db->query('UPDATE {users}
+ SET real_name = ?, email_address = ?, notify_own = ?,
+ jabber_id = ?, notify_type = ?,
+ dateformat = ?, dateformat_extended = ?,
+ tasks_perpage = ?, time_zone = ?, lang_code = ?,
+ hide_my_email = ?, notify_online = ?
+ WHERE user_id = ?',
+ array(Post::val('real_name'), Post::val('email_address'), Post::num('notify_own', 0),
+ Post::val('jabber_id', ''), Post::num('notify_type'),
+ Post::val('dateformat', 0), Post::val('dateformat_extended', 0),
+ Post::num('tasks_perpage'), Post::num('time_zone'), Post::val('lang_code', 'en'),
+ Post::num('hide_my_email', 0), Post::num('notify_online', 0), Post::num('user_id')));
+
+ # 20150307 peterdd: Now we must reload translations, because the user maybe changed his language preferences!
+ # first reload user info
+ $user=new User($user->id);
+ load_translations();
+
+ $profile_image = 'profile_image';
+
+ if(isset($_FILES[$profile_image])) {
+ if(!empty($_FILES[$profile_image]['name'])) {
+ $allowed = array('jpg', 'jpeg', 'gif', 'png');
+
+ $image_name = $_FILES[$profile_image]['name'];
+ $explode = explode('.', $image_name);
+ $image_extn = strtolower(end($explode));
+ $image_temp = $_FILES[$profile_image]['tmp_name'];
+
+ if(in_array($image_extn, $allowed)) {
+ $sql = $db->query('SELECT profile_image FROM {users} WHERE user_id = ?', array(Post::val('user_id')));
+ $avatar_oldname = $db->fetchRow($sql);
+
+ if (is_file(BASEDIR.'/avatars/'.$avatar_oldname['profile_image']))
+ unlink(BASEDIR.'/avatars/'.$avatar_oldname['profile_image']);
+
+ $avatar_name = substr(md5(time()), 0, 10).'.'.$image_extn;
+ $image_path = BASEDIR.'/avatars/'.$avatar_name;
+ move_uploaded_file($image_temp, $image_path);
+ resizeImage($avatar_name, $fs->prefs['max_avatar_size'], $fs->prefs['max_avatar_size']);
+ $db->query('UPDATE {users} SET profile_image = ? WHERE user_id = ?',
+ array($avatar_name, Post::num('user_id')));
+ } else {
+ Flyspray::show_error(L('incorrectfiletype'));
+ break;
+ }
+ }
+ }
+
+ endif; // end only admin or user himself can change
+
+ if ($user->perms('is_admin')) {
+ if($user->id == (int)Post::val('user_id')) {
+ if (Post::val('account_enabled', 0) <= 0 || Post::val('old_global_id') != 1) {
+ Flyspray::show_error(L('nosuicide'));
+ break;
+ }
+ } else {
+ $db->query('UPDATE {users} SET account_enabled = ? WHERE user_id = ?',
+ array(Post::val('account_enabled', 0), Post::val('user_id')));
+ $db->query('UPDATE {users_in_groups} SET group_id = ?
+ WHERE group_id = ? AND user_id = ?',
+ array(Post::val('group_in'), Post::val('old_global_id'), Post::val('user_id')));
+ }
+ }
+
+ endif; // end non project group changes
+
+ if ($user->perms('manage_project') && !is_null(Post::val('project_group_in')) && Post::val('project_group_in') != Post::val('old_group_id')) {
+ $db->query('DELETE FROM {users_in_groups} WHERE group_id = ? AND user_id = ?',
+ array(Post::val('old_group_id'), Post::val('user_id')));
+ if (Post::val('project_group_in')) {
+ $db->query('INSERT INTO {users_in_groups} (group_id, user_id) VALUES(?, ?)',
+ array(Post::val('project_group_in'), Post::val('user_id')));
+ }
+ }
+
+ $_SESSION['SUCCESS'] = L('userupdated');
+ if ($action === 'myprofile.edituser') {
+ Flyspray::redirect(createURL('myprofile'));
+ } elseif ($action === 'admin.edituser' && Post::val('area') === 'users') {
+ Flyspray::redirect(createURL('edituser', Post::val('user_id')));
+ } else {
+ Flyspray::redirect(createURL('user', Post::val('user_id')));
+ }
+ break;
+ // ##################
+ // approving a new user registration
+ // ##################
+ case 'approve.user':
+ if($user->perms('is_admin')) {
+ $db->query('UPDATE {users} SET account_enabled = ? WHERE user_id = ?',
+ array(1, Post::val('user_id')));
+
+ $db->query('UPDATE {admin_requests}
+ SET resolved_by = ?, time_resolved = ?
+ WHERE submitted_by = ? AND request_type = ?',
+ array($user->id, time(), Post::val('user_id'), 3));
+ // Missing event constant, can't log yet...
+ // Missing notification constant, can't notify yet...
+ // Notification constant added, write the code for sending that message...
+
+ }
+ break;
+ // ##################
+ // updating a group definition
+ // ##################
+ case 'pm.editgroup':
+ case 'admin.editgroup':
+ if (!$user->perms('manage_project')) {
+ break;
+ }
+
+ if (!Post::val('group_name')) {
+ Flyspray::show_error(L('groupanddesc'));
+ break;
+ }
+
+ $cols = array('group_name', 'group_desc');
+
+ // Add a user to a group
+ if ($uid = Post::val('uid')) {
+ $uids = preg_split('/[,;]+/', $uid, -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($uids as $uid) {
+ $uid = Flyspray::usernameToId($uid);
+ if (!$uid) {
+ continue;
+ }
+
+ // If user is already a member of one of the project's groups, **move** (not add) him to the new group
+ $sql = $db->query('SELECT g.group_id
+ FROM {users_in_groups} uig, {groups} g
+ WHERE g.group_id = uig.group_id AND uig.user_id = ? AND project_id = ?',
+ array($uid, $proj->id));
+ if ($db->countRows($sql)) {
+ $oldid = $db->fetchOne($sql);
+ $db->query('UPDATE {users_in_groups} SET group_id = ? WHERE user_id = ? AND group_id = ?',
+ array(Post::val('group_id'), $uid, $oldid));
+ } else {
+ $db->query('INSERT INTO {users_in_groups} (group_id, user_id) VALUES(?, ?)',
+ array(Post::val('group_id'), $uid));
+ }
+ }
+ }
+
+ if (Post::val('delete_group') && Post::val('group_id') != '1') {
+ $db->query('DELETE FROM {groups} WHERE group_id = ?', Post::val('group_id'));
+
+ if (Post::val('move_to')) {
+ $db->query('UPDATE {users_in_groups} SET group_id = ? WHERE group_id = ?',
+ array(Post::val('move_to'), Post::val('group_id')));
+ }
+
+ $_SESSION['SUCCESS'] = L('groupupdated');
+ Flyspray::redirect(createURL( (($proj->id) ? 'pm' : 'admin'), 'groups', $proj->id));
+ }
+ // Allow all groups to update permissions except for global Admin
+ if (Post::val('group_id') != '1') {
+ $cols = array_merge($cols,
+ array('manage_project', 'view_tasks', 'edit_own_comments',
+ 'open_new_tasks', 'modify_own_tasks', 'modify_all_tasks',
+ 'view_comments', 'add_comments', 'edit_comments', 'delete_comments',
+ 'create_attachments', 'delete_attachments', 'show_as_assignees',
+ 'view_history', 'close_own_tasks', 'close_other_tasks', 'edit_assignments',
+ 'assign_to_self', 'assign_others_to_self', 'add_to_assignees', 'view_reports',
+ 'add_votes', 'group_open', 'view_estimated_effort', 'track_effort',
+ 'view_current_effort_done', 'add_multiple_tasks', 'view_roadmap',
+ 'view_own_tasks', 'view_groups_tasks'));
+ }
+
+ $args = array_map('Post_to0', $cols);
+ $args[] = Post::val('group_id');
+ $args[] = $proj->id;
+
+ $db->query("UPDATE {groups}
+ SET ".join('=?,', $cols)."=?
+ WHERE group_id = ? AND project_id = ?", $args);
+
+ $_SESSION['SUCCESS'] = L('groupupdated');
+ break;
+
+ // ##################
+ // updating a list
+ // ##################
+ case 'update_list':
+ if (!$user->perms('manage_project') || !isset($list_table_name)) {
+ break;
+ }
+
+ $listnames = Post::val('list_name');
+ $listposition = Post::val('list_position');
+ $listshow = Post::val('show_in_list');
+ $listdelete = Post::val('delete');
+ if($lt=='tag'){
+ $listclass = Post::val('list_class');
+ }
+ foreach ($listnames as $id => $listname) {
+ if ($listname != '') {
+ if (!isset($listshow[$id])) {
+ $listshow[$id] = 0;
+ }
+
+ $check = $db->query("SELECT COUNT(*)
+ FROM $list_table_name
+ WHERE (project_id = 0 OR project_id = ?)
+ AND $list_column_name = ?
+ AND $list_id <> ?",
+ array($proj->id, $listnames[$id], $id)
+ );
+ $itemexists = $db->fetchOne($check);
+
+ if ($itemexists) {
+ Flyspray::show_error(sprintf(L('itemexists'), $listnames[$id]));
+ return;
+ }
+
+ if($lt=='tag'){
+ $update = $db->query("UPDATE $list_table_name
+ SET $list_column_name=?, list_position=?, show_in_list=?, class=?
+ WHERE $list_id=? AND project_id=?",
+ array($listnames[$id], intval($listposition[$id]), intval($listshow[$id]), $listclass[$id], $id, $proj->id)
+ );
+ } else{
+ $update = $db->query("UPDATE $list_table_name
+ SET $list_column_name=?, list_position=?, show_in_list=?
+ WHERE $list_id=? AND project_id=?",
+ array($listnames[$id], intval($listposition[$id]), intval($listshow[$id]), $id, $proj->id)
+ );
+ }
+ } else {
+ Flyspray::show_error(L('fieldsmissing'));
+ }
+ }
+
+ if (is_array($listdelete) && count($listdelete)) {
+ $deleteids = "$list_id = " . join(" OR $list_id =", array_map('intval', array_keys($listdelete)));
+ $db->query("DELETE FROM $list_table_name WHERE project_id = ? AND ($deleteids)", array($proj->id));
+ }
+
+ $_SESSION['SUCCESS'] = L('listupdated');
+ break;
+
+ // ##################
+ // adding a list item
+ // ##################
+ case 'pm.add_to_list':
+ case 'admin.add_to_list':
+ if (!$user->perms('manage_project') || !isset($list_table_name)) {
+ break;
+ }
+
+ if (!Post::val('list_name')) {
+ Flyspray::show_error(L('fillallfields'));
+ break;
+ }
+
+ $position = Post::num('list_position');
+ if (!$position) {
+ $position = intval($db->fetchOne($db->query("SELECT max(list_position)+1
+ FROM $list_table_name
+ WHERE project_id = ?",
+ array($proj->id))));
+ }
+
+ $check = $db->query("SELECT COUNT(*)
+ FROM $list_table_name
+ WHERE (project_id = 0 OR project_id = ?)
+ AND $list_column_name = ?",
+ array($proj->id, Post::val('list_name')));
+ $itemexists = $db->fetchOne($check);
+
+ if ($itemexists) {
+ Flyspray::show_error(sprintf(L('itemexists'), Post::val('list_name')));
+ return;
+ }
+
+ $db->query("INSERT INTO $list_table_name
+ (project_id, $list_column_name, list_position, show_in_list)
+ VALUES (?, ?, ?, ?)",
+ array($proj->id, Post::val('list_name'), $position, '1'));
+
+ $_SESSION['SUCCESS'] = L('listitemadded');
+ break;
+
+ // ##################
+ // updating the version list
+ // ##################
+ case 'update_version_list':
+ if (!$user->perms('manage_project') || !isset($list_table_name)) {
+ break;
+ }
+
+ $listnames = Post::val('list_name');
+ $listposition = Post::val('list_position');
+ $listshow = Post::val('show_in_list');
+ $listtense = Post::val('version_tense');
+ $listdelete = Post::val('delete');
+
+ foreach ($listnames as $id => $listname) {
+ if (is_numeric($listposition[$id]) && $listnames[$id] != '') {
+ if (!isset($listshow[$id])) {
+ $listshow[$id] = 0;
+ }
+
+ $check = $db->query("SELECT COUNT(*)
+ FROM $list_table_name
+ WHERE (project_id = 0 OR project_id = ?)
+ AND $list_column_name = ?
+ AND $list_id <> ?",
+ array($proj->id, $listnames[$id], $id));
+ $itemexists = $db->fetchOne($check);
+
+ if ($itemexists) {
+ Flyspray::show_error(sprintf(L('itemexists'), $listnames[$id]));
+ return;
+ }
+
+ $update = $db->query("UPDATE $list_table_name
+ SET $list_column_name = ?, list_position = ?,
+ show_in_list = ?, version_tense = ?
+ WHERE $list_id = ? AND project_id = ?",
+ array($listnames[$id], intval($listposition[$id]),
+ intval($listshow[$id]), intval($listtense[$id]), $id, $proj->id));
+ } else {
+ Flyspray::show_error(L('fieldsmissing'));
+ }
+ }
+
+ if (is_array($listdelete) && count($listdelete)) {
+ $deleteids = "$list_id = " . join(" OR $list_id =", array_map('intval', array_keys($listdelete)));
+ $db->query("DELETE FROM $list_table_name WHERE project_id = ? AND ($deleteids)", array($proj->id));
+ }
+
+ $_SESSION['SUCCESS'] = L('listupdated');
+ break;
+
+ // ##################
+ // adding a version list item
+ // ##################
+ case 'pm.add_to_version_list':
+ case 'admin.add_to_version_list':
+ if (!$user->perms('manage_project') || !isset($list_table_name)) {
+ break;
+ }
+
+ if (!Post::val('list_name')) {
+ Flyspray::show_error(L('fillallfields'));
+ break;
+ }
+
+ $position = Post::num('list_position');
+ if (!$position) {
+ $position = $db->fetchOne($db->query("SELECT max(list_position)+1
+ FROM $list_table_name
+ WHERE project_id = ?",
+ array($proj->id)));
+ }
+
+ $check = $db->query("SELECT COUNT(*)
+ FROM $list_table_name
+ WHERE (project_id = 0 OR project_id = ?)
+ AND $list_column_name = ?",
+ array($proj->id, Post::val('list_name')));
+ $itemexists = $db->fetchOne($check);
+
+ if ($itemexists) {
+ Flyspray::show_error(sprintf(L('itemexists'), Post::val('list_name')));
+ return;
+ }
+
+ $db->query("INSERT INTO $list_table_name
+ (project_id, $list_column_name, list_position, show_in_list, version_tense)
+ VALUES (?, ?, ?, ?, ?)",
+ array($proj->id, Post::val('list_name'),
+ intval($position), '1', Post::val('version_tense')));
+
+ $_SESSION['SUCCESS'] = L('listitemadded');
+ break;
+
+ // ##################
+ // updating the category list
+ // ##################
+ case 'update_category':
+ if (!$user->perms('manage_project')) {
+ break;
+ }
+
+ $listnames = Post::val('list_name');
+ $listshow = Post::val('show_in_list');
+ $listdelete = Post::val('delete');
+ $listlft = Post::val('lft');
+ $listrgt = Post::val('rgt');
+ $listowners = Post::val('category_owner');
+
+ foreach ($listnames as $id => $listname) {
+ if ($listname != '') {
+ if (!isset($listshow[$id])) {
+ $listshow[$id] = 0;
+ }
+
+ // Check for duplicates on the same sub-level under same parent category.
+ // First, we'll have to find the right parent for the current category.
+ $sql = $db->query('SELECT *
+ FROM {list_category}
+ WHERE project_id = ? AND lft < ? and rgt > ?
+ AND lft = (SELECT MAX(lft) FROM {list_category} WHERE lft < ? and rgt > ?)',
+ array($proj->id, intval($listlft[$id]), intval($listrgt[$id]), intval($listlft[$id]), intval($listrgt[$id])));
+
+ $parent = $db->fetchRow($sql);
+
+ $check = $db->query('SELECT COUNT(*)
+ FROM {list_category} c
+ WHERE project_id = ? AND category_name = ? AND lft > ? AND rgt < ?
+ AND category_id <> ?
+ AND NOT EXISTS (SELECT *
+ FROM {list_category}
+ WHERE project_id = ?
+ AND lft > ? AND rgt < ?
+ AND lft < c.lft AND rgt > c.rgt)',
+ array($proj->id, $listname, $parent['lft'], $parent['rgt'], intval($id), $proj->id, $parent['lft'], $parent['rgt']));
+ $itemexists = $db->fetchOne($check);
+
+ // echo "<pre>" . $parent['category_name'] . "," . $listname . ", " . intval($id) . ", " . intval($listlft[$id]) . ", " . intval($listrgt[$id]) . ", " . $itemexists ."</pre>";
+
+ if ($itemexists) {
+ Flyspray::show_error(sprintf(L('categoryitemexists'), $listname, $parent['category_name']));
+ return;
+ }
+
+
+ $update = $db->query('UPDATE {list_category}
+ SET category_name = ?,
+ show_in_list = ?, category_owner = ?,
+ lft = ?, rgt = ?
+ WHERE category_id = ? AND project_id = ?',
+ array($listname, intval($listshow[$id]), Flyspray::UserNameToId($listowners[$id]), intval($listlft[$id]), intval($listrgt[$id]), intval($id), $proj->id));
+ // Correct visibility for sub categories
+ if ($listshow[$id] == 0) {
+ foreach ($listnames as $key => $value) {
+ if ($listlft[$key] > $listlft[$id] && $listrgt[$key] < $listrgt[$id]) {
+ $listshow[$key] = 0;
+ }
+ }
+ }
+ } else {
+ Flyspray::show_error(L('fieldsmissing'));
+ }
+ }
+
+ if (is_array($listdelete) && count($listdelete)) {
+ $deleteids = "$list_id = " . join(" OR $list_id =", array_map('intval', array_keys($listdelete)));
+ $db->query("DELETE FROM {list_category} WHERE project_id = ? AND ($deleteids)", array($proj->id));
+ }
+
+ $_SESSION['SUCCESS'] = L('listupdated');
+ break;
+
+ // ##################
+ // adding a category list item
+ // ##################
+ case 'pm.add_category':
+ case 'admin.add_category':
+ if (!$user->perms('manage_project')) {
+ break;
+ }
+
+ if (!Post::val('list_name')) {
+ Flyspray::show_error(L('fillallfields'));
+ break;
+ }
+
+ // Get right value of last node
+ // Need also left value of parent for duplicate check and category name for errormessage.
+ $sql = $db->query('SELECT rgt, lft, category_name FROM {list_category} WHERE category_id = ?', array(Post::val('parent_id', -1)));
+ $parent = $db->fetchRow($sql);
+ $right = $parent['rgt'];
+ $left = $parent['lft'];
+
+ // echo "<pre>Parent: " . Post::val('parent_id', -1) . ", left: $left, right: $right</pre>";
+
+ // If parent has subcategories, check for possible duplicates
+ // on the same sub-level and under the same parent.
+ if ($left + 1 != $right) {
+ $check = $db->query('SELECT COUNT(*)
+ FROM {list_category} c
+ WHERE project_id = ? AND category_name = ? AND lft > ? AND rgt < ?
+ AND NOT EXISTS (SELECT *
+ FROM {list_category}
+ WHERE project_id = ?
+ AND lft > ? AND rgt < ?
+ AND lft < c.lft AND rgt > c.rgt)',
+ array($proj->id, Post::val('list_name'), $left, $right, $proj->id, $left, $right));
+ $itemexists = $db->fetchOne($check);
+
+ if ($itemexists) {
+ Flyspray::show_error(sprintf(L('categoryitemexists'), Post::val('list_name'), $parent['category_name']));
+ return;
+ }
+ }
+
+ $db->query('UPDATE {list_category} SET rgt=rgt+2 WHERE rgt >= ? AND project_id = ?', array($right, $proj->id));
+ $db->query('UPDATE {list_category} SET lft=lft+2 WHERE lft >= ? AND project_id = ?', array($right, $proj->id));
+
+ $db->query("INSERT INTO {list_category}
+ ( project_id, category_name, show_in_list, category_owner, lft, rgt )
+ VALUES (?, ?, 1, ?, ?, ?)",
+ array($proj->id, Post::val('list_name'),
+ Post::val('category_owner', 0) == '' ? '0' : Flyspray::usernameToId(Post::val('category_owner', 0)), $right, $right+1));
+
+ $_SESSION['SUCCESS'] = L('listitemadded');
+ break;
+
+ // ##################
+ // adding a related task entry
+ // ##################
+ case 'details.add_related':
+ if (!$user->can_edit_task($task)) {
+ Flyspray::show_error(L('nopermission'));//TODO: create a better error message
+ break;
+ }
+
+ // if the user has not the permission to view all tasks, check if the task
+ // is in tasks allowed to see, otherwise tell that the task does not exist.
+ if (!$user->perms('view_tasks')) {
+ $taskcheck = Flyspray::getTaskDetails(Post::val('related_task'));
+ if (!$user->can_view_task($taskcheck)) {
+ Flyspray::show_error(L('relatedinvalid'));
+ break;
+ }
+ }
+
+ $sql = $db->query('SELECT project_id
+ FROM {tasks}
+ WHERE task_id = ?',
+ array(Post::val('related_task')));
+ if (!$db->countRows($sql)) {
+ Flyspray::show_error(L('relatedinvalid'));
+ break;
+ }
+
+ $sql = $db->query("SELECT related_id
+ FROM {related}
+ WHERE this_task = ? AND related_task = ?
+ OR
+ related_task = ? AND this_task = ?",
+ array($task['task_id'], Post::val('related_task'),
+ $task['task_id'], Post::val('related_task')));
+
+ if ($db->countRows($sql)) {
+ Flyspray::show_error(L('relatederror'));
+ break;
+ }
+
+ $db->query("INSERT INTO {related} (this_task, related_task) VALUES(?,?)",
+ array($task['task_id'], Post::val('related_task')));
+
+ Flyspray::logEvent($task['task_id'], 11, Post::val('related_task'));
+ Flyspray::logEvent(Post::val('related_task'), 15, $task['task_id']);
+ $notify->create(NOTIFY_REL_ADDED, $task['task_id'], Post::val('related_task'), null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+
+ $_SESSION['SUCCESS'] = L('relatedaddedmsg');
+ break;
+
+ // ##################
+ // Removing a related task entry
+ // ##################
+ case 'remove_related':
+ if (!$user->can_edit_task($task)) {
+ Flyspray::show_error(L('nopermission'));//TODO: create a better error message
+ break;
+ }
+ if (!is_array(Post::val('related_id'))) {
+ Flyspray::show_error(L('formnotcomplete'));
+ break;
+ }
+
+ foreach (Post::val('related_id') as $related) {
+ $sql = $db->query('SELECT this_task, related_task FROM {related} WHERE related_id = ?',
+ array($related));
+ $db->query('DELETE FROM {related} WHERE related_id = ? AND (this_task = ? OR related_task = ?)',
+ array($related, $task['task_id'], $task['task_id']));
+ if ($db->affectedRows()) {
+ $related_task = $db->fetchRow($sql);
+ $related_task = ($related_task['this_task'] == $task['task_id']) ? $related_task['related_task'] : $task['task_id'];
+ Flyspray::logEvent($task['task_id'], 12, $related_task);
+ Flyspray::logEvent($related_task, 16, $task['task_id']);
+ $_SESSION['SUCCESS'] = L('relatedremoved');
+ }
+ }
+
+ break;
+
+ // ##################
+ // adding a user to the notification list
+ // ##################
+ case 'details.add_notification':
+ if (Req::val('user_id')) {
+ $userId = Req::val('user_id');
+ } else {
+ $userId = Flyspray::usernameToId(Req::val('user_name'));
+ }
+ if (!Backend::add_notification($userId, Req::val('ids'))) {
+ Flyspray::show_error(L('couldnotaddusernotif'));
+ break;
+ }
+
+ // TODO: Log event in a later version.
+
+ $_SESSION['SUCCESS'] = L('notifyadded');
+ Flyspray::redirect(createURL('details', $task['task_id']).'#notify');
+ break;
+
+ // ##################
+ // removing a notification entry
+ // ##################
+ case 'remove_notification':
+ Backend::remove_notification(Req::val('user_id'), Req::val('ids'));
+
+ // TODO: Log event in a later version.
+
+ $_SESSION['SUCCESS'] = L('notifyremoved');
+ # if on details page we should redirect to details with a GET
+ # but what if the request comes from another page (like myprofile for instance maybe in future)
+ Flyspray::redirect(createURL('details', $task['task_id']).'#notify');
+ break;
+
+ // ##################
+ // editing a comment
+ // ##################
+ case 'editcomment':
+ if (!($user->perms('edit_comments') || $user->perms('edit_own_comments'))) {
+ break;
+ }
+
+ $where = '';
+
+ $comment_text=Post::val('comment_text');
+ $previous_text=Post::val('previous_text');
+
+ # dokuwiki syntax plugin filters on output
+ if($conf['general']['syntax_plugin'] != 'dokuwiki'){
+ $purifierconfig = HTMLPurifier_Config::createDefault();
+ $purifier = new HTMLPurifier($purifierconfig);
+ $comment_text = $purifier->purify($comment_text);
+ $previous_text= $purifier->purify($comment_text);
+ }
+
+ $params = array($comment_text, time(), Post::val('comment_id'), $task['task_id']);
+
+ if ($user->perms('edit_own_comments') && !$user->perms('edit_comments')) {
+ $where = ' AND user_id = ?';
+ array_push($params, $user->id);
+ }
+
+ $db->query("UPDATE {comments}
+ SET comment_text = ?, last_edited_time = ?
+ WHERE comment_id = ? AND task_id = ? $where", $params);
+ $db->query("DELETE FROM {cache} WHERE topic = ? AND type = ?", array(Post::val('comment_id'), 'comm'));
+
+ Flyspray::logEvent($task['task_id'], 5, $comment_text, $previous_text, Post::val('comment_id'));
+
+ Backend::upload_files($task['task_id'], Post::val('comment_id'));
+ Backend::delete_files(Post::val('delete_att'));
+ Backend::upload_links($task['task_id'], Post::val('comment_id'));
+ Backend::delete_links(Post::val('delete_link'));
+
+ $_SESSION['SUCCESS'] = L('editcommentsaved');
+ break;
+
+ // ##################
+ // deleting a comment
+ // ##################
+ case 'details.deletecomment':
+ if (!$user->perms('delete_comments')) {
+ break;
+ }
+
+ $result = $db->query('SELECT task_id, comment_text, user_id, date_added
+ FROM {comments}
+ WHERE comment_id = ?',
+ array(Get::val('comment_id')));
+ $comment = $db->fetchRow($result);
+
+ // Check for files attached to this comment
+ $check_attachments = $db->query('SELECT *
+ FROM {attachments}
+ WHERE comment_id = ?',
+ array(Req::val('comment_id')));
+
+ if ($db->countRows($check_attachments) && !$user->perms('delete_attachments')) {
+ Flyspray::show_error(L('commentattachperms'));
+ break;
+ }
+
+ $db->query("DELETE FROM {comments} WHERE comment_id = ? AND task_id = ?",
+ array(Req::val('comment_id'), $task['task_id']));
+
+ if ($db->affectedRows()) {
+ Flyspray::logEvent($task['task_id'], 6, $comment['user_id'],
+ $comment['comment_text'], '', $comment['date_added']);
+ }
+
+ while ($attachment = $db->fetchRow($check_attachments)) {
+ $db->query("DELETE from {attachments} WHERE attachment_id = ?",
+ array($attachment['attachment_id']));
+
+ @unlink(BASEDIR .'/attachments/' . $attachment['file_name']);
+
+ Flyspray::logEvent($attachment['task_id'], 8, $attachment['orig_name']);
+ }
+
+ $_SESSION['SUCCESS'] = L('commentdeletedmsg');
+ break;
+
+ // ##################
+ // adding a reminder
+ // ##################
+ case 'details.addreminder':
+ $how_often = Post::val('timeamount1', 1) * Post::val('timetype1');
+ $start_time = Flyspray::strtotime(Post::val('timeamount2', 0));
+
+ $userId = Flyspray::usernameToId(Post::val('to_user_id'));
+ if (!Backend::add_reminder($task['task_id'], Post::val('reminder_message'), $how_often, $start_time, $userId)) {
+ Flyspray::show_error(L('usernotexist'));
+ break;
+ }
+
+ // TODO: Log event in a later version.
+
+ $_SESSION['SUCCESS'] = L('reminderaddedmsg');
+ break;
+
+ // ##################
+ // removing a reminder
+ // ##################
+ case 'deletereminder':
+ if (!$user->perms('manage_project') || !is_array(Post::val('reminder_id'))) {
+ break;
+ }
+
+ foreach (Post::val('reminder_id') as $reminder_id) {
+ $sql = $db->query('SELECT to_user_id FROM {reminders} WHERE reminder_id = ?',
+ array($reminder_id));
+ $reminder = $db->fetchOne($sql);
+ $db->query('DELETE FROM {reminders} WHERE reminder_id = ? AND task_id = ?',
+ array($reminder_id, $task['task_id']));
+ if ($db && $db->affectedRows()) {
+ Flyspray::logEvent($task['task_id'], 18, $reminder);
+ }
+ }
+
+ $_SESSION['SUCCESS'] = L('reminderdeletedmsg');
+ break;
+
+ // ##################
+ // change a bunch of users' groups
+ // ##################
+ case 'movetogroup':
+ // Check that both groups belong to the same project
+ $sql = $db->query('SELECT project_id FROM {groups} WHERE group_id = ? OR group_id = ?',
+ array(Post::val('switch_to_group'), Post::val('old_group')));
+ $old_pr = $db->fetchOne($sql);
+ $new_pr = $db->fetchOne($sql);
+ if ($proj->id != $old_pr || ($new_pr && $new_pr != $proj->id)) {
+ break;
+ }
+
+ if (!$user->perms('manage_project', $old_pr) || !is_array(Post::val('users'))) {
+ break;
+ }
+
+ foreach (Post::val('users') as $user_id => $val) {
+ if($user->id!=$user_id || $proj->id!=0){
+ if (Post::val('switch_to_group') == '0') {
+ $db->query('DELETE FROM {users_in_groups} WHERE user_id=? AND group_id=?',
+ array($user_id, Post::val('old_group'))
+ );
+ } else {
+ # special case: user exists in multiple global groups (shouldn't, but happened)
+ # avoids duplicate entry error
+ if($old_pr==0){
+ $sql = $db->query('SELECT group_id FROM {users_in_groups} WHERE user_id = ? AND group_id = ?',
+ array($user_id, Post::val('switch_to_group'))
+ );
+ $uigexists = $db->fetchOne($sql);
+ if($uigexists > 0){
+ $db->query('DELETE FROM {users_in_groups} WHERE user_id=? AND group_id=?',
+ array($user_id, Post::val('old_group'))
+ );
+ }
+ }
+
+ $db->query('UPDATE {users_in_groups} SET group_id=? WHERE user_id=? AND group_id=?',
+ array(Post::val('switch_to_group'), $user_id, Post::val('old_group'))
+ );
+ }
+ } else {
+ Flyspray::show_error(L('nosuicide'));
+ }
+ }
+
+ // TODO: Log event in a later version.
+
+ $_SESSION['SUCCESS'] = L('groupswitchupdated');
+ break;
+
+ // ##################
+ // taking ownership
+ // ##################
+ case 'takeownership':
+ Backend::assign_to_me($user->id, Req::val('ids'));
+
+ // TODO: Log event in a later version.
+
+ $_SESSION['SUCCESS'] = L('takenownershipmsg');
+ break;
+
+ // ##################
+ // add to assignees list
+ // ##################
+ case 'addtoassignees':
+ Backend::add_to_assignees($user->id, Req::val('ids'));
+
+ // TODO: Log event in a later version.
+
+ $_SESSION['SUCCESS'] = L('addedtoassignees');
+ break;
+
+ // ##################
+ // admin request
+ // ##################
+ case 'requestclose':
+ case 'requestreopen':
+ if ($action == 'requestclose') {
+ Flyspray::adminRequest(1, $proj->id, $task['task_id'], $user->id, Post::val('reason_given'));
+ Flyspray::logEvent($task['task_id'], 20, Post::val('reason_given'));
+ } elseif ($action == 'requestreopen') {
+ Flyspray::adminRequest(2, $proj->id, $task['task_id'], $user->id, Post::val('reason_given'));
+ Flyspray::logEvent($task['task_id'], 21, Post::val('reason_given'));
+ Backend::add_notification($user->id, $task['task_id']);
+ }
+
+ // Now, get the project managers' details for this project
+ $sql = $db->query("SELECT u.user_id
+ FROM {users} u
+ LEFT JOIN {users_in_groups} uig ON u.user_id = uig.user_id
+ LEFT JOIN {groups} g ON uig.group_id = g.group_id
+ WHERE g.project_id = ? AND g.manage_project = '1'",
+ array($proj->id));
+
+ $pms = $db->fetchCol($sql);
+ if (count($pms)) {
+ // Call the functions to create the address arrays, and send notifications
+ $notify->create(NOTIFY_PM_REQUEST, $task['task_id'], null, $notify->specificAddresses($pms), NOTIFY_BOTH, $proj->prefs['lang_code']);
+ }
+
+ $_SESSION['SUCCESS'] = L('adminrequestmade');
+ break;
+
+ // ##################
+ // denying a PM request
+ // ##################
+ case 'denypmreq':
+ $result = $db->query("SELECT task_id, project_id
+ FROM {admin_requests}
+ WHERE request_id = ?",
+ array(Req::val('req_id')));
+ $req_details = $db->fetchRow($result);
+
+ if (!$user->perms('manage_project', $req_details['project_id'])) {
+ break;
+ }
+
+ // Mark the PM request as 'resolved'
+ $db->query("UPDATE {admin_requests}
+ SET resolved_by = ?, time_resolved = ?, deny_reason = ?
+ WHERE request_id = ?",
+ array($user->id, time(), Req::val('deny_reason'), Req::val('req_id')));
+
+ Flyspray::logEvent($req_details['task_id'], 28, Req::val('deny_reason'));
+ $notify->create(NOTIFY_PM_DENY_REQUEST, $req_details['task_id'], Req::val('deny_reason'), null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+
+ $_SESSION['SUCCESS'] = L('pmreqdeniedmsg');
+ break;
+
+ // ##################
+ // deny a new user request
+ // ##################
+ case 'denyuserreq':
+ if($user->perms('is_admin')) {
+ $db->query("UPDATE {admin_requests}
+ SET resolved_by = ?, time_resolved = ?, deny_reason = ?
+ WHERE request_id = ?",
+ array($user->id, time(), Req::val('deny_reason'), Req::val('req_id')));
+ // Wrong event constant
+ Flyspray::logEvent(0, 28, Req::val('deny_reason'));//nee a new event number. need notification. fix smtp first
+ // Missing notification constant, can't notify yet...
+ $_SESSION['SUCCESS'] = "New user register request denied";
+ }
+ break;
+
+ // ##################
+ // adding a dependency
+ // ##################
+ case 'details.newdep':
+ if (!$user->can_edit_task($task)) {
+ Flyspray::show_error(L('nopermission'));//TODO: create a better error message
+ break;
+ }
+
+ if (!Post::val('dep_task_id')) {
+ Flyspray::show_error(L('formnotcomplete'));
+ break;
+ }
+
+ // TODO: do the checks in some other order. Think about possibility
+ // to combine many of the checks used to to see if a task exists,
+ // if it's something user is allowed to know about etc to just one
+ // function taking the necessary arguments and could be used in
+ // several other places too.
+
+ // if the user has not the permission to view all tasks, check if the task
+ // is in tasks allowed to see, otherwise tell that the task does not exist.
+ if (!$user->perms('view_tasks')) {
+ $taskcheck = Flyspray::getTaskDetails(Post::val('dep_task_id'));
+ if (!$user->can_view_task($taskcheck)) {
+ Flyspray::show_error(L('dependaddfailed'));
+ break;
+ }
+ }
+
+ // First check that the user hasn't tried to add this twice
+ $sql1 = $db->query('SELECT COUNT(*) FROM {dependencies}
+ WHERE task_id = ? AND dep_task_id = ?',
+ array($task['task_id'], Post::val('dep_task_id')));
+
+ // or that they are trying to reverse-depend the same task, creating a mutual-block
+ $sql2 = $db->query('SELECT COUNT(*) FROM {dependencies}
+ WHERE task_id = ? AND dep_task_id = ?',
+ array(Post::val('dep_task_id'), $task['task_id']));
+
+ // Check that the dependency actually exists!
+ $sql3 = $db->query('SELECT COUNT(*) FROM {tasks} WHERE task_id = ?',
+ array(Post::val('dep_task_id')));
+
+ if ($db->fetchOne($sql1) || $db->fetchOne($sql2) || !$db->fetchOne($sql3)
+ // Check that the user hasn't tried to add the same task as a dependency
+ || Post::val('task_id') == Post::val('dep_task_id'))
+ {
+ Flyspray::show_error(L('dependaddfailed'));
+ break;
+ }
+ $notify->create(NOTIFY_DEP_ADDED, $task['task_id'], Post::val('dep_task_id'), null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+ $notify->create(NOTIFY_REV_DEP, Post::val('dep_task_id'), $task['task_id'], null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+
+ // Log this event to the task history, both ways
+ Flyspray::logEvent($task['task_id'], 22, Post::val('dep_task_id'));
+ Flyspray::logEvent(Post::val('dep_task_id'), 23, $task['task_id']);
+
+ $db->query('INSERT INTO {dependencies} (task_id, dep_task_id)
+ VALUES (?,?)',
+ array($task['task_id'], Post::val('dep_task_id')));
+
+ $_SESSION['SUCCESS'] = L('dependadded');
+ break;
+
+ // ##################
+ // removing a subtask
+ // ##################
+ case 'removesubtask':
+
+ //check if the user has permissions to remove the subtask
+ if (!$user->can_edit_task($task)) {
+ Flyspray::show_error(L('nopermission'));//TODO: create a better error message
+ break;
+ }
+
+ //set the subtask supertask_id to 0 removing parent child relationship
+ $db->query("UPDATE {tasks} SET supertask_id=0 WHERE task_id = ?",
+ array(Post::val('subtaskid')));
+
+ //write event log
+ Flyspray::logEvent(Get::val('task_id'), 33, Post::val('subtaskid'));
+ //post success message to the user
+ $_SESSION['SUCCESS'] = L('subtaskremovedmsg');
+ //redirect the user back to the right task
+ Flyspray::redirect(createURL('details', Get::val('task_id')));
+ break;
+
+ // ##################
+ // removing a dependency
+ // ##################
+ case 'removedep':
+ if (!$user->can_edit_task($task)) {
+ Flyspray::show_error(L('nopermission'));//TODO: create a better error message
+ break;
+ }
+
+ $result = $db->query('SELECT * FROM {dependencies}
+ WHERE depend_id = ?',
+ array(Post::val('depend_id')));
+ $dep_info = $db->fetchRow($result);
+
+ $db->query('DELETE FROM {dependencies} WHERE depend_id = ? AND task_id = ?',
+ array(Post::val('depend_id'), $task['task_id']));
+
+ if ($db->affectedRows()) {
+ $notify->create(NOTIFY_DEP_REMOVED, $dep_info['task_id'], $dep_info['dep_task_id'], null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+ $notify->create(NOTIFY_REV_DEP_REMOVED, $dep_info['dep_task_id'], $dep_info['task_id'], null, NOTIFY_BOTH, $proj->prefs['lang_code']);
+
+ Flyspray::logEvent($dep_info['task_id'], 24, $dep_info['dep_task_id']);
+ Flyspray::logEvent($dep_info['dep_task_id'], 25, $dep_info['task_id']);
+
+ $_SESSION['SUCCESS'] = L('depremovedmsg');
+ } else {
+ Flyspray::show_error(L('erroronform'));
+ }
+
+ //redirect the user back to the right task
+ Flyspray::redirect(createURL('details', Post::val('return_task_id')));
+ break;
+
+ // ##################
+ // user requesting a password change
+ // ##################
+ case 'lostpw.sendmagic':
+ // Check that the username exists
+ $sql = $db->query('SELECT * FROM {users} WHERE user_name = ?',
+ array(Post::val('user_name')));
+
+ // If the username doesn't exist, throw an error
+ if (!$db->countRows($sql)) {
+ Flyspray::show_error(L('usernotexist'));
+ break;
+ }
+
+ $user_details = $db->fetchRow($sql);
+
+ if ($user_details['oauth_provider']) {
+ Flyspray::show_error(sprintf(L('oauthreqpass'), ucfirst($user_details['oauth_provider'])));
+ Flyspray::redirect($baseurl);
+ break;
+ }
+
+ //no microtime(), time,even with microseconds is predictable ;-)
+ $magic_url = md5(function_exists('openssl_random_pseudo_bytes') ?
+ openssl_random_pseudo_bytes(32) :
+ uniqid(mt_rand(), true));
+
+
+ // Insert the random "magic url" into the user's profile
+ $db->query('UPDATE {users}
+ SET magic_url = ?
+ WHERE user_id = ?',
+ array($magic_url, $user_details['user_id']));
+
+ if(count($user_details)) {
+ $notify->create(NOTIFY_PW_CHANGE, null, array($baseurl, $magic_url), $notify->specificAddresses(array($user_details['user_id']), NOTIFY_EMAIL));
+ }
+
+ // TODO: Log event in a later version.
+
+ $_SESSION['SUCCESS'] = L('magicurlsent');
+ break;
+
+ // ##################
+ // Change the user's password
+ // ##################
+ case 'lostpw.chpass':
+ // Check that the user submitted both the fields, and they are the same
+ if (!Post::val('pass1') || strlen(trim(Post::val('magic_url'))) !== 32) {
+ Flyspray::show_error(L('erroronform'));
+ break;
+ }
+
+ if ($fs->prefs['repeat_password'] && Post::val('pass1') != Post::val('pass2')) {
+ Flyspray::show_error(L('passnomatch'));
+ break;
+ }
+
+ $new_pass_hash = Flyspray::cryptPassword(Post::val('pass1'));
+ $db->query("UPDATE {users} SET user_pass = ?, magic_url = ''
+ WHERE magic_url = ?",
+ array($new_pass_hash, Post::val('magic_url')));
+
+ // TODO: Log event in a later version.
+
+ $_SESSION['SUCCESS'] = L('passchanged');
+ Flyspray::redirect($baseurl);
+ break;
+
+ // ##################
+ // making a task private
+ // ##################
+ case 'makeprivate':
+ // TODO: Have to think about this one a bit more. Are project manager
+ // rights really needed for making a task a private? Are there some
+ // other conditions that would permit it? Also making it back to public.
+ if (!$user->perms('manage_project')) {
+ break;
+ }
+
+ $db->query('UPDATE {tasks}
+ SET mark_private = 1
+ WHERE task_id = ?', array($task['task_id']));
+
+ Flyspray::logEvent($task['task_id'], 3, 1, 0, 'mark_private');
+
+ $_SESSION['SUCCESS'] = L('taskmadeprivatemsg');
+ break;
+
+ // ##################
+ // making a task public
+ // ##################
+ case 'makepublic':
+ if (!$user->perms('manage_project')) {
+ break;
+ }
+
+ $db->query('UPDATE {tasks}
+ SET mark_private = 0
+ WHERE task_id = ?', array($task['task_id']));
+
+ Flyspray::logEvent($task['task_id'], 3, 0, 1, 'mark_private');
+
+ $_SESSION['SUCCESS'] = L('taskmadepublicmsg');
+ break;
+
+ // ##################
+ // Adding a vote for a task
+ // ##################
+ case 'details.addvote':
+ if (Backend::add_vote($user->id, $task['task_id'])) {
+ $_SESSION['SUCCESS'] = L('voterecorded');
+ } else {
+ Flyspray::show_error(L('votefailed'));
+ break;
+ }
+ // TODO: Log event in a later version.
+ break;
+
+
+ // ##################
+ // Removing a vote for a task
+ // ##################
+ # used to remove a vote from myprofile page
+ case 'removevote':
+ # peterdd: I found no details.removevote action in source, so details.removevote is not used, but was planned on the task details page or in the old blue theme?
+ case 'details.removevote':
+ if (Backend::remove_vote($user->id, $task['task_id'])) {
+ $_SESSION['SUCCESS'] = L('voteremoved');
+ } else {
+ Flyspray::show_error(L('voteremovefailed'));
+ break;
+ }
+ // TODO: Log event in a later version, but also see if maybe done here Backend::remove_vote()...
+ break;
+
+
+ // ##################
+ // set supertask id
+ // ##################
+ case 'details.setparent':
+ if (!$user->can_edit_task($task)) {
+ Flyspray::show_error(L('nopermission'));//TODO: create a better error message
+ break;
+ }
+
+ if (!Post::val('supertask_id')) {
+ Flyspray::show_error(L('formnotcomplete'));
+ break;
+ }
+
+ // check that supertask_id is not same as task_id
+ // preventing it from referring to itself
+ if (Post::val('task_id') == Post::val('supertask_id')) {
+ Flyspray::show_error(L('selfsupertasknotallowed'));
+ break;
+ }
+
+ // Check that the supertask_id looks like unsigned integer
+ if ( !preg_match("/^[1-9][0-9]{0,8}$/", Post::val('supertask_id')) ) {
+ Flyspray::show_error(L('invalidsupertaskid'));
+ break;
+ }
+
+ $sql = $db->query('SELECT project_id FROM {tasks} WHERE task_id = ?', array(Post::val('supertask_id')) );
+ // check that supertask_id is a valid task id
+ $parent = $db->fetchRow($sql);
+ if (!$parent) {
+ Flyspray::show_error(L('invalidsupertaskid'));
+ break;
+ }
+
+ // if the user has not the permission to view all tasks, check if the task
+ // is in tasks allowed to see, otherwise tell that the task does not exist.
+ if (!$user->perms('view_tasks')) {
+ $taskcheck = Flyspray::getTaskDetails(Post::val('supertask_id'));
+ if (!$user->can_view_task($taskcheck)) {
+ Flyspray::show_error(L('invalidsupertaskid'));
+ break;
+ }
+ }
+
+ // check to see that both tasks belong to the same project
+ if ($task['project_id'] != $parent['project_id']) {
+ Flyspray::show_error(L('musthavesameproject'));
+ break;
+ }
+
+ // finally looks like all the checks are valid so update the supertask_id for the current task
+ $db->query('UPDATE {tasks}
+ SET supertask_id = ?
+ WHERE task_id = ?',
+ array(Post::val('supertask_id'),Post::val('task_id')));
+
+ // If task already had a different parent, then log removal too
+ if ($task['supertask_id']) {
+ Flyspray::logEvent($task['supertask_id'], 33, Post::val('task_id'));
+ Flyspray::logEvent(Post::val('task_id'), 35, $task['supertask_id']);
+ }
+
+ // Log the events in the task history
+ Flyspray::logEvent(Post::val('supertask_id'), 32, Post::val('task_id'));
+ Flyspray::logEvent(Post::val('task_id'), 34, Post::val('supertask_id'));
+
+ // set success message
+ $_SESSION['SUCCESS'] = L('supertaskmodified');
+
+ break;
+ case 'notifications.remove':
+ if(!isset($_POST['message_id'])) {
+ // Flyspray::show_error(L('summaryanddetails'));
+ break;
+ }
+
+ if (!is_array($_POST['message_id'])) {
+ // Flyspray::show_error(L('summaryanddetails'));
+ break;
+ }
+ if (!count($_POST['message_id'])) {
+ // Nothing to do.
+ break;
+ }
+
+ $validids = array();
+ foreach ($_POST['message_id'] as $id) {
+ if (is_numeric($id)) {
+ if (settype($id, 'int') && $id > 0) {
+ $validids[] = $id;
+ }
+ }
+ }
+
+ if (!count($validids)) {
+ // Nothing to do.
+ break;
+ }
+
+ Notifications::NotificationsHaveBeenRead($validids);
+ break;
+ case 'task.bulkupdate':
+ # TODO check if the user has the right to do each action on each task id he send with the form!
+ # TODO check if tasks have open subtasks before closing
+ # TODO SQL Transactions with rollback function if something went wrong in the middle of bulk action
+ # disabled by default and if currently allowed only for admins until proper checks are done
+ if(isset($fs->prefs['massops']) && $fs->prefs['massops']==1 && $user->perms('is_admin')){
+
+ // TODO: Log events in a later version.
+
+ if(Post::val('updateselectedtasks') == "true") {
+ //process quick actions
+ switch(Post::val('bulk_quick_action'))
+ {
+ case 'bulk_take_ownership':
+ Backend::assign_to_me(Post::val('user_id'),Post::val('ids'));
+ break;
+ case 'bulk_start_watching':
+ Backend::add_notification(Post::val('user_id'),Post::val('ids'));
+ break;
+ case 'bulk_stop_watching':
+ Backend::remove_notification(Post::val('user_id'),Post::val('ids'));
+ break;
+ }
+
+ //Process the tasks.
+ $columns = array();
+ $values = array();
+
+ //determine the tasks properties that have been modified.
+ if(!Post::val('bulk_status')==0){
+ array_push($columns,'item_status');
+ array_push($values, Post::val('bulk_status'));
+ }
+ if(!Post::val('bulk_percent_complete')==0){
+ array_push($columns,'percent_complete');
+ array_push($values, Post::val('bulk_percent_complete'));
+ }
+ if(!Post::val('bulk_task_type')==0){
+ array_push($columns,'task_type');
+ array_push($values, Post::val('bulk_task_type'));
+ }
+ if(!Post::val('bulk_category')==0){
+ array_push($columns,'product_category');
+ array_push($values, Post::val('bulk_category'));
+ }
+ if(!Post::val('bulk_os')==0){
+ array_push($columns,'operating_system');
+ array_push($values, Post::val('bulk_os'));
+ }
+ if(!Post::val('bulk_severity')==0){
+ array_push($columns,'task_severity');
+ array_push($values, Post::val('bulk_severity'));
+ }
+ if(!Post::val('bulk_priority')==0){
+ array_push($columns,'task_priority');
+ array_push($values, Post::val('bulk_priority'));
+ }
+ if(!Post::val('bulk_reportedver')==0){
+ array_push($columns,'product_version');
+ array_push($values, Post::val('bulk_reportedver'));
+ }
+ if(!Post::val('bulk_due_version')==0){
+ array_push($columns,'closedby_version');
+ array_push($values, Post::val('bulk_due_version'));
+ }
+ # TODO Does the user has similiar rights in current and target projects?
+ # TODO Does a task has subtasks? What happens to them? What if they are open/closed?
+ # But: Allowing task dependencies between tasks in different projects is a feature!
+ if(!Post::val('bulk_projects')==0){
+ array_push($columns,'project_id');
+ array_push($values, Post::val('bulk_projects'));
+ }
+ if(!is_null(Post::val('bulk_due_date'))){
+ array_push($columns,'due_date');
+ array_push($values, Flyspray::strtotime(Post::val('bulk_due_date')));
+ }
+
+ //only process if one of the task fields has been updated.
+ if(!array_count_values($columns)==0 && Post::val('ids')){
+ //add the selected task id's to the query string
+ $task_ids = Post::val('ids');
+ $valuesAndTasks = array_merge_recursive($values,$task_ids);
+
+ //execute the database update on all selected queries
+ $update = $db->query("UPDATE {tasks}
+ SET ".join('=?, ', $columns)."=?
+ WHERE". substr(str_repeat(' task_id = ? OR ', count(Post::val('ids'))), 0, -3), $valuesAndTasks);
+ }
+
+ //Set the assignments
+ if(Post::val('bulk_assignment')){
+ // Delete the current assignees for the selected tasks
+ $db->query("DELETE FROM {assigned} WHERE". substr(str_repeat(' task_id = ? OR ', count(Post::val('ids'))), 0, -3),Post::val('ids'));
+
+ // Convert assigned_to and store them in the 'assigned' table
+ foreach ((array)Post::val('ids') as $id){
+ //iterate the users that are selected on the user list.
+ foreach ((array) Post::val('bulk_assignment') as $assignee){
+ //if 'noone' has been selected then dont do the database update.
+ if(!$assignee == 0){
+ //insert the task and user id's into the assigned table.
+ $db->query('INSERT INTO {assigned}
+ (task_id,user_id)
+ VALUES (?, ?)',array($id,$assignee));
+ }
+ }
+ }
+ }
+
+ // set success message
+ $_SESSION['SUCCESS'] = L('tasksupdated');
+ break;
+ }
+ //bulk close
+ else {
+ if (!Post::val('resolution_reason')) {
+ Flyspray::show_error(L('noclosereason'));
+ break;
+ }
+ $task_ids = Post::val('ids');
+ foreach($task_ids as $task_id) {
+ $task = Flyspray::getTaskDetails($task_id);
+ if (!$user->can_close_task($task)) {
+ continue;
+ }
+
+ if ($task['is_closed']) {
+ continue;
+ }
+
+ Backend::close_task($task_id, Post::val('resolution_reason'), Post::val('closure_comment', ''), Post::val('mark100', false));
+ }
+ $_SESSION['SUCCESS'] = L('taskclosedmsg');
+ break;
+ }
+ } # end if massopsenabled
+ else{
+ Flyspray::show_error(L('massopsdisabled'));
+ }
+ }
diff --git a/includes/password_compat.php b/includes/password_compat.php
new file mode 100644
index 0000000..d681634
--- /dev/null
+++ b/includes/password_compat.php
@@ -0,0 +1,319 @@
+<?php
+/**
+ * A Compatibility library with PHP 5.5's simplified password hashing API.
+ *
+ * @author Anthony Ferrara <ircmaxell@php.net>
+ * @license http://www.opensource.org/licenses/mit-license.html MIT License
+ * @copyright 2012 The Authors
+ *
+ * Comment of Flyspray dev peterdd: This is lib/password.php from https://github.com/ircmaxell/password_compat master branch at 2016-06-27
+ */
+
+namespace {
+
+ if (!defined('PASSWORD_BCRYPT')) {
+ /**
+ * PHPUnit Process isolation caches constants, but not function declarations.
+ * So we need to check if the constants are defined separately from
+ * the functions to enable supporting process isolation in userland
+ * code.
+ */
+ define('PASSWORD_BCRYPT', 1);
+ define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
+ define('PASSWORD_BCRYPT_DEFAULT_COST', 10);
+ }
+
+ if (!function_exists('password_hash')) {
+
+ /**
+ * Hash the password using the specified algorithm
+ *
+ * @param string $password The password to hash
+ * @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
+ * @param array $options The options for the algorithm to use
+ *
+ * @return string|false The hashed password, or false on error.
+ */
+ function password_hash($password, $algo, array $options = array()) {
+ if (!function_exists('crypt')) {
+ trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
+ return null;
+ }
+ if (is_null($password) || is_int($password)) {
+ $password = (string) $password;
+ }
+ if (!is_string($password)) {
+ trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
+ return null;
+ }
+ if (!is_int($algo)) {
+ trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
+ return null;
+ }
+ $resultLength = 0;
+ switch ($algo) {
+ case PASSWORD_BCRYPT:
+ $cost = PASSWORD_BCRYPT_DEFAULT_COST;
+ if (isset($options['cost'])) {
+ $cost = (int) $options['cost'];
+ if ($cost < 4 || $cost > 31) {
+ trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
+ return null;
+ }
+ }
+ // The length of salt to generate
+ $raw_salt_len = 16;
+ // The length required in the final serialization
+ $required_salt_len = 22;
+ $hash_format = sprintf("$2y$%02d$", $cost);
+ // The expected length of the final crypt() output
+ $resultLength = 60;
+ break;
+ default:
+ trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
+ return null;
+ }
+ $salt_req_encoding = false;
+ if (isset($options['salt'])) {
+ switch (gettype($options['salt'])) {
+ case 'NULL':
+ case 'boolean':
+ case 'integer':
+ case 'double':
+ case 'string':
+ $salt = (string) $options['salt'];
+ break;
+ case 'object':
+ if (method_exists($options['salt'], '__tostring')) {
+ $salt = (string) $options['salt'];
+ break;
+ }
+ case 'array':
+ case 'resource':
+ default:
+ trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
+ return null;
+ }
+ if (PasswordCompat\binary\_strlen($salt) < $required_salt_len) {
+ trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING);
+ return null;
+ } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
+ $salt_req_encoding = true;
+ }
+ } else {
+ $buffer = '';
+ $buffer_valid = false;
+ if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
+ $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);
+ if ($buffer) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
+ $strong = false;
+ $buffer = openssl_random_pseudo_bytes($raw_salt_len, $strong);
+ if ($buffer && $strong) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid && @is_readable('/dev/urandom')) {
+ $file = fopen('/dev/urandom', 'r');
+ $read = 0;
+ $local_buffer = '';
+ while ($read < $raw_salt_len) {
+ $local_buffer .= fread($file, $raw_salt_len - $read);
+ $read = PasswordCompat\binary\_strlen($local_buffer);
+ }
+ fclose($file);
+ if ($read >= $raw_salt_len) {
+ $buffer_valid = true;
+ }
+ $buffer = str_pad($buffer, $raw_salt_len, "\0") ^ str_pad($local_buffer, $raw_salt_len, "\0");
+ }
+ if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) {
+ $buffer_length = PasswordCompat\binary\_strlen($buffer);
+ for ($i = 0; $i < $raw_salt_len; $i++) {
+ if ($i < $buffer_length) {
+ $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
+ } else {
+ $buffer .= chr(mt_rand(0, 255));
+ }
+ }
+ }
+ $salt = $buffer;
+ $salt_req_encoding = true;
+ }
+ if ($salt_req_encoding) {
+ // encode string with the Base64 variant used by crypt
+ $base64_digits =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+ $bcrypt64_digits =
+ './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+ $base64_string = base64_encode($salt);
+ $salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits);
+ }
+ $salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len);
+
+ $hash = $hash_format . $salt;
+
+ $ret = crypt($password, $hash);
+
+ if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) {
+ return false;
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Get information about the password hash. Returns an array of the information
+ * that was used to generate the password hash.
+ *
+ * array(
+ * 'algo' => 1,
+ * 'algoName' => 'bcrypt',
+ * 'options' => array(
+ * 'cost' => PASSWORD_BCRYPT_DEFAULT_COST,
+ * ),
+ * )
+ *
+ * @param string $hash The password hash to extract info from
+ *
+ * @return array The array of information about the hash.
+ */
+ function password_get_info($hash) {
+ $return = array(
+ 'algo' => 0,
+ 'algoName' => 'unknown',
+ 'options' => array(),
+ );
+ if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) {
+ $return['algo'] = PASSWORD_BCRYPT;
+ $return['algoName'] = 'bcrypt';
+ list($cost) = sscanf($hash, "$2y$%d$");
+ $return['options']['cost'] = $cost;
+ }
+ return $return;
+ }
+
+ /**
+ * Determine if the password hash needs to be rehashed according to the options provided
+ *
+ * If the answer is true, after validating the password using password_verify, rehash it.
+ *
+ * @param string $hash The hash to test
+ * @param int $algo The algorithm used for new password hashes
+ * @param array $options The options array passed to password_hash
+ *
+ * @return boolean True if the password needs to be rehashed.
+ */
+ function password_needs_rehash($hash, $algo, array $options = array()) {
+ $info = password_get_info($hash);
+ if ($info['algo'] !== (int) $algo) {
+ return true;
+ }
+ switch ($algo) {
+ case PASSWORD_BCRYPT:
+ $cost = isset($options['cost']) ? (int) $options['cost'] : PASSWORD_BCRYPT_DEFAULT_COST;
+ if ($cost !== $info['options']['cost']) {
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Verify a password against a hash using a timing attack resistant approach
+ *
+ * @param string $password The password to verify
+ * @param string $hash The hash to verify against
+ *
+ * @return boolean If the password matches the hash
+ */
+ function password_verify($password, $hash) {
+ if (!function_exists('crypt')) {
+ trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
+ return false;
+ }
+ $ret = crypt($password, $hash);
+ if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) {
+ return false;
+ }
+
+ $status = 0;
+ for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) {
+ $status |= (ord($ret[$i]) ^ ord($hash[$i]));
+ }
+
+ return $status === 0;
+ }
+ }
+
+}
+
+namespace PasswordCompat\binary {
+
+ if (!function_exists('PasswordCompat\\binary\\_strlen')) {
+
+ /**
+ * Count the number of bytes in a string
+ *
+ * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension.
+ * In this case, strlen() will count the number of *characters* based on the internal encoding. A
+ * sequence of bytes might be regarded as a single multibyte character.
+ *
+ * @param string $binary_string The input string
+ *
+ * @internal
+ * @return int The number of bytes
+ */
+ function _strlen($binary_string) {
+ if (function_exists('mb_strlen')) {
+ return mb_strlen($binary_string, '8bit');
+ }
+ return strlen($binary_string);
+ }
+
+ /**
+ * Get a substring based on byte limits
+ *
+ * @see _strlen()
+ *
+ * @param string $binary_string The input string
+ * @param int $start
+ * @param int $length
+ *
+ * @internal
+ * @return string The substring
+ */
+ function _substr($binary_string, $start, $length) {
+ if (function_exists('mb_substr')) {
+ return mb_substr($binary_string, $start, $length, '8bit');
+ }
+ return substr($binary_string, $start, $length);
+ }
+
+ /**
+ * Check if current PHP version is compatible with the library
+ *
+ * @return boolean the check result
+ */
+ function check() {
+ static $pass = NULL;
+
+ if (is_null($pass)) {
+ if (function_exists('crypt')) {
+ $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
+ $test = crypt("password", $hash);
+ $pass = $test == $hash;
+ } else {
+ $pass = false;
+ }
+ }
+ return $pass;
+ }
+
+ }
+}
diff --git a/includes/utf8.inc.php b/includes/utf8.inc.php
new file mode 100644
index 0000000..f950721
--- /dev/null
+++ b/includes/utf8.inc.php
@@ -0,0 +1,118 @@
+<?php
+
+if (!defined('IN_FS')) {
+ die('Do not access this file directly.');
+}
+
+require_once(dirname(__DIR__) . '/plugins/dokuwiki/inc/utf8.php');
+
+// a-z A-Z . _ -, extended latin chars, Cyrillic and Greek
+global $UTF8_ALPHA_CHARS;
+$UTF8_ALPHA_CHARS = array(
+ 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c,
+ 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
+ 0x59, 0x5a, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,
+ 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76,
+ 0x77, 0x78, 0x79, 0x7a, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
+ 0x38, 0x39, 0x2e, 0x2d, 0x5f, 0x20, 0x00c1, 0x00e1, 0x0106, 0x0107,
+ 0x00c9, 0x00e9, 0x00cd, 0x00ed, 0x0139, 0x013a, 0x0143, 0x0144, 0x00d3,
+ 0x00f3, 0x0154, 0x0155, 0x015a, 0x015b, 0x00da, 0x00fa, 0x00dd, 0x00fd,
+ 0x0179, 0x017a, 0x010f, 0x013d, 0x013e, 0x0165, 0x0102, 0x0103, 0x011e,
+ 0x011f, 0x016c, 0x016d, 0x010c, 0x010d, 0x010e, 0x011a, 0x011b, 0x0147,
+ 0x0148, 0x0158, 0x0159, 0x0160, 0x0161, 0x0164, 0x017d, 0x017e, 0x00c7,
+ 0x00e7, 0x0122, 0x0123, 0x0136, 0x0137, 0x013b, 0x013c, 0x0145, 0x0146,
+ 0x0156, 0x0157, 0x015e, 0x015f, 0x0162, 0x0163, 0x00c2, 0x00e2, 0x0108,
+ 0x0109, 0x00ca, 0x00ea, 0x011c, 0x011d, 0x0124, 0x0125, 0x00ce, 0x00ee,
+ 0x0134, 0x0135, 0x00d4, 0x00f4, 0x015c, 0x015d, 0x00db, 0x00fb, 0x0174,
+ 0x0175, 0x0176, 0x0177, 0x00c4, 0x00e4, 0x00cb, 0x00eb, 0x00cf, 0x00ef,
+ 0x00d6, 0x00f6, 0x00dc, 0x00fc, 0x0178, 0x00ff, 0x010a, 0x010b, 0x0116,
+ 0x0117, 0x0120, 0x0121, 0x0130, 0x0131, 0x017b, 0x017c, 0x0150, 0x0151,
+ 0x0170, 0x0171, 0x00c0, 0x00e0, 0x00c8, 0x00e8, 0x00cc, 0x00ec, 0x00d2,
+ 0x00f2, 0x00d9, 0x00f9, 0x01a0, 0x01a1, 0x01af, 0x01b0, 0x0100, 0x0101,
+ 0x0112, 0x0113, 0x012a, 0x012b, 0x014c, 0x014d, 0x016a, 0x016b, 0x0104,
+ 0x0105, 0x0118, 0x0119, 0x012e, 0x012f, 0x0172, 0x0173, 0x00c5, 0x00e5,
+ 0x016e, 0x016f, 0x0110, 0x0111, 0x0126, 0x0127, 0x0141, 0x0142, 0x00d8,
+ 0x00f8, 0x00c3, 0x00e3, 0x00d1, 0x00f1, 0x00d5, 0x00f5, 0x00c6, 0x00e6,
+ 0x0152, 0x0153, 0x00d0, 0x00f0, 0x00de, 0x00fe, 0x00df, 0x017f, 0x0391,
+ 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039a,
+ 0x039b, 0x039c, 0x039d, 0x039e, 0x039f, 0x03a0, 0x03a1, 0x03a3, 0x03a4,
+ 0x03a5, 0x03a6, 0x03a7, 0x03a8, 0x03a9, 0x0386, 0x0388, 0x0389, 0x038a,
+ 0x038c, 0x038e, 0x038f, 0x03aa, 0x03ab, 0x03b1, 0x03b2, 0x03b3, 0x03b4,
+ 0x03b5, 0x03b6, 0x03b7, 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd,
+ 0x03be, 0x03bf, 0x03c0, 0x03c1, 0x03c3, 0x03c2, 0x03c4, 0x03c5, 0x03c6,
+ 0x03c7, 0x03c8, 0x03c9, 0x03ac, 0x03ad, 0x03ae, 0x03af, 0x03cc, 0x03cd,
+ 0x03ce, 0x03ca, 0x03cb, 0x0390, 0x03b0, 0x0410, 0x0411, 0x0412, 0x0413,
+ 0x0414, 0x0415, 0x0401, 0x0416, 0x0417, 0x0406, 0x0419, 0x041a, 0x041b,
+ 0x041c, 0x041d, 0x041e, 0x041f, 0x0420, 0x0421, 0x0422, 0x0423, 0x040e,
+ 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x042b, 0x042c, 0x042d, 0x042e,
+ 0x042f, 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0451, 0x0436,
+ 0x0437, 0x0456, 0x0439, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f,
+ 0x0440, 0x0441, 0x0442, 0x0443, 0x045e, 0x0444, 0x0445, 0x0446, 0x0447,
+ 0x0448, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f, 0x0418, 0x0429, 0x042a,
+ 0x0438, 0x0449, 0x044a, 0x0403, 0x0405, 0x0408, 0x0409, 0x040a, 0x040c,
+ 0x040f, 0x0453, 0x0455, 0x0458, 0x0459, 0x045a, 0x045c, 0x045f, 0x0402,
+ 0x040b, 0x0452, 0x045b, 0x0490, 0x0404, 0x0407, 0x0491, 0x0454, 0x0457,
+ 0x04e8, 0x04ae, 0x04e9, 0x04af,
+);
+
+function utf8_keepalphanum($string)
+{
+
+ // a-z A-Z . _ -, extended latin chars, Cyrillic and Greek
+ static $UTF8_ALPHA_CHARS = array(
+ 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c,
+ 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
+ 0x59, 0x5a, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,
+ 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76,
+ 0x77, 0x78, 0x79, 0x7a, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
+ 0x38, 0x39, 0x2e, 0x2d, 0x5f, 0x20, 0x00c1, 0x00e1, 0x0106, 0x0107,
+ 0x00c9, 0x00e9, 0x00cd, 0x00ed, 0x0139, 0x013a, 0x0143, 0x0144, 0x00d3,
+ 0x00f3, 0x0154, 0x0155, 0x015a, 0x015b, 0x00da, 0x00fa, 0x00dd, 0x00fd,
+ 0x0179, 0x017a, 0x010f, 0x013d, 0x013e, 0x0165, 0x0102, 0x0103, 0x011e,
+ 0x011f, 0x016c, 0x016d, 0x010c, 0x010d, 0x010e, 0x011a, 0x011b, 0x0147,
+ 0x0148, 0x0158, 0x0159, 0x0160, 0x0161, 0x0164, 0x017d, 0x017e, 0x00c7,
+ 0x00e7, 0x0122, 0x0123, 0x0136, 0x0137, 0x013b, 0x013c, 0x0145, 0x0146,
+ 0x0156, 0x0157, 0x015e, 0x015f, 0x0162, 0x0163, 0x00c2, 0x00e2, 0x0108,
+ 0x0109, 0x00ca, 0x00ea, 0x011c, 0x011d, 0x0124, 0x0125, 0x00ce, 0x00ee,
+ 0x0134, 0x0135, 0x00d4, 0x00f4, 0x015c, 0x015d, 0x00db, 0x00fb, 0x0174,
+ 0x0175, 0x0176, 0x0177, 0x00c4, 0x00e4, 0x00cb, 0x00eb, 0x00cf, 0x00ef,
+ 0x00d6, 0x00f6, 0x00dc, 0x00fc, 0x0178, 0x00ff, 0x010a, 0x010b, 0x0116,
+ 0x0117, 0x0120, 0x0121, 0x0130, 0x0131, 0x017b, 0x017c, 0x0150, 0x0151,
+ 0x0170, 0x0171, 0x00c0, 0x00e0, 0x00c8, 0x00e8, 0x00cc, 0x00ec, 0x00d2,
+ 0x00f2, 0x00d9, 0x00f9, 0x01a0, 0x01a1, 0x01af, 0x01b0, 0x0100, 0x0101,
+ 0x0112, 0x0113, 0x012a, 0x012b, 0x014c, 0x014d, 0x016a, 0x016b, 0x0104,
+ 0x0105, 0x0118, 0x0119, 0x012e, 0x012f, 0x0172, 0x0173, 0x00c5, 0x00e5,
+ 0x016e, 0x016f, 0x0110, 0x0111, 0x0126, 0x0127, 0x0141, 0x0142, 0x00d8,
+ 0x00f8, 0x00c3, 0x00e3, 0x00d1, 0x00f1, 0x00d5, 0x00f5, 0x00c6, 0x00e6,
+ 0x0152, 0x0153, 0x00d0, 0x00f0, 0x00de, 0x00fe, 0x00df, 0x017f, 0x0391,
+ 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039a,
+ 0x039b, 0x039c, 0x039d, 0x039e, 0x039f, 0x03a0, 0x03a1, 0x03a3, 0x03a4,
+ 0x03a5, 0x03a6, 0x03a7, 0x03a8, 0x03a9, 0x0386, 0x0388, 0x0389, 0x038a,
+ 0x038c, 0x038e, 0x038f, 0x03aa, 0x03ab, 0x03b1, 0x03b2, 0x03b3, 0x03b4,
+ 0x03b5, 0x03b6, 0x03b7, 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd,
+ 0x03be, 0x03bf, 0x03c0, 0x03c1, 0x03c3, 0x03c2, 0x03c4, 0x03c5, 0x03c6,
+ 0x03c7, 0x03c8, 0x03c9, 0x03ac, 0x03ad, 0x03ae, 0x03af, 0x03cc, 0x03cd,
+ 0x03ce, 0x03ca, 0x03cb, 0x0390, 0x03b0, 0x0410, 0x0411, 0x0412, 0x0413,
+ 0x0414, 0x0415, 0x0401, 0x0416, 0x0417, 0x0406, 0x0419, 0x041a, 0x041b,
+ 0x041c, 0x041d, 0x041e, 0x041f, 0x0420, 0x0421, 0x0422, 0x0423, 0x040e,
+ 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x042b, 0x042c, 0x042d, 0x042e,
+ 0x042f, 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0451, 0x0436,
+ 0x0437, 0x0456, 0x0439, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f,
+ 0x0440, 0x0441, 0x0442, 0x0443, 0x045e, 0x0444, 0x0445, 0x0446, 0x0447,
+ 0x0448, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f, 0x0418, 0x0429, 0x042a,
+ 0x0438, 0x0449, 0x044a, 0x0403, 0x0405, 0x0408, 0x0409, 0x040a, 0x040c,
+ 0x040f, 0x0453, 0x0455, 0x0458, 0x0459, 0x045a, 0x045c, 0x045f, 0x0402,
+ 0x040b, 0x0452, 0x045b, 0x0490, 0x0404, 0x0407, 0x0491, 0x0454, 0x0457,
+ 0x04e8, 0x04ae, 0x04e9, 0x04af,
+ );
+ $chars = utf8_to_unicode($string);
+
+ for ($i = 0, $size = count($chars); $i < $size; ++$i)
+ {
+ if (!in_array($chars[$i], $UTF8_ALPHA_CHARS))
+ {
+ unset($chars[$i]);
+ }
+ }
+ return unicode_to_utf8($chars);
+}