Conversation
invoice_date_created should not be updated and always stay the same
* remove unnecessary settings tmp_invoice_date and tmp_due_date settings * remove unnecessary helper function reset_invoice_due_dates
Bumps [qs](https://github.com/ljharb/qs) from 6.14.0 to 6.14.1. - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](ljharb/qs@v6.14.0...v6.14.1) --- updated-dependencies: - dependency-name: qs dependency-version: 6.14.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…cing fields to handle unmigrated databases (#1381) * Initial plan * Add defensive checks for client_einvoicing fields to handle unmigrated databases Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Add version checking and extensive logging for einvoicing field access Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Add null checks for version retrieval to prevent null concatenation in logs Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update application/modules/clients/controllers/Clients.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Simplify redundant null coalescing in view.php by extracting to variable Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Add comment explaining defensive null coalescing pattern in view.php Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Fix potential null dereference in Mdl_versions::get_current_version() Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Fix incorrect property access in Mdl_versions::get_current_version() Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
… reusable helper (#1383) * Initial plan * Fix critical security vulnerabilities in guest/Get.php file access Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Fix log injection vulnerabilities in security logging Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Optimize hash calculation and strengthen path validation Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Fix HTTP header injection vulnerability in Content-Disposition Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Strengthen header injection protection with comprehensive control character filtering Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Create reusable file security helper and apply to all file access points Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Address code review feedback - improve string formatting and null handling Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Address code review feedback - improve path traversal detection and header sanitization Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> * Fix code review issues - remove redundant checks and security bypass Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com>
|
This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on this overview. Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out the documentation. |
…n permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…n permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…n permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…n permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
📝 WalkthroughWalkthroughThis release introduces security enhancements including file validation helpers, password reset rate limiting, and path-traversal protections. Additionally, it expands PayPal gateway capabilities with advanced credit card fields and Venmo support, improves e-invoicing with database migration handling, adds null-safety checks across views, and updates GitHub Actions workflows with explicit permissions. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR merges version 1.6.4-beta-1 into master, introducing security enhancements, PayPal payment improvements, and various bug fixes. The changes include comprehensive file security hardening, password reset rate limiting, SQL injection prevention, and enhanced PayPal integration with advanced credit card support.
Changes:
- Major security improvements including file upload/download validation, SQL injection fixes, and path traversal prevention
- Enhanced PayPal integration with card fields and improved error handling
- UI improvements for invoice/quote templates with conditional discount display
- Dependency updates (yarn, composer) and Docker configuration refinements
Reviewed changes
Copilot reviewed 46 out of 51 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| application/helpers/file_security_helper.php | New comprehensive file security validation helper |
| application/modules/upload/controllers/Upload.php | Enhanced file upload security with validation |
| application/modules/sessions/controllers/Sessions.php | Password reset rate limiting implementation |
| application/modules/guest/controllers/gateways/Paypal.php | Improved PayPal capture with pending status handling |
| application/libraries/gateways/PaypalLib.php | Added partner attribution and request ID generation |
| assets/core/js/paypal.js | New PayPal SDK integration with card fields |
| application/modules/settings/models/Mdl_versions.php | Fixed null reference bug in version retrieval |
| application/modules/reports/models/Mdl_reports.php | SQL injection fix with type casting |
| application/helpers/mpdf_helper.php | PDF footer handling improvements |
| yarn.lock | Dependency updates for Parcel watcher and other packages |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| AND ' . $this->db->escape($from_date) . ' <= inv.invoice_date_created | ||
| AND ' . $this->db->escape($to_date) . ' >= inv.invoice_date_created | ||
| AND ' . $minQuantity . ' <= | ||
| AND ' . $this->db->escape($minQuantity) . ' <= |
There was a problem hiding this comment.
The escaping approach is inconsistent with the pattern used on line 564. Line 687 should use explicit (int) casting like line 564, rather than using db->escape(). This maintains consistency and makes the integer type constraint explicit.
|
|
||
| public function default_order_by() | ||
| { | ||
| $this->db->order_by( 'recur_status ASC, recur_next_date ASC' ); |
There was a problem hiding this comment.
The default_order_by method has inconsistent formatting. The method body should use either 2 or 4 spaces for indentation (not mixed), and there should be a blank line after the method opening brace. The closing brace on line 67 also appears to have extra spacing.
| $this->db->order_by( 'recur_status ASC, recur_next_date ASC' ); | |
| $this->db->order_by( 'recur_status ASC, recur_next_date ASC' ); |
| // Set the status of the actual transaction (not just the API call result.) | ||
| $capture_status = mb_strtoupper($paypal_object->purchase_units[0]->payments->captures[0]->status) ?? null; | ||
|
|
||
| // If either Completed or Pending, we're treating it as completed from the buyer's perspective. | ||
| if ($capture_status === 'COMPLETED' || $capture_status === 'PENDING') { | ||
| $invoice_id = $paypal_object->purchase_units[0]->payments->captures[0]->invoice_id; | ||
| $amount = $paypal_object->purchase_units[0]->payments->captures[0]->amount->value; |
There was a problem hiding this comment.
The property access $paypal_object->purchase_units[0]->payments->captures[0]->status on line 73 should use null-safe operators or explicit null checks. The ?? null at the end doesn't prevent potential errors from accessing array indices or properties that may not exist earlier in the chain.
| // Set the status of the actual transaction (not just the API call result.) | |
| $capture_status = mb_strtoupper($paypal_object->purchase_units[0]->payments->captures[0]->status) ?? null; | |
| // If either Completed or Pending, we're treating it as completed from the buyer's perspective. | |
| if ($capture_status === 'COMPLETED' || $capture_status === 'PENDING') { | |
| $invoice_id = $paypal_object->purchase_units[0]->payments->captures[0]->invoice_id; | |
| $amount = $paypal_object->purchase_units[0]->payments->captures[0]->amount->value; | |
| // Safely extract the first capture object from the PayPal response. | |
| $capture = null; | |
| if ( | |
| isset($paypal_object->purchase_units) && | |
| is_array($paypal_object->purchase_units) && | |
| isset($paypal_object->purchase_units[0]) && | |
| isset($paypal_object->purchase_units[0]->payments) && | |
| isset($paypal_object->purchase_units[0]->payments->captures) && | |
| is_array($paypal_object->purchase_units[0]->payments->captures) && | |
| isset($paypal_object->purchase_units[0]->payments->captures[0]) | |
| ) { | |
| $capture = $paypal_object->purchase_units[0]->payments->captures[0]; | |
| } | |
| // Set the status of the actual transaction (not just the API call result.) | |
| $capture_status = null; | |
| if (is_object($capture) && isset($capture->status)) { | |
| $capture_status = mb_strtoupper($capture->status); | |
| } | |
| // If either Completed or Pending, we're treating it as completed from the buyer's perspective. | |
| if ($capture_status === 'COMPLETED' || $capture_status === 'PENDING') { | |
| $invoice_id = $capture->invoice_id; | |
| $amount = $capture->amount->value; |
| if (!$attempts) { | ||
| $attempts = []; |
There was a problem hiding this comment.
Inconsistent spacing in array initialization. Line 342 has no spaces inside the array braces, while most other array initializations in the codebase use spaces. Should be $attempts = [ ]; or consistently use no spaces throughout.
| $result = $this->limit(1)->get(); | ||
|
|
||
| // Check if any rows were returned to avoid null dereference | ||
| if ($result->query->num_rows() === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| $current_version = $result->query->row()->version_file; |
There was a problem hiding this comment.
The code accesses $result->query->num_rows() and $result->query->row(), but $result is already the query result object from $this->limit(1)->get(). It should be $result->num_rows() and $result->row() without the intermediate query property access.
| if ($thousands_separator) { | ||
| $amount = strtr($amount, [$thousands_separator => '', $decimal_point => '.']); | ||
| } else { | ||
| $amount = strtr($amount, [$decimal_point => '.']); | ||
| } |
There was a problem hiding this comment.
The condition if ($thousands_separator) on line 96 will evaluate to false when $thousands_separator is '0' or an empty string, which may not be the intended behavior. Consider using if ($thousands_separator !== null && $thousands_separator !== '') for explicit checking, or using isset() if appropriate.
| private function sanitize_for_log($value): string | ||
| { | ||
| $sanitized = (string) $value; | ||
| $sanitized = str_replace(array("\r", "\n"), ' ', $sanitized); | ||
|
|
||
| return $sanitized; | ||
| } |
There was a problem hiding this comment.
Missing return type declaration. The method should declare : void as its return type to match modern PHP standards and improve code clarity. The method signature should be private function sanitize_for_log($value): string based on the return statement.
| * @param array $items | ||
| * @return boolean | ||
| */ | ||
| private function has_discounts(array $items) : bool { |
There was a problem hiding this comment.
The return type hint uses boolean but should use bool which is the standard PHP type hint. The comment on line 277 correctly uses @return boolean for PHPDoc (which is acceptable), but the actual type hint should be : bool.
| private function has_discounts(array $items) : bool { | |
| private function has_discounts(array $items): bool | |
| { |
| AND ' . $this->db->escape($to_date) . ' >= inv2.invoice_date_created | ||
| ) | ||
| ) AND ' . $maxQuantity . ' >= | ||
| ) AND ' . $this->db->escape($maxQuantity) . ' >= |
There was a problem hiding this comment.
Inconsistent use of escaping for SQL query parameters. Line 564 uses casting with (int), but line 574 uses db->escape(). For integer values, both locations should use consistent type casting to (int) instead of mixing approaches. This ensures predictable behavior and better security.
| $CI = &get_instance(); | ||
| $CI->load->model('settings/mdl_versions'); | ||
| $current_version = $CI->mdl_versions->get_current_version(); | ||
| $current_version = $current_version ?: 'unknown'; |
There was a problem hiding this comment.
Inconsistent property access syntax. Line 314 uses $current_version = $current_version ?: 'unknown'; but could be more explicit with $current_version = $current_version ?? 'unknown'; to match the null coalescing operator pattern used elsewhere in the codebase.
| $current_version = $current_version ?: 'unknown'; | |
| $current_version = $current_version ?? 'unknown'; |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
application/views/quote_templates/public/InvoicePlane_Web.php (1)
210-218: Use$colspanfor discount rowsBoth discount rows still hardcode
colspan="4", which misaligns the table when the item‑discount column is hidden.🔧 Proposed fix
- <td class="no-bottom-border" colspan="4"></td> + <td class="no-bottom-border" colspan="<?php echo $colspan; ?>"></td> ... - <td class="no-bottom-border" colspan="4"></td> + <td class="no-bottom-border" colspan="<?php echo $colspan; ?>"></td>Also applies to: 264-270
application/views/invoice_templates/public/InvoicePlane_Web.php (1)
275-281: Legacy discount row should use dynamic colspan
colspan="4"will misalign when item discounts are hidden; use$colspanfor consistency.🔧 Proposed fix
- <td class="no-bottom-border" colspan="4"></td> + <td class="no-bottom-border" colspan="<?php echo $colspan; ?>"></td>application/views/quote_templates/pdf/InvoicePlane.php (1)
169-181: Remove the duplicate discount cell and extra closing brace.The unconditional discount
<td>duplicates output and introduces an extra}that breaks the loop/HTML structure.🛠️ Proposed fix
<?php if ($show_item_discounts) { ?> <td class="text-right"> <?php echo format_currency($item->item_discount); ?> </td> <?php } ?> - <td class="text-right"> - <?php echo format_currency($item->item_discount); ?> - </td> -<?php - } -?>application/modules/guest/controllers/gateways/Paypal.php (1)
150-162: Bug: SameshowOrderDetailsreturn value issue in error branch.This has the same bug as line 124 -
showOrderDetailsreturns an array with a Guzzle response, not a JSON string.Proposed fix
//get the order details to have the invoice id from paypal - $order_details = json_decode($this->lib_paypal->showOrderDetails($order_id)); + $order_result = $this->lib_paypal->showOrderDetails($order_id); + $order_details = null; + $invoice_id = null; + + if ($order_result['status']) { + $order_details = json_decode($order_result['response']->getBody()); + $invoice_id = $order_details->purchase_units[0]->payments->captures[0]->invoice_id ?? null; + } //record the failed transaction in the logs $this->db->insert('ip_merchant_responses', [ - 'invoice_id' => $order_details->purchase_units[0]->payments->captures[0]->invoice_id, + 'invoice_id' => $invoice_id,
🤖 Fix all issues with AI agents
In `@application/helpers/mailer_helper.php`:
- Around line 248-257: The current prefix-based referer check in the block that
sets $redirect (using $referer = $_SERVER['HTTP_REFERER'] ?? '' and
strpos($referer, $base_url) === 0) can be abused for open-redirects; replace the
prefix check with a parsed-origin comparison: use parse_url() to extract scheme
and host for both $referer and base_url(), ensure both scheme and host exactly
match before assigning $redirect = $referer, and otherwise fall back to
base_url(); keep the same variables ($referer, $redirect, $base_url) and logic
flow but validate origin via parse_url host/scheme equality to prevent malicious
domains like example.com.evil.com from passing.
- Around line 212-231: The validate_email_address function mishandles spaces and
mixed delimiters and uses PHP 8-only str_contains; change it to split on both
commas and semicolons (e.g. use preg_split or explode with a regex like /[;,]/),
trim each resulting $emailItem, ignore empty items, validate each trimmed
address with filter_var(FILTER_VALIDATE_EMAIL), and replace str_contains usage
with a PHP 7-compatible check (e.g. strpos) or avoid it entirely by always using
the delimiter-agnostic split in validate_email_address to ensure robust
validation.
In `@application/helpers/mpdf_helper.php`:
- Around line 97-99: The 'footerWithPageNumbers' footer defined via
mpdf->DefHTMLFooterByName currently only injects the quote/invoice footer HTML
and omits the page-number block, so the quote page-number toggle won't display;
update the mpdf->DefHTMLFooterByName('footerWithPageNumbers', ...) call to
include the same page-number HTML used for invoices (i.e., the page number
container/placeholder used elsewhere) alongside
CI->mdl_settings->settings['pdf_quote_footer'] (and invoice footer if intended)
so its behavior matches the invoice footer; ensure the other footer definitions
(footer, defaultFooter) remain unchanged.
In `@application/modules/guest/controllers/gateways/Paypal.php`:
- Around line 122-126: The code incorrectly json_decodes the return of
$this->lib_paypal->showOrderDetails($order_id) because showOrderDetails returns
an array with ['status'=>..., 'response'=><GuzzleResponse>]; update the logic to
call showOrderDetails into a variable (e.g. $result), verify $result['status']
is true, extract the Guzzle response via $result['response'], read its body
(->getBody()->getContents() or cast to string), json_decode that body to
populate $order_details, then pull invoice_id from
$order_details->purchase_units[0]->payments->captures[0]->invoice_id (with null
checks); replace the existing direct
json_decode($this->lib_paypal->showOrderDetails(...)) usage accordingly and
handle failure cases when status is false or body decoding fails.
In `@application/modules/guest/views/gateways/paypal.php`:
- Around line 52-64: The PayPal JS config is interpolating PHP variables
directly into window.PayPalConfig (advEnabled, venmoEnabled, clientId, currency,
invoiceUrlKey, createOrderUrl, capturePaymentUrl, successUrl, errorUrl), which
risks breaking JS or enabling XSS; update the template to emit these values
using PHP's json_encode (or json_encode the entire associative array and echo
it) so booleans, strings and URLs are safely escaped for JavaScript (ensure
calls that build URLs like site_url(...) are json_encoded before embedding into
the JS object).
- Around line 50-51: The PHP conditional closing tag "<?php endif; ?>" is
currently placed inside the inner "col-xs-12" div causing mismatched HTML when
$adv_enabled is false; move the "<?php endif; ?>" so the conditional wraps the
entire card block (or relocate it to after the outer card container's closing
</div>) ensuring the outer div that began at the "col-xs-12" start is always
closed, and verify that the conditional opening for $adv_enabled and its
corresponding "<?php endif; ?>" enclose complete HTML elements rather than
splitting container tags.
In `@application/modules/reports/models/Mdl_reports.php`:
- Around line 214-215: Remove the (int) casts on $minQuantity and $maxQuantity
so monetary thresholds keep decimal precision (these are compared against
SUM(invoice_item_subtotal)/SUM(invoice_total)), and instead cast to (float) or
validate with is_numeric() to preserve cents; also change the truthy check from
if ($maxQuantity) to an explicit numeric/null check (e.g., if
(is_numeric($maxQuantity)) or if ($maxQuantity !== null && $maxQuantity !== '')
) so a legitimate zero maximum is honored; update any related variable usage or
SQL parameter binding in the same function to use the new float/validated values
(or rename if these are actually quantities vs monetary thresholds).
- Around line 217-218: The current logic in Mdl_reports.php sets $from_date and
$to_date to today's date when empty, causing mandatory filtering to today;
change this to match the pattern used by sales_by_client and payment_history by
leaving $from_date and $to_date as NULL/empty when the inputs are empty and only
applying the date WHERE clauses if those variables are provided. Update the
handling around date_to_mysql(...) and any subsequent query-building in the same
function (referencing $from_date, $to_date, and date_to_mysql) so date filters
are skipped when inputs are not supplied rather than defaulting to
date('Y-m-d').
In `@application/modules/sessions/controllers/Sessions.php`:
- Around line 331-359: The function _is_ip_rate_limited_password_reset filters
out old timestamps but never persists the pruned array back to session, causing
permanent lockout; update the session after pruning by calling the session
setter with the same session key (session_key built from
'password_reset_attempts_' . md5($ip_address)) e.g. persist the filtered
$attempts via $this->session->set_userdata($session_key, $attempts) before
returning, and apply the same fix to the other similar block referenced (lines
392-419) so both pruning operations write the cleaned list back to session.
In `@assets/core/js/paypal.js`:
- Around line 217-218: The fetch calls that capture payments currently use GET
and should be changed to POST: update the onApprove handler's fetch invocation
(referenced by onApprove and config.capturePaymentUrl) to use method: 'POST'
instead of 'GET' and likewise update the second fetch call at the other
occurrence around line 250; also ensure the server-side payment capture
controller is modified to accept POST requests and read the order ID from the
request body (or POST parameters) rather than the URL/query string.
In `@index.php`:
- Around line 306-311: The cleanup code assumes glob() returns arrays but glob()
can return false, causing warnings when passed to array_merge; update the $files
assembly to normalize glob results to arrays (e.g., coerce each
glob(UPLOADS_TEMP_FOLDER . '*.pdf') and glob(UPLOADS_TEMP_FOLDER . '*.xml') to
[] when false) before calling array_merge, then proceed with array_map('unlink',
$files) so unlink only receives a proper array; reference the existing symbols
$files, array_merge, glob, UPLOADS_TEMP_FOLDER, array_map and unlink when making
the change.
🧹 Nitpick comments (11)
application/modules/mailer/helpers/phpmailer_helper.php (1)
100-123: Harden recipient parsing and confirm PHP version forstr_contains.Common inputs like
"a@b.com, b@c.com"or mixed delimiters can produce invalid addresses due to whitespace/semicolon leftovers. Also,str_containsrequires PHP 8—please verify the minimum PHP version or use a delimiter-agnostic split.💡 Suggested parsing (handles commas/semicolons + trims + skips empties)
- $to = (str_contains($to, ',')) ? explode(',', $to) : explode(';', $to); + $to = preg_split('/\s*[;,]\s*/', $to, -1, PREG_SPLIT_NO_EMPTY); ... - $cc = (str_contains($cc, ',')) ? explode(',', $cc) : explode(';', $cc); + $cc = preg_split('/\s*[;,]\s*/', $cc, -1, PREG_SPLIT_NO_EMPTY); ... - $bcc = (str_contains($bcc, ',')) ? explode(',', $bcc) : explode(';', $bcc); + $bcc = preg_split('/\s*[;,]\s*/', $bcc, -1, PREG_SPLIT_NO_EMPTY);resources/docker/php-fpm/Dockerfile (1)
28-28: Removelibonig-dev— it's not needed for the pre-built mbstring in php:8.1-fpm.The base image
php:8.1-fpmincludes the mbstring extension pre-compiled and enabled by default. Thelibonig-devpackage is only required when manually compiling mbstring from source, which is not the case here. Removing it will reduce the image size without impact.application/helpers/pdf_helper.php (1)
129-137: Reduce duplicate XML ID validation calls.
is_valid_xml_config_id()is invoked twice andfile_exists()is redundant. Caching the result improves readability and avoids duplicate checks.♻️ Refactor suggestion
- $path = APPPATH . 'helpers/XMLconfigs/'; - - // Security: Validate XML config ID to prevent path traversal - if ($xml_id && is_valid_xml_config_id($xml_id) && file_exists($path . $xml_id . '.php') && include $path . $xml_id . '.php') { + $path = APPPATH . 'helpers/XMLconfigs/'; + $xml_id_valid = $xml_id && is_valid_xml_config_id($xml_id); + + // Security: Validate XML config ID to prevent path traversal + if ($xml_id_valid && include $path . $xml_id . '.php') { $embed_xml = $xml_setting['embedXML']; $XMLname = $xml_setting['XMLname']; $options = (empty($xml_setting['options']) ? $options : $xml_setting['options']); // Optional $generator = (empty($xml_setting['generator']) ? $generator : $xml_setting['generator']); // Optional - } elseif ($xml_id && !is_valid_xml_config_id($xml_id)) { + } elseif ($xml_id && ! $xml_id_valid) { log_message('error', trans('log_invalid_xml_config_id_pdf_helper') . ': ' . $xml_id); }assets/core/css/paypal.css (1)
49-53: Deduplicate#card-spinnerstylesThere are two
#card-spinnerblocks; keeping just one avoids confusion and accidental overrides.♻️ Suggested cleanup
-/* Spinner styling */ -#card-spinner { - display: none; - margin-left: 10px; -}Also applies to: 84-88
application/libraries/gateways/PaypalLib.php (1)
153-165: Avoidmb_str_splitdependency unless guaranteed
mb_str_splitrequires ext‑mbstring. If that extension isn’t guaranteed in your deployment, this can fatal. Considerstr_splitor explicitly require mbstring.♻️ Suggested change
- $uuid = vsprintf('%s%s-%s-%s-%s-%s%s%s', mb_str_split(bin2hex($data), 4)); + $uuid = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));application/modules/clients/controllers/Clients.php (1)
363-399: Avoid per-client version lookups & log spam when schema is unmigrated.Line 363–399: When fields are missing, this runs for every client in list views. Consider caching the version and logging once per request to reduce DB calls and log noise.
♻️ Possible refactor (log/cache once per request)
- if (!property_exists($client, 'client_einvoicing_active') || !property_exists($client, 'client_einvoicing_version')) { - // Fields don't exist - database hasn't been migrated to 1.6.3+ - $this->load->model('settings/mdl_versions'); - $current_version = $this->mdl_versions->get_current_version(); - $current_version = $current_version ?: 'unknown'; - $currentVersionForLog = $this->sanitize_for_log($current_version); - - log_message('warning', '[eInvoicing] Database version mismatch detected in check_client_einvoice_active: Running source code 1.6.3+ with database version ' . $currentVersionForLog); - log_message('warning', '[eInvoicing] Missing fields: client_einvoicing_active and client_einvoicing_version not found in client object (client_id=' . $clientIdForLog . ')'); - log_message('warning', '[eInvoicing] Please run database migration 039_1.6.3.sql to add these fields'); + static $einvoiceSchemaChecked = false; + static $cachedVersion = null; + if (!property_exists($client, 'client_einvoicing_active') || !property_exists($client, 'client_einvoicing_version')) { + if (!$einvoiceSchemaChecked) { + $this->load->model('settings/mdl_versions'); + $cachedVersion = $this->mdl_versions->get_current_version() ?: 'unknown'; + $currentVersionForLog = $this->sanitize_for_log($cachedVersion); + log_message('warning', '[eInvoicing] Database version mismatch detected in check_client_einvoice_active: Running source code 1.6.3+ with database version ' . $currentVersionForLog); + log_message('warning', '[eInvoicing] Missing fields: client_einvoicing_active and client_einvoicing_version not found in client object (client_id=' . $clientIdForLog . ')'); + log_message('warning', '[eInvoicing] Please run database migration 039_1.6.3.sql to add these fields'); + $einvoiceSchemaChecked = true; + }application/modules/clients/views/view.php (1)
309-313: Consider a fallback if get_xml_full_name() returns null.Line 309–313: If the XML config is invalid/missing,
get_xml_full_name()returns null and the UI shows a blank value. Prefer a clear fallback (e.g., “none”) for consistency.♻️ Possible tweak
- echo (($client->client_einvoicing_active ?? 0) && $einvoicing_version) ? get_xml_full_name($einvoicing_version) : trans('none'); + echo (($client->client_einvoicing_active ?? 0) && $einvoicing_version) + ? (get_xml_full_name($einvoicing_version) ?: trans('none')) + : trans('none');application/modules/guest/controllers/Payment_information.php (1)
124-128: Normalize venmo flag to boolean for consistency.Line 124–128:
advanced_credit_cardsis a boolean butvenmois passed as raw setting value. Casting avoids strict-check surprises in the view/JS.♻️ Suggested change
- 'venmo' => get_setting('gateway_paypal_venmo'), + 'venmo' => (int) get_setting('gateway_paypal_venmo') === 1,assets/core/js/paypal.js (1)
213-226: Consider adding response validation before parsing JSON.The
fetchcalls assume the response is always valid JSON. If the server returns an error page or non-JSON response,response.json()will throw, but the error won't be user-friendly.Proposed improvement
createOrder() { - return fetch(config.createOrderUrl, { method: "GET" }) - .then(response => response.json()) - .then(order => order.id); + return fetch(config.createOrderUrl, { method: "GET" }) + .then(response => { + if (!response.ok) { + throw new Error('Failed to create order: HTTP ' + response.status); + } + return response.json(); + }) + .then(order => { + if (!order || !order.id) { + throw new Error('Invalid order response'); + } + return order.id; + }); },application/modules/upload/controllers/Upload.php (2)
178-184: Redundant checks after basename() and potential log injection.After
basename()on line 175, the filename cannot contain/or\\by definition. More importantly, line 182 logs the raw$filenameafterbasename(), which could still contain malicious content for log injection (e.g., newlines aren't stripped by basename).Proposed fix using hash_for_logging
// Security: Check for path traversal attempts before sanitization if (str_contains($filename, '..') || str_contains($filename, '/') || str_contains($filename, '\\') || str_contains($filename, "\0")) { - log_message('error', 'Path traversal attempt detected in filename: ' . $filename); + log_message('error', 'Path traversal attempt detected in filename (hash: ' . hash_for_logging($filename) . ')'); return ''; }
54-60: MIME validation silently skipped without fileinfo extension.When the
fileinfoextension isn't loaded, MIME type validation is silently skipped. This could allow bypassing content-type restrictions. Consider logging a warning or requiring the extension.Proposed improvement
- if (extension_loaded('fileinfo')) { - $this->validate_mime_type(mime_content_type($tempFile)); - } + if (extension_loaded('fileinfo')) { + $this->validate_mime_type(mime_content_type($tempFile)); + } else { + log_message('warning', 'fileinfo extension not loaded - MIME validation skipped'); + }
| /** | ||
| * Validate email address syntax | ||
| * $email string can be a single email or a list of emails. | ||
| * The emails can either be comma (,) or semicolon (;) separated. | ||
| * | ||
| * @param string $email | ||
| * | ||
| * @return bool returs true if all emails are valid otherwise false | ||
| */ | ||
| function validate_email_address(string $email): bool | ||
| { | ||
| $emails = (str_contains($email, ',')) ? explode(',', $email) : explode(';', $email); | ||
|
|
||
| foreach ($emails as $emailItem) { | ||
| if ( ! filter_var($emailItem, FILTER_VALIDATE_EMAIL)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; |
There was a problem hiding this comment.
Email validation rejects common inputs and depends on PHP 8 str_contains.
Addresses like "a@b.com, b@c.com" (with spaces) or mixed delimiters will currently fail. Also, str_contains requires PHP 8. Consider a delimiter-agnostic split and trim.
✅ Robust validation (trims + handles commas/semicolons + avoids PHP 8 dependency)
-function validate_email_address(string $email): bool
-{
- $emails = (str_contains($email, ',')) ? explode(',', $email) : explode(';', $email);
-
- foreach ($emails as $emailItem) {
- if ( ! filter_var($emailItem, FILTER_VALIDATE_EMAIL)) {
- return false;
- }
- }
-
- return true;
-}
+function validate_email_address(string $email): bool
+{
+ $emails = preg_split('/\s*[;,]\s*/', $email, -1, PREG_SPLIT_NO_EMPTY);
+ if (empty($emails)) {
+ return false;
+ }
+ foreach ($emails as $emailItem) {
+ if ( ! filter_var(trim($emailItem), FILTER_VALIDATE_EMAIL)) {
+ return false;
+ }
+ }
+ return true;
+}🤖 Prompt for AI Agents
In `@application/helpers/mailer_helper.php` around lines 212 - 231, The
validate_email_address function mishandles spaces and mixed delimiters and uses
PHP 8-only str_contains; change it to split on both commas and semicolons (e.g.
use preg_split or explode with a regex like /[;,]/), trim each resulting
$emailItem, ignore empty items, validate each trimmed address with
filter_var(FILTER_VALIDATE_EMAIL), and replace str_contains usage with a PHP
7-compatible check (e.g. strpos) or avoid it entirely by always using the
delimiter-agnostic split in validate_email_address to ensure robust validation.
| // Use provided redirect, or validate HTTP_REFERER against base_url | ||
| if (empty($redirect)) { | ||
| $referer = $_SERVER['HTTP_REFERER'] ?? ''; | ||
| $base_url = base_url(); | ||
| // Only use referer if it's from same domain | ||
| if (!empty($referer) && strpos($referer, $base_url) === 0) { | ||
| $redirect = $referer; | ||
| } else { | ||
| $redirect = base_url(); // Safe default | ||
| } |
There was a problem hiding this comment.
Prefix-based referer check allows open-redirects.
A referer like https://example.com.evil.com (or https://example.com@evil.com) can pass the prefix check. Use parse_url host/scheme comparison instead.
🔒 Safer same-origin validation
- if (empty($redirect)) {
- $referer = $_SERVER['HTTP_REFERER'] ?? '';
- $base_url = base_url();
- // Only use referer if it's from same domain
- if (!empty($referer) && strpos($referer, $base_url) === 0) {
- $redirect = $referer;
- } else {
- $redirect = base_url(); // Safe default
- }
- }
+ if (empty($redirect)) {
+ $referer = $_SERVER['HTTP_REFERER'] ?? '';
+ $base_url = base_url();
+ $refParts = $referer ? parse_url($referer) : null;
+ $baseParts = parse_url($base_url);
+
+ if (
+ $refParts && $baseParts &&
+ isset($refParts['host'], $baseParts['host']) &&
+ strcasecmp($refParts['host'], $baseParts['host']) === 0 &&
+ ($refParts['scheme'] ?? '') === ($baseParts['scheme'] ?? '') &&
+ ($refParts['port'] ?? null) === ($baseParts['port'] ?? null)
+ ) {
+ $redirect = $referer;
+ } else {
+ $redirect = $base_url; // Safe default
+ }
+ }🤖 Prompt for AI Agents
In `@application/helpers/mailer_helper.php` around lines 248 - 257, The current
prefix-based referer check in the block that sets $redirect (using $referer =
$_SERVER['HTTP_REFERER'] ?? '' and strpos($referer, $base_url) === 0) can be
abused for open-redirects; replace the prefix check with a parsed-origin
comparison: use parse_url() to extract scheme and host for both $referer and
base_url(), ensure both scheme and host exactly match before assigning $redirect
= $referer, and otherwise fall back to base_url(); keep the same variables
($referer, $redirect, $base_url) and logic flow but validate origin via
parse_url host/scheme equality to prevent malicious domains like
example.com.evil.com from passing.
| $mpdf->DefHTMLFooterByName('footerWithPageNumbers', '<div id="footer">' . $CI->mdl_settings->settings['pdf_invoice_footer'] . '</div><div id="footer">' . $CI->mdl_settings->settings['pdf_quote_footer'] . '</div>'); | ||
| $mpdf->DefHTMLFooterByName('footer', '<div id="footer">' . $CI->mdl_settings->settings['pdf_quote_footer'] . '</div>'); | ||
| $mpdf->DefHTMLFooterByName('defaultFooter', '<div id="footer">' . $CI->mdl_settings->settings['pdf_quote_footer'] . '</div>'); |
There was a problem hiding this comment.
Quote footerWithPageNumbers doesn’t actually include page numbers.
The template’s optional page-number toggle won’t work for quotes as-is. Add the page number block to match invoice behavior.
🛠️ Proposed fix
- $mpdf->DefHTMLFooterByName('footerWithPageNumbers', '<div id="footer">' . $CI->mdl_settings->settings['pdf_invoice_footer'] . '</div><div id="footer">' . $CI->mdl_settings->settings['pdf_quote_footer'] . '</div>');
+ $mpdf->DefHTMLFooterByName(
+ 'footerWithPageNumbers',
+ '<div id="footer">' . $CI->mdl_settings->settings['pdf_invoice_footer'] . '</div>'
+ . '<div id="footer">' . $CI->mdl_settings->settings['pdf_quote_footer'] . '</div>'
+ . '<div><p align="center">' . str_replace('_', ' ', $filename) . ' - ' . trans('page') . ' {PAGENO} / {nbpg}</p></div>'
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $mpdf->DefHTMLFooterByName('footerWithPageNumbers', '<div id="footer">' . $CI->mdl_settings->settings['pdf_invoice_footer'] . '</div><div id="footer">' . $CI->mdl_settings->settings['pdf_quote_footer'] . '</div>'); | |
| $mpdf->DefHTMLFooterByName('footer', '<div id="footer">' . $CI->mdl_settings->settings['pdf_quote_footer'] . '</div>'); | |
| $mpdf->DefHTMLFooterByName('defaultFooter', '<div id="footer">' . $CI->mdl_settings->settings['pdf_quote_footer'] . '</div>'); | |
| $mpdf->DefHTMLFooterByName( | |
| 'footerWithPageNumbers', | |
| '<div id="footer">' . $CI->mdl_settings->settings['pdf_invoice_footer'] . '</div>' | |
| . '<div id="footer">' . $CI->mdl_settings->settings['pdf_quote_footer'] . '</div>' | |
| . '<div><p align="center">' . str_replace('_', ' ', $filename) . ' - ' . trans('page') . ' {PAGENO} / {nbpg}</p></div>' | |
| ); | |
| $mpdf->DefHTMLFooterByName('footer', '<div id="footer">' . $CI->mdl_settings->settings['pdf_quote_footer'] . '</div>'); | |
| $mpdf->DefHTMLFooterByName('defaultFooter', '<div id="footer">' . $CI->mdl_settings->settings['pdf_quote_footer'] . '</div>'); |
🤖 Prompt for AI Agents
In `@application/helpers/mpdf_helper.php` around lines 97 - 99, The
'footerWithPageNumbers' footer defined via mpdf->DefHTMLFooterByName currently
only injects the quote/invoice footer HTML and omits the page-number block, so
the quote page-number toggle won't display; update the
mpdf->DefHTMLFooterByName('footerWithPageNumbers', ...) call to include the same
page-number HTML used for invoices (i.e., the page number container/placeholder
used elsewhere) alongside CI->mdl_settings->settings['pdf_quote_footer'] (and
invoice footer if intended) so its behavior matches the invoice footer; ensure
the other footer definitions (footer, defaultFooter) remain unchanged.
| // If we can't get invoice_id from captures, try to get it from order details | ||
| if ( ! $invoice_id) { | ||
| $order_details = json_decode($this->lib_paypal->showOrderDetails($order_id)); | ||
| $invoice_id = $order_details->purchase_units[0]->payments->captures[0]->invoice_id ?? null; | ||
| } |
There was a problem hiding this comment.
Bug: showOrderDetails returns array, not JSON string.
Based on the PaypalLib.php snippet, showOrderDetails returns ['status' => bool, 'response' => $response] where $response is a Guzzle response object. The current code attempts to json_decode the array directly, which will return null.
Proposed fix
// If we can't get invoice_id from captures, try to get it from order details
if ( ! $invoice_id) {
- $order_details = json_decode($this->lib_paypal->showOrderDetails($order_id));
- $invoice_id = $order_details->purchase_units[0]->payments->captures[0]->invoice_id ?? null;
+ $order_result = $this->lib_paypal->showOrderDetails($order_id);
+ if ($order_result['status']) {
+ $order_details = json_decode($order_result['response']->getBody());
+ $invoice_id = $order_details->purchase_units[0]->payments->captures[0]->invoice_id ?? null;
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // If we can't get invoice_id from captures, try to get it from order details | |
| if ( ! $invoice_id) { | |
| $order_details = json_decode($this->lib_paypal->showOrderDetails($order_id)); | |
| $invoice_id = $order_details->purchase_units[0]->payments->captures[0]->invoice_id ?? null; | |
| } | |
| // If we can't get invoice_id from captures, try to get it from order details | |
| if ( ! $invoice_id) { | |
| $order_result = $this->lib_paypal->showOrderDetails($order_id); | |
| if ($order_result['status']) { | |
| $order_details = json_decode($order_result['response']->getBody()); | |
| $invoice_id = $order_details->purchase_units[0]->payments->captures[0]->invoice_id ?? null; | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@application/modules/guest/controllers/gateways/Paypal.php` around lines 122 -
126, The code incorrectly json_decodes the return of
$this->lib_paypal->showOrderDetails($order_id) because showOrderDetails returns
an array with ['status'=>..., 'response'=><GuzzleResponse>]; update the logic to
call showOrderDetails into a variable (e.g. $result), verify $result['status']
is true, extract the Guzzle response via $result['response'], read its body
(->getBody()->getContents() or cast to string), json_decode that body to
populate $order_details, then pull invoice_id from
$order_details->purchase_units[0]->payments->captures[0]->invoice_id (with null
checks); replace the existing direct
json_decode($this->lib_paypal->showOrderDetails(...)) usage accordingly and
handle failure cases when status is false or body decoding fails.
| <?php endif; ?> | ||
| </div> |
There was a problem hiding this comment.
HTML structure issue: endif closes inside wrong container.
The <?php endif; ?> on line 50 is inside the col-xs-12 div that started on line 21, but the div on line 51 closes the card container. This creates mismatched nesting when $adv_enabled is false - the outer div from line 21 won't be closed.
Suggested fix
</div>
- <?php endif; ?>
</div>
+<?php endif; ?>Or restructure so the conditional wraps complete HTML blocks.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <?php endif; ?> | |
| </div> | |
| </div> | |
| <?php endif; ?> |
🤖 Prompt for AI Agents
In `@application/modules/guest/views/gateways/paypal.php` around lines 50 - 51,
The PHP conditional closing tag "<?php endif; ?>" is currently placed inside the
inner "col-xs-12" div causing mismatched HTML when $adv_enabled is false; move
the "<?php endif; ?>" so the conditional wraps the entire card block (or
relocate it to after the outer card container's closing </div>) ensuring the
outer div that began at the "col-xs-12" start is always closed, and verify that
the conditional opening for $adv_enabled and its corresponding "<?php endif; ?>"
enclose complete HTML elements rather than splitting container tags.
| $minQuantity = (int) $minQuantity; | ||
| $maxQuantity = (int) $maxQuantity; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check if the file exists and get an overview
file="application/modules/reports/models/Mdl_reports.php"
if [ -f "$file" ]; then
wc -l "$file"
else
echo "File not found at expected path"
find . -name "Mdl_reports.php" -type f 2>/dev/null
fiRepository: InvoicePlane/InvoicePlane
Length of output: 123
🏁 Script executed:
# Read the context around lines 214-215 (casting section)
sed -n '200,230p' application/modules/reports/models/Mdl_reports.php | cat -nRepository: InvoicePlane/InvoicePlane
Length of output: 1342
🏁 Script executed:
# Read the context around lines 564-575 (mentioned in scratchpad)
sed -n '550,590p' application/modules/reports/models/Mdl_reports.php | cat -nRepository: InvoicePlane/InvoicePlane
Length of output: 3010
🏁 Script executed:
# Read the context around lines 687-688 (mentioned in scratchpad)
sed -n '675,700p' application/modules/reports/models/Mdl_reports.php | cat -nRepository: InvoicePlane/InvoicePlane
Length of output: 1837
🏁 Script executed:
# Search for where minQuantity and maxQuantity are defined/used
rg -n "minQuantity|maxQuantity" application/modules/reports/models/Mdl_reports.php -B 2 -A 2Repository: InvoicePlane/InvoicePlane
Length of output: 3502
Remove integer casting; thresholds should preserve decimal precision when compared against monetary totals.
The (int) casts at lines 214-215 truncate cents from monetary thresholds. Since the SQL compares these against SUM(invoice_item_subtotal) and SUM(invoice_total) (decimal values), integer casting changes report filtering accuracy. Additionally, if ($maxQuantity) at line 228 would fail for a legitimate zero maximum. Either preserve decimal precision using (float) or null checks, or clarify whether these are truly intended as quantity filters (in which case the variable names and SQL logic need correction).
🤖 Prompt for AI Agents
In `@application/modules/reports/models/Mdl_reports.php` around lines 214 - 215,
Remove the (int) casts on $minQuantity and $maxQuantity so monetary thresholds
keep decimal precision (these are compared against
SUM(invoice_item_subtotal)/SUM(invoice_total)), and instead cast to (float) or
validate with is_numeric() to preserve cents; also change the truthy check from
if ($maxQuantity) to an explicit numeric/null check (e.g., if
(is_numeric($maxQuantity)) or if ($maxQuantity !== null && $maxQuantity !== '')
) so a legitimate zero maximum is honored; update any related variable usage or
SQL parameter binding in the same function to use the new float/validated values
(or rename if these are actually quantities vs monetary thresholds).
| $from_date = $from_date == '' ? date('Y-m-d') : date_to_mysql($from_date); | ||
| $to_date = $to_date == '' ? date('Y-m-d') : date_to_mysql($to_date); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the file structure and locate the file
find . -name "Mdl_reports.php" -type fRepository: InvoicePlane/InvoicePlane
Length of output: 194
🏁 Script executed:
# Read the relevant section with context around lines 217-218
sed -n '200,235p' application/modules/reports/models/Mdl_reports.php | cat -nRepository: InvoicePlane/InvoicePlane
Length of output: 1603
🏁 Script executed:
# Get the full function to understand the context
# First, let's see the size of the file
wc -l application/modules/reports/models/Mdl_reports.phpRepository: InvoicePlane/InvoicePlane
Length of output: 123
🏁 Script executed:
# Search for function definition around line 217
sed -n '180,220p' application/modules/reports/models/Mdl_reports.php | cat -nRepository: InvoicePlane/InvoicePlane
Length of output: 1774
🏁 Script executed:
# Search for how sales_by_year is called in controllers
rg "sales_by_year" --type php -B 2 -A 2Repository: InvoicePlane/InvoicePlane
Length of output: 2880
🏁 Script executed:
# Look for tests related to date handling or sales_by_year
find . -name "*test*" -o -name "*Test*" | grep -i reportRepository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Check if there's a controller that calls this method
rg "sales_by_year" --type php -lRepository: InvoicePlane/InvoicePlane
Length of output: 224
🏁 Script executed:
# Search for date_to_mysql function to understand what it does
rg "function date_to_mysql" --type php -A 5Repository: InvoicePlane/InvoicePlane
Length of output: 474
🏁 Script executed:
# Check how null vs empty string is handled elsewhere in date processing
rg "== ''" --type php | grep -i date | head -20Repository: InvoicePlane/InvoicePlane
Length of output: 596
🏁 Script executed:
# Now let's see how from_date and to_date are used in the sales_by_year method
sed -n '200,350p' application/modules/reports/models/Mdl_reports.php | cat -nRepository: InvoicePlane/InvoicePlane
Length of output: 9753
🏁 Script executed:
# Check if there are WHERE clauses that use these dates
rg "from_date|to_date" application/modules/reports/models/Mdl_reports.php -A 2 -B 2Repository: InvoicePlane/InvoicePlane
Length of output: 18046
🏁 Script executed:
# Look at the view that displays the form to understand if dates are optional
find . -path "*reports*" -name "*sales_by_year*" -type fRepository: InvoicePlane/InvoicePlane
Length of output: 174
🏁 Script executed:
# Check if dates can be null/empty in the form
rg "from_date|to_date" application/modules/reports/views/ --type php | head -30Repository: InvoicePlane/InvoicePlane
Length of output: 3199
🏁 Script executed:
# Look at what date_to_mysql does with null input
cat application/helpers/date_helper.php | head -50Repository: InvoicePlane/InvoicePlane
Length of output: 1415
🏁 Script executed:
# Look at the controller method that calls sales_by_year
sed -n '1,100p' application/modules/reports/controllers/Reports.php | grep -A 20 "public function sales_by_year"Repository: InvoicePlane/InvoicePlane
Length of output: 51
🏁 Script executed:
# Compare the handling in the controller vs the model for different methods
rg "sales_by_client|payment_history|sales_by_year" application/modules/reports/controllers/Reports.php -B 2 -A 5Repository: InvoicePlane/InvoicePlane
Length of output: 2405
Reconsider defaulting empty dates to "today"—inconsistent with other report methods.
This defaults empty/NULL date inputs to the current day, which differs from sales_by_client and payment_history methods that allow optional date filtering. Users leaving date fields blank expecting "all time" results will instead get only today's data. If intentional, confirm; otherwise, follow the pattern of skipping date filters when dates are not provided.
🤖 Prompt for AI Agents
In `@application/modules/reports/models/Mdl_reports.php` around lines 217 - 218,
The current logic in Mdl_reports.php sets $from_date and $to_date to today's
date when empty, causing mandatory filtering to today; change this to match the
pattern used by sales_by_client and payment_history by leaving $from_date and
$to_date as NULL/empty when the inputs are empty and only applying the date
WHERE clauses if those variables are provided. Update the handling around
date_to_mysql(...) and any subsequent query-building in the same function
(referencing $from_date, $to_date, and date_to_mysql) so date filters are
skipped when inputs are not supplied rather than defaulting to date('Y-m-d').
| private function _is_ip_rate_limited_password_reset() | ||
| { | ||
| $max_attempts = env('PASSWORD_RESET_IP_MAX_ATTEMPTS', 5); | ||
| $window_minutes = env('PASSWORD_RESET_IP_WINDOW_MINUTES', 60); | ||
|
|
||
| $ip_address = $this->input->ip_address(); | ||
| $session_key = 'password_reset_attempts_' . md5($ip_address); | ||
|
|
||
| // Get current attempts from session | ||
| $attempts = $this->session->userdata($session_key); | ||
|
|
||
| if (!$attempts) { | ||
| $attempts = []; | ||
| } | ||
|
|
||
| // Clean up old attempts outside the time window | ||
| $cutoff_time = time() - ($window_minutes * 60); | ||
| $attempts = array_filter($attempts, function($timestamp) use ($cutoff_time) { | ||
| return $timestamp > $cutoff_time; | ||
| }); | ||
|
|
||
| // Check if rate limited | ||
| if (count($attempts) >= $max_attempts) { | ||
| log_message('info', trans('log_ip_rate_limit_check') . ': ' . count($attempts) . ' attempts from IP: ' . $ip_address); | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } |
There was a problem hiding this comment.
Rate‑limit pruning isn’t persisted (leads to permanent lockout)
You filter out old attempts but never write the filtered list back to the session. Old timestamps accumulate and eventually always exceed the limit, even after the window should expire.
🔧 Proposed fix
- $attempts = array_filter($attempts, function($timestamp) use ($cutoff_time) {
+ $attempts = array_filter($attempts, function($timestamp) use ($cutoff_time) {
return $timestamp > $cutoff_time;
});
+ $this->session->set_userdata($session_key, $attempts);
...
- $attempts = array_filter($attempts, function($timestamp) use ($cutoff_time) {
+ $attempts = array_filter($attempts, function($timestamp) use ($cutoff_time) {
return $timestamp > $cutoff_time;
});
+ $this->session->set_userdata($session_key, $attempts);Also applies to: 392-419
🤖 Prompt for AI Agents
In `@application/modules/sessions/controllers/Sessions.php` around lines 331 -
359, The function _is_ip_rate_limited_password_reset filters out old timestamps
but never persists the pruned array back to session, causing permanent lockout;
update the session after pruning by calling the session setter with the same
session key (session_key built from 'password_reset_attempts_' .
md5($ip_address)) e.g. persist the filtered $attempts via
$this->session->set_userdata($session_key, $attempts) before returning, and
apply the same fix to the other similar block referenced (lines 392-419) so both
pruning operations write the cleaned list back to session.
| onApprove: function(data) { | ||
| return fetch(config.capturePaymentUrl + data.orderID, { method: 'GET' }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for paypal capture payment endpoint implementation
rg -n 'paypal_capture_payment|capturePaymentUrl|capture.*payment' --type=php -A 5 -B 2Repository: InvoicePlane/InvoicePlane
Length of output: 2887
🏁 Script executed:
#!/bin/bash
# Also check the context in the JavaScript file
rg -n 'capturePaymentUrl|onApprove' assets/core/js/paypal.js -B 5 -A 10Repository: InvoicePlane/InvoicePlane
Length of output: 1957
🏁 Script executed:
#!/bin/bash
# Look for any configuration or comments about GET vs POST
rg -n 'GET|POST|method.*payment|capture' assets/core/js/paypal.js -B 2 -A 2Repository: InvoicePlane/InvoicePlane
Length of output: 2141
🏁 Script executed:
#!/bin/bash
# Check for overall PayPal integration structure
git ls-files | grep -i paypalRepository: InvoicePlane/InvoicePlane
Length of output: 275
Change payment capture to use POST method instead of GET.
Using GET for capturePaymentUrl violates HTTP best practices for state-changing operations. Payment capture modifies server state and should use POST, not GET. The current implementation exposes the sensitive order ID in browser history, server logs, and referrer headers, creating unnecessary security and privacy risks. Update the fetch call at line 218 (and line 250) to use method: 'POST' and adjust the server-side controller to accept POST requests instead.
🤖 Prompt for AI Agents
In `@assets/core/js/paypal.js` around lines 217 - 218, The fetch calls that
capture payments currently use GET and should be changed to POST: update the
onApprove handler's fetch invocation (referenced by onApprove and
config.capturePaymentUrl) to use method: 'POST' instead of 'GET' and likewise
update the second fetch call at the other occurrence around line 250; also
ensure the server-side payment capture controller is modified to accept POST
requests and read the order ID from the request body (or POST parameters) rather
than the URL/query string.
| $files = array_merge( | ||
| glob(UPLOADS_TEMP_FOLDER . '*.pdf'), | ||
| glob(UPLOADS_TEMP_FOLDER . '*.xml') | ||
| ); | ||
|
|
||
| array_map('unlink', $files); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
PHP glob() function return value false error empty array documentation
💡 Result:
Short answer: glob() returns an array of matched pathnames; it returns an empty array when no files match, and FALSE on error. Some platforms may make it impossible to distinguish an empty match from an error. See PHP manual. [1][2]
References
- PHP manual: glob() — Return values and flags. [1]
- Note about platform differences / indistinguishable empty match vs error. [2]
Sources
[1] https://www.php.net/manual/en/function.glob.php
[2] https://www.php-resource.de/en/php-manual/function.glob.html
🌐 Web query:
PHP array_merge() warning false non-array argument type error
💡 Result:
Cause: array_merge() accepts only arrays — passing false, null, a scalar, or an empty/non-array value triggers warnings or unexpected results. PHP 7.4+ allows calling array_merge() with no args but still requires each argument to be an array. [1][3]
Fixes (pick one that fits your code):
-
Ensure the value is an array before merging:
- if (is_array($x)) $res = array_merge($res, $x);. [2]
-
Cast to array (quickest):
- $res = array_merge($res, (array)$maybeArray); — turns null/false/scalar into an array. [1][2]
-
Use a safe default with null coalescing or ternary:
- $res = array_merge($res, $maybeArray ?? []); or $res = array_merge($res, is_array($maybeArray) ? $maybeArray : []);. [1]
-
If using PHP 7.4+ and spread operator, ensure the source is a non-empty array of arrays (avoid spreading empty or non-array elements):
- $res = array_merge(...$arrays); // only when $arrays contains only arrays. [1][3]
Example:
- Bad: array_merge($a, $b) where $b can be false/null.
- Safe: array_merge($a, (array)$b) or array_merge($a, $b ?? []). [1][2]
References:
- PHP manual: array_merge (behavior, casting example). [1]
- Common fixes on StackOverflow (cast to array / is_array check). [2]
- Static-analysis guidance / defaults: use [[]] or cast when collecting arrays. [3]
Guard against glob() returning false before array_merge.
If the temp folder is unreadable or missing, glob() can return false, which triggers warnings in array_merge. Normalizing to empty arrays keeps cleanup silent and resilient.
🔧 Suggested hardening
-$files = array_merge(
- glob(UPLOADS_TEMP_FOLDER . '*.pdf'),
- glob(UPLOADS_TEMP_FOLDER . '*.xml')
-);
+$files = array_merge(
+ glob(UPLOADS_TEMP_FOLDER . '*.pdf') ?: [],
+ glob(UPLOADS_TEMP_FOLDER . '*.xml') ?: []
+);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $files = array_merge( | |
| glob(UPLOADS_TEMP_FOLDER . '*.pdf'), | |
| glob(UPLOADS_TEMP_FOLDER . '*.xml') | |
| ); | |
| array_map('unlink', $files); | |
| $files = array_merge( | |
| glob(UPLOADS_TEMP_FOLDER . '*.pdf') ?: [], | |
| glob(UPLOADS_TEMP_FOLDER . '*.xml') ?: [] | |
| ); | |
| array_map('unlink', $files); |
🤖 Prompt for AI Agents
In `@index.php` around lines 306 - 311, The cleanup code assumes glob() returns
arrays but glob() can return false, causing warnings when passed to array_merge;
update the $files assembly to normalize glob results to arrays (e.g., coerce
each glob(UPLOADS_TEMP_FOLDER . '*.pdf') and glob(UPLOADS_TEMP_FOLDER . '*.xml')
to [] when false) before calling array_merge, then proceed with
array_map('unlink', $files) so unlink only receives a proper array; reference
the existing symbols $files, array_merge, glob, UPLOADS_TEMP_FOLDER, array_map
and unlink when making the change.
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Security
Chores
✏️ Tip: You can customize this high-level summary in your review settings.