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;
}
}