From 7f2df46c0e903abe3783ffe040e0db58479f46cc Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Sat, 22 Apr 2023 15:17:26 +0000 Subject: [PATCH 01/43] Addressed pr#129 and pr#130. --- WEB-INF/lib/ttProjectHelper.class.php | 4 ++-- WEB-INF/lib/ttUser.class.php | 4 ++-- initialize.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/WEB-INF/lib/ttProjectHelper.class.php b/WEB-INF/lib/ttProjectHelper.class.php index bd166e86..30328968 100644 --- a/WEB-INF/lib/ttProjectHelper.class.php +++ b/WEB-INF/lib/ttProjectHelper.class.php @@ -220,8 +220,8 @@ static function delete($id) { if (!ttClientHelper::deleteProject($id)) return false; - // Mark project custom fields as deleted, - require_once('plugins/CustomFields.class.php'); + // Mark project custom fields as deleted. + require_once(APP_DIR.'/plugins/CustomFields.class.php'); $entity_type = CustomFields::ENTITY_PROJECT; $modified_part = ', modified = now(), modified_ip = '.$mdb2->quote($_SERVER['REMOTE_ADDR']).', modified_by = '.$mdb2->quote($user->id); $sql = "update tt_entity_custom_fields set status = null $modified_part". diff --git a/WEB-INF/lib/ttUser.class.php b/WEB-INF/lib/ttUser.class.php index 2e86eacb..2bf5c029 100644 --- a/WEB-INF/lib/ttUser.class.php +++ b/WEB-INF/lib/ttUser.class.php @@ -771,8 +771,8 @@ function markUserDeleted($user_id) { if (is_a($affected, 'PEAR_Error')) return false; - // Mark user custom fields as deleted, - require_once('plugins/CustomFields.class.php'); + // Mark user custom fields as deleted. + require_once(APP_DIR.'/plugins/CustomFields.class.php'); $entity_type = CustomFields::ENTITY_USER; $modified_part = ', modified = now(), modified_ip = '.$mdb2->quote($_SERVER['REMOTE_ADDR']).', modified_by = '.$mdb2->quote($this->id); $sql = "update tt_entity_custom_fields set status = null $modified_part". diff --git a/initialize.php b/initialize.php index 1a032a75..746e9146 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.10.5776"); +define("APP_VERSION", "1.22.10.5777"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From c299c8b0d6e3f11d99b47de525621a4f64fdf334 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Sun, 23 Apr 2023 14:53:34 +0000 Subject: [PATCH 02/43] A couple of fixes related to pr #129 and pr #130. --- WEB-INF/lib/ttChartHelper.class.php | 2 +- WEB-INF/lib/ttReportHelper.class.php | 2 +- initialize.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WEB-INF/lib/ttChartHelper.class.php b/WEB-INF/lib/ttChartHelper.class.php index eaa5a4c0..6550f5fa 100644 --- a/WEB-INF/lib/ttChartHelper.class.php +++ b/WEB-INF/lib/ttChartHelper.class.php @@ -137,7 +137,7 @@ static function getTotalsForFavReport($fav_report_id, $chart_type) { // Use custom fields plugin if it is enabled. if ($user->isPluginEnabled('cf')) { - require_once('plugins/CustomFields.class.php'); + require_once(APP_DIR.'/plugins/CustomFields.class.php'); $custom_fields = new CustomFields(); } diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index a3f89449..ef11ba80 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -8,7 +8,7 @@ import('ttTimeHelper'); import('ttConfigHelper'); -require_once(dirname(__FILE__).'/../../plugins/CustomFields.class.php'); +require_once(APP_DIR.'/plugins/CustomFields.class.php'); // Definitions of types for timesheet dropdown. define('TIMESHEET_ALL', 0); // Include all records. diff --git a/initialize.php b/initialize.php index 746e9146..bd6c4a06 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.10.5777"); +define("APP_VERSION", "1.22.10.5778"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From cd59a5694cee1b284ae66c895536438ae56d4147 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Thu, 4 May 2023 19:39:30 +0000 Subject: [PATCH 03/43] Added some more user input validation for reports. --- WEB-INF/lib/common.lib.php | 9 +++++++++ WEB-INF/lib/ttReportHelper.class.php | 19 ++++++++++++++++++- initialize.php | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/WEB-INF/lib/common.lib.php b/WEB-INF/lib/common.lib.php index 86570a5f..4bc91296 100644 --- a/WEB-INF/lib/common.lib.php +++ b/WEB-INF/lib/common.lib.php @@ -374,6 +374,15 @@ function ttValidInteger($val, $emptyValid = false) return true; } +// ttValidCheckbox is used to check user input to validate a checkbox on form. +function ttValidCheckbox($val) +{ + if ($val == null) + return true; + + return ($val == '1'); +} + // ttValidCronSpec is used to check user input to validate cron specification. function ttValidCronSpec($val) { diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index ef11ba80..b183d840 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1866,7 +1866,24 @@ static function verifyBean($bean) { } } - // TODO: add additional checks here. Perhaps do it before saving the bean for consistency. + // Validate checkboxes. + if (!ttValidCheckbox($bean->getAttribute('chclient'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chproject'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chtask'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chinvoice'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chpaid'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chip'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chstart'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chfinish'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chduration'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chnote'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chcost'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chunits'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chtimesheet'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chapproved'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chfiles'))) return false; + + // TODO: add additional checks here. return true; } diff --git a/initialize.php b/initialize.php index bd6c4a06..78d98ca9 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.10.5778"); +define("APP_VERSION", "1.22.11.5779"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 010a05a6e7b76d1bafb4d362ee83be548b9e8671 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Thu, 4 May 2023 19:59:26 +0000 Subject: [PATCH 04/43] Added one more checkbox selection validation for reports. --- WEB-INF/lib/ttReportHelper.class.php | 1 + initialize.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index b183d840..06da55e9 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1882,6 +1882,7 @@ static function verifyBean($bean) { if (!ttValidCheckbox($bean->getAttribute('chtimesheet'))) return false; if (!ttValidCheckbox($bean->getAttribute('chapproved'))) return false; if (!ttValidCheckbox($bean->getAttribute('chfiles'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chtotalsonly'))) return false; // TODO: add additional checks here. return true; diff --git a/initialize.php b/initialize.php index 78d98ca9..93345f37 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.11.5779"); +define("APP_VERSION", "1.22.11.5780"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 8a7367d7f77ea697c090f5ca4e19669181cc7bcf Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Fri, 5 May 2023 14:43:41 +0000 Subject: [PATCH 05/43] Addressed an sql vulnerability in invoices.php for bogus post parameters for sorting. --- initialize.php | 2 +- invoices.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/initialize.php b/initialize.php index 93345f37..f8cb044a 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.11.5780"); +define("APP_VERSION", "1.22.11.5781"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); diff --git a/invoices.php b/invoices.php index 3f6438b9..f1eda484 100644 --- a/invoices.php +++ b/invoices.php @@ -76,7 +76,7 @@ if (!ttInvoiceHelper::validSortOrder($sort_order_2)) $err->add($i18n->get('error.field'), $i18n->get('label.sort')); if ($sort_option_1 == $sort_option_2) $err->add($i18n->get('error.field'), $i18n->get('label.sort')); - if($request->getParameter('sorting_changed')) { + if($err->no() && $request->getParameter('sorting_changed')) { // User changed sorting. Get invoices sorted accordingly. $sort_options = array('sort_option_1'=>$sort_option_1, 'sort_order_1'=>$sort_order_1, From ec4d03d4c6fa87fc08c316e414ee3ef3c510db0c Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Fri, 5 May 2023 16:27:05 +0000 Subject: [PATCH 06/43] Improving report security by casting some things to int. --- WEB-INF/lib/ttReportHelper.class.php | 38 ++++++++++++++-------------- initialize.php | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index 06da55e9..553d0efd 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1783,21 +1783,21 @@ static function getReportOptions($bean) { $options['period_start'] = $bean->getAttribute('start_date'); $options['period_end'] = $bean->getAttribute('end_date'); $options['note_containing'] = $bean->getAttribute('note_containing'); - $options['show_client'] = $bean->getAttribute('chclient'); - $options['show_invoice'] = $bean->getAttribute('chinvoice'); - $options['show_approved'] = $bean->getAttribute('chapproved'); - $options['show_paid'] = $bean->getAttribute('chpaid'); - $options['show_ip'] = $bean->getAttribute('chip'); - $options['show_project'] = $bean->getAttribute('chproject'); - $options['show_start'] = $bean->getAttribute('chstart'); - $options['show_duration'] = $bean->getAttribute('chduration'); - $options['show_cost'] = $bean->getAttribute('chcost'); - $options['show_task'] = $bean->getAttribute('chtask'); - $options['show_end'] = $bean->getAttribute('chfinish'); - $options['show_note'] = $bean->getAttribute('chnote'); - $options['show_work_units'] = $bean->getAttribute('chunits'); - $options['show_timesheet'] = $bean->getAttribute('chtimesheet'); - $options['show_files'] = $bean->getAttribute('chfiles'); + $options['show_client'] = (int)$bean->getAttribute('chclient'); + $options['show_invoice'] = (int)$bean->getAttribute('chinvoice'); + $options['show_approved'] = (int)$bean->getAttribute('chapproved'); + $options['show_paid'] = (int)$bean->getAttribute('chpaid'); + $options['show_ip'] = (int)$bean->getAttribute('chip'); + $options['show_project'] = (int)$bean->getAttribute('chproject'); + $options['show_start'] = (int)$bean->getAttribute('chstart'); + $options['show_duration'] = (int)$bean->getAttribute('chduration'); + $options['show_cost'] = (int)$bean->getAttribute('chcost'); + $options['show_task'] = (int)$bean->getAttribute('chtask'); + $options['show_end'] = (int)$bean->getAttribute('chfinish'); + $options['show_note'] = (int)$bean->getAttribute('chnote'); + $options['show_work_units'] = (int)$bean->getAttribute('chunits'); + $options['show_timesheet'] = (int)$bean->getAttribute('chtimesheet'); + $options['show_files'] = (int)$bean->getAttribute('chfiles'); // Prepare custom field options. if ($user->isPluginEnabled('cf')) { @@ -1810,7 +1810,7 @@ static function getReportOptions($bean) { $control_name = 'time_field_'.$timeField['id']; $checkbox_control_name = 'show_'.$control_name; $options[$control_name] = $bean->getAttribute($control_name); - $options[$checkbox_control_name] = $bean->getAttribute($checkbox_control_name); + $options[$checkbox_control_name] = (int)$bean->getAttribute($checkbox_control_name); } } @@ -1820,7 +1820,7 @@ static function getReportOptions($bean) { $control_name = 'user_field_'.$userField['id']; $checkbox_control_name = 'show_'.$control_name; $options[$control_name] = $bean->getAttribute($control_name); - $options[$checkbox_control_name] = $bean->getAttribute($checkbox_control_name); + $options[$checkbox_control_name] = (int)$bean->getAttribute($checkbox_control_name); } } @@ -1830,7 +1830,7 @@ static function getReportOptions($bean) { $control_name = 'project_field_'.$projectField['id']; $checkbox_control_name = 'show_'.$control_name; $options[$control_name] = $bean->getAttribute($control_name); - $options[$checkbox_control_name] = $bean->getAttribute($checkbox_control_name); + $options[$checkbox_control_name] = (int)$bean->getAttribute($checkbox_control_name); } } } @@ -1838,7 +1838,7 @@ static function getReportOptions($bean) { $options['group_by1'] = $bean->getAttribute('group_by1'); $options['group_by2'] = $bean->getAttribute('group_by2'); $options['group_by3'] = $bean->getAttribute('group_by3'); - $options['show_totals_only'] = $bean->getAttribute('chtotalsonly'); + $options['show_totals_only'] = (int)$bean->getAttribute('chtotalsonly'); return $options; } diff --git a/initialize.php b/initialize.php index f8cb044a..3b5b0fcf 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.11.5781"); +define("APP_VERSION", "1.22.11.5782"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 093cfe158099704ffd4a1624be217f9935e914eb Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Fri, 5 May 2023 18:19:31 +0000 Subject: [PATCH 07/43] Addressed stored XSS vulnerability in week.php by escaping cell title. --- initialize.php | 2 +- week.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/initialize.php b/initialize.php index 3b5b0fcf..ac6cd707 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.11.5782"); +define("APP_VERSION", "1.22.12.5783"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); diff --git a/week.php b/week.php index 9d96abfb..16a00dff 100644 --- a/week.php +++ b/week.php @@ -242,7 +242,7 @@ function render(&$table, $value, $row, $column, $selected = false) { $field->setValue($table->getValueAt($row,$column)['duration']); // Duration for even rows. } else { $field->setValue($table->getValueAt($row,$column)['note']); // Comment for odd rows. - $field->setTitle($table->getValueAt($row,$column)['note']); // Tooltip to help view the entire comment. + $field->setTitle(htmlspecialchars($table->getValueAt($row,$column)['note'])); // Tooltip to help view the entire comment. } } else { $field->setValue($table->getValueAt($row,$column)['duration']); From 41a56db904ab357a3eb1f6df275e3c067ca1dc14 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Sat, 6 May 2023 18:34:34 +0000 Subject: [PATCH 08/43] Minor improvement in data validation for reports. --- WEB-INF/lib/ttReportHelper.class.php | 4 + initialize.php | 2 +- reports.php | 142 ++++++++++++++------------- 3 files changed, 79 insertions(+), 69 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index 553d0efd..c329fcb4 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1866,6 +1866,10 @@ static function verifyBean($bean) { } } + // Check fav report id. + $fav_report_id = $bean->getAttribute('favorite_report'); + if (!($fav_report_id == -1 || ttValidInteger($fav_report_id))) return false; + // Validate checkboxes. if (!ttValidCheckbox($bean->getAttribute('chclient'))) return false; if (!ttValidCheckbox($bean->getAttribute('chproject'))) return false; diff --git a/initialize.php b/initialize.php index ac6cd707..5de06cb2 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.12.5783"); +define("APP_VERSION", "1.22.12.5784"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); diff --git a/reports.php b/reports.php index fd4ca156..5cfdd1d8 100644 --- a/reports.php +++ b/reports.php @@ -417,84 +417,90 @@ $form->getElement('btn_delete')->setEnabled(false); if ($request->isPost()) { - if((!$bean->getAttribute('btn_generate') && ($request->getParameter('fav_report_changed')))) { - // User changed favorite report. We need to load new values into the form. - if ($bean->getAttribute('favorite_report')) { - // This loads new favorite report options into the bean (into our form). - ttFavReportHelper::loadReport($bean); - - // If user selected no favorite report - mark all user checkboxes (most probable scenario). - if ($bean->getAttribute('favorite_report') == -1) { - $form->setValueByElement('users_active', array_keys($user_list_active)); - $form->setValueByElement('users_inactive', false); + // Verify bean. Do not proceed with bogus post data. + if (!ttReportHelper::verifyBean($bean)) $err->add($i18n->get('error.sys')); + + if($err->no()) { + if (!$bean->getAttribute('btn_generate') && $request->getParameter('fav_report_changed')) { + // User changed favorite report. We need to load new values into the form. + if ($bean->getAttribute('favorite_report')) { + // This loads new favorite report options into the bean (into our form). + ttFavReportHelper::loadReport($bean); + + // If user selected no favorite report - mark all user checkboxes (most probable scenario). + if ($bean->getAttribute('favorite_report') == -1) { + $form->setValueByElement('users_active', array_keys($user_list_active)); + $form->setValueByElement('users_inactive', false); + } + + // Save form data in session for future use. + $bean->saveBean(); + header('Location: reports.php'); + exit(); } + } elseif ($bean->getAttribute('btn_save')) { + // User clicked the Save button. We need to save form options as new favorite report. + + // We check new_fav_report here, rather than in ttReportHelper::verifyBean to display a specific error, instead of 'error.sys'. + if (!ttValidString($bean->getAttribute('new_fav_report'))) $err->add($i18n->get('error.field'), $i18n->get('form.reports.save_as_favorite')); - // Save form data in session for future use. - $bean->saveBean(); - header('Location: reports.php'); - exit(); - } - } elseif ($bean->getAttribute('btn_save')) { - // User clicked the Save button. We need to save form options as new favorite report. - if (!ttValidString($bean->getAttribute('new_fav_report'))) $err->add($i18n->get('error.field'), $i18n->get('form.reports.save_as_favorite')); - - if ($err->no()) { - $id = ttFavReportHelper::saveReport($bean); - if (!$id) - $err->add($i18n->get('error.db')); if ($err->no()) { - $bean->setAttribute('favorite_report', $id); + $id = ttFavReportHelper::saveReport($bean); + if (!$id) + $err->add($i18n->get('error.db')); + if ($err->no()) { + $bean->setAttribute('favorite_report', $id); + $bean->saveBean(); + header('Location: reports.php'); + exit(); + } + } + } elseif($bean->getAttribute('btn_delete')) { + // Delete button pressed. User wants to delete a favorite report. + if ($bean->getAttribute('favorite_report')) { + ttFavReportHelper::deleteReport($bean->getAttribute('favorite_report')); + // Load default report. + $bean->setAttribute('favorite_report',''); + $bean->setAttribute('new_fav_report', $report_list[0]['name']); + ttFavReportHelper::loadReport($bean); + $form->setValueByElement('users', array_keys($user_list)); $bean->saveBean(); header('Location: reports.php'); exit(); } - } - } elseif($bean->getAttribute('btn_delete')) { - // Delete button pressed. User wants to delete a favorite report. - if ($bean->getAttribute('favorite_report')) { - ttFavReportHelper::deleteReport($bean->getAttribute('favorite_report')); - // Load default report. - $bean->setAttribute('favorite_report',''); - $bean->setAttribute('new_fav_report', $report_list[0]['name']); - ttFavReportHelper::loadReport($bean); - $form->setValueByElement('users', array_keys($user_list)); - $bean->saveBean(); - header('Location: reports.php'); - exit(); - } - } else { - // Generate button pressed. Check some values. - if (!$bean->getAttribute('period')) { - // Validate start date. - if (!ttValidDate($bean->getAttribute('start_date'))) $err->add($i18n->get('error.field'), $i18n->get('label.start_date')); - // Validate end date. - if (!ttValidDate($bean->getAttribute('end_date'))) $err->add($i18n->get('error.field'), $i18n->get('label.end_date')); - // Make sure that the end date is equal or after the start date. - if ($err->no()) { - $startDate = new ttDate($bean->getAttribute('start_date'), $user->getDateFormat()); - $endDate = new ttDate($bean->getAttribute('end_date'), $user->getDateFormat()); - if ($startDate->compare($endDate) > 0) { - $err->add($i18n->get('error.interval'), $i18n->get('label.end_date'), $i18n->get('label.start_date')); + } else { + // Generate button pressed. Check some values. + if (!$bean->getAttribute('period')) { + // Validate start date. + if (!ttValidDate($bean->getAttribute('start_date'))) $err->add($i18n->get('error.field'), $i18n->get('label.start_date')); + // Validate end date. + if (!ttValidDate($bean->getAttribute('end_date'))) $err->add($i18n->get('error.field'), $i18n->get('label.end_date')); + // Make sure that the end date is equal or after the start date. + if ($err->no()) { + $startDate = new ttDate($bean->getAttribute('start_date'), $user->getDateFormat()); + $endDate = new ttDate($bean->getAttribute('end_date'), $user->getDateFormat()); + if ($startDate->compare($endDate) > 0) { + $err->add($i18n->get('error.interval'), $i18n->get('label.end_date'), $i18n->get('label.start_date')); + } } } + $group_by1 = $bean->getAttribute('group_by1'); + $group_by2 = $bean->getAttribute('group_by2'); + $group_by3 = $bean->getAttribute('group_by3'); + if (($group_by3 != null && $group_by3 != 'no_grouping') && ($group_by3 == $group_by1 || $group_by3 == $group_by2)) + $err->add($i18n->get('error.field'), $i18n->get('form.reports.group_by')); + if (($group_by2 != null && $group_by2 != 'no_grouping') && ($group_by2 == $group_by1 || $group_by3 == $group_by2)) + $err->add($i18n->get('error.field'), $i18n->get('form.reports.group_by')); + // TODO: review the above checks and possibly move some of them to verifyBean that we call when entering isPost above. + + if ($err->no()) { + $bean->saveBean(); + // Now we can go ahead and create a report. + header('Location: report.php'); + exit(); + } } - $group_by1 = $bean->getAttribute('group_by1'); - $group_by2 = $bean->getAttribute('group_by2'); - $group_by3 = $bean->getAttribute('group_by3'); - if (($group_by3 != null && $group_by3 != 'no_grouping') && ($group_by3 == $group_by1 || $group_by3 == $group_by2)) - $err->add($i18n->get('error.field'), $i18n->get('form.reports.group_by')); - if (($group_by2 != null && $group_by2 != 'no_grouping') && ($group_by2 == $group_by1 || $group_by3 == $group_by2)) - $err->add($i18n->get('error.field'), $i18n->get('form.reports.group_by')); - // Check remaining values. - if (!ttReportHelper::verifyBean($bean)) $err->add($i18n->get('error.sys')); - - if ($err->no()) { - $bean->saveBean(); - // Now we can go ahead and create a report. - header('Location: report.php'); - exit(); - } - } + } // if($err->no()) } // isPost $smarty->assign('client_list', $client_list); From 28e13a2deab266ee30354df8b95dd6e47af6f2d8 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Mon, 8 May 2023 14:35:05 +0000 Subject: [PATCH 09/43] Incremental improvement in data validation for reports. --- WEB-INF/lib/ttReportHelper.class.php | 5 ++++- initialize.php | 2 +- report.php | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index c329fcb4..4a7870bd 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1868,7 +1868,10 @@ static function verifyBean($bean) { // Check fav report id. $fav_report_id = $bean->getAttribute('favorite_report'); - if (!($fav_report_id == -1 || ttValidInteger($fav_report_id))) return false; + if (!($fav_report_id == -1 || (ttValidInteger($fav_report_id) && ttFavReportHelper::get($fav_report_id)))) return false; + + // Check client id. + if (!ttValidInteger($bean->getAttribute('client'), true)) return false; // Validate checkboxes. if (!ttValidCheckbox($bean->getAttribute('chclient'))) return false; diff --git a/initialize.php b/initialize.php index 5de06cb2..394bdb96 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.12.5784"); +define("APP_VERSION", "1.22.12.5785"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); diff --git a/report.php b/report.php index 4945ac17..b8a549b6 100644 --- a/report.php +++ b/report.php @@ -59,7 +59,7 @@ // If we are in post, load the bean from session, as the constructor does it only in get. if ($request->isPost()) $bean->loadBean(); -$client_id = $bean->getAttribute('client'); +$client_id = (int)$bean->getAttribute('client'); $options = ttReportHelper::getReportOptions($bean); // Do we need to show checkboxes? We show them in the following 4 situations: From ec9496c95d86e26f785ab0c7dbc9d8ed07fc8fe8 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Tue, 9 May 2023 19:37:20 +0000 Subject: [PATCH 10/43] Incremental improvenet in data validation for reports. --- WEB-INF/lib/ttReportHelper.class.php | 13 ++++++++++--- initialize.php | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index 4a7870bd..30c41c8d 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1756,9 +1756,9 @@ static function getReportOptions($bean) { // Construct one by one. $options['name'] = null; // No name required. $options['user_id'] = $user->id; // Not sure if we need user_id here. Fav reports use it to recycle $user object in cron.php. - $options['client_id'] = $bean->getAttribute('client'); - $options['project_id'] = $bean->getAttribute('project'); - $options['task_id'] = $bean->getAttribute('task'); + $options['client_id'] = (int)$bean->getAttribute('client'); + $options['project_id'] = (int)$bean->getAttribute('project'); + $options['task_id'] = (int)$bean->getAttribute('task'); $options['billable'] = $bean->getAttribute('include_records'); $options['invoice'] = $bean->getAttribute('invoice'); $options['paid_status'] = $bean->getAttribute('paid_status'); @@ -1873,6 +1873,13 @@ static function verifyBean($bean) { // Check client id. if (!ttValidInteger($bean->getAttribute('client'), true)) return false; + // Check project id. + if (!ttValidInteger($bean->getAttribute('project'), true)) return false; + + // Check task id. + if (!ttValidInteger($bean->getAttribute('task'), true)) return false; + + // Validate checkboxes. if (!ttValidCheckbox($bean->getAttribute('chclient'))) return false; if (!ttValidCheckbox($bean->getAttribute('chproject'))) return false; diff --git a/initialize.php b/initialize.php index 394bdb96..391cb2da 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.12.5785"); +define("APP_VERSION", "1.22.12.5786"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 1302b99e81dbb791656778bb0d36205a1ae7bf39 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Wed, 10 May 2023 17:21:06 +0000 Subject: [PATCH 11/43] Work in progress on validating data for reports. --- WEB-INF/lib/ttReportHelper.class.php | 26 +++++++++++++++++--------- initialize.php | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index 30c41c8d..305aa473 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -48,10 +48,10 @@ static function getWhere($options) { $dropdown_parts .= ' and l.client_id = '.$user->client_id; if ($options['project_id']) $dropdown_parts .= ' and l.project_id = '.$options['project_id']; if ($options['task_id']) $dropdown_parts .= ' and l.task_id = '.$options['task_id']; - if ($options['billable']=='1') $dropdown_parts .= ' and l.billable = 1'; - if ($options['billable']=='2') $dropdown_parts .= ' and l.billable = 0'; - if ($options['invoice']=='1') $dropdown_parts .= ' and l.invoice_id is not null'; - if ($options['invoice']=='2') $dropdown_parts .= ' and l.invoice_id is null'; + if ($options['billable']==1) $dropdown_parts .= ' and l.billable = 1'; + if ($options['billable']==2) $dropdown_parts .= ' and l.billable = 0'; + if ($options['invoice']==1) $dropdown_parts .= ' and l.invoice_id is not null'; + if ($options['invoice']==2) $dropdown_parts .= ' and l.invoice_id is null'; if ($options['timesheet']==TIMESHEET_NOT_ASSIGNED) $dropdown_parts .= ' and l.timesheet_id is null'; if ($options['timesheet']==TIMESHEET_ASSIGNED) $dropdown_parts .= ' and l.timesheet_id is not null'; if ($options['approved']=='1') $dropdown_parts .= ' and l.approved = 1'; @@ -181,8 +181,8 @@ static function getExpenseWhere($options) { elseif ($user->isClient() && $user->client_id) $dropdown_parts .= ' and ei.client_id = '.$user->client_id; if ($options['project_id']) $dropdown_parts .= ' and ei.project_id = '.$options['project_id']; - if ($options['invoice']=='1') $dropdown_parts .= ' and ei.invoice_id is not null'; - if ($options['invoice']=='2') $dropdown_parts .= ' and ei.invoice_id is null'; + if ($options['invoice']==1) $dropdown_parts .= ' and ei.invoice_id is not null'; + if ($options['invoice']==2) $dropdown_parts .= ' and ei.invoice_id is null'; if (isset($options['timesheet']) && ($options['timesheet']!=TIMESHEET_ALL && $options['timesheet']!=TIMESHEET_NOT_ASSIGNED)) { $dropdown_parts .= ' and 0 = 1'; // Expense items do not have a timesheet_id. } @@ -1755,12 +1755,13 @@ static function getReportOptions($bean) { // Construct one by one. $options['name'] = null; // No name required. - $options['user_id'] = $user->id; // Not sure if we need user_id here. Fav reports use it to recycle $user object in cron.php. + // $options['user_id'] = $user->id; // We don't use user_id in regular reports. But fav reports use it to recycle $user object in cron.php. $options['client_id'] = (int)$bean->getAttribute('client'); $options['project_id'] = (int)$bean->getAttribute('project'); $options['task_id'] = (int)$bean->getAttribute('task'); - $options['billable'] = $bean->getAttribute('include_records'); - $options['invoice'] = $bean->getAttribute('invoice'); + $options['billable'] = (int)$bean->getAttribute('include_records'); + $options['invoice'] = (int)$bean->getAttribute('invoice'); + $options['paid_status'] = $bean->getAttribute('paid_status'); $options['approved'] = $bean->getAttribute('approved'); if ($user->isPluginEnabled('ap') && $user->isClient() && !$user->can('view_client_unapproved')) @@ -1873,6 +1874,12 @@ static function verifyBean($bean) { // Check client id. if (!ttValidInteger($bean->getAttribute('client'), true)) return false; + // Check billable control. + if (!ttValidInteger($bean->getAttribute('include_records'), true)) return false; + + // Check invoiced / not invoiced control. + if (!ttValidInteger($bean->getAttribute('invoice'), true)) return false; + // Check project id. if (!ttValidInteger($bean->getAttribute('project'), true)) return false; @@ -1880,6 +1887,7 @@ static function verifyBean($bean) { if (!ttValidInteger($bean->getAttribute('task'), true)) return false; + // Validate checkboxes. if (!ttValidCheckbox($bean->getAttribute('chclient'))) return false; if (!ttValidCheckbox($bean->getAttribute('chproject'))) return false; diff --git a/initialize.php b/initialize.php index 391cb2da..4fe6c6ef 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.12.5786"); +define("APP_VERSION", "1.22.12.5787"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 0795d5896fc961f12cec61c63aedb779e2c98c74 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Wed, 10 May 2023 17:29:07 +0000 Subject: [PATCH 12/43] More work in progress on data validation for reports. --- WEB-INF/lib/ttReportHelper.class.php | 4 +++- initialize.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index 305aa473..73f3f165 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1761,8 +1761,8 @@ static function getReportOptions($bean) { $options['task_id'] = (int)$bean->getAttribute('task'); $options['billable'] = (int)$bean->getAttribute('include_records'); $options['invoice'] = (int)$bean->getAttribute('invoice'); + $options['paid_status'] = (int)$bean->getAttribute('paid_status'); - $options['paid_status'] = $bean->getAttribute('paid_status'); $options['approved'] = $bean->getAttribute('approved'); if ($user->isPluginEnabled('ap') && $user->isClient() && !$user->can('view_client_unapproved')) $options['approved'] = 1; // Restrict clients to approved records only. @@ -1886,6 +1886,8 @@ static function verifyBean($bean) { // Check task id. if (!ttValidInteger($bean->getAttribute('task'), true)) return false; + // Check paid status control. + if (!ttValidInteger($bean->getAttribute('paid_status'), true)) return false; // Validate checkboxes. diff --git a/initialize.php b/initialize.php index 4fe6c6ef..439f2ee6 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.12.5787"); +define("APP_VERSION", "1.22.12.5788"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 6f1fa8fddd5ee4b53eb0850408ccac957478fd8c Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Wed, 10 May 2023 17:44:35 +0000 Subject: [PATCH 13/43] Some more data validation for reports. --- WEB-INF/lib/ttReportHelper.class.php | 26 +++++++++++++++----------- initialize.php | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index 73f3f165..c7b147d9 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -54,10 +54,10 @@ static function getWhere($options) { if ($options['invoice']==2) $dropdown_parts .= ' and l.invoice_id is null'; if ($options['timesheet']==TIMESHEET_NOT_ASSIGNED) $dropdown_parts .= ' and l.timesheet_id is null'; if ($options['timesheet']==TIMESHEET_ASSIGNED) $dropdown_parts .= ' and l.timesheet_id is not null'; - if ($options['approved']=='1') $dropdown_parts .= ' and l.approved = 1'; - if ($options['approved']=='2') $dropdown_parts .= ' and l.approved = 0'; - if ($options['paid_status']=='1') $dropdown_parts .= ' and l.paid = 1'; - if ($options['paid_status']=='2') $dropdown_parts .= ' and l.paid = 0'; + if ($options['approved']==1) $dropdown_parts .= ' and l.approved = 1'; + if ($options['approved']==2) $dropdown_parts .= ' and l.approved = 0'; + if ($options['paid_status']==1) $dropdown_parts .= ' and l.paid = 1'; + if ($options['paid_status']==2) $dropdown_parts .= ' and l.paid = 0'; // Add time custom fields. if (isset($custom_fields) && $custom_fields->timeFields) { @@ -186,10 +186,10 @@ static function getExpenseWhere($options) { if (isset($options['timesheet']) && ($options['timesheet']!=TIMESHEET_ALL && $options['timesheet']!=TIMESHEET_NOT_ASSIGNED)) { $dropdown_parts .= ' and 0 = 1'; // Expense items do not have a timesheet_id. } - if ($options['approved']=='1') $dropdown_parts .= ' and ei.approved = 1'; - if ($options['approved']=='2') $dropdown_parts .= ' and ei.approved = 0'; - if ($options['paid_status']=='1') $dropdown_parts .= ' and ei.paid = 1'; - if ($options['paid_status']=='2') $dropdown_parts .= ' and ei.paid = 0'; + if ($options['approved']==1) $dropdown_parts .= ' and ei.approved = 1'; + if ($options['approved']==2) $dropdown_parts .= ' and ei.approved = 0'; + if ($options['paid_status']==1) $dropdown_parts .= ' and ei.paid = 1'; + if ($options['paid_status']==2) $dropdown_parts .= ' and ei.paid = 0'; // Not adding conditions for time custom fields by design because expenses are not associated with them. // Whether or not this is proper, we'll know eventually if users complain. @@ -1762,11 +1762,10 @@ static function getReportOptions($bean) { $options['billable'] = (int)$bean->getAttribute('include_records'); $options['invoice'] = (int)$bean->getAttribute('invoice'); $options['paid_status'] = (int)$bean->getAttribute('paid_status'); - - $options['approved'] = $bean->getAttribute('approved'); + $options['approved'] = (int)$bean->getAttribute('approved'); if ($user->isPluginEnabled('ap') && $user->isClient() && !$user->can('view_client_unapproved')) $options['approved'] = 1; // Restrict clients to approved records only. - $options['timesheet'] = $bean->getAttribute('timesheet'); + $options['timesheet'] = (int)$bean->getAttribute('timesheet'); $active_users_in_bean = $bean->getAttribute('users_active'); $users = ''; @@ -1889,6 +1888,11 @@ static function verifyBean($bean) { // Check paid status control. if (!ttValidInteger($bean->getAttribute('paid_status'), true)) return false; + // Check approved status control. + if (!ttValidInteger($bean->getAttribute('approved'), true)) return false; + + // Check timesheet status control. + if (!ttValidInteger($bean->getAttribute('timesheet'), true)) return false; // Validate checkboxes. if (!ttValidCheckbox($bean->getAttribute('chclient'))) return false; diff --git a/initialize.php b/initialize.php index 439f2ee6..3a738d7f 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.12.5788"); +define("APP_VERSION", "1.22.12.5789"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 0c3c61df8de402970fe804234ada80f3ac1af670 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Thu, 11 May 2023 16:39:59 +0000 Subject: [PATCH 14/43] Some more data validating for reports. --- WEB-INF/lib/ttReportHelper.class.php | 15 +++++++++------ initialize.php | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index c7b147d9..5c8fdbf6 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1779,7 +1779,7 @@ static function getReportOptions($bean) { } if ($users) $options['users'] = $users; - $options['period'] = $bean->getAttribute('period'); + $options['period'] = (int)$bean->getAttribute('period'); $options['period_start'] = $bean->getAttribute('start_date'); $options['period_end'] = $bean->getAttribute('end_date'); $options['note_containing'] = $bean->getAttribute('note_containing'); @@ -1846,6 +1846,9 @@ static function getReportOptions($bean) { static function verifyBean($bean) { global $user; + // Check period control. + if (!ttValidInteger($bean->getAttribute('period'), true)) return false; + // Check users. $active_users_in_bean = (array) $bean->getAttribute('users_active'); $inactive_users_in_bean = (array) $bean->getAttribute('users_inactive'); @@ -1898,17 +1901,17 @@ static function verifyBean($bean) { if (!ttValidCheckbox($bean->getAttribute('chclient'))) return false; if (!ttValidCheckbox($bean->getAttribute('chproject'))) return false; if (!ttValidCheckbox($bean->getAttribute('chtask'))) return false; - if (!ttValidCheckbox($bean->getAttribute('chinvoice'))) return false; - if (!ttValidCheckbox($bean->getAttribute('chpaid'))) return false; - if (!ttValidCheckbox($bean->getAttribute('chip'))) return false; if (!ttValidCheckbox($bean->getAttribute('chstart'))) return false; if (!ttValidCheckbox($bean->getAttribute('chfinish'))) return false; if (!ttValidCheckbox($bean->getAttribute('chduration'))) return false; if (!ttValidCheckbox($bean->getAttribute('chnote'))) return false; - if (!ttValidCheckbox($bean->getAttribute('chcost'))) return false; if (!ttValidCheckbox($bean->getAttribute('chunits'))) return false; - if (!ttValidCheckbox($bean->getAttribute('chtimesheet'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chcost'))) return false; if (!ttValidCheckbox($bean->getAttribute('chapproved'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chpaid'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chip'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chinvoice'))) return false; + if (!ttValidCheckbox($bean->getAttribute('chtimesheet'))) return false; if (!ttValidCheckbox($bean->getAttribute('chfiles'))) return false; if (!ttValidCheckbox($bean->getAttribute('chtotalsonly'))) return false; diff --git a/initialize.php b/initialize.php index 3a738d7f..511ac11a 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.12.5789"); +define("APP_VERSION", "1.22.12.5790"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 0ecd0c99a2d45b0dc0670d8444edfdacaa84f66e Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Thu, 11 May 2023 17:57:35 +0000 Subject: [PATCH 15/43] Added data validation for custom field controls for reports. --- WEB-INF/lib/ttReportHelper.class.php | 72 ++++++++++++++++++++++++++-- initialize.php | 2 +- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index 5c8fdbf6..cdaa3e33 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1809,7 +1809,13 @@ static function getReportOptions($bean) { foreach ($custom_fields->timeFields as $timeField) { $control_name = 'time_field_'.$timeField['id']; $checkbox_control_name = 'show_'.$control_name; - $options[$control_name] = $bean->getAttribute($control_name); + if ($timeField['type'] == CustomFields::TYPE_DROPDOWN) { + // Cast to int for a dropdown for extra security. + $options[$control_name] = (int)$bean->getAttribute($control_name); + } elseif ($timeField['type'] == CustomFields::TYPE_TEXT) { + // No need to cast to int for a text field. + $options[$control_name] = $bean->getAttribute($control_name); + } $options[$checkbox_control_name] = (int)$bean->getAttribute($checkbox_control_name); } } @@ -1819,7 +1825,13 @@ static function getReportOptions($bean) { foreach ($custom_fields->userFields as $userField) { $control_name = 'user_field_'.$userField['id']; $checkbox_control_name = 'show_'.$control_name; - $options[$control_name] = $bean->getAttribute($control_name); + if ($userField['type'] == CustomFields::TYPE_DROPDOWN) { + // Cast to int for a dropdown for extra security. + $options[$control_name] = (int)$bean->getAttribute($control_name); + } elseif ($userField['type'] == CustomFields::TYPE_TEXT) { + // No need to cast to int for a text field. + $options[$control_name] = $bean->getAttribute($control_name); + } $options[$checkbox_control_name] = (int)$bean->getAttribute($checkbox_control_name); } } @@ -1829,7 +1841,13 @@ static function getReportOptions($bean) { foreach ($custom_fields->projectFields as $projectField) { $control_name = 'project_field_'.$projectField['id']; $checkbox_control_name = 'show_'.$control_name; - $options[$control_name] = $bean->getAttribute($control_name); + if ($projectField['type'] == CustomFields::TYPE_DROPDOWN) { + // Cast to int for a dropdown for extra security. + $options[$control_name] = (int)$bean->getAttribute($control_name); + } elseif ($projectField['type'] == CustomFields::TYPE_TEXT) { + // No need to cast to int for a text field. + $options[$control_name] = $bean->getAttribute($control_name); + } $options[$checkbox_control_name] = (int)$bean->getAttribute($checkbox_control_name); } } @@ -1915,6 +1933,54 @@ static function verifyBean($bean) { if (!ttValidCheckbox($bean->getAttribute('chfiles'))) return false; if (!ttValidCheckbox($bean->getAttribute('chtotalsonly'))) return false; + // Verify custom field controls. + if ($user->isPluginEnabled('cf')) { + global $custom_fields; + if (!$custom_fields) $custom_fields = new CustomFields(); + + // Time fields. + if ($custom_fields->timeFields) { + foreach ($custom_fields->timeFields as $timeField) { + $control_name = 'time_field_'.$timeField['id']; + $checkbox_control_name = 'show_'.$control_name; + if ($timeField['type'] == CustomFields::TYPE_DROPDOWN) { + if (!ttValidInteger($bean->getAttribute($control_name), true)) + return false; + } + if (!ttValidCheckbox($bean->getAttribute($checkbox_control_name), true)) + return false; + } + } + + // User fields. + if ($custom_fields->userFields) { + foreach ($custom_fields->userFields as $userField) { + $control_name = 'user_field_'.$userField['id']; + $checkbox_control_name = 'show_'.$control_name; + if ($userField['type'] == CustomFields::TYPE_DROPDOWN) { + if (!ttValidInteger($bean->getAttribute($control_name), true)) + return false; + } + if (!ttValidCheckbox($bean->getAttribute($checkbox_control_name), true)) + return false; + } + } + + // Project fields. + if ($custom_fields->projectFields) { + foreach ($custom_fields->projectFields as $projectField) { + $control_name = 'project_field_'.$projectField['id']; + $checkbox_control_name = 'show_'.$control_name; + if ($projectField['type'] == CustomFields::TYPE_DROPDOWN) { + if (!ttValidInteger($bean->getAttribute($control_name), true)) + return false; + } + if (!ttValidCheckbox($bean->getAttribute($checkbox_control_name), true)) + return false; + } + } + } + // TODO: add additional checks here. return true; } diff --git a/initialize.php b/initialize.php index 511ac11a..86bb008d 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.12.5790"); +define("APP_VERSION", "1.22.12.5791"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From a1975fbe027e5581da852fa0e802e09bfcaafce0 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Thu, 11 May 2023 18:56:46 +0000 Subject: [PATCH 16/43] Finished, hopefully, data validation for reports. --- WEB-INF/lib/ttReportHelper.class.php | 65 ++++++++++++++++++++++++---- initialize.php | 2 +- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index cdaa3e33..4a04f2bb 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1811,12 +1811,12 @@ static function getReportOptions($bean) { $checkbox_control_name = 'show_'.$control_name; if ($timeField['type'] == CustomFields::TYPE_DROPDOWN) { // Cast to int for a dropdown for extra security. - $options[$control_name] = (int)$bean->getAttribute($control_name); + $options[$control_name] = (int)$bean->getAttribute($control_name); } elseif ($timeField['type'] == CustomFields::TYPE_TEXT) { // No need to cast to int for a text field. - $options[$control_name] = $bean->getAttribute($control_name); + $options[$control_name] = $bean->getAttribute($control_name); } - $options[$checkbox_control_name] = (int)$bean->getAttribute($checkbox_control_name); + $options[$checkbox_control_name] = (int)$bean->getAttribute($checkbox_control_name); } } @@ -1827,12 +1827,12 @@ static function getReportOptions($bean) { $checkbox_control_name = 'show_'.$control_name; if ($userField['type'] == CustomFields::TYPE_DROPDOWN) { // Cast to int for a dropdown for extra security. - $options[$control_name] = (int)$bean->getAttribute($control_name); + $options[$control_name] = (int)$bean->getAttribute($control_name); } elseif ($userField['type'] == CustomFields::TYPE_TEXT) { // No need to cast to int for a text field. - $options[$control_name] = $bean->getAttribute($control_name); + $options[$control_name] = $bean->getAttribute($control_name); } - $options[$checkbox_control_name] = (int)$bean->getAttribute($checkbox_control_name); + $options[$checkbox_control_name] = (int)$bean->getAttribute($checkbox_control_name); } } @@ -1843,10 +1843,10 @@ static function getReportOptions($bean) { $checkbox_control_name = 'show_'.$control_name; if ($projectField['type'] == CustomFields::TYPE_DROPDOWN) { // Cast to int for a dropdown for extra security. - $options[$control_name] = (int)$bean->getAttribute($control_name); + $options[$control_name] = (int)$bean->getAttribute($control_name); } elseif ($projectField['type'] == CustomFields::TYPE_TEXT) { // No need to cast to int for a text field. - $options[$control_name] = $bean->getAttribute($control_name); + $options[$control_name] = $bean->getAttribute($control_name); } $options[$checkbox_control_name] = (int)$bean->getAttribute($checkbox_control_name); } @@ -1981,10 +1981,57 @@ static function verifyBean($bean) { } } - // TODO: add additional checks here. + if (!ttReportHelper::validGroupByOption($bean->getAttribute('group_by1'))) return false; + if (!ttReportHelper::validGroupByOption($bean->getAttribute('group_by2'))) return false; + if (!ttReportHelper::validGroupByOption($bean->getAttribute('group_by3'))) return false; + return true; } + // validGroupByOption is a security function to verify selection options for group by controls. + static function validGroupByOption($groupByOption) { + + global $user; + + if ($groupByOption == 'no_grouping' || $groupByOption == 'date' || $groupByOption == 'user' || $groupByOption == 'client' || $groupByOption == 'project' || $groupByOption == 'task') + return true; + + // Verify custom field group by options. + if ($user->isPluginEnabled('cf')) { + global $custom_fields; + if (!$custom_fields) $custom_fields = new CustomFields(); + + // Time fields. + if ($custom_fields->timeFields) { + foreach ($custom_fields->timeFields as $timeField) { + $control_name = 'time_field_'.$timeField['id']; + if ($groupByOption == $control_name) + return true; + } + } + + // User fields. + if ($custom_fields->userFields) { + foreach ($custom_fields->userFields as $userField) { + $control_name = 'user_field_'.$userField['id']; + if ($groupByOption == $control_name) + return true; + } + } + + // Project fields. + if ($custom_fields->projectFields) { + foreach ($custom_fields->projectFields as $projectField) { + $control_name = 'project_field_'.$projectField['id']; + if ($groupByOption == $control_name) + return true; + } + } + } // if ($user->isPluginEnabled('cf')) { + + return false; + } + // makeGroupByKey builds a combined group by key from group_by1, group_by2 and group_by3 values // (passed in $options) and a row of data ($row obtained from a db query). static function makeGroupByKey($options, $row) { diff --git a/initialize.php b/initialize.php index 86bb008d..23c326ec 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.12.5791"); +define("APP_VERSION", "1.22.13.5792"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 66e182f6c0427e00a1dd1ec9244156e909bfbf77 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Fri, 12 May 2023 13:51:50 +0000 Subject: [PATCH 17/43] Securing fav reports in notifications by casting some values to int. --- WEB-INF/lib/ttFavReportHelper.class.php | 13 +++++++++++++ initialize.php | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/WEB-INF/lib/ttFavReportHelper.class.php b/WEB-INF/lib/ttFavReportHelper.class.php index 00a1bd5b..51bfa7ab 100644 --- a/WEB-INF/lib/ttFavReportHelper.class.php +++ b/WEB-INF/lib/ttFavReportHelper.class.php @@ -454,6 +454,19 @@ static function getReportOptions($id) { unset($options['id']); unset($options['status']); + // Cast to int some values similar to ttReportHelper::getReportOptions + // where we do it for extra protection against SQL injections. + // This step is redundant, though, as data validation is supposed to work. + // However, in case we missed something, casting to int reduces our risks. + $options['client_id'] = isset($options['client_id']) ? (int)$options['client_id'] : 0; + $options['project_id'] = isset($options['project_id']) ? (int)$options['project_id'] : 0; + $options['task_id'] = isset($options['task_id']) ? (int)$options['task_id'] : 0; + $options['billable'] = isset($options['billable']) ? (int)$options['billable'] : 0; + $options['invoice'] = isset($options['invoice']) ? (int)$options['invoice'] : 0; + $options['paid_status'] = isset($options['paid_status']) ? (int)$options['paid_status'] : 0; + $options['approved'] = isset($options['approved']) ? (int)$options['approved'] : 0; + $options['timesheet'] = isset($options['timesheet']) ? (int)$options['timesheet'] : 0; + // Note: special handling for NULL users field is done in cron.php // $options now is a subset of db fields from tt_fav_reports table. diff --git a/initialize.php b/initialize.php index 23c326ec..f453544e 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.13.5792"); +define("APP_VERSION", "1.22.13.5793"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 6852f65a2b4f86a0a8daed905037fb5b7d3d9c4d Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Fri, 12 May 2023 16:37:45 +0000 Subject: [PATCH 18/43] Fixed JavaScript on reports.php to work with task control population better. --- WEB-INF/templates/reports.tpl | 9 +++++++-- initialize.php | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/WEB-INF/templates/reports.tpl b/WEB-INF/templates/reports.tpl index 42cdd661..1e5f2609 100644 --- a/WEB-INF/templates/reports.tpl +++ b/WEB-INF/templates/reports.tpl @@ -77,7 +77,12 @@ function fillProjectDropdown(id) { // Remove existing content. dropdown.length = 0; - var project_reset = true; + + // project_reset is set to true if there is no previously selected project for a passed in client id. + // For example, a newly selected client is not associated with prevoisly selected project. + // This means that previous project selection is lost and we set to --- all ---. + var project_reset = selected_item ? true : false; + // Add mandatory top option. dropdown.options[0] = new Option(empty_label, '', true); @@ -133,7 +138,7 @@ function fillTaskDropdown(project_id) { var dropdown = document.getElementById("task"); // Determine previously selected item. var selected_item = dropdown.options[dropdown.selectedIndex].value; - +//alert(selected_item); // Remove existing content. dropdown.length = 0; // Add mandatory top option. diff --git a/initialize.php b/initialize.php index f453544e..4af93462 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.13.5793"); +define("APP_VERSION", "1.22.13.5794"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 604d5dd0896fa6bef433c79aacf5494b4e15b982 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Fri, 12 May 2023 16:40:50 +0000 Subject: [PATCH 19/43] Cosmetic. Comment. --- WEB-INF/templates/reports.tpl | 3 +-- initialize.php | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/WEB-INF/templates/reports.tpl b/WEB-INF/templates/reports.tpl index 1e5f2609..6c0cde76 100644 --- a/WEB-INF/templates/reports.tpl +++ b/WEB-INF/templates/reports.tpl @@ -80,7 +80,7 @@ function fillProjectDropdown(id) { // project_reset is set to true if there is no previously selected project for a passed in client id. // For example, a newly selected client is not associated with prevoisly selected project. - // This means that previous project selection is lost and we set to --- all ---. + // This means that previous project selection is lost and we set to --- all ---. var project_reset = selected_item ? true : false; // Add mandatory top option. @@ -138,7 +138,6 @@ function fillTaskDropdown(project_id) { var dropdown = document.getElementById("task"); // Determine previously selected item. var selected_item = dropdown.options[dropdown.selectedIndex].value; -//alert(selected_item); // Remove existing content. dropdown.length = 0; // Add mandatory top option. diff --git a/initialize.php b/initialize.php index 4af93462..fe8603d2 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.13.5794"); +define("APP_VERSION", "1.22.13.5795"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From d26cd7271f0a20b0a30d7d0ac55e64f165ba75dd Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Fri, 12 May 2023 18:00:06 +0000 Subject: [PATCH 20/43] Fixing missing timesheet name in emailed reports. --- WEB-INF/lib/ttReportHelper.class.php | 2 +- initialize.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index 4a04f2bb..55f73e13 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1536,7 +1536,7 @@ static function prepareReportBody($options, $comment = null) if ($options['show_invoice']) $body .= ''.htmlspecialchars($record['invoice']).''; if ($options['show_timesheet']) - $body .= ''.htmlspecialchars($record['timesheet']).''; + $body .= ''.htmlspecialchars($record['timesheet_name']).''; $body .= ''; if ($show_note_row && $record['note']) { $body .= ''; diff --git a/initialize.php b/initialize.php index fe8603d2..54ce21f6 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.13.5795"); +define("APP_VERSION", "1.22.14.5796"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 4b429dff1ad88eaa3a2e35e0a5d9d1902a1f0bf5 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Fri, 12 May 2023 18:05:58 +0000 Subject: [PATCH 21/43] Fixed alignment for timesheet name in emailed reports. --- WEB-INF/lib/ttReportHelper.class.php | 2 +- initialize.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index 55f73e13..ca99e6a1 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1536,7 +1536,7 @@ static function prepareReportBody($options, $comment = null) if ($options['show_invoice']) $body .= ''.htmlspecialchars($record['invoice']).''; if ($options['show_timesheet']) - $body .= ''.htmlspecialchars($record['timesheet_name']).''; + $body .= ''.htmlspecialchars($record['timesheet_name']).''; $body .= ''; if ($show_note_row && $record['note']) { $body .= ''; diff --git a/initialize.php b/initialize.php index 54ce21f6..67f823aa 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.14.5796"); +define("APP_VERSION", "1.22.14.5797"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From f28a652f4d7eb4df88b91c4b0f405d375e215708 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Sat, 13 May 2023 13:57:27 +0000 Subject: [PATCH 22/43] Work in progress on bringing fav report options data types in sync with regular reports. --- WEB-INF/lib/ttFavReportHelper.class.php | 18 ++++++++++++++++++ initialize.php | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/WEB-INF/lib/ttFavReportHelper.class.php b/WEB-INF/lib/ttFavReportHelper.class.php index 51bfa7ab..aed2bf6d 100644 --- a/WEB-INF/lib/ttFavReportHelper.class.php +++ b/WEB-INF/lib/ttFavReportHelper.class.php @@ -466,6 +466,24 @@ static function getReportOptions($id) { $options['paid_status'] = isset($options['paid_status']) ? (int)$options['paid_status'] : 0; $options['approved'] = isset($options['approved']) ? (int)$options['approved'] : 0; $options['timesheet'] = isset($options['timesheet']) ? (int)$options['timesheet'] : 0; + $options['period'] = isset($options['period']) ? (int)$options['period'] : 0; + + $options['show_client'] = isset($options['show_client']) ? (int)$options['show_client'] : 0; + $options['show_invoice'] = isset($options['show_invoice']) ? (int)$options['show_invoice'] : 0; + $options['show_approved'] = isset($options['show_approved']) ? (int)$options['show_approved'] : 0; + $options['show_paid'] = isset($options['show_paid']) ? (int)$options['show_paid'] : 0; + $options['show_ip'] = isset($options['show_ip']) ? (int)$options['show_ip'] : 0; + $options['show_project'] = isset($options['show_project']) ? (int)$options['show_project'] : 0; + $options['show_start'] = isset($options['show_start']) ? (int)$options['show_start'] : 0; + $options['show_duration'] = isset($options['show_duration']) ? (int)$options['show_duration'] : 0; + $options['show_cost'] = isset($options['show_cost']) ? (int)$options['show_cost'] : 0; + $options['show_task'] = isset($options['show_task']) ? (int)$options['show_task'] : 0; + $options['show_end'] = isset($options['show_end']) ? (int)$options['show_end'] : 0; + $options['show_note'] = isset($options['show_note']) ? (int)$options['show_note'] : 0; + $options['show_work_units'] = isset($options['show_work_units']) ? (int)$options['show_work_units'] : 0; + $options['show_timesheet'] = isset($options['show_timesheet']) ? (int)$options['show_timesheet'] : 0; + $options['show_files'] = isset($options['show_files']) ? (int)$options['show_files'] : 0; + $options['show_totals_only'] = isset($options['show_totals_only']) ? (int)$options['show_totals_only'] : 0; // Note: special handling for NULL users field is done in cron.php diff --git a/initialize.php b/initialize.php index 67f823aa..6ff5d4f6 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.14.5797"); +define("APP_VERSION", "1.22.14.5798"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 14d06fa90d6688677524ac6b40d344e0917152a9 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Sat, 13 May 2023 14:52:59 +0000 Subject: [PATCH 23/43] Brought fav report options data types in sync with regular report options. --- WEB-INF/lib/ttFavReportHelper.class.php | 33 ++++++++++++++++++++----- initialize.php | 2 +- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/WEB-INF/lib/ttFavReportHelper.class.php b/WEB-INF/lib/ttFavReportHelper.class.php index aed2bf6d..cf6090b2 100644 --- a/WEB-INF/lib/ttFavReportHelper.class.php +++ b/WEB-INF/lib/ttFavReportHelper.class.php @@ -485,6 +485,9 @@ static function getReportOptions($id) { $options['show_files'] = isset($options['show_files']) ? (int)$options['show_files'] : 0; $options['show_totals_only'] = isset($options['show_totals_only']) ? (int)$options['show_totals_only'] : 0; + // Note: We can't cast anything to int for custom fields because global $user object is not recycled + // yet for user id. See cron.php. Because of this we do it in adjustOptions instead. + // Note: special handling for NULL users field is done in cron.php // $options now is a subset of db fields from tt_fav_reports table. @@ -556,9 +559,15 @@ static function adjustOptions($options) { $field_name = 'time_field_'.$timeField['id']; $checkbox_field_name = 'show_'.$field_name; $field_value = ttFavReportHelper::getFieldSettingFromReportSpec($field_name, $report_spec); - $options[$field_name] = $field_value; + if ($timeField['type'] == CustomFields::TYPE_DROPDOWN) { + // Cast to int for a dropdown for extra security. + $options[$field_name] = (int)$field_value; + } elseif ($timeField['type'] == CustomFields::TYPE_TEXT) { + // No need to cast to int for a text field. + $options[$field_name] = $field_value; + } $checkbox_value = ttFavReportHelper::getFieldSettingFromReportSpec($checkbox_field_name, $report_spec); - $options[$checkbox_field_name] = $checkbox_value; + $options[$checkbox_field_name] = (int)$checkbox_value; } } // User fields. @@ -567,9 +576,15 @@ static function adjustOptions($options) { $field_name = 'user_field_'.$userField['id']; $checkbox_field_name = 'show_'.$field_name; $field_value = ttFavReportHelper::getFieldSettingFromReportSpec($field_name, $report_spec); - $options[$field_name] = $field_value; + if ($userField['type'] == CustomFields::TYPE_DROPDOWN) { + // Cast to int for a dropdown for extra security. + $options[$field_name] = (int)$field_value; + } elseif ($userField['type'] == CustomFields::TYPE_TEXT) { + // No need to cast to int for a text field. + $options[$field_name] = $field_value; + } $checkbox_value = ttFavReportHelper::getFieldSettingFromReportSpec($checkbox_field_name, $report_spec); - $options[$checkbox_field_name] = $checkbox_value; + $options[$checkbox_field_name] = (int)$checkbox_value; } } // Project fields. @@ -578,9 +593,15 @@ static function adjustOptions($options) { $field_name = 'project_field_'.$projectField['id']; $checkbox_field_name = 'show_'.$field_name; $field_value = ttFavReportHelper::getFieldSettingFromReportSpec($field_name, $report_spec); - $options[$field_name] = $field_value; + if ($projectField['type'] == CustomFields::TYPE_DROPDOWN) { + // Cast to int for a dropdown for extra security. + $options[$field_name] = (int)$field_value; + } elseif ($projectField['type'] == CustomFields::TYPE_TEXT) { + // No need to cast to int for a text field. + $options[$field_name] = $field_value; + } $checkbox_value = ttFavReportHelper::getFieldSettingFromReportSpec($checkbox_field_name, $report_spec); - $options[$checkbox_field_name] = $checkbox_value; + $options[$checkbox_field_name] = (int)$checkbox_value; } } } diff --git a/initialize.php b/initialize.php index 6ff5d4f6..e61dfcbf 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.14.5798"); +define("APP_VERSION", "1.22.14.5799"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From ca1b213332fe57befccf37b2ccf6df1e3438e15c Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Wed, 17 May 2023 16:13:12 +0000 Subject: [PATCH 24/43] Fixed validation routine for group by controls on reports. --- WEB-INF/lib/ttReportHelper.class.php | 2 +- charts.php | 4 ++-- expenses.php | 3 ++- initialize.php | 2 +- invoices.php | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index ca99e6a1..842529a6 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -1993,7 +1993,7 @@ static function validGroupByOption($groupByOption) { global $user; - if ($groupByOption == 'no_grouping' || $groupByOption == 'date' || $groupByOption == 'user' || $groupByOption == 'client' || $groupByOption == 'project' || $groupByOption == 'task') + if ($groupByOption == 'no_grouping' || $groupByOption == 'date' || $groupByOption == 'user' || $groupByOption == 'client' || $groupByOption == 'project' || $groupByOption == 'task' || $groupByOption == null) return true; // Verify custom field group by options. diff --git a/charts.php b/charts.php index a4c24f0a..9dbb95c7 100644 --- a/charts.php +++ b/charts.php @@ -37,7 +37,7 @@ header('Location: access_denied.php'); // Trying as self, but no right for self, and noone to view on behalf. exit(); } -$userDropdownSelectionId = (int) $request->getParameter('user'); // Resused below access checks. +$userDropdownSelectionId = (int)$request->getParameter('user'); // Reused below access checks. if ($request->isPost() && $request->getParameter('user')) { if ($userDropdownSelectionId == constant('ALL_USERS_OPTION_ID') && !ttAccessAllowed('view_all_charts')) { header('Location: access_denied.php'); // All users option is only for users with view_all_charts access right. @@ -56,7 +56,7 @@ // End of access checks. // Determine user for whom we display this page. -$userChanged = (int) $request->getParameter('user_changed'); +$userChanged = (int)$request->getParameter('user_changed'); if ($request->isPost() && $userChanged ) { if ($userDropdownSelectionId != constant('ALL_USERS_OPTION_ID')) { $user->setOnBehalfUser($userDropdownSelectionId); diff --git a/expenses.php b/expenses.php index ba091a63..3cbbd8be 100644 --- a/expenses.php +++ b/expenses.php @@ -33,7 +33,8 @@ exit(); } if ($request->isPost() && $request->getParameter('user')) { - if (!$user->isUserValid((int)$request->getParameter('user'))) { + $posted_user_id = $request->getParameter('user'); + if (!ttValidInteger($posted_user_id) && !$user->isUserValid($posted_user_id)) { header('Location: access_denied.php'); // Wrong user id on post. exit(); } diff --git a/initialize.php b/initialize.php index e61dfcbf..81ebc2aa 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.14.5799"); +define("APP_VERSION", "1.22.15.5800"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); diff --git a/invoices.php b/invoices.php index f1eda484..d34e1220 100644 --- a/invoices.php +++ b/invoices.php @@ -76,7 +76,7 @@ if (!ttInvoiceHelper::validSortOrder($sort_order_2)) $err->add($i18n->get('error.field'), $i18n->get('label.sort')); if ($sort_option_1 == $sort_option_2) $err->add($i18n->get('error.field'), $i18n->get('label.sort')); - if($err->no() && $request->getParameter('sorting_changed')) { + if ($err->no() && $request->getParameter('sorting_changed')) { // User changed sorting. Get invoices sorted accordingly. $sort_options = array('sort_option_1'=>$sort_option_1, 'sort_order_1'=>$sort_order_1, From d54c1aca4c19322fd4a3f23352f6eb07c1548090 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Sun, 21 May 2023 15:56:12 +0000 Subject: [PATCH 25/43] Fixed sorting of tasks in Tasks dropdown on time entry pages. --- WEB-INF/lib/ttUser.class.php | 2 +- WEB-INF/templates/time_script.tpl | 19 ++++++------------- initialize.php | 2 +- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/WEB-INF/lib/ttUser.class.php b/WEB-INF/lib/ttUser.class.php index 2bf5c029..fedf3daf 100644 --- a/WEB-INF/lib/ttUser.class.php +++ b/WEB-INF/lib/ttUser.class.php @@ -359,7 +359,7 @@ function getAssignedProjects($options = null) // Do a query with inner join to get assigned projects. $sql = "select p.id, p.name, p.description, p.tasks, upb.rate $filePart from tt_projects p $fileJoin". " inner join tt_user_project_binds upb on (upb.user_id = $user_id and upb.project_id = p.id and upb.status = 1)". - " where p.group_id = $group_id and p.org_id = $org_id and p.status = 1 order by p.name"; + " where p.group_id = $group_id and p.org_id = $org_id and p.status = 1 order by upper(p.name)"; $res = $mdb2->query($sql); if (!is_a($res, 'PEAR_Error')) { $bindTemplatesWithProjects = isset($options['include_templates']) && $options['include_templates']; diff --git a/WEB-INF/templates/time_script.tpl b/WEB-INF/templates/time_script.tpl index 141df7ea..0dafae8c 100644 --- a/WEB-INF/templates/time_script.tpl +++ b/WEB-INF/templates/time_script.tpl @@ -12,7 +12,6 @@ // project_ids[143] = "325,370,390,400"; // Comma-separated list of project ids for client. // project_names[325] = "Time Tracker"; // Project name. // task_ids[325] = "100,101,302,303,304"; // Comma-separated list ot task ids for project. -// task_names[100] = "Coding"; // Task name. // template_ids[325] = "17,21"; // Comma-separated list ot template ids for project. NOT YET IMPLEMENTED. // Prepare an array of project ids for clients. @@ -38,11 +37,6 @@ var task_ids = new Array(); {foreach $project_list as $project} task_ids[{$project.id}] = "{$project.tasks}"; {/foreach} -// Prepare an array of task names. -var task_names = new Array(); -{foreach $task_list as $task} - task_names[{$task.id}] = "{$task.name|escape:'javascript'}"; -{/foreach} // Prepare an array of template ids for projects. var template_ids = new Array(); @@ -143,7 +137,6 @@ function fillProjectDropdown(id) { // tasks associated with a selected project (project id is passed here as id). function fillTaskDropdown(id) { var str_ids = task_ids[id]; - var dropdown = document.getElementById("task"); if (dropdown == null) return; // Nothing to do. @@ -155,20 +148,20 @@ function fillTaskDropdown(id) { // Add mandatory top option. dropdown.options[0] = new Option(empty_label_task, '', true); - // Populate the dropdown from the task_names array. + // Populate the Task dropdown. if (str_ids) { var ids = new Array(); ids = str_ids.split(","); var len = ids.length; + // Iterate through $task_list because it is sorted by upper(name). var idx = 1; - for (var i = 0; i < len; i++) { - var t_id = ids[i]; - if (task_names[t_id]) { - dropdown.options[idx] = new Option(task_names[t_id], t_id); + {foreach $task_list as $task} + if (ids.includes("{$task.id}")) { + dropdown.options[idx] = new Option("{$task.name|escape:'javascript'}", {$task.id}); idx++; } - } + {/foreach} // If a previously selected item is still in dropdown - select it. if (dropdown.options.length > 0) { diff --git a/initialize.php b/initialize.php index 81ebc2aa..a1fa769c 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.15.5800"); +define("APP_VERSION", "1.22.16.5801"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 3fd640e1fe5913739ee73226368aae9efb6353c2 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Thu, 15 Jun 2023 14:15:36 +0000 Subject: [PATCH 26/43] A fix for 0 duration entries in day and week views. --- WEB-INF/lib/ttTimeHelper.class.php | 5 ++--- initialize.php | 2 +- time.php | 2 +- time_edit.php | 12 +++++------- week.php | 5 +---- 5 files changed, 10 insertions(+), 16 deletions(-) diff --git a/WEB-INF/lib/ttTimeHelper.class.php b/WEB-INF/lib/ttTimeHelper.class.php index 2e9eddbb..424c8b9c 100644 --- a/WEB-INF/lib/ttTimeHelper.class.php +++ b/WEB-INF/lib/ttTimeHelper.class.php @@ -440,8 +440,7 @@ static function insert($fields) if (!$billable) $billable = 0; if (!$paid) $paid = 0; - - if ($duration) { + if (!is_null($duration)) { $sql = "insert into tt_log (user_id, group_id, org_id, date, duration, client_id, project_id, task_id, invoice_id, comment, billable, paid, created, created_ip, created_by) ". "values ($user_id, $group_id, $org_id, ".$mdb2->quote($date).", '$duration', ".$mdb2->quote($client).", ".$mdb2->quote($project).", ".$mdb2->quote($task).", ".$mdb2->quote($invoice).", ".$mdb2->quote($note).", $billable, $paid $created_v)"; $affected = $mdb2->exec($sql); @@ -502,7 +501,7 @@ static function update($fields) if ($start) $duration = ''; - if ($duration) { + if (!is_null($duration)) { $sql = "UPDATE tt_log set start = NULL, duration = '$duration', client_id = ".$mdb2->quote($client).", project_id = ".$mdb2->quote($project).", task_id = ".$mdb2->quote($task).", ". "comment = ".$mdb2->quote($note)."$billable_part $paid_part $modified_part, date = '$date' WHERE id = $id and user_id = $user_id and group_id = $group_id and org_id = $org_id"; $affected = $mdb2->exec($sql); diff --git a/initialize.php b/initialize.php index a1fa769c..344ce607 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.16.5801"); +define("APP_VERSION", "1.22.17.5802"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); diff --git a/time.php b/time.php index 931436fa..ee90770b 100644 --- a/time.php +++ b/time.php @@ -386,7 +386,7 @@ if ($showTask && $taskRequired) { if (!$cl_task) $err->add($i18n->get('error.task')); } - if ($cl_duration == null || strlen($cl_duration) == 0) { + if ($cl_duration == null) { if ($cl_start || $cl_finish) { if (!ttTimeHelper::isValidTime($cl_start)) $err->add($i18n->get('error.field'), $i18n->get('label.start')); diff --git a/time_edit.php b/time_edit.php index 7c3491ea..7bcfe4aa 100644 --- a/time_edit.php +++ b/time_edit.php @@ -63,9 +63,9 @@ // Initialize variables. $cl_start = $cl_finish = $cl_duration = $cl_date = $cl_note = $cl_project = $cl_task = $cl_billable = null; if ($request->isPost()) { - $cl_start = trim($request->getParameter('start')); - $cl_finish = trim($request->getParameter('finish')); - $cl_duration = trim($request->getParameter('duration')); + $cl_start = is_null($request->getParameter('start')) ? null : trim($request->getParameter('start')); + $cl_finish = is_null($request->getParameter('finish')) ? null : trim($request->getParameter('finish')); + $cl_duration = is_null($request->getParameter('duration')) ? null : trim($request->getParameter('duration')); $cl_date = $request->getParameter('date'); $cl_note = trim($request->getParameter('note')); // If we have time custom fields - collect input. @@ -292,10 +292,8 @@ if ($showTask && $taskRequired) { if (!$cl_task) $err->add($i18n->get('error.task')); } - if (!$cl_duration) { - if ('0' == $cl_duration) - $err->add($i18n->get('error.field'), $i18n->get('label.duration')); - elseif ($cl_start || $cl_finish) { + if ($cl_duration == null) { + if ($cl_start || $cl_finish) { if (!ttTimeHelper::isValidTime($cl_start)) $err->add($i18n->get('error.field'), $i18n->get('label.start')); if ($cl_finish) { diff --git a/week.php b/week.php index 16a00dff..4e1e88cb 100644 --- a/week.php +++ b/week.php @@ -481,9 +481,6 @@ function render(&$table, $value, $row, $column, $selected = false) { continue; // Posted value is different. if ($existingDuration == null) { - // Skip inserting 0 duration values. - if (0 == ttTimeHelper::toMinutes($postedDuration)) - continue; // Insert a new record. $fields = array(); $fields['row_id'] = $dataArray[$rowNumber]['row_id']; @@ -524,7 +521,7 @@ function render(&$table, $value, $row, $column, $selected = false) { } } $result = ttWeekViewHelper::insertDurationFromWeekView($fields, $custom_fields, $err); - } elseif ($postedDuration == null || 0 == ttTimeHelper::toMinutes($postedDuration)) { + } elseif ($postedDuration == null) { // Delete an already existing record here. $result = ttTimeHelper::delete($dataArray[$rowNumber][$dayHeader]['tt_log_id']); } else { From c5e4c12c8675fbf5313292f16ef323a52103ab27 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Thu, 15 Jun 2023 14:43:34 +0000 Subject: [PATCH 27/43] Fixed editig of records. --- WEB-INF/lib/ttTimeHelper.class.php | 2 +- initialize.php | 2 +- time_edit.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WEB-INF/lib/ttTimeHelper.class.php b/WEB-INF/lib/ttTimeHelper.class.php index 424c8b9c..6019b2f1 100644 --- a/WEB-INF/lib/ttTimeHelper.class.php +++ b/WEB-INF/lib/ttTimeHelper.class.php @@ -499,7 +499,7 @@ static function update($fields) $finish = ttTimeHelper::to24HourFormat($finish); if ('00:00' == $finish) $finish = '24:00'; - if ($start) $duration = ''; + if ($start) $duration = null; if (!is_null($duration)) { $sql = "UPDATE tt_log set start = NULL, duration = '$duration', client_id = ".$mdb2->quote($client).", project_id = ".$mdb2->quote($project).", task_id = ".$mdb2->quote($task).", ". diff --git a/initialize.php b/initialize.php index 344ce607..07bb0ee8 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.17.5802"); +define("APP_VERSION", "1.22.17.5803"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); diff --git a/time_edit.php b/time_edit.php index 7bcfe4aa..ca951b69 100644 --- a/time_edit.php +++ b/time_edit.php @@ -354,7 +354,7 @@ $err->add($i18n->get('error.range_locked')); // 3) Prohibit saving uncompleted unlocked entries when another uncompleted entry exists. - $uncompleted = ($cl_finish == '' && $cl_duration == ''); + $uncompleted = ($cl_finish == null && $cl_duration == null); if ($uncompleted && $oneUncompleted) { $not_completed_rec = ttTimeHelper::getUncompleted($user_id); if ($not_completed_rec && ($time_rec['id'] <> $not_completed_rec['id'])) { From 144ba915c4aa883438ba5be936123e2aeec74bc1 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Thu, 15 Jun 2023 14:57:12 +0000 Subject: [PATCH 28/43] Cosmetic. --- initialize.php | 2 +- time_edit.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/initialize.php b/initialize.php index 07bb0ee8..3ebf911e 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.17.5803"); +define("APP_VERSION", "1.22.17.5804"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); diff --git a/time_edit.php b/time_edit.php index ca951b69..35037dca 100644 --- a/time_edit.php +++ b/time_edit.php @@ -117,8 +117,8 @@ // Add an info message to the form if we are editing an uncompleted record. if (strlen($cl_start) > 0 && $cl_start == $cl_finish && $cl_duration == '0:00') { - $cl_finish = ''; - $cl_duration = ''; + $cl_finish = null; + $cl_duration = null; $msg->add($i18n->get('form.time_edit.uncompleted')); } } From 334f96dbfd5389f3c4f7fc914c54ceccfa1de0fa Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Tue, 20 Jun 2023 13:10:01 +0000 Subject: [PATCH 29/43] Fixed issue #131 - unable to set some values from week view. --- WEB-INF/lib/ttWeekViewHelper.class.php | 17 ++++++++-------- initialize.php | 2 +- week.php | 27 +++++++++++++++----------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/WEB-INF/lib/ttWeekViewHelper.class.php b/WEB-INF/lib/ttWeekViewHelper.class.php index 4ee5346a..cd1e4a95 100644 --- a/WEB-INF/lib/ttWeekViewHelper.class.php +++ b/WEB-INF/lib/ttWeekViewHelper.class.php @@ -179,7 +179,7 @@ static function getDataForWeekView($records, $dayHeaders) { $dataArray[0][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'duration' => null); } } else { - for ($i = 1; $i < 6; $i++) { + for ($i = 0; $i < 5; $i++) { $control_id = '0_'. $dayHeaders[$i]; $dataArray[0][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'duration' => null); } @@ -194,7 +194,7 @@ static function getDataForWeekView($records, $dayHeaders) { $dataArray[1][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'note' => null); } } else { - for ($i = 1; $i < 6; $i++) { + for ($i = 0; $i < 5; $i++) { $control_id = '1_'. $dayHeaders[$i]; $dataArray[1][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'note' => null); } @@ -226,7 +226,7 @@ static function getDataForWeekView($records, $dayHeaders) { $dataArray[$pos][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'duration' => null); } } else { - for ($i = 1; $i < 6; $i++) { + for ($i = 0; $i < 5; $i++) { $control_id = $pos.'_'. $dayHeaders[$i]; $dataArray[$pos][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'duration' => null); } @@ -242,7 +242,7 @@ static function getDataForWeekView($records, $dayHeaders) { $dataArray[$pos][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'note' => null); } } else { - for ($i = 1; $i < 6; $i++) { + for ($i = 0; $i < 5; $i++) { $control_id = $pos.'_'. $dayHeaders[$i]; $dataArray[$pos][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'note' => null); } @@ -308,7 +308,7 @@ static function prePopulateFromPastWeeks($startDate, $dayHeaders) { $dataArray[0][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'duration' => null); } } else { - for ($i = 1; $i < 6; $i++) { + for ($i = 0; $i < 5; $i++) { $control_id = '0_'. $dayHeaders[$i]; $dataArray[0][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'duration' => null); } @@ -324,7 +324,7 @@ static function prePopulateFromPastWeeks($startDate, $dayHeaders) { $dataArray[1][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'note' => null); } } else { - for ($i = 1; $i < 6; $i++) { + for ($i = 0; $i < 5; $i++) { $control_id = '1_'. $dayHeaders[$i]; $dataArray[1][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'note' => null); } @@ -348,7 +348,7 @@ static function prePopulateFromPastWeeks($startDate, $dayHeaders) { $dataArray[$pos][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'duration' => null); } } else { - for ($i = 1; $i < 6; $i++) { + for ($i = 0; $i < 5; $i++) { $control_id = $pos.'_'. $dayHeaders[$i]; $dataArray[$pos][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'duration' => null); } @@ -364,7 +364,7 @@ static function prePopulateFromPastWeeks($startDate, $dayHeaders) { $dataArray[$pos][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'note' => null); } } else { - for ($i = 1; $i < 6; $i++) { + for ($i = 0; $i < 5; $i++) { $control_id = $pos.'_'. $dayHeaders[$i]; $dataArray[$pos][$dayHeaders[$i]] = array('control_id' => $control_id, 'tt_log_id' => null,'note' => null); } @@ -443,7 +443,6 @@ static function getLockedDaysForWeek($start_date) { $lockedDays[] = $user->isDateLocked($objDate); $objDate->incrementDay(); } - unset($objDate); return $lockedDays; } diff --git a/initialize.php b/initialize.php index 3ebf911e..57f5a366 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.17.5804"); +define("APP_VERSION", "1.22.18.5805"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); diff --git a/week.php b/week.php index 4e1e88cb..0a573bf2 100644 --- a/week.php +++ b/week.php @@ -169,10 +169,15 @@ $weekend_start_idx = 6 - $weekStartDay; $weekend_end_idx = (7 - $weekStartDay) % 7; } - unset($dayHeaders[$weekend_start_idx]); - unset($dayHeaders[$weekend_end_idx]); - unset($lockedDays[$weekend_start_idx]); - unset($lockedDays[$weekend_end_idx]); + if ($weekend_end_idx > $weekend_start_idx) { + array_splice($dayHeaders, $weekend_start_idx, 2); + array_splice($lockedDays, $weekend_start_idx, 2); + } else { + array_splice($dayHeaders, $weekend_start_idx, 1); + array_splice($dayHeaders, $weekend_end_idx, 1); + array_splice($lockedDays, $weekend_start_idx, 1); + array_splice($lockedDays, $weekend_end_idx, 1); + } } // Get already existing records. @@ -291,14 +296,14 @@ function render(&$table, $value, $row, $column, $selected = false) { // Add columns to table. $table->addColumn(new TableColumn('label', '', new LabelCellRenderer(), $dayTotals['label'])); - - -for ($i = 0; $i < 7; $i++) { - if ($showWeekends) +if ($showWeekends) { + for ($i = 0; $i < 7; $i++) { + $table->addColumn(new TableColumn($dayHeaders[$i], $dayHeaders[$i], new WeekViewCellRenderer(), $dayTotals[$dayHeaders[$i]])); + } +} +else { + for ($i = 0; $i < 5; $i++) { $table->addColumn(new TableColumn($dayHeaders[$i], $dayHeaders[$i], new WeekViewCellRenderer(), $dayTotals[$dayHeaders[$i]])); - else { - if ($i <> $weekend_start_idx && $i <> $weekend_end_idx) - $table->addColumn(new TableColumn($dayHeaders[$i], $dayHeaders[$i], new WeekViewCellRenderer(), $dayTotals[$dayHeaders[$i]])); } } $table->setInteractive(false); From 43a19fcb51a21f6e3169084ac81308a6ef751e63 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Thu, 22 Jun 2023 14:40:14 +0000 Subject: [PATCH 30/43] Addressed issue #132 - unable to change user on week view. --- initialize.php | 2 +- week.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/initialize.php b/initialize.php index 57f5a366..e9849a97 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.18.5805"); +define("APP_VERSION", "1.22.19.5806"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); diff --git a/week.php b/week.php index 0a573bf2..3c2384b9 100644 --- a/week.php +++ b/week.php @@ -411,7 +411,7 @@ function render(&$table, $value, $row, $column, $selected = false) { $form->addInput(array('type'=>'calendar','name'=>'date','value'=>$cl_date)); // A hidden control for today's date from user's browser. -$form->addInput(array('type'=>'hidden','name'=>'browser_today','value'=>'get_date()')); // User current date, which gets filled in on btn_submit click. +$form->addInput(array('type'=>'hidden','name'=>'browser_today','value'=>'')); // User current date, which gets filled in on btn_submit click. // Submit button. $form->addInput(array('type'=>'submit','name'=>'btn_submit','onclick'=>'browser_today.value=get_date()','value'=>$i18n->get('button.submit'))); From a49a5c28228a9502d0b639d610324884d2346e10 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Fri, 30 Jun 2023 18:29:45 +0000 Subject: [PATCH 31/43] Initial implementation of multiple project select for projects. --- WEB-INF/lib/form/ActionForm.class.php | 2 - WEB-INF/lib/form/Form.class.php | 10 ++ .../lib/form/MultipleSelectCombobox.class.php | 90 ++++++++++++++ WEB-INF/lib/ttFavReportHelper.class.php | 29 ++++- WEB-INF/lib/ttReportHelper.class.php | 34 +++++- WEB-INF/templates/reports.tpl | 114 +++++++++++++----- initialize.php | 2 +- reports.php | 13 +- 8 files changed, 251 insertions(+), 43 deletions(-) create mode 100644 WEB-INF/lib/form/MultipleSelectCombobox.class.php diff --git a/WEB-INF/lib/form/ActionForm.class.php b/WEB-INF/lib/form/ActionForm.class.php index 253091b4..a60800a4 100644 --- a/WEB-INF/lib/form/ActionForm.class.php +++ b/WEB-INF/lib/form/ActionForm.class.php @@ -164,14 +164,12 @@ function loadBean() { $el_list = @$_SESSION[$this->mSessionCell . "session_store_elements"]; if (is_array($el_list)) { foreach ($el_list as $ref_el) { - // restore form elements import('form.'.$ref_el["class"]); $class_name = $ref_el["class"]; $el = new $class_name($ref_el["name"]); $el->localize(); $el->setValueSafe(@$_SESSION[$this->mSessionCell . "_" .$el->getName()]); - if ($this->mForm && !isset($this->mForm->elements[$ref_el["name"]])) { $this->mForm->elements[$ref_el["name"]] = &$el; } diff --git a/WEB-INF/lib/form/Form.class.php b/WEB-INF/lib/form/Form.class.php index 27a36e9b..cee608fe 100644 --- a/WEB-INF/lib/form/Form.class.php +++ b/WEB-INF/lib/form/Form.class.php @@ -103,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/MultipleSelectCombobox.class.php b/WEB-INF/lib/form/MultipleSelectCombobox.class.php new file mode 100644 index 00000000..9139c609 --- /dev/null +++ b/WEB-INF/lib/form/MultipleSelectCombobox.class.php @@ -0,0 +1,90 @@ +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)) + foreach ($this->mOptions as $key=>$value) { + + if ($this->mDataDeep>1) { + $key = $value[$this->mDataKeys[0]]; + $value = $value[$this->mDataKeys[1]]; + } + $html .= "\n"; + } + + $html .= ""; + + return $html; + } +} diff --git a/WEB-INF/lib/ttFavReportHelper.class.php b/WEB-INF/lib/ttFavReportHelper.class.php index cf6090b2..98a5c5c3 100644 --- a/WEB-INF/lib/ttFavReportHelper.class.php +++ b/WEB-INF/lib/ttFavReportHelper.class.php @@ -97,7 +97,7 @@ static function insertReport($fields) { $org_id = $user->org_id; $sql = "insert into tt_fav_reports". - " (name, user_id, group_id, org_id, report_spec, client_id, project_id, task_id,". + " (name, user_id, group_id, org_id, report_spec, client_id, task_id,". " billable, approved, invoice, timesheet, paid_status, note_containing, users, period, period_start,". " period_end, show_client, show_invoice, show_paid, show_ip,". " show_project, show_timesheet, show_start, show_duration, show_cost,". @@ -107,7 +107,7 @@ static function insertReport($fields) { $mdb2->quote($fields['name']).", $user_id, $group_id, $org_id, ". $mdb2->quote($fields['report_spec']).", ". $mdb2->quote($fields['client']).", ". - $mdb2->quote($fields['project']).", ".$mdb2->quote($fields['task']).", ". + $mdb2->quote($fields['task']).", ". $mdb2->quote($fields['billable']).", ".$mdb2->quote($fields['approved']).", ". $mdb2->quote($fields['invoice']).", ".$mdb2->quote($fields['timesheet']).", ". $mdb2->quote($fields['paid_status']).", ".$mdb2->quote($fields['note_containing']).",". @@ -139,7 +139,7 @@ static function updateReport($fields) { "name = ".$mdb2->quote($fields['name']).", ". "report_spec = ".$mdb2->quote($fields['report_spec']).", ". "client_id = ".$mdb2->quote($fields['client']).", ". - "project_id = ".$mdb2->quote($fields['project']).", ". + //"project_id = ".$mdb2->quote($fields['project']).", ". "task_id = ".$mdb2->quote($fields['task']).", ". "billable = ".$mdb2->quote($fields['billable']).", ". "approved = ".$mdb2->quote($fields['approved']).", ". @@ -227,7 +227,7 @@ static function saveReport($bean) { 'report_spec'=>ttFavReportHelper::makeReportSpec($bean), 'client'=>$bean->getAttribute('client'), 'option'=>$bean->getAttribute('option'), - 'project'=>$bean->getAttribute('project'), + //'project'=>$bean->getAttribute('project'), 'task'=>$bean->getAttribute('task'), 'billable'=>$bean->getAttribute('include_records'), 'approved'=>$bean->getAttribute('approved'), @@ -339,10 +339,17 @@ static function loadReport(&$bean) { $bean->setAttribute($checkbox_field_name, $checkbox_value); } } + + // Project ids. + $project_ids = ttFavReportHelper::getFieldSettingFromReportSpec('project_ids', $report_spec); + if (isset($project_ids)) { + $projects = explode('&', $project_ids); + $bean->setAttribute('project', $projects); + } } $bean->setAttribute('client', $val['client_id']); - $bean->setAttribute('project', $val['project_id']); + // $bean->setAttribute('project', $val['project_id']); $bean->setAttribute('task', $val['task_id']); $bean->setAttribute('include_records', $val['billable']); $bean->setAttribute('approved', $val['approved']); @@ -459,7 +466,7 @@ static function getReportOptions($id) { // This step is redundant, though, as data validation is supposed to work. // However, in case we missed something, casting to int reduces our risks. $options['client_id'] = isset($options['client_id']) ? (int)$options['client_id'] : 0; - $options['project_id'] = isset($options['project_id']) ? (int)$options['project_id'] : 0; + $options['project_id'] = isset($options['project_id']) ? (int)$options['project_id'] : 0; // TODO: get rid of this eventually. $options['task_id'] = isset($options['task_id']) ? (int)$options['task_id'] : 0; $options['billable'] = isset($options['billable']) ? (int)$options['billable'] : 0; $options['invoice'] = isset($options['invoice']) ? (int)$options['invoice'] : 0; @@ -664,6 +671,16 @@ static function makeReportSpec($bean) { } } + // Add projects ids. Format: project_ids:25&756&567&1023. Use the & sign to join multiple project ids. + $selected_projects_in_bean = $bean->getAttribute('project'); + if (!is_array($selected_projects_in_bean)) { + $selectedProjects = $bean->getAttribute('project[]'); + } + if (is_array($selected_projects_in_bean) && !empty($selected_projects_in_bean[0])) { + $selectedProjectsSpec = join('&', $selected_projects_in_bean); + } + if (isset($selectedProjectsSpec)) $reportSpecArray[] = 'project_ids:'.$selectedProjectsSpec; + $reportSpec = null; if (isset($reportSpecArray)) $reportSpec = implode(',', $reportSpecArray); diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index 842529a6..caf86f9d 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -46,7 +46,11 @@ static function getWhere($options) { $dropdown_parts .= ' and l.client_id = '.$options['client_id']; elseif ($user->isClient() && $user->client_id) $dropdown_parts .= ' and l.client_id = '.$user->client_id; - if ($options['project_id']) $dropdown_parts .= ' and l.project_id = '.$options['project_id']; + + if (isset($options['project_ids'])) + $dropdown_parts .= ' and l.project_id in ('.$options['project_ids'].')'; + // if ($options['project_id']) $dropdown_parts .= ' and l.project_id = '.$options['project_id']; // This was here for a single select. + if ($options['task_id']) $dropdown_parts .= ' and l.task_id = '.$options['task_id']; if ($options['billable']==1) $dropdown_parts .= ' and l.billable = 1'; if ($options['billable']==2) $dropdown_parts .= ' and l.billable = 0'; @@ -1757,7 +1761,18 @@ static function getReportOptions($bean) { $options['name'] = null; // No name required. // $options['user_id'] = $user->id; // We don't use user_id in regular reports. But fav reports use it to recycle $user object in cron.php. $options['client_id'] = (int)$bean->getAttribute('client'); - $options['project_id'] = (int)$bean->getAttribute('project'); + + // Handle selected projects. Just in case check both "project[]" and "project". + $selected_projects_in_bean = $bean->getAttribute('project[]'); + if (is_array($selected_projects_in_bean) && !empty($selected_projects_in_bean[0])) { + $options['project_ids'] = join(',', $selected_projects_in_bean); + } + $selected_projects_in_bean = $bean->getAttribute('project'); + if (is_array($selected_projects_in_bean) && !empty($selected_projects_in_bean[0])) { + $options['project_ids'] = join(',', $selected_projects_in_bean); + } + // $options['project_id'] = (int)$bean->getAttribute('project'); // This was here for a single project select. + $options['task_id'] = (int)$bean->getAttribute('task'); $options['billable'] = (int)$bean->getAttribute('include_records'); $options['invoice'] = (int)$bean->getAttribute('invoice'); @@ -1900,8 +1915,19 @@ static function verifyBean($bean) { // Check invoiced / not invoiced control. if (!ttValidInteger($bean->getAttribute('invoice'), true)) return false; - // Check project id. - if (!ttValidInteger($bean->getAttribute('project'), true)) return false; + // Check selected project ids. Just in case check both project[] and project. + $selectedProjects = $bean->getAttribute('project'); + if (is_array($selectedProjects)) { + foreach ($selectedProjects as $singleProjectId) { + if (!ttValidInteger($singleProjectId, true)) return false; + } + } + $selectedProjects = $bean->getAttribute('project[]'); + if (is_array($selectedProjects)) { + foreach ($selectedProjects as $singleProjectId) { + if (!ttValidInteger($singleProjectId, true)) return false; + } + } // Check task id. if (!ttValidInteger($bean->getAttribute('task'), true)) return false; diff --git a/WEB-INF/templates/reports.tpl b/WEB-INF/templates/reports.tpl index 6c0cde76..7b844703 100644 --- a/WEB-INF/templates/reports.tpl +++ b/WEB-INF/templates/reports.tpl @@ -123,17 +123,47 @@ function fillProjectDropdown(id) { // The fillTaskDropdown function populates the task combo box with // tasks associated with a selected project_id. -function fillTaskDropdown(project_id) { - var str_task_ids; - // Get a string of comma-separated task ids. - if (project_id) { - var property = "p" + project_id; - str_task_ids = obj_tasks[property]; +function fillTaskDropdown() { + + // Collect selected options from the project dropdown. + var projectDropdown = document.getElementById("project[]"); + var selectedProjects = []; + var options = projectDropdown && projectDropdown.options; + var optionsLength = options ? options.length : 0; + var opt; + var allSelected = options[0].selected; + if (!allSelected) { + for (var i = 1; i < optionsLength; i++) { + opt = options[i]; + if (opt.selected) { + selectedProjects.push(opt.value); + } + } } - if (str_task_ids) { - var task_ids = new Array(); // Array of task ids. - task_ids = str_task_ids.split(","); + + // Iterate through all selected projects and build up an array of all relevant task ids. + var task_ids_for_selected_projects = []; + if (selectedProjects.length > 0) { + for (var i = 0; i < selectedProjects.length; i++) { + var project_id = selectedProjects[i]; + + var property = "p" + project_id; + str_task_ids = obj_tasks[property]; // Task ids for a single selected project. + + if (str_task_ids) { + var task_ids = new Array(); // Array of task ids for a single selected project. + task_ids = str_task_ids.split(","); + + for (var j = 0; j < task_ids.length; j++) { + var single_task_id = task_ids[j]; + if (task_ids_for_selected_projects.indexOf(single_task_id) === -1) + task_ids_for_selected_projects.push(single_task_id); + } + } + } } + // By now task_ids_for_selected_projects contains all relevenat tasks for all selected projects. + var taskIdsLen = task_ids_for_selected_projects.length; var dropdown = document.getElementById("task"); // Determine previously selected item. @@ -143,17 +173,17 @@ function fillTaskDropdown(project_id) { // Add mandatory top option. dropdown.options[0] = new Option(empty_label, '', true); - // Populate the dropdown with associated tasks. + // Populate the task dropdown with associated tasks. len = task_names.length; var dropdown_idx = 0; for (var i = 0; i < len; i++) { - if (!project_id) { - // No project is selected. Fill in all tasks. + if (taskIdsLen == 0) { + // No project, or --- all --- projects are selected. Fill in all tasks. dropdown.options[dropdown_idx+1] = new Option(task_names[i][1], task_names[i][0]); dropdown_idx++; - } else if (str_task_ids) { - // Project is selected and has associated tasks. Fill them in. - if (inArray(task_names[i][0], task_ids)) { + } else { + // Some projects are selected and has associated tasks. Fill them in. + if (inArray(task_names[i][0], task_ids_for_selected_projects)) { dropdown.options[dropdown_idx+1] = new Option(task_names[i][1], task_names[i][0]); dropdown_idx++; } @@ -194,30 +224,60 @@ var assigned_projects = new Array(); // selectAssignedUsers is called when a project is changed in project dropdown. // It selects users on the form who are assigned to this project. -function selectAssignedUsers(project_id) { +function selectAssignedUsers() { + + // Collect selected options from the project dropdown. + var projectDropdown = document.getElementById("project[]"); + var selectedProjects = []; + var options = projectDropdown && projectDropdown.options; + var optionsLength = options ? options.length : 0; + var opt; + var allSelected = options[0].selected; + if (!allSelected) { + for (var i = 1; i < optionsLength; i++) { + opt = options[i]; + if (opt.selected) { + selectedProjects.push(opt.value); + } + } + } + var user_id; var len; + // Iterate through document elements. for (var i = 0; i < document.reportForm.elements.length; i++) { - if ((document.reportForm.elements[i].type == 'checkbox') && (document.reportForm.elements[i].name == 'users[]')) { - user_id = document.reportForm.elements[i].value; - if (project_id) - document.reportForm.elements[i].checked = false; - else + // Iterate through 'active_users[]' checkboxes on the form. + if ((document.reportForm.elements[i].type == 'checkbox') && (document.reportForm.elements[i].name == 'users_active[]')) { + // If we have -- all --- projects or none selected, check all active user checkboxes. + if (allSelected || selectedProjects.length == 0) document.reportForm.elements[i].checked = true; + else + document.reportForm.elements[i].checked = false; + + // Obtain database user id from the checkbox value. + user_id = document.reportForm.elements[i].value; - if(assigned_projects[user_id] != undefined) + if (assigned_projects[user_id] != undefined) len = assigned_projects[user_id].length; else len = 0; - if (project_id != '') - for (var j = 0; j < len; j++) { - if (project_id == assigned_projects[user_id][j]) { - document.reportForm.elements[i].checked = true; - break; + if (!allSelected && selectedProjects.length > 0) { + // Iterate through selected projects. + for (var j = 0; j < selectedProjects.length; j++) { + var selectedProjectId = selectedProjects[j]; + + // Iterate through assigned_projects array for user. + for (var k = 0; k < len; k++) { + if (selectedProjectId == assigned_projects[user_id][k]) { + // A selected project is assigned to user. Tick the checkbox fpr user. + document.reportForm.elements[i].checked = true; + break; + } } } + } } } } diff --git a/initialize.php b/initialize.php index e9849a97..9ab337f8 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.19.5806"); +define("APP_VERSION", "1.22.20.5807"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); diff --git a/reports.php b/reports.php index 5cfdd1d8..aabeec08 100644 --- a/reports.php +++ b/reports.php @@ -84,9 +84,16 @@ if (count($project_list) == 0) $showProject = false; } if ($showProject) { - $form->addInput(array('type'=>'combobox', - 'onchange'=>'fillTaskDropdown(this.value);selectAssignedUsers(this.value);', - 'name'=>'project', + $form->addInput(array('type'=>'multipleselectcombobox', + 'onchange'=>'fillTaskDropdown();selectAssignedUsers();', + 'name'=>'project', // Multiple select now, but the name without "[]" in the end because we can't use it in reports.tpl. + // Example: {$forms.reportForm.project.control}, we can't use {$forms.reportForm.project[].control}. + // This creates an ugly complication: + // Form.class.php addInput adds "[]" to a multiple select combobox element name. + // When we verify bean in a form post, the name is "project". + // But if bean is loaded from session, the name is "project[]". + // It is unclear how to deal with this properly. + //'multiple'=>true, 'data'=>$project_list, 'datakeys'=>array('id','name'), 'empty'=>array(''=>$i18n->get('dropdown.all')))); From 7180723180dfa79447c31ce15c8fb364bd84aea5 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Fri, 30 Jun 2023 18:34:56 +0000 Subject: [PATCH 32/43] Fixed a problem in last commit. --- WEB-INF/lib/form/MultipleSelectCombobox.class.php | 2 +- initialize.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WEB-INF/lib/form/MultipleSelectCombobox.class.php b/WEB-INF/lib/form/MultipleSelectCombobox.class.php index 9139c609..701d42ec 100644 --- a/WEB-INF/lib/form/MultipleSelectCombobox.class.php +++ b/WEB-INF/lib/form/MultipleSelectCombobox.class.php @@ -2,7 +2,7 @@ /* Copyright (c) Anuko International Ltd. https://www.anuko.com License: See license.txt */ -import('form.FormElement') +import('form.FormElement'); // name CDATA #IMPLIED -- field name -- // size NUMBER #IMPLIED -- rows visible -- diff --git a/initialize.php b/initialize.php index 9ab337f8..14a405df 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.20.5807"); +define("APP_VERSION", "1.22.20.5808"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 693c92c801242d2aeac196c76640efc572e06417 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Sat, 1 Jul 2023 16:26:49 +0000 Subject: [PATCH 33/43] A fix for project multiple select on reports. --- WEB-INF/lib/form/ActionForm.class.php | 5 +++++ initialize.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/WEB-INF/lib/form/ActionForm.class.php b/WEB-INF/lib/form/ActionForm.class.php index a60800a4..358f8af5 100644 --- a/WEB-INF/lib/form/ActionForm.class.php +++ b/WEB-INF/lib/form/ActionForm.class.php @@ -69,6 +69,11 @@ function initAttributes(&$request) { if ($this->mForm) { $elements = $this->mForm->getElements(); foreach ($elements as $name=>$el) { +// TODO: Refactor this. This is a temporary fix for project multiple select. mValues should probably contain just project, not project[]. +if ($name == 'project' && isset($this->mValues['project[]'])) { + $this->mForm->elements['project']->setValue($this->mValues['project[]']); +} +// End of TODO. if ($this->mForm->elements[$name] && isset($this->mValues[$name])) { $this->mForm->elements[$name]->setValue($this->mValues[$name]); } diff --git a/initialize.php b/initialize.php index 14a405df..8b4c6c3d 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.20.5808"); +define("APP_VERSION", "1.22.20.5809"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 40ca86fed5c361d0723973985aade2d279fc096c Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Sun, 2 Jul 2023 14:29:49 +0000 Subject: [PATCH 34/43] Fixed JavaScript on reports.php to filter projects relevant to a selected client. --- WEB-INF/templates/reports.tpl | 53 +++++++++++++++++++---------------- initialize.php | 2 +- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/WEB-INF/templates/reports.tpl b/WEB-INF/templates/reports.tpl index 7b844703..485cfc12 100644 --- a/WEB-INF/templates/reports.tpl +++ b/WEB-INF/templates/reports.tpl @@ -70,31 +70,39 @@ function inArray(needle, haystack) { // projects associated with a selected client (client id is passed here as id). function fillProjectDropdown(id) { var str_ids = project_ids[id]; - var dropdown = document.getElementById("project"); + var projectDropdown = document.getElementById("project[]"); - // Determine previously selected item. - var selected_item = dropdown.options[dropdown.selectedIndex].value; + // Collect selected options from the project dropdown. + var selectedProjects = []; + var options = projectDropdown && projectDropdown.options; + var optionsLength = options ? options.length : 0; + var opt; + var allSelected = options[0].selected; + if (!allSelected) { + for (var i = 1; i < optionsLength; i++) { + opt = options[i]; + if (opt.selected) { + selectedProjects.push(opt.value); + } + } + } // Remove existing content. - dropdown.length = 0; - - // project_reset is set to true if there is no previously selected project for a passed in client id. - // For example, a newly selected client is not associated with prevoisly selected project. - // This means that previous project selection is lost and we set to --- all ---. - var project_reset = selected_item ? true : false; + projectDropdown.length = 0; // Add mandatory top option. - dropdown.options[0] = new Option(empty_label, '', true); + projectDropdown.options[0] = new Option(empty_label, '', true); // Populate project dropdown. if (!id) { // If we are here, client is not selected. var len = projects.length; for (var i = 0; i < len; i++) { - dropdown.options[i+1] = new Option(projects[i][1], projects[i][0]); - if (dropdown.options[i+1].value == selected_item) { - dropdown.options[i+1].selected = true; - project_reset = false; + projectDropdown.options[i+1] = new Option(projects[i][1], projects[i][0]); + if (selectedProjects.length > 0) { + if (inArray(projects[i][0], selectedProjects)) { + projectDropdown.options[i+1].selected = true; + } } } } else if (str_ids) { @@ -104,20 +112,17 @@ function fillProjectDropdown(id) { for (var i = 0; i < len; i++) { var p_id = ids[i]; - dropdown.options[i+1] = new Option(project_names[p_id], p_id); - if (dropdown.options[i+1].value == selected_item) { - dropdown.options[i+1].selected = true; - project_reset = false; + projectDropdown.options[i+1] = new Option(project_names[p_id], p_id); + if (selectedProjects.length > 0) { + if (inArray(projects[i][0], selectedProjects)) { + projectDropdown.options[i+1].selected = true; + } } } } - // If project selection was reset - clear the tasks dropdown. - if (project_reset) { - dropdown = document.getElementById("task"); - dropdown.length = 0; - dropdown.options[0] = new Option(empty_label, '', true); - } + // Refill task dropdown. + fillTaskDropdown(); } diff --git a/initialize.php b/initialize.php index 8b4c6c3d..977903f3 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.20.5809"); +define("APP_VERSION", "1.22.20.5810"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 007080955c2c2f64a9e33b13c9777c33ab52b8f1 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Sun, 2 Jul 2023 15:24:55 +0000 Subject: [PATCH 35/43] Fixed emailed fav reports for selected project ids. --- WEB-INF/lib/ttFavReportHelper.class.php | 9 ++++++++- initialize.php | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/WEB-INF/lib/ttFavReportHelper.class.php b/WEB-INF/lib/ttFavReportHelper.class.php index 98a5c5c3..2b247043 100644 --- a/WEB-INF/lib/ttFavReportHelper.class.php +++ b/WEB-INF/lib/ttFavReportHelper.class.php @@ -466,7 +466,14 @@ static function getReportOptions($id) { // This step is redundant, though, as data validation is supposed to work. // However, in case we missed something, casting to int reduces our risks. $options['client_id'] = isset($options['client_id']) ? (int)$options['client_id'] : 0; - $options['project_id'] = isset($options['project_id']) ? (int)$options['project_id'] : 0; // TODO: get rid of this eventually. + + // Obtain project ids from report_spec. + $project_ids = ttFavReportHelper::getFieldSettingFromReportSpec('project_ids', $options['report_spec']); + if (isset($project_ids)) { + $projects = explode('&', $project_ids); + $options['project_ids'] = join(',', $projects); + } + $options['task_id'] = isset($options['task_id']) ? (int)$options['task_id'] : 0; $options['billable'] = isset($options['billable']) ? (int)$options['billable'] : 0; $options['invoice'] = isset($options['invoice']) ? (int)$options['invoice'] : 0; diff --git a/initialize.php b/initialize.php index 977903f3..878d1efc 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.20.5810"); +define("APP_VERSION", "1.22.20.5811"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 11864b22e641930f2bde8bd903bcb0d54b4f2f29 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Tue, 4 Jul 2023 06:49:18 +0000 Subject: [PATCH 36/43] Fixed reports for expenses for multiple project select. --- WEB-INF/lib/ttReportHelper.class.php | 6 +++++- initialize.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/WEB-INF/lib/ttReportHelper.class.php b/WEB-INF/lib/ttReportHelper.class.php index caf86f9d..fb802f95 100644 --- a/WEB-INF/lib/ttReportHelper.class.php +++ b/WEB-INF/lib/ttReportHelper.class.php @@ -184,7 +184,11 @@ static function getExpenseWhere($options) { $dropdown_parts .= ' and ei.client_id = '.$options['client_id']; elseif ($user->isClient() && $user->client_id) $dropdown_parts .= ' and ei.client_id = '.$user->client_id; - if ($options['project_id']) $dropdown_parts .= ' and ei.project_id = '.$options['project_id']; + + if (isset($options['project_ids'])) + $dropdown_parts .= ' and ei.project_id in ('.$options['project_ids'].')'; + // if ($options['project_id']) $dropdown_parts .= ' and l.project_id = '.$options['project_id']; // This was here for a single select. + if ($options['invoice']==1) $dropdown_parts .= ' and ei.invoice_id is not null'; if ($options['invoice']==2) $dropdown_parts .= ' and ei.invoice_id is null'; if (isset($options['timesheet']) && ($options['timesheet']!=TIMESHEET_ALL && $options['timesheet']!=TIMESHEET_NOT_ASSIGNED)) { diff --git a/initialize.php b/initialize.php index 878d1efc..54b7cc3f 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.20.5811"); +define("APP_VERSION", "1.22.21.5812"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From a7c5d3a4e7802cba95a2ef0af0c7dd6e8c19e04a Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Wed, 5 Jul 2023 16:17:36 +0000 Subject: [PATCH 37/43] Addressed issue #135. --- WEB-INF/lib/form/MultipleSelectCombobox.class.php | 8 +++----- initialize.php | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/WEB-INF/lib/form/MultipleSelectCombobox.class.php b/WEB-INF/lib/form/MultipleSelectCombobox.class.php index 701d42ec..2c2cf050 100644 --- a/WEB-INF/lib/form/MultipleSelectCombobox.class.php +++ b/WEB-INF/lib/form/MultipleSelectCombobox.class.php @@ -69,19 +69,17 @@ function getHtml() { $html .= ">".$value."\n"; } if (is_array($this->mOptions) && (count($this->mOptions) > 0)) - foreach ($this->mOptions as $key=>$value) { + $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 .= "\n"; - } + } $html .= ""; diff --git a/initialize.php b/initialize.php index 54b7cc3f..f641f447 100644 --- a/initialize.php +++ b/initialize.php @@ -21,7 +21,7 @@ die("mysqli_report function is not available."); // No point to continue as mysqli will not work. } -define("APP_VERSION", "1.22.21.5812"); +define("APP_VERSION", "1.22.22.5813"); define("APP_DIR", dirname(__FILE__)); define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib"); define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates"); From 26e77cce18bd060221894ebf66f8b2eefd5a9c8a Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Thu, 6 Jul 2023 15:49:54 +0000 Subject: [PATCH 38/43] Addressed issue #134. --- WEB-INF/lib/common.lib.php | 6 +++++- initialize.php | 5 ++++- task_add.php | 2 +- task_edit.php | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/WEB-INF/lib/common.lib.php b/WEB-INF/lib/common.lib.php index 4bc91296..79aba106 100644 --- a/WEB-INF/lib/common.lib.php +++ b/WEB-INF/lib/common.lib.php @@ -126,7 +126,7 @@ function isTrue($val) } // ttValidString is used to check user input to validate a string. -function ttValidString($val, $emptyValid = false) +function ttValidString($val, $emptyValid = false, $maxChars = 0) { if (is_null($val)) { return $emptyValid ? true : false; @@ -140,6 +140,10 @@ function ttValidString($val, $emptyValid = false) if (stristr($val, '