From 8df3db566a3a937b45ebf11adb90d265e6f5e2d4 Mon Sep 17 00:00:00 2001 From: Andreas Baumann Date: Sun, 17 Nov 2019 20:45:02 +0100 Subject: initial checking of customized version 1.0rc9 --- includes/.htaccess | 1 + includes/GithubProvider.php | 60 + includes/class.backend.php | 1869 ++++++++++++++++++++++++++ includes/class.csp.php | 106 ++ includes/class.database.php | 434 ++++++ includes/class.effort.php | 302 +++++ includes/class.flyspray.php | 1471 ++++++++++++++++++++ includes/class.gpc.php | 257 ++++ includes/class.jabber2.php | 943 +++++++++++++ includes/class.notify.php | 1114 +++++++++++++++ includes/class.project.php | 474 +++++++ includes/class.recaptcha.php | 33 + includes/class.tpl.php | 1525 +++++++++++++++++++++ includes/class.user.php | 555 ++++++++ includes/constants.inc.php | 96 ++ includes/events.inc.php | 308 +++++ includes/fix.inc.php | 205 +++ includes/i18n.inc.php | 166 +++ includes/modify.inc.php | 3053 ++++++++++++++++++++++++++++++++++++++++++ includes/password_compat.php | 319 +++++ includes/utf8.inc.php | 118 ++ 21 files changed, 13409 insertions(+) create mode 100644 includes/.htaccess create mode 100644 includes/GithubProvider.php create mode 100644 includes/class.backend.php create mode 100644 includes/class.csp.php create mode 100644 includes/class.database.php create mode 100644 includes/class.effort.php create mode 100644 includes/class.flyspray.php create mode 100644 includes/class.gpc.php create mode 100644 includes/class.jabber2.php create mode 100644 includes/class.notify.php create mode 100644 includes/class.project.php create mode 100644 includes/class.recaptcha.php create mode 100644 includes/class.tpl.php create mode 100644 includes/class.user.php create mode 100644 includes/constants.inc.php create mode 100644 includes/events.inc.php create mode 100644 includes/fix.inc.php create mode 100644 includes/i18n.inc.php create mode 100644 includes/modify.inc.php create mode 100644 includes/password_compat.php create mode 100644 includes/utf8.inc.php (limited to 'includes') 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 @@ +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 @@ +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

" + 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 '
' . print_r($visible, true) . '
'; + // echo '
' . print_r($args, true) . '
'; + // 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 '
' . print_r($args, true) . '
'; + // echo '
' . print_r($cgroupbyarr, true) . '
'; + $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 '
'.print_r($sql_params, true).'
'; # for debugging +// echo '
'.$sqlcount.'
'; # for debugging +// echo '
'.$sqltext.'
'; # 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 "
$offset : $perpage : $totalcount : $forbidden_tasks_count
"; + 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 @@ +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= ''; + # 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.= ''; + # for Chrome 15-24, Safari 5.1-6, .. + $out.= ''; + 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 @@ +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 "
";
+                var_dump(debug_backtrace());
+                echo "
"; + } + + $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 @@ +_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 @@ + 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: %s.', 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 + * @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 @@ + + * @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 @@ +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("\n"); + $this->send("\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(" + + "); + 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(" + + + + "); + 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 = ''. $type .''; + } else { + $type = ''; + } + + $unavailable = ($unavailable) ? " type='unavailable'" : ''; + $message = ($message) ? '' . Jabber::jspecialchars($message) .'' : ''; + + $this->session['sent_presence'] = !$unavailable; + + return $this->send("" . + $type . + $message . + ''); + } + + /** + * 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(" + + " . Jabber::jspecialchars($this->resource) . " + + "); + 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("\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(""); + // 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("" + . base64_encode(chr(0) . $this->user . '@' . $this->server . chr(0) . $this->password) . + ""); + } else if (in_array('ANONYMOUS', $methods)) { + $this->send(""); + // 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(""); + } 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("" . + base64_encode(Jabber::implode_data($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("\n"); + $this->send("\n"); + + return $this->response($this->listen()); + + case 'success': + // Yay, authentication successful. + $this->send("\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(" + + "); + return $this->response($this->listen()); + } + return true; + + case 'sess_1': + return true; + + case 'reg_1': + $this->send(" + + " . Jabber::jspecialchars($this->user) . " + " . Jabber::jspecialchars($this->password) . " + + "); + 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(" + " . Jabber::jspecialchars($subject) . " + " . Jabber::jspecialchars($text) . " + "); + } + + 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(''); + $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 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) != ''. $data . ''; // 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) != '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 "
Error 3!
"; + $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(""); + $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("/(?$2', $body); + $htmlbody = str_replace("\n","
", $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('', $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 "
";
+            echo var_dump($proj_emails);
+            echo var_dump($proj_jids);
+            echo "
"; + */ + // 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 @@ +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 @@ + $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 @@ +_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 '
'. + ( $method=='post' ? '':''); +} + +/** + * 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('%s',$url, $title_text, join_attrs($attrs), $text); + + if ($task['is_closed']) { + $link = ' ' . $link . ' '; + } + 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('%s', 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 ''; + } + + 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 = ''; + } 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 = ''; + } else { + $image = ''; + } + } + 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] = ''.$image.''; + } + } + 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=''; + } + + $out.=''; + 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 '; + 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).'$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).''; + } else{ + # we have a simple option + $html.="\n".str_repeat("\t",$level).''; + } + } + + 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('', $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('', $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(''; +} + +/** + * Image display + */ +function tpl_img($src, $alt = '') +{ + global $baseurl; + if (is_file(BASEDIR .'/'.$src)) { + return sprintf('%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, '

    1. '); + if ( $conf['general']['syntax_plugin'] + && $conf['general']['syntax_plugin'] != 'none' + && $conf['general']['syntax_plugin'] != 'html') { + $text='Unsupported output plugin '.$conf['general']['syntax_plugin'].'!' + .'
      Couldn\'t call '.$conf['general']['syntax_plugin'].'_TextFormatter::render()' + .'
      Temporarily handled like it is HTML until fixed.
      ' + .$text; + } + + //TODO: Remove Redundant Code once tested completely + //Author: Steve Tredinnick + //Have removed this as creating additional
      lines even though

      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('