diff --git a/.gitignore b/.gitignore index 92e2728c3..d81178e11 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ Thumbs.db *.DS_Store *~ .idea/ +api/ diff --git a/.htaccess b/.htaccess index 189e6b4b8..3b6168c75 100644 --- a/.htaccess +++ b/.htaccess @@ -2,7 +2,7 @@ AddDefaultCharset utf-8 # Restrict access to Time Tracker only from certain IPs. # -# See https://www.anuko.com/time_tracker/faq/restrict_access_by_ip.htm for help. +# See https://www.anuko.com/time-tracker/faq/restrict-access-by-ip.htm for help. # For this to work make sure AllowOverride is set to All in web server config file. # Uncomment 3 lines below and set your IP accordingly. # diff --git a/2fa.php b/2fa.php new file mode 100644 index 000000000..a7e3dbb29 --- /dev/null +++ b/2fa.php @@ -0,0 +1,64 @@ +getParameter('login'); +if ($cl_login == null && $request->isGet()) $cl_login = @$_COOKIE[LOGIN_COOKIE_NAME]; +$cl_password = $request->getParameter('password'); +$cl_auth_code = $request->getParameter('auth_code'); + +$form = new Form('twoFactorAuthForm'); +$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'login','value'=>$cl_login)); +$form->getElement('login')->setEnabled(false); +$form->addInput(array('type'=>'password','maxlength'=>'50','name'=>'password','value'=>$cl_password)); +$form->addInput(array('type'=>'text','maxlength'=>'100','name'=>'auth_code','value'=>$cl_auth_code)); +$form->addInput(array('type'=>'submit','name'=>'btn_login','value'=>$i18n->get('button.login'))); + +if ($request->isPost()) { + // Validate user input. + if (!ttValidString($cl_login)) $err->add($i18n->get('error.field'), $i18n->get('label.login')); + if (!ttValidString($cl_password)) $err->add($i18n->get('error.field'), $i18n->get('label.password')); + if (!ttValidString($cl_auth_code)) $err->add($i18n->get('error.field'), $i18n->get('form.2fa.2fa_code')); + + if ($err->no()) { + // Get user id. + $user_id = ttUserHelper::getUserIdByTmpRef($cl_auth_code); + if (!$user_id) + $err->add($i18n->get('error.2fa_code')); + + if ($err->no()) { + // Additionally check user password for better protection + // against brute force attacks guessing 2FA codes. + $user = new ttUser(null, $user_id); // Note: reusing $user from initialize.php. + // Check user password. + if (!$auth->doLogin($user->login, $cl_password)) + $err->add($i18n->get('error.auth')); + } + + if ($err->no()) { + // Redirect, depending on user role. + if ($user->isClient()) { + header('Location: reports.php'); + } else { + header('Location: time.php'); + } + exit(); + } + } +} // isPost + +$smarty->assign('forms', array($form->getName()=>$form->toArray())); +$smarty->assign('onload', 'onLoad="document.twoFactorAuthForm.auth_code.focus()"'); +$smarty->assign('title', $i18n->get('title.2fa')); +$smarty->assign('content_page_name', '2fa.tpl'); +$smarty->display('index.tpl'); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7311238a..58c971801 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Resources -* [docs](https://www.anuko.com/time_tracker/features.htm) - detailed documentation about this project (needs updating). +* [docs](https://www.anuko.com/time-tracker/features.htm) - detailed documentation about this project (needs updating). * [forum](https://www.anuko.com/forum/viewforum.php?f=4) - general discussion. @@ -29,4 +29,4 @@ usr: admin psw: secret ``` -Without docker, perform a manual install of a web server, php, database server, and Time Tracker. Full installation and setup guide can be found [here](https://www.anuko.com/time_tracker/install_guide/index.htm). +Without docker, perform a manual install of a web server, php, database server, and Time Tracker. Full installation and setup guide can be found [here](https://www.anuko.com/time-tracker/install-guide/index.htm). diff --git a/README.md b/README.md index d6a1d2d34..022c0c030 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ # Anuko Time Tracker ## About -Anuko [Time Tracker](https://www.anuko.com/time_tracker/index.htm) is a simple, easy to use, open source, web-based time tracking application written in PHP. It allows you to track the time that employees or colleagues spend working on projects and tasks. It needs a web server such as Apache, IIS, etc. to run on, and a database to keep the data in, such as MySQL. +Anuko [Time Tracker](https://www.anuko.com/time-tracker/index.htm) is an open source, web-based time tracking application written in PHP. It allows you to track the time that employees or colleagues spend working on projects and tasks. It needs a web server such as Apache, IIS, etc. to run on, and a database to keep the data in, such as MySQL. + +## Terminal Illness of the Owner +December 28, 2023: Nik Okuntseff, the owner and lead software developer at Anuko, developed a terminal illness. Anuko, the company behind Time Tracker, will cease to exist at soon. ## Free Hosting -[Anuko](https://www.anuko.com) provides [free hosting](https://www.anuko.com/time_tracker/free_hosting/index.htm) of Time Tracker to individuals and small groups up to 5 users. To start using Time Tracker immediately, create a group at https://timetracker.anuko.com +[Anuko](https://www.anuko.com) provides [free hosting](https://www.anuko.com/time-tracker/free-hosting/index.htm) of Time Tracker to individuals and small groups up to 5 users. To start using Time Tracker immediately, create a group at https://timetracker.anuko.com ## Resources -* Project home page: https://www.anuko.com/time_tracker/index.htm +* Project home page: https://www.anuko.com/time-tracker/index.htm * Forum: https://www.anuko.com/forum/viewforum.php?f=4 -* Info for developers: https://www.anuko.com/time_tracker/info_for_developers.htm -* How to contribute: https://www.anuko.com/time_tracker/contribute.htm +* Info for developers: https://www.anuko.com/time-tracker/info-for-developers.htm +* How to contribute: https://www.anuko.com/time-tracker/contribute.htm diff --git a/WEB-INF/config.php.dist b/WEB-INF/config.php.dist index a288ec0bf..6b6a68d84 100644 --- a/WEB-INF/config.php.dist +++ b/WEB-INF/config.php.dist @@ -1,30 +1,6 @@ '); +define('SENDER', 'Anuko Time Tracker '); // MAIL_MODE - mail sending mode. Can be 'mail' or 'smtp'. // 'mail' - sending through php mail() function. // 'smtp' - sending directly through SMTP server. -// See https://www.anuko.com/time_tracker/install_guide/mail.htm +// See https://www.anuko.com/time-tracker/install-guide/mail.htm // define('MAIL_MODE', 'smtp'); define('MAIL_SMTP_HOST', 'localhost'); // For gmail use 'ssl://smtp.gmail.com' instead of 'localhost' and port 465. @@ -142,7 +133,7 @@ define('REPORT_FOOTER', true); define('AUTH_MODULE', 'db'); // LDAP authentication examples. -// Go to https://www.anuko.com/time_tracker/install_guide/ldap_auth/index.htm for detailed configuration instructions. +// Go to https://www.anuko.com/time-tracker/install-guide/ldap-auth/index.htm for detailed configuration instructions. // Configuration example for OpenLDAP server: // define('AUTH_MODULE', 'ldap'); @@ -151,8 +142,10 @@ define('AUTH_MODULE', 'db'); // 'type' => 'openldap', // Type of server. openldap type should also work with Sun Directory Server when member_of is empty. // It may work with other (non Windows AD) LDAP servers. For Windows AD use the 'ad' type. // 'base_dn' => 'ou=People,dc=example,dc=com', // Path of user's base distinguished name in LDAP catalog. -// 'user_login_attribute' => 'uid', // LDAP attribute used for login +// 'user_login_attribute' => 'uid', // LDAP attribute used for login. // 'default_domain' => 'example.com', // Default domain. +// 'tls_cacertdir' => null, // Path to a directory containing CA certificates for secure ldap. +// 'tls_cacertfile' => null, // CA certificate file name for secure ldap. // 'member_of' => array()); // List of groups, membership in which is required for user to be authenticated. @@ -163,6 +156,8 @@ define('AUTH_MODULE', 'db'); // 'type' => 'ad', // Type of server. // 'base_dn' => 'DC=example,DC=com', // Base distinguished name in LDAP catalog. // 'default_domain' => 'example.com', // Default domain. +// 'tls_cacertdir' => null, // Path to a directory containing CA certificates for secure ldap. +// 'tls_cacertfile' => null, // CA certificate file name for secure ldap. // 'member_of' => array()); // List of groups, membership in which is required for user to be authenticated. // Leave it empty if membership is not necessary. Otherwise list CN parts only. // For example: @@ -172,12 +167,15 @@ define('AUTH_MODULE', 'db'); // define('DEBUG', false); // Note: enabling DEBUG breaks redirects as debug output is printed before setting redirect header. Do not enable on production systems. -// Team managers can set monthly work hour quota for years between the following values. +// HTTP_TARGET - defines http target for cross site request forgery protection. +// It can be used when you access the application via a proxy. +// define('HTTP_TARGET', 'localhost:8080'); + + +// Group managers can set monthly work hour quota for years between the following values. // define('MONTHLY_QUOTA_YEAR_START', 2010); // If nothing is specified, it falls back to 2015. // define('MONTHLY_QUOTA_YEAR_END', 2025); // If nothing is specified, it falls back to 2030. -// Height in pixels for the note input field in time.php. Defaults to 40. -// define('NOTE_INPUT_HEIGHT', 100); // A comma-separated list of default plugins for new group registrations. // Example below enables charts and attachments. diff --git a/WEB-INF/lib/Auth.class.php b/WEB-INF/lib/Auth.class.php index ecb48340b..783721090 100644 --- a/WEB-INF/lib/Auth.class.php +++ b/WEB-INF/lib/Auth.class.php @@ -23,7 +23,7 @@ // | // +----------------------------------------------------------------------+ // | Contributors: -// | https://www.anuko.com/time_tracker/credits.htm +// | https://www.anuko.com/time-tracker/credits.htm // +----------------------------------------------------------------------+ class Auth { @@ -32,7 +32,7 @@ class Auth { function isAuthenticated() { if (isset($_SESSION['authenticated'])) { // This check does not work properly because we are not getting here. Need to improve. -// if (!isset($_COOKIE['tt_login'])) { +// if (!isset($_COOKIE[LOGIN_COOKIE_NAME])) { // die ("Your browser's cookie functionality is turned off. Please turn it on."); // } diff --git a/WEB-INF/lib/DateAndTime.class.php b/WEB-INF/lib/DateAndTime.class.php deleted file mode 100644 index 20f274265..000000000 --- a/WEB-INF/lib/DateAndTime.class.php +++ /dev/null @@ -1,347 +0,0 @@ -$sDate parsed, or false on error. - */ - -function my_strptime($sDate, $sFormat) -{ - $aResult = array - ( - 'tm_sec' => 0, - 'tm_min' => 0, - 'tm_hour' => 0, - 'tm_mday' => 1, - 'tm_mon' => 0, - 'tm_year' => 0, - 'tm_wday' => 0, - 'tm_yday' => 0, - 'unparsed' => $sDate, - ); - - while($sFormat != "") - { - // ===== Search a %x element, Check the static string before the %x ===== - $nIdxFound = strpos($sFormat, '%'); - if($nIdxFound === false) - { - - // There is no more format. Check the last static string. - $aResult['unparsed'] = ($sFormat == $sDate) ? "" : $sDate; - break; - } - - $sFormatBefore = mb_substr($sFormat, 0, $nIdxFound); - $sDateBefore = mb_substr($sDate, 0, $nIdxFound); - - if($sFormatBefore != $sDateBefore) break; - - // ===== Read the value of the %x found ===== - $sFormat = mb_substr($sFormat, $nIdxFound); - $sDate = mb_substr($sDate, $nIdxFound); - - $aResult['unparsed'] = $sDate; - - $sFormatCurrent = mb_substr($sFormat, 0, 2); - $sFormatAfter = mb_substr($sFormat, 2); - - $nValue = -1; - $sDateAfter = ""; - switch($sFormatCurrent) - { - case '%S': // Seconds after the minute (0-59) - - sscanf($sDate, "%2d%[^\\n]", $nValue, $sDateAfter); - - if(($nValue < 0) || ($nValue > 59)) return false; - - $aResult['tm_sec'] = $nValue; - break; - - // ---------- - case '%M': // Minutes after the hour (0-59) - sscanf($sDate, "%2d%[^\\n]", $nValue, $sDateAfter); - - if(($nValue < 0) || ($nValue > 59)) return false; - - $aResult['tm_min'] = $nValue; - break; - - // ---------- - case '%H': // Hour since midnight (0-23) - sscanf($sDate, "%2d%[^\\n]", $nValue, $sDateAfter); - - if(($nValue < 0) || ($nValue > 23)) return false; - - $aResult['tm_hour'] = $nValue; - break; - - // ---------- - case '%d': // Day of the month (1-31) - sscanf($sDate, "%2d%[^\\n]", $nValue, $sDateAfter); - - if(($nValue < 1) || ($nValue > 31)) return false; - - $aResult['tm_mday'] = $nValue; - break; - - // ---------- - case '%m': // Months since January (0-11) - sscanf($sDate, "%2d%[^\\n]", $nValue, $sDateAfter); - - if(($nValue < 1) || ($nValue > 12)) return false; - - $aResult['tm_mon'] = ($nValue - 1); - break; - - // ---------- - case '%Y': // Years since 1900 - sscanf($sDate, "%4d%[^\\n]", $nValue, $sDateAfter); - - if($nValue < 1900) return false; - - $aResult['tm_year'] = ($nValue - 1900); - break; - - // ---------- - default: - //sscanf($sDate, "%s%[^\\n]", $skip, $sDateAfter); - preg_match('/^(.+)(\s|$)/uU', $sDate, $matches); - if (isset($matches[1])) { - $sDateAfter = mb_substr($sDate, mb_strlen($matches[1])); - } else { - $sDateAfter = ''; - } - //break 2; // Break Switch and while - break; - } - - // ===== Next please ===== - $sFormat = $sFormatAfter; - $sDate = $sDateAfter; - - $aResult['unparsed'] = $sDate; - - } // END while($sFormat != "") - - - // ===== Create the other value of the result array ===== - $nParsedDateTimestamp = mktime($aResult['tm_hour'], $aResult['tm_min'], $aResult['tm_sec'], - $aResult['tm_mon'] + 1, $aResult['tm_mday'], $aResult['tm_year'] + 1900); - - // Before PHP 5.1 return -1 when error - if(($nParsedDateTimestamp === false) - ||($nParsedDateTimestamp === -1)) return false; - - $aResult['tm_wday'] = (int) strftime("%w", $nParsedDateTimestamp); // Days since Sunday (0-6) - $aResult['tm_yday'] = (strftime("%j", $nParsedDateTimestamp) - 1); // Days since January 1 (0-365) - - return $aResult; -} // END of function - -class DateAndTime { - var $mHour = 0; - var $mMinute = 0; - var $mSecond = 0; - var $mMonth; - var $mDay; // day of week - var $mDate; // day of month - var $mYear; - var $mIntrFormat = "%d.%m.%Y %H:%M:%S"; //29.02.2004 16:21:42 internal format date - var $mLocalFormat; - var $mParseResult = 0; - var $mAutoComplete = true; - - /** - * Constructor - * - * @param String $format - * @param String $strfDateTime - * @return DateAndTime - */ - function __construct($format="",$strfDateTime="") { - $this->mLocalFormat = ($format ? $format : $this->mIntrFormat); - $d = ($strfDateTime ? $strfDateTime : $this->do_strftime($this->mLocalFormat)); - $this->parseVal($d); - } - - function setFormat($format) { - $this->mLocalFormat = $format; - } - - function getFormat() { - return $this->mLocalFormat; - } - - //01 to 31 - function getDate() { return $this->mDate; } - - //0 (for Sunday) through 6 (for Saturday) - function getDay() { return $this->mDay; } - - //01 through 12 - function getMonth() { return $this->mMonth; } - - //1999 or 2003 - function getYear() { return $this->mYear; } - - function setDate($value) { $this->mDate = $value; } - function setMonth($value) { $this->mMonth = $value; } - function setYear($value) { $this->mYear = $value; } - - function setTimestamp($ts) { - $this->mDate = date("d",$ts); - $this->mDay = date("w",$ts); - $this->mMonth = date("m",$ts); - $this->mYear = date("Y",$ts); - $this->mHour = date("H",$ts); - $this->mMinute = date("i",$ts); - $this->mSecond = date("s",$ts); - } - - /** - * Return UNIX timestamp - */ - function getTimestamp() { - return @mktime($this->mHour, $this->mMinute, $this->mSecond, $this->mMonth, $this->mDate, $this->mYear); - } - - function compare($datetime) { - $ts1 = $this->getTimestamp(); - $ts2 = $datetime->getTimestamp(); - if ($ts1<$ts2) return -1; - if ($ts1==$ts2) return 0; - if ($ts1>$ts2) return 1; - } - - function toString($format="") { - if ($this->mParseResult==0) { - if ($format) { - return $this->do_strftime($format, $this->getTimestamp()); - } else { - return $this->do_strftime($this->mLocalFormat, $this->getTimestamp()); - } - } else { - if ($format) { - return $this->do_strftime($format); - } else { - return $this->do_strftime($this->mLocalFormat); - } - } - } - - function parseVal($szDate, $format="") { - $useformat = ($format ? $format : $this->mLocalFormat); - $res = my_strptime($szDate, $useformat); - if ($res !== false) { - $this->mDate = $res['tm_mday']; - $this->mDay = $res['tm_wday']; - $this->mMonth = $res['tm_mon'] + 1; // tm_mon - Months since January (0-11) - $this->mYear = 1900 + $res['tm_year']; // tm_year - Years since 1900 - $this->mHour = $res['tm_hour']; - $this->mMinute = $res['tm_min']; - $this->mSecond = $res['tm_sec']; - $this->mParseResult = 0; - } elseif ($this->mAutoComplete) { - $this->setTimestamp(time()); - $this->mParseResult = 1; - } - } - - function isError() { - if ($this->mParseResult != 0) return true; - return false; - } - - function before(/*DateAndTime*/ $obj) { - if ($this->getTimestamp() < $obj->getTimestamp()) return true; - return false; - } - - function after(/*DateAndTime*/ $obj) { - if ($this->getTimestamp() > $obj->getTimestamp()) return true; - return false; - } - - function equals(/*DateAndTime*/ $obj) { - if ($this->getTimestamp() == $obj->getTimestamp()) return true; - return false; - } - - function decDay(/*int*/$days=1) { - $this->setTimestamp(@mktime($this->mHour, $this->mMinute, $this->mSecond, $this->mMonth, $this->mDate - $days, $this->mYear)); - } - - function incDay(/*int*/$days=1) { - $this->setTimestamp(@mktime($this->mHour, $this->mMinute, $this->mSecond, $this->mMonth, $this->mDate + $days, $this->mYear)); - } - - /** - * @param $format string Datetime format string - * @return string Preprocessed string with all locale-depended format - * characters replaced by localized i18n strings. - */ - function preprocessFormatString($format) { - global $i18n; - - // replace locale-dependent strings - $format = str_replace('%a', mb_substr($i18n->getWeekDayName($this->mDay), 0, 3, 'utf-8'), $format); - $format = str_replace('%A', $i18n->getWeekDayName($this->mDay), $format); - $abbrev_month = mb_substr($i18n->monthNames[$this->mMonth], 0, 3, 'utf-8'); - $format = str_replace('%b', $abbrev_month, $format); - $format = str_replace('%h', $abbrev_month, $format); - $format = str_replace('%z', date('O'), $format); - $format = str_replace('%Z', date('O'), $format); // format as 'O' for consistency with JS strftime - if (strpos($format, '%c') !== false) { - $format = str_replace('%c', $this->preprocessFormatString('%a %d %b %Y %T %Z'), $format); - } - return $format; - } - - function do_strftime($format, $timestamp = null) - { - if (!is_null($timestamp)) { - return strftime($this->preprocessFormatString($format), $timestamp); - } else { - return strftime($this->preprocessFormatString($format)); - } - } -} diff --git a/WEB-INF/lib/I18n.class.php b/WEB-INF/lib/I18n.class.php index 7eb6228ff..bd4e9ffd3 100644 --- a/WEB-INF/lib/I18n.class.php +++ b/WEB-INF/lib/I18n.class.php @@ -23,7 +23,7 @@ // | // +----------------------------------------------------------------------+ // | Contributors: -// | https://www.anuko.com/time_tracker/credits.htm +// | https://www.anuko.com/time-tracker/credits.htm // +----------------------------------------------------------------------+ class I18n { @@ -32,7 +32,6 @@ class I18n { var $monthNames; var $weekdayNames; var $weekdayShortNames; - var $holidays; var $keys = array(); // These are our localized strings. // get - obtains a localized value from $keys array. @@ -52,24 +51,25 @@ function get($key) { return $value; } - // TODO: refactoring ongoing down from here... - function getWeekDayName($id) { - $id = (int) $id; - return $this->weekdayNames[$id]; - } - - function load($localName) { - $kw = array(); - $filename = strtolower($localName) . '.lang.php'; - $inc_filename = RESOURCE_DIR . '/' . $this->defaultLang . '.lang.php'; + // get - keyExists determines if a key exists. + function keyExists($key) { + $value = $this->get($key); + return ($value !== null); + } - if (file_exists($inc_filename)) { - include($inc_filename); + // load - loads localized strings into $keys array by first going through the default file (en.lang.php) + // and then through the requested language file (which is supplied as parameter), + // (this means we end up with default English strings when keys are missing in the translation file), + // and then from a group custom translation field, if available. + function load($langName) { + // Load default English keys first. + $defaultFileName = RESOURCE_DIR . '/' . $this->defaultLang . '.lang.php'; + if (file_exists($defaultFileName)) { + include($defaultFileName); $this->monthNames = $i18n_months; $this->weekdayNames = $i18n_weekdays; - - $this->weekdayShortNames = $i18n_weekdays_short; + $this->weekdayShortNames = $i18n_weekdays_short; foreach ($i18n_key_words as $kword=>$value) { $pos = strpos($kword, "."); @@ -86,15 +86,17 @@ function load($localName) { } } - $inc_filename = RESOURCE_DIR . '/' . $filename; - if (file_exists($inc_filename) && ($localName != $this->defaultLang)) { - require($inc_filename); + // Now load the keys from the requested file. + // This overwrites already loaded English strings. + $requestedFileName = strtolower($langName) . '.lang.php'; + $requestedFileName = RESOURCE_DIR . '/' . $requestedFileName; + if (file_exists($requestedFileName) && ($langName != $this->defaultLang)) { + require($requestedFileName); - $this->lang = $localName; + $this->lang = $langName; $this->monthNames = $i18n_months; $this->weekdayNames = $i18n_weekdays; - $this->weekdayShortNames = $i18n_weekdays_short; - $this->holidays = $i18n_holidays; + $this->weekdayShortNames = $i18n_weekdays_short; foreach ($i18n_key_words as $kword=>$value) { if (!$value) continue; $pos = strpos($kword, "."); @@ -109,10 +111,40 @@ function load($localName) { $this->keys[$kword] = $value; } } - return true; + } + + // Now load custom translation for group. + global $user; + $customTranslation = $user->getCustomTranslation(); + if ($customTranslation != null) { + $lines = preg_split("/\r\n|\n|\r/", $customTranslation); + for ($i = 0; $i < count($lines); $i++) { + $parts = explode('=', $lines[$i]); + if (count($parts) != 2) continue; + + $key = trim($parts[0]); + $value = trim($parts[1]); + // Escape single quotes and backslashes. + $value = addcslashes($value, "'\\"); + $value = htmlspecialchars($value); + + $pos = strpos($key, "."); + if (!($pos === false)) { + $p = explode(".", $key); + $str = ""; + foreach ($p as $w) { + $str .= "[\"".$w."\"]"; + } + eval("\$this->keys".$str."='".$value."';"); + } else { + $this->keys[$key] = $value; + } + } } } + // hasLang determines if a file for requested language exists. + // This is a helper function for getBrowserLanguage below. function hasLang($lang) { $filename = RESOURCE_DIR . '/' . strtolower($lang) . '.lang.php'; @@ -146,7 +178,7 @@ function getBrowserLanguage() return false; } - // getLangFileList() returns a list of language files. + // getLangFileList() returns a list of available language files. static function getLangFileList() { $fileList = array(); $d = @opendir(RESOURCE_DIR); @@ -161,8 +193,15 @@ static function getLangFileList() { return $fileList; } + // getLangFromFilename returns language designation from a file name such as (ru, pt-br, etc.). static function getLangFromFilename($filename) { return substr($filename, 0, strpos($filename, '.')); } + + // getWeekDayName returns a localized weekday name. + function getWeekDayName($id) { + $id = (int) $id; + return $this->weekdayNames[$id]; + } } diff --git a/WEB-INF/lib/Period.class.php b/WEB-INF/lib/Period.class.php deleted file mode 100644 index 2d2269e52..000000000 --- a/WEB-INF/lib/Period.class.php +++ /dev/null @@ -1,136 +0,0 @@ -getWeekStart(); - - $this->startDate = new DateAndTime(); - $this->startDate->setFormat($date_point->getFormat()); - $this->endDate = new DateAndTime(); - $this->endDate->setFormat($date_point->getFormat()); - $t_arr = localtime($date_point->getTimestamp()); - $t_arr[5] = $t_arr[5] + 1900; - - if ($t_arr[6] < $weekStartDay) { - $startWeekBias = $weekStartDay - 7; - } else { - $startWeekBias = $weekStartDay; - } - - switch ($period_type) { - case INTERVAL_THIS_DAY: - $this->startDate->setTimestamp($date_point->getTimestamp()); - $this->endDate->setTimestamp($date_point->getTimestamp()); - break; - - case INTERVAL_LAST_DAY: - $this->startDate->setTimestamp(mktime(0,0,0,$t_arr[4]+1,$t_arr[3]-1,$t_arr[5])); - $this->endDate->setTimestamp(mktime(0,0,0,$t_arr[4]+1,$t_arr[3]-1,$t_arr[5])); - break; - - case INTERVAL_THIS_WEEK: - $this->startDate->setTimestamp(mktime(0,0,0,$t_arr[4]+1,$t_arr[3]-$t_arr[6]+$startWeekBias,$t_arr[5])); - $this->endDate->setTimestamp(mktime(0,0,0,$t_arr[4]+1,$t_arr[3]-$t_arr[6]+6+$startWeekBias,$t_arr[5])); - break; - case INTERVAL_LAST_WEEK: - $this->startDate->setTimestamp(mktime(0,0,0,$t_arr[4]+1,$t_arr[3]-$t_arr[6]-7+$startWeekBias,$t_arr[5])); - $this->endDate->setTimestamp(mktime(0,0,0,$t_arr[4]+1,$t_arr[3]-$t_arr[6]-1+$startWeekBias,$t_arr[5])); - break; - case INTERVAL_THIS_MONTH: - $this->startDate->setTimestamp(mktime(0,0,0,$t_arr[4]+1,1,$t_arr[5])); - $this->endDate->setTimestamp(mktime(0,0,0,$t_arr[4]+2,0,$t_arr[5])); - break; - case INTERVAL_LAST_MONTH: - $this->startDate->setTimestamp(mktime(0,0,0,$t_arr[4],1,$t_arr[5])); - $this->endDate->setTimestamp(mktime(0,0,0,$t_arr[4]+1,0,$t_arr[5])); - break; - - case INTERVAL_THIS_YEAR: - $this->startDate->setTimestamp(mktime(0, 0, 0, 1, 1, $t_arr[5])); - $this->endDate->setTimestamp(mktime(0, 0, 0, 12, 31, $t_arr[5])); - break; - } - } - - function setPeriod($b_date, $e_date) { - $this->startDate = $b_date; - $this->endDate = $e_date; - } - - // return date string - function getStartDate($format="") { - return $this->startDate->toString($format); - } - - // return date string - function getEndDate($format="") { - return $this->endDate->toString($format); - } -} diff --git a/WEB-INF/lib/auth/Auth_db.class.php b/WEB-INF/lib/auth/Auth_db.class.php index a7ae0d3fa..b5fbdaef9 100644 --- a/WEB-INF/lib/auth/Auth_db.class.php +++ b/WEB-INF/lib/auth/Auth_db.class.php @@ -1,30 +1,6 @@ fetchRow(); - if ($val['id'] > 0) { + if (isset($val['id']) && $val['id'] > 0) { return array('login'=>$login,'id'=>$val['id']); } else { // If the OLD_PASSWORDS option is defined - set it. @@ -77,7 +53,7 @@ function authenticate($login, $password) // support in November 2018, there were login issues with such users. } $val = $res->fetchRow(); - if ($val['id'] > 0) { + if (isset($val['id']) && $val['id'] > 0) { return array('login'=>$login,'id'=>$val['id']); } } @@ -92,7 +68,7 @@ function authenticate($login, $password) die($res->getMessage()); } $val = $res->fetchRow(); - if ($val['id'] > 0) { + if (isset($val['id']) && $val['id'] > 0) { return array('login'=>$val['login'],'id'=>$val['id']); } } diff --git a/WEB-INF/lib/auth/Auth_ldap.class.php b/WEB-INF/lib/auth/Auth_ldap.class.php index 7f3495737..b783276cf 100644 --- a/WEB-INF/lib/auth/Auth_ldap.class.php +++ b/WEB-INF/lib/auth/Auth_ldap.class.php @@ -1,30 +1,6 @@ params['tls_cacertdir'])) { + ldap_set_option(null, LDAP_OPT_X_TLS_CACERTDIR, $this->params['tls_cacertdir']); + } + if (isset($this->params['tls_cacertfile'])) { + ldap_set_option(null, LDAP_OPT_X_TLS_CACERTFILE, $this->params['tls_cacertfile']); + } + // End of addiitional options for secure ldap. // We need to handle Windows AD and OpenLDAP differently. if ($this->params['type'] == 'ad') { diff --git a/WEB-INF/lib/common.lib.php b/WEB-INF/lib/common.lib.php index 1f2829442..79aba106b 100644 --- a/WEB-INF/lib/common.lib.php +++ b/WEB-INF/lib/common.lib.php @@ -1,30 +1,6 @@ ') || stristr($val, '\n"; - - return $str; + $html .= "\n"; } - function getHtml() { - return $this->toString(); - } + // Finished printing calendar table. - function _getWeekDayBefore($year, $month) { - $weekday = date ( "w", mktime ( 2, 0, 0, $month, 1 - $this->weekStartDay, $year ) ); - return array( - mktime ( 0, 0, 0, $month, 1 - $weekday, $year ), - mktime ( 0, 0, 0, $month, 1, $year ), - mktime ( 0, 0, 0, $month + 1, 0, $year ), - (1 - $weekday) - ); - } + // Print Today link. + $html .= " name."=".ttDate::dateFromUnixTimestamp()."\" tabindex=\"-1\">".$i18n->get('label.today')."\n"; + $html .= "\n"; - function _genStyles() { - $str = "\n"; - return $str; - } - - // _getActiveDates returns an array of dates, for which entries exist for user. - // Type of entries (time or expenses) is determined by $this->highlight value. - function _getActiveDates($start, $end) { + // Add a hidden control for selected date. + $html .= "name\" value=\"$selectedDate\">\n"; + $html .= "\n\n"; + return $html; + } + + // getActiveDates returns an array of dates, for which entries exist for user. + // Type of entries (time or expenses) is determined by $this->highlight value. + function getActiveDates($start, $end) { - global $user; - $user_id = $user->getUser(); + global $user; + $user_id = $user->getUser(); - $table = ($this->highlight == 'expenses') ? 'tt_expense_items' : 'tt_log'; + $table = ($this->highlight == 'expenses') ? 'tt_expense_items' : 'tt_log'; - $mdb2 = getConnection(); - - $start_date = date("Y-m-d", $start); - $end_date = date("Y-m-d", $end); - $sql = "SELECT date FROM $table WHERE date >= '$start_date' AND date <= '$end_date' AND user_id = $user_id AND status = 1"; - $res = $mdb2->query($sql); - if (!is_a($res, 'PEAR_Error')) { - while ($row = $res->fetchRow()) { - $out[] = date('Y-m-d', strtotime($row['date'])); - } - return @$out; + $mdb2 = getConnection(); + + $start_date = date("Y-m-d", $start); + $end_date = date("Y-m-d", $end); + $sql = "SELECT date FROM $table WHERE date >= '$start_date' AND date <= '$end_date' AND user_id = $user_id AND status = 1"; + $res = $mdb2->query($sql); + if (!is_a($res, 'PEAR_Error')) { + while ($row = $res->fetchRow()) { + $out[] = date('Y-m-d', strtotime($row['date'])); } - else - return false; + return @$out; } + else + return false; + } } diff --git a/WEB-INF/lib/form/Checkbox.class.php b/WEB-INF/lib/form/Checkbox.class.php index c4acbc5f5..619d39fb3 100644 --- a/WEB-INF/lib/form/Checkbox.class.php +++ b/WEB-INF/lib/form/Checkbox.class.php @@ -23,7 +23,7 @@ // | // +----------------------------------------------------------------------+ // | Contributors: -// | https://www.anuko.com/time_tracker/credits.htm +// | https://www.anuko.com/time-tracker/credits.htm // +----------------------------------------------------------------------+ import('form.FormElement'); diff --git a/WEB-INF/lib/form/CheckboxCellRenderer.class.php b/WEB-INF/lib/form/CheckboxCellRenderer.class.php index d117c12ef..cd9682703 100644 --- a/WEB-INF/lib/form/CheckboxCellRenderer.class.php +++ b/WEB-INF/lib/form/CheckboxCellRenderer.class.php @@ -23,7 +23,7 @@ // | // +----------------------------------------------------------------------+ // | Contributors: -// | https://www.anuko.com/time_tracker/credits.htm +// | https://www.anuko.com/time-tracker/credits.htm // +----------------------------------------------------------------------+ import('form.DefaultCellRenderer'); diff --git a/WEB-INF/lib/form/CheckboxGroup.class.php b/WEB-INF/lib/form/CheckboxGroup.class.php index 9488b5cce..68b24853b 100644 --- a/WEB-INF/lib/form/CheckboxGroup.class.php +++ b/WEB-INF/lib/form/CheckboxGroup.class.php @@ -23,7 +23,7 @@ // | // +----------------------------------------------------------------------+ // | Contributors: -// | https://www.anuko.com/time_tracker/credits.htm +// | https://www.anuko.com/time-tracker/credits.htm // +----------------------------------------------------------------------+ import('form.FormElement'); diff --git a/WEB-INF/lib/form/Combobox.class.php b/WEB-INF/lib/form/Combobox.class.php index 88f0b50a9..d494d0f6d 100644 --- a/WEB-INF/lib/form/Combobox.class.php +++ b/WEB-INF/lib/form/Combobox.class.php @@ -1,30 +1,6 @@ class = 'Combobox'; + $this->css_class = 'dropdown-field'; $this->name = $name; } @@ -68,6 +45,7 @@ function getHtml() { if ($this->id=="") $this->id = $this->name; $html = "\n\tcss_class\""; $html .= " name=\"$this->name\" id=\"$this->id\""; if ($this->size!="") @@ -84,7 +62,7 @@ function getHtml() { if (!$this->isEnabled()) $html .= " disabled"; - + $html .= ">\n"; if (is_array($this->mOptionsEmpty) && (count($this->mOptionsEmpty) > 0)) foreach ($this->mOptionsEmpty as $key=>$value) { diff --git a/WEB-INF/lib/form/DateField.class.php b/WEB-INF/lib/form/DateField.class.php index a197126fb..d349d5687 100644 --- a/WEB-INF/lib/form/DateField.class.php +++ b/WEB-INF/lib/form/DateField.class.php @@ -1,46 +1,20 @@ 'Today', 'close'=>'Close'); function __construct($name) { $this->class = 'DateField'; $this->name = $name; - $this->mDateObj = new DateAndTime(); $this->localize(); } @@ -48,8 +22,6 @@ function localize() { global $user; global $i18n; - $this->mDateObj->setFormat($user->getDateFormat()); - $this->mMonthNames = $i18n->monthNames; $this->mWeekDayShortNames = $i18n->weekdayShortNames; $this->lToday = $i18n->get('label.today'); @@ -63,15 +35,15 @@ function localize() { // set current value taken from session or database function setValueSafe($value) { if (isset($value) && (strlen($value) > 0)) { - $this->mDateObj->parseVal($value, DB_DATEFORMAT); - $this->value = $this->mDateObj->toString($this->mDateFormat); //? + $ttDate = new ttDate($value); + $this->value = $ttDate->toString($this->mDateFormat); } } // get value for storing in session or database function getValueSafe() { if (strlen($this->value)>0) { - $this->mDateObj->parseVal($this->value, $this->mDateFormat); //? - return $this->mDateObj->toString(DB_DATEFORMAT); + $ttDate = new ttDate($this->value, $this->mDateFormat); + return $ttDate->toString(); } else { return null; } @@ -406,10 +378,13 @@ function adjustiFrame(pickerDiv, iFrameDiv) { $html .= " value=\"".htmlspecialchars($this->getValue())."\""; $html .= ">"; - if (APP_NAME) - $app_root = '/'.APP_NAME; + $dir_name = $app_root = ''; + if (defined('DIR_NAME')) + $dir_name = trim(constant('DIR_NAME'), '/'); + if (!empty($dir_name)) + $app_root = '/'.$dir_name; - $html .= " name."');\">\n"; + $html .= " name."');\">\n"; } return $html; diff --git a/WEB-INF/lib/form/DefaultCellRenderer.class.php b/WEB-INF/lib/form/DefaultCellRenderer.class.php index 1e36f79b5..55f4bbca5 100644 --- a/WEB-INF/lib/form/DefaultCellRenderer.class.php +++ b/WEB-INF/lib/form/DefaultCellRenderer.class.php @@ -23,7 +23,7 @@ // | // +----------------------------------------------------------------------+ // | Contributors: -// | https://www.anuko.com/time_tracker/credits.htm +// | https://www.anuko.com/time-tracker/credits.htm // +----------------------------------------------------------------------+ class DefaultCellRenderer { diff --git a/WEB-INF/lib/form/FloatField.class.php b/WEB-INF/lib/form/FloatField.class.php index 5b939754e..e6cbf1068 100644 --- a/WEB-INF/lib/form/FloatField.class.php +++ b/WEB-INF/lib/form/FloatField.class.php @@ -23,7 +23,7 @@ // | // +----------------------------------------------------------------------+ // | Contributors: -// | https://www.anuko.com/time_tracker/credits.htm +// | https://www.anuko.com/time-tracker/credits.htm // +----------------------------------------------------------------------+ import('form.TextField'); diff --git a/WEB-INF/lib/form/Form.class.php b/WEB-INF/lib/form/Form.class.php index 1f77414b1..cee608fe5 100644 --- a/WEB-INF/lib/form/Form.class.php +++ b/WEB-INF/lib/form/Form.class.php @@ -1,30 +1,6 @@ setCssClass($params['class']); if (isset($params['maxlength'])) $el->setMaxLength($params['maxlength']); + if (isset($params['placeholder'])) $el->setPLaceholder($params['placeholder']); break; case 'password': import('form.PasswordField'); $el = new PasswordField($params['name']); + if (isset($params['class'])) $el->setCssClass($params['class']); if (isset($params['maxlength'])) $el->setMaxLength($params['maxlength']); break; @@ -116,6 +95,7 @@ function addInput($params) { $el = new Combobox($params["name"]); $el->setData(@$params["data"]); $el->setDataDefault(@$params["empty"]); + if (isset($params['class'])) $el->setCssClass($params['class']); if (isset($params["multiple"])) { $el->setMultiple($params["multiple"]); $el->name .= '[]'; // Add brackets to the end of name to get back an array on POST. @@ -123,6 +103,16 @@ function addInput($params) { if (isset($params["datakeys"])) $el->setDataKeys($params["datakeys"]); break; + case "multipleselectcombobox": + import('form.MultipleSelectCombobox'); + $multipleSelectComboboxName = $params["name"].'[]'; + $el = new MultipleSelectCombobox($multipleSelectComboboxName); + $el->setData(@$params["data"]); + $el->setDataDefault(@$params["empty"]); + if (isset($params['class'])) $el->setCssClass($params['class']); + if (isset($params["datakeys"])) $el->setDataKeys($params["datakeys"]); + break; + case "calendar": import('form.Calendar'); $el = new Calendar($params["name"]); diff --git a/WEB-INF/lib/form/FormElement.class.php b/WEB-INF/lib/form/FormElement.class.php index 0443e6c9a..16b8d76d1 100644 --- a/WEB-INF/lib/form/FormElement.class.php +++ b/WEB-INF/lib/form/FormElement.class.php @@ -1,54 +1,31 @@ name; } function getClass() { return $this->class; } - function getCssClass() { return $this->cssClass; } - function setCssClass($cssClass) { $this->cssClass = $cssClass; } + function getCssClass() { return $this->css_class; } + function setCssClass($css_class) { $this->css_class = $css_class; } function setFormName($name) { $this->form_name = $name; } function getFormName() { return $this->form_name; } @@ -80,9 +57,10 @@ function getStyle() { return $this->style; } function setEnabled($flag) { $this->enabled = $flag; } function isEnabled() { return $this->enabled; } - + function setOnChange($str) { $this->on_change = $str; } function setOnClick($str) { $this->on_click = $str; } + function setPlaceholder($str) { $this->placeholder = $str; } function localize() {} // Localization occurs in derived classes and is dependent on control type. // For example, in calendar control we need to localize day and month names. diff --git a/WEB-INF/lib/form/Hidden.class.php b/WEB-INF/lib/form/Hidden.class.php index 6e59bc8e1..89e90c7eb 100644 --- a/WEB-INF/lib/form/Hidden.class.php +++ b/WEB-INF/lib/form/Hidden.class.php @@ -23,7 +23,7 @@ // | // +----------------------------------------------------------------------+ // | Contributors: -// | https://www.anuko.com/time_tracker/credits.htm +// | https://www.anuko.com/time-tracker/credits.htm // +----------------------------------------------------------------------+ import('form.FormElement'); diff --git a/WEB-INF/lib/form/MultipleSelectCombobox.class.php b/WEB-INF/lib/form/MultipleSelectCombobox.class.php new file mode 100644 index 000000000..2c2cf0501 --- /dev/null +++ b/WEB-INF/lib/form/MultipleSelectCombobox.class.php @@ -0,0 +1,88 @@ +class = 'MultipleSelectCombobox'; + $this->css_class = 'dropdown-field'; + $this->name = $name; + } + + //function setMultiple($value) { $this->mMultiple = $value; } + //function isMultiple() { return $this->mMultiple; + + function setData($value) { $this->mOptions = $value; } + function getData() { return $this->mOptions; } + + function setDataDefault($value) { $this->mOptionsEmpty = $value; } + function getDataDefault() { return $this->mOptionsEmpty; } + + function setDataKeys($keys) { $this->mDataKeys = $keys; $this->mDataDeep = 2; } + function getDataKeys() { return $this->mDataKeys; } + + function getHtml() { + + if ($this->id=="") $this->id = $this->name; + + $html = "\n\tcss_class\""; + $html .= " name=\"$this->name\" id=\"$this->id\""; + + if ($this->size!="") + $html .= " size=\"$this->size\""; + + $html .= " multiple"; + + if ($this->on_change!="") + $html .= " onchange=\"$this->on_change\""; + + if ($this->style!="") + $html .= " style=\"$this->style\""; + + if (!$this->isEnabled()) + $html .= " disabled"; + + $html .= ">\n"; + if (is_array($this->mOptionsEmpty) && (count($this->mOptionsEmpty) > 0)) + foreach ($this->mOptionsEmpty as $key=>$value) { + $html .= "\n"; + } + if (is_array($this->mOptions) && (count($this->mOptions) > 0)) + $selectedProjects = is_array($this->value) ? $this->value : array(); + foreach ($this->mOptions as $key=>$value) { + + if ($this->mDataDeep>1) { + $key = $value[$this->mDataKeys[0]]; + $value = $value[$this->mDataKeys[1]]; + } + $html .= "