- Correct mocking of dropdowns
- Correct expected value in test.
- Correct return value of function
- Remove whitespace
Signed-off-by: objec <objecttothis@gmail.com>
- Like other save_value() functions, the location_data variable is passed by reference.
- Unlike other save_value() functions, the location_data variable is not being updated with the primary key id.
- Corrected the logic in itemUpdate test
Signed-off-by: objec <objecttothis@gmail.com>
Users without the 'employees' permission can no longer impersonate other
employees when creating or editing expenses and receivings. The employee
field is now restricted to the current user for new records and shows the
stored employee for existing records.
Changes:
- Expenses controller: Add permission check in getView() and postSave()
- Receivings controller: Add permission check in getEdit() and postSave()
- Form views: Conditionally display dropdown or read-only field
Fixes#3616
- Refactored out a function to get attribute data from the row in a CSV item import.
- Refactored the naming of saveAttributeData() to better reflect the functions purpose.
- Moved the function to the attribute model where it belongs which allows testability.
Signed-off-by: objec <objecttothis@gmail.com>
- Modified generate_message in Db_log.php to be defensive.
- Created a seeder to automatically prepare the test database.
- Modified the Unit Test setup to properly seed the test database.
- Wrote a unit test to test deleting an attribute from an item through the CSV.
Signed-off-by: objec <objecttothis@gmail.com>
- Ensure total = subtotal + tax by deriving total from rounded components
- Use assertEqualsWithDelta for float comparisons in tests
- Add defensive null coalescing in calculateSummary helper
- Add missing 'count' key to test data rows
- Add testRoundingAtBoundary test case
The report had calculation inconsistencies where:
1. Per-line totals (subtotal + tax) didn't equal the total column
2. Column totals didn't match the sum of individual rows
Root cause: subtotal, tax, and total were calculated independently
using different formulas and rounding at different stages, leading to
cumulative rounding errors.
Fix:
- Use item_tax_amount from database as the source of truth for tax
- Derive subtotal from sale_amount (handling both tax_included and
tax_not_included modes correctly)
- Calculate total = subtotal + tax consistently for each line
- Override getSummaryData() to sum values from getData() rows,
ensuring summary totals match the sum of displayed rows
Fixes#4112
Add 'only_debit' filter to Daily Sales and Takings dropdown. Reuses
existing 'Sales.debit' language string for the filter label. Includes
filter default initialization in getSearch() to prevent PHP warnings.
Fixes#4439
- Remove incorrect %C mapping (was mapping century to full year)
- Add special handling for %C (century), %c (datetime), %n (newline), %t (tab), %x (date)
- Add %h mapping (same as %b for abbreviated month)
- Tighten edge-case test assertions to use assertSame/assertMatchesRegularExpression
- Add tests for new directives: %C, %c, %n, %t, %x, %h
- Fixed bug where render() was not passing caller-supplied to
generate(), causing ad-hoc tokens to be ignored
- Added %F (yyyy-MM-dd) and %D (MM/dd/yy) composite date formats to
the IntlDateFormatter pattern map
- Added test coverage for composite date format directives (%F, %D, %T, %R)
- Replaced deprecated strftime() with IntlDateFormatter
- Added proper handling for edge cases:
- Strings with '%' not in date format (e.g., 'Discount: 50%')
- Invalid date formats (e.g., '%-%-%', '%Y-%q-%bad')
- Very long strings
- Added comprehensive unit tests for Token_lib
- All date format specifiers now mapped to IntlDateFormatter patterns
Security: Prevent Host Header Injection attacks by validating HTTP_HOST
against a whitelist of allowed hostnames before constructing the baseURL.
Changes:
- Add getValidHost() method to validate HTTP_HOST against allowedHostnames
- If allowedHostnames is empty, log warning and fall back to 'localhost'
- If host not in whitelist, log warning and use first allowed hostname
- Update .env.example with allowedHostnames documentation
- Add security configuration section to INSTALL.md
- Add unit tests for host validation
This addresses the security advisory where the application constructed
baseURL from the attacker-controllable HTTP_HOST header, allowing:
- Login form phishing via manipulated form actions
- Cache poisoning via poisoned asset URLs
Fixes GHSA-jchf-7hr6-h4f3
* Fix stored XSS vulnerability in Attribute Definitions
GHSA-rvfg-ww4r-rwqf: Stored XSS via Attribute Definition Name
Security Impact:
- Authenticated users with attribute management permission can inject XSS payloads
- Payloads execute when viewing/editing attributes in admin panel
- Can steal session cookies, perform CSRF attacks, or compromise admin operations
Root Cause:
1. Input: Attributes.php postSaveDefinition() accepts definition_name without sanitization
2. Output: Views echo definition_name without proper escaping
Fix Applied:
- Input sanitization: Added FILTER_SANITIZE_FULL_SPECIAL_CHARS to definition_name and definition_unit
- Output escaping: Added esc() wrapper when displaying definition_name in views
- Defense-in-depth: htmlspecialchars on attribute values saved to database
Files Changed:
- app/Controllers/Attributes.php - Sanitize inputs on save
- app/Views/attributes/form.php - Escape output on display
- app/Views/attributes/item.php - Escape output on display
* Remove input sanitization, keep output escaping only
Use escaping on output (esc() in views) as the sole XSS prevention
measure instead of sanitizing on input. This preserves the original
data in the database while still protecting against XSS attacks.
* Add validation for definition_fk foreign key in attribute definitions
Validate definition_group input before saving:
- Must be a positive integer (> 0)
- Must exist in attribute_definitions table
- Must be of type GROUP to ensure data integrity
Also add translation for definition_invalid_group error message
in all 45 language files (English placeholder for translations).
* Refactor definition_fk validation into single conditional statement
* Add esc() to attribute value outputs for XSS protection
- Add esc() to TEXT input value in item.php
- Add esc() to definition_unit in form.php
These fields display user-provided content and need output escaping
to prevent stored XSS attacks.
* Refactor definition_group validation into separate method
Extract validation logic for definition_fk into validateDefinitionGroup()
private method to improve code readability and reduce method complexity.
Returns:
- null if input is empty (no group selected)
- false if validation fails (invalid group)
- integer ID if valid
* Add translations for definition_invalid_group in all languages
- Added proper translations for 28 languages (de, es, fr, it, nl, pl, pt-BR, ru, tr, uk, th, zh-Hans, zh-Hant, ro, sv, vi, id, el, he, fa, hu, da, sw-KE, sw-TZ, ar-LB, ar-EG)
- Set empty string for 14 languages to fallback to English (cs, hr-HR, bg, bs, ckb, hy, km, lo, ml, nb, ta, tl, ur, az)
---------
Co-authored-by: Ollama <ollama@steganos.dev>
* Fix DECIMAL attribute not respecting locale format
Issue: DECIMAL attribute values were displayed as raw database values
instead of being formatted according to the user's locale settings.
Fix:
1. Modified Attribute::get_definitions_by_flags() to optionally return
definition types along with names (new $include_types parameter)
2. Updated expand_attribute_values() in tabular_helper.php to detect
DECIMAL attributes and apply to_decimals() locale formatting
3. Updated callers (Reports, Items table) to pass include_types=true
where attributes are displayed
The DECIMAL values in table views (items, sales reports, receiving reports)
now respect the configured locale number format, matching DATE attributes
which already use locale-based formatting.
* Apply PSR-12 camelCase naming to new variables
Response to PR review comments:
- Rename to
- Rename to
- Rename to
---------
Co-authored-by: Ollama <ollama@steganos.dev>
* Fix SQL injection in suggestions column configuration
The suggestions_first_column, suggestions_second_column, and
suggestions_third_column configuration values were concatenated
directly into SQL SELECT statements without validation, allowing
SQL injection attacks through the item search suggestions.
Changes:
- Add whitelist validation in Config controller to only allow
valid column names (name, item_number, description, cost_price,
unit_price)
- Add defensive validation in Item model's get_search_suggestion_format()
and get_search_suggestion_label() methods
- Default invalid values to 'name' column for safety
- Add unit tests to verify malicious inputs are rejected
This is a critical security fix as attackers with config permissions
could inject arbitrary SQL through these configuration fields.
Vulnerability reported as additional injection point in bug report.
* Refactor: Move allowed suggestions columns to Item model constants
Extract the list of valid suggestion columns into two constants in the Item model for better cohesion:
- ALLOWED_SUGGESTIONS_COLUMNS: valid column names
- ALLOWED_SUGGESTIONS_COLUMNS_WITH_EMPTY: includes empty string for config validation
This consolidates the validation logic in one place and makes it reusable across Config controller and Item model.
* Address PR review comments: improve validation and code quality
Changes:
- Use camelCase naming for validateSuggestionsColumn() method (PSR-12)
- Add field-aware validation with different fallbacks for first vs other columns
- Handle non-string POST input by checking is_string() before validation
- Refactor duplicate validation logic into suggestionColumnIsAllowed() helper
- Use consistent camelCase variable names ($suggestionsFirstColumn)
- Update tests to validate constants and behavior rather than implementation
- Tests now focus on security properties of the allowlist itself
The validation now properly handles:
- First column: defaults to 'name' when invalid
- Second/Third columns: defaults to '' (empty) when invalid
- Non-string inputs: treated as invalid with appropriate fallback
---------
Co-authored-by: Ollama <ollama@steganos.dev>
The redirect() in getManage() returned a RedirectResponse that was never
executed, allowing unauthorized access to reports_sales. Updated method
signature to return ResponseInterface|string and properly return the
redirect response.
Refs: GHSA-94jm-c32g-48r5
Co-authored-by: Ollama <ollama@steganos.dev>
* Fix IDOR vulnerability in password change (GHSA-mcc2-8rp2-q6ch)
The previous authorization check using can_modify_employee() was too
permissive - it allowed non-admin users to change other non-admin users'
passwords. For password changes, users should only be able to change
their own password. Only admins should be able to change any user's
password.
This fix replaces the can_modify_employee() check with a stricter
authorization that only allows:
- Users to change their own password
- Admins to change any user's password
Affected endpoints:
- GET /home/changePassword/{employee_id}
- POST /home/save/{employee_id}
Added tests to verify non-admin users cannot access or change other
non-admin users' passwords.
* Address PR review feedback
- Replace header/exit redirect with proper 403 response in getChangePassword
- Refactor createNonAdminEmployee helper to accept overrides array
- Simplify tests by reusing the helper
- Update tests to expect 403 response instead of redirect
---------
Co-authored-by: Ollama <ollama@steganos.dev>
This commit adds URL-based filter persistence for table views, allowing
users to navigate away from a filtered view (e.g., clicking into sale
details) and return without losing their filter settings.
The solution uses history.replaceState() to update the URL without
triggering a page reload, providing a seamless user experience while
maintaining shareable/bookmarkable URLs.
Fixes navigation issue where filters are lost when viewing details or
navigating away from table views.
* Move filter restoration to server-side for cleaner architecture
Changes:
- Controllers now restore filters from URL query string on initial page load:
* Sales.php: Reads start_date, end_date, and filters[] from GET
* Items.php: Reads start_date, end_date, filters[], and stock_location
* Expenses.php: Reads start_date, end_date, and filters[]
* Cashups.php: Reads start_date, end_date, and filters[]
- Views now receive restored filter values from controllers:
* Server-side date override via JavaScript variables
* form_multiselect() receives $selected_filters from controller
* Removed setTimeout hack from table_filter_persistence.php
- Simplified table_filter_persistence.php:
* Now only handles URL updates on filter changes
* No longer responsible for restoring state
* Cleaner, single responsibility (client-side URL management)
Benefits:
- Works without JavaScript for initial render
- Cleaner architecture (server controls initial state)
- Client-side JS only handles "live" filter updates
- Filters persist across navigation via URL query string
- Shareable/bookmarkable URLs
How it works:
1. User visits /sales/manage?start_date=2024-01-01&filters[]=only_cash
2. Controller reads GET params and passes to view
3. View renders with correct initial filter values
4. User changes filter → JavaScript updates URL via replaceState()
5. User navigates away and back → Controller restores from URL again
* Refactor filter restoration into helper function and use PSR-12 naming
* Use array_merge with helper to reduce code duplication
---------
Co-authored-by: Ollama <ollama@steganos.dev>