Skip to content

Add extra fields (client_number, etc) and functions to client and quotes and invoices#1029

Draft
novafirst wants to merge 13 commits intodevelopfrom
1022-add-extra-fields-and-functions-to-client-and-quotes-and-invoices
Draft

Add extra fields (client_number, etc) and functions to client and quotes and invoices#1029
novafirst wants to merge 13 commits intodevelopfrom
1022-add-extra-fields-and-functions-to-client-and-quotes-and-invoices

Conversation

@novafirst
Copy link
Copy Markdown

@novafirst novafirst commented Feb 7, 2024

Description

Added a few new fields and additional functions to clients, invoices and quotes modules

List of all new fields:
client_number
invoice_quote_number
invoice_work_order
invoice_agreement
quote_work_order
quote_agreement

Related Issue

https://community.invoiceplane.com/t/topic/10662

Motivation and Context

This is an improvement to already great application. Having ability to have client_number for each client is sometimes mandatory for a larger size company or when billing requires that number.

So in Clients module, added one additional field:
client_number

For Invoices three additional fields and a new function:
invoice_quote_number
invoice_work_order
invoice_agreement

invoice_quote_number -> This is a text field, you can type in a related quote number or using a new function you can select related quote from a list by clicking on Change Related Quote. This will associate the invoice with specific quote and import quote_work_order and quote_agreement values (if any) from the quotes and pass it to invoice_work_order and invoice_agreement.

For Quotes two additional fields:
quote_work_order
quote_agreement

Welcome to any suggestions!

Screenshots (if appropriate):

Pull Request Checklist

  • My code follows the code formatting guidelines.
  • I have an issue ID for this pull request.
  • I selected the corresponding branch.
  • I have rebased my changes on top of the corresponding branch.

Issue Type (Please check one or more)

  • Bugfix
  • Improvement of an existing Feature
  • New Feature

Summary by CodeRabbit

  • New Features

    • Added related quote, work order, and agreement tracking to invoices and quotes.
    • Added quote lookup modal for streamlined related quote selection.
    • Introduced HTML sanitization for email template previews.
  • Chores

    • Implemented automated dependency management workflows for Composer and Yarn packages.
    • Added automated translation synchronization and release pipeline orchestration.
    • Upgraded production dependencies and development tooling versions.

✏️ Tip: You can customize this high-level summary in your review settings.

Copy link
Copy Markdown

@accesslint accesslint Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are accessibility issues in these changes.

Comment thread application/modules/quotes/views/view.php Outdated
Comment thread application/modules/quotes/views/view.php Outdated
Comment thread application/modules/invoices/views/view.php Outdated
Comment thread application/modules/invoices/views/view.php Outdated
Comment thread application/modules/invoices/views/view.php Outdated
@novafirst novafirst self-assigned this Feb 7, 2024
@novafirst novafirst requested a review from UnderDogg February 7, 2024 02:29
@novafirst novafirst added the Feature Request Improvement or Feature Request label Feb 7, 2024
@novafirst novafirst changed the title 1022 add extra fields and functions to client and quotes and invoices Add extra fields and functions to client and quotes and invoices Feb 7, 2024
@nielsdrost7 nielsdrost7 modified the milestones: 1.7 beyond, 1.7.x Aug 7, 2025
@nielsdrost7
Copy link
Copy Markdown
Contributor

Hey @novafirst since I released version 1.6.3 some merge conflicts would occur while dealing with your pull-request.
I'll try to resolve them and then I'll take a look at your PR.

I could merge it into 1.7, however in version 2.0 of InvoicePlane the Customer Number is offered out of the box.
If you can wait till then that's also fine.

@nielsdrost7 nielsdrost7 self-assigned this Aug 7, 2025
@nielsdrost7 nielsdrost7 changed the title Add extra fields and functions to client and quotes and invoices Add extra fields (client_number, etc) and functions to client and quotes and invoices Aug 7, 2025
Copy link
Copy Markdown

@accesslint accesslint Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏 You fixed the issue(s)! Great work.

if ( typeof options.of === "string" ) {
target = $( document ).find( options.of );
} else {
target = $( options.of );

Check warning

Code scanning / CodeQL

Unsafe jQuery plugin

Potential XSS vulnerability in the ['$.fn.position' plugin](1).
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
application/modules/upload/controllers/Upload.php (1)

172-193: Avoid logging raw attacker‑supplied filenames.
Prefer hashing to reduce PII exposure and log‑injection risk.

🔧 Suggested change
-            log_message('error', 'Path traversal attempt detected in filename: ' . $filename);
+            $filenameHash = hash_for_logging($filename);
+            log_message('error', 'Path traversal attempt detected in filename (hash: ' . $filenameHash . ')');
🤖 Fix all issues with AI agents
In @.github/workflows/composer-update.yml:
- Around line 77-84: The "Check for changes" step (id: check-changes) only
inspects composer.lock via `git diff --quiet composer.lock` so it misses updates
to composer.json; update the check to detect changes to both composer.lock and
composer.json (e.g., test diffs or git status for composer.json composer.lock)
and set GITHUB_OUTPUT changes_detected accordingly so PRs are opened when either
file was modified.

In @.github/workflows/crowdin-sync.yml:
- Around line 1-105: The workflow is using the wrong branch name for Crowdin
operations: update the two occurrences of localization_branch_name currently set
to "master" to "development" inside the "Upload sources to Crowdin" and
"Download translations from Crowdin" steps (look for the
localization_branch_name key in those steps and replace its value), so Crowdin
actions target the repository's default branch.

In @.github/workflows/docker-phpmyadmin.yml:
- Around line 1-5: The workflow's triggers were changed to only
workflow_dispatch, removing automatic builds; update the top-level "on" block to
include workflow_dispatch plus automatic triggers by adding a "push" event for
the development branch and a "push" with "tags: ['v*.*.*']" (i.e., restore
push/branches and tag pushes) so the workflow runs both manually and
automatically upon branch pushes and semver tags; ensure the "on" block contains
workflow_dispatch, push.branches: [development], and push.tags: ['v*.*.*'] to
restore the original behavior.

In @.github/workflows/phpunit.yml:
- Around line 1-49: The workflow uses Laravel-specific files and commands (steps
named "Prepare Laravel environment" and "Run PHPUnit" that call php artisan and
reference .env.testing.example) which don't exist in this CodeIgniter 3.3 repo;
replace those steps with CodeIgniter-appropriate test setup: install PHPUnit via
Composer (ensure composer.json includes phpunit), add or reference a
phpunit.xml/phpunit.xml.dist suited to this project, remove php artisan
key:generate/.env.testing.example usage, and change the Run PHPUnit step to
invoke vendor/bin/phpunit (or the configured test command) and any necessary DB
setup (create sqlite or mysql service and migration/seeding commands if you add
CI test helpers). Ensure the workflow's "Setup PHP with Composer" step provides
required PHP extensions and that the final test step runs the repository's
actual test runner instead of php artisan.

In @.github/workflows/release.yml:
- Line 427: Update the GitHub Actions step that references
softprops/action-gh-release by changing the action version from v1 to v2
(replace softprops/action-gh-release@v1 with softprops/action-gh-release@v2) so
the workflow runs on Node 20; ensure any inputs or outputs used with that step
remain compatible after the version bump and adjust them if the v2 action
introduced any breaking changes.

In @.github/workflows/setup.yml:
- Around line 196-223: The workflow step with id key_generate currently runs the
Laravel command "php artisan key:generate", but this is a CodeIgniter 3 project;
replace that call with a CI-appropriate key setup: remove "php artisan
key:generate" and instead generate a random encryption key (for example via a
short PHP one-liner or openssl) and write it into your CodeIgniter config (set
$config['encryption_key'] in application/config/config.php or write to your .env
if you use one); update the step id key_generate to echo success/failure exactly
as before based on that command’s exit code. Also scan other workflow steps that
call "php artisan migrate" or "php artisan db:seed" and replace them with your
project's migration/seed approach (invoke your custom CLI controllers via "php
index.php ControllerName method" or run your project-specific migration
scripts).

In @.github/workflows/test-php.yml:
- Around line 3-15: Update the GitHub Actions workflow triggers and matrix so CI
runs on PRs and includes the project-required minimum PHP version: add
pull_request to the top-level on: triggers (in addition to workflow_dispatch)
and expand the strategy.matrix.php-versions array to include '8.1' alongside
'8.2'; confirm the matrix key names operating-system and php-versions remain
unchanged so the run job picks up the new entries.

In @.github/workflows/yarn-update.yml:
- Around line 203-211: The workflow step with id updated-packages invokes the
missing script generate-package-update-report.cjs; either add that script under
the repository (implement the logic that generates updated-packages.txt and
exits non-zero on errors) or change the step to point to the existing script
path/name that produces updated-packages.txt; update the run block to call the
correct script name and ensure the script writes updated-packages.txt before the
echo to GITHUB_OUTPUT so the "Get updated packages" step succeeds.

In `@application/helpers/file_security_helper.php`:
- Line 177: The code calls a non-existent mb_rtrim when building $fullPath in
file_security_helper.php; replace the mb_rtrim usage with PHP's rtrim() so
$fullPath = rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR .
$basename['filename']; (i.e., update the expression that constructs $fullPath to
use rtrim instead of mb_rtrim).
- Line 87: The code calls a non-existent mb_rtrim which will cause a fatal
error; replace the mb_rtrim call with the standard rtrim when building
$realBaseWithSep from $realBase and DIRECTORY_SEPARATOR (e.g., update the
expression that sets $realBaseWithSep to use rtrim($realBase,
DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR) because DIRECTORY_SEPARATOR is a
single-byte ASCII char and multibyte handling is unnecessary.

In `@application/modules/clients/Enums/ClientTitleEnum.php`:
- Around line 20-31: The tryFrom method currently uses array_search on
self::getValues() and checks if ($searchResult), which skips the first enum
(index 0) and returns the index rather than the matched string; change the
lookup to perform a strict search (e.g., use array_search($value, $values, true)
or use in_array with strict true) and when a match is found return the matched
value string (the element from $values), not the numeric index; ensure tryFrom
returns null (or the expected empty/false value) when no strict match exists.

In `@application/modules/invoices/views/view.php`:
- Around line 479-498: The echoed input values for invoice_quote_number,
invoice_work_order, and invoice_agreement are not escaped and can cause XSS;
update the value outputs in the view to escape user content (e.g., wrap the
properties with htmlsc() or htmlspecialchars()) when echoing inside the value
attribute for the inputs in view.php (references: invoice_quote_number,
invoice_work_order, invoice_agreement) so the rendered value attribute uses the
escaped string.

In `@application/modules/quotes/controllers/Ajax.php`:
- Around line 21-43: In modal_quote_lookups ensure $data is always defined
before calling $this->layout->load_view to avoid the undefined variable when
$client_id is empty: either initialize $data = array() (or with default keys
'related_quotes','client_id','quote_statuses') before the if block, or add an
early return/conditional so load_view is only called when $data is set;
reference the modal_quote_lookups method, the $this->mdl_quotes->... calls, and
the final $this->layout->load_view('quotes/modal_quote_lookups', $data) when
applying the fix.
- Around line 144-146: The POST-extracted variables $quote_work_order and
$quote_agreement are never added to the record being saved; update the
controller method in application/modules/quotes/controllers/Ajax.php to include
these keys in the $db_array (e.g., 'quote_work_order' => $quote_work_order and
'quote_agreement' => $quote_agreement) before the save/insert call so the values
persist to the DB; ensure the $db_array used by the existing save/update
function contains those two fields and run existing migrations/column names to
match.

In `@application/modules/quotes/views/modal_quote_lookups.php`:
- Around line 9-29: Validate that a quote is selected in the
.select-items-confirm click handler by checking quote_id (from
$("input[name='quote_id']:checked").val()) and aborting with a user-facing
message if invalid; wrap the $.post callback parsing in a try/catch around
JSON.parse(data) and handle parse or AJAX errors by logging and showing an error
to the user; replace the for..in loop over items with a deterministic iteration
(e.g., use items[0] or a for loop over numeric indices) so all fields are set
correctly and move $('#modal-choose-quote').modal('hide') outside the loop so
the modal is closed only after processing completes; ensure the error-handling
branches do not attempt to use the parsed items when parsing fails.

In `@application/modules/quotes/views/view.php`:
- Around line 349-362: The quote_work_order and quote_agreement values are
echoed into input value attributes without escaping, enabling XSS; update the
view (inputs with id "quote_work_order" and "quote_agreement" in view.php) to
output escaped values using a safe HTML-escaping function (e.g.
htmlspecialchars($quote->quote_work_order, ENT_QUOTES, 'UTF-8') and
htmlspecialchars($quote->quote_agreement, ENT_QUOTES, 'UTF-8') or your
framework's html_escape helper) and always render the value attribute (even when
empty) to avoid malformed HTML.

In `@application/modules/setup/sql/041_1.7.0.sql`:
- Line 1: The migration file 041_1.7.0.sql currently contains only a comment;
add ALTER TABLE statements to create the missing columns used by the app: add
client_number (e.g., VARCHAR(255) DEFAULT NULL) to ip_clients, add
invoice_quote_number (TEXT DEFAULT NULL), invoice_work_order (VARCHAR(255)
DEFAULT NULL) and invoice_agreement (VARCHAR(255) DEFAULT NULL) to ip_invoices,
and add quote_work_order (VARCHAR(255) DEFAULT NULL) and quote_agreement
(VARCHAR(255) DEFAULT NULL) to ip_quotes; ensure the statements are idempotent
for your DB flavor (or wrapped in a safe conditional/transaction) and follow the
repository’s naming and datatype conventions so functions referencing these
fields (client_number, invoice_quote_number, invoice_work_order,
invoice_agreement, quote_work_order, quote_agreement) will find the columns at
runtime.

In `@application/views/quote_templates/public/InvoicePlane_Web.php`:
- Around line 79-83: Escape the user-supplied VAT/tax ID and attachment name
outputs to prevent stored XSS by replacing direct echoes of $quote->user_vat_id
and $quote->user_tax_code (and any attachment name echoes around the other
mentioned lines) with an HTML-escaped variant; use the project's escaping helper
(e.g., html_escape()) or PHP's htmlspecialchars(..., ENT_QUOTES, 'UTF-8') when
outputting these variables so all printed values are properly encoded before
echoing.

In `@assets/core/js/jquery-ui.js`:
- Around line 888-894: The .position() widget is vulnerable to XSS when a string
passed to options.of is handed directly to $() — change handling so that when
options.of is a string you call $(document).find(options.of) (ensuring strings
are treated only as selectors) and otherwise continue to pass non-string values
(DOM element, jQuery object, Event, window/document) directly to $(); update the
branch around options.of and target to reflect this secure pattern and consider
replacing the vendored assets/core/js/jquery-ui.js with jQuery UI 1.13.0+ which
includes this fix upstream.

In `@assets/core/js/scripts.js`:
- Around line 76-77: The preview currently uses .text() in
update_email_template_preview(), which escapes HTML and shows raw tags; change
it to write sanitized HTML into the iframe/body (use .html() on
$('#email-template-preview').contents().find('body')) after running the template
content through your sanitization function (e.g., sanitizeHtml or a safeSanitize
helper) to prevent XSS while allowing tags to render; update references to
$('.email-template-body').val() as the input to the sanitizer before injecting.
♻️ Duplicate comments (2)
application/modules/invoices/views/view.php (1)

468-499: Accessibility: Input fields are missing accessible labels.

The <label> elements are not properly associated with their corresponding <input> elements via for attributes. This makes the form less accessible for screen readers and voice control users.

Suggested fix
                            <div class="col-xs-12 col-md-4">
                                <div class="invoice-properties">
-                                    <label><?php _trans('related_quote_number'); ?></label>
+                                    <label for="invoice_quote_number"><?php _trans('related_quote_number'); ?></label>
                                    ...
                                </div>
                                <div class="invoice-properties">
-                                    <label><?php _trans('work_order'); ?></label>
+                                    <label for="invoice_work_order"><?php _trans('work_order'); ?></label>
                                    ...
                                </div>
                                <div class="invoice-properties">
-                                    <label><?php _trans('agreement'); ?></label>
+                                    <label for="invoice_agreement"><?php _trans('agreement'); ?></label>
                                    ...
                                </div>
                            </div>
application/modules/quotes/views/view.php (1)

349-350: Accessibility: Input fields missing associated label for attribute.

The labels for quote_work_order and quote_agreement inputs don't have a for attribute linking them to their respective inputs, which impacts screen reader users.

🟡 Minor comments (9)
application/helpers/client_helper.php-45-56 (1)

45-56: Add null safety and use strict comparison.

The function lacks null safety checks that format_client() above implements. If $client is null or missing the client_number property, this will throw an error.

Additionally, consider using strict comparison (!== "") or empty() for consistency with PHP best practices.

Proposed fix with null safety
 /**
  * `@param` object $client
  * `@return` string
  */
-function format_client_number($client)
+function format_client_number($client): string
 {
-    if ($client->client_number != "") {
+    if (!empty($client->client_number)) {
         return $client->client_number;
     }
 
     return "00-00000";
 }
.github/workflows/README.md-221-226 (1)

221-226: Minor inconsistency in PHP version documentation.

The documentation mentions "PHP 8.2" but the actual workflow (pint.yml) uses PHP 8.4. Consider updating for consistency.

 **What it does:**
 1. **Checks out the PR branch** - Gets the latest code from the pull request
-2. **Sets up PHP environment** - Installs PHP 8.2 with required extensions
+2. **Sets up PHP environment** - Installs PHP 8.4 with required extensions
.github/workflows/phpstan.yml-65-93 (1)

65-93: Dead code: PR comment step will never execute.

Since this workflow only triggers on workflow_dispatch, the condition github.event_name == 'pull_request' will never be true. Either add pull_request to the triggers or remove this step.

If you intend to support PR comments in the future, add the trigger:

 on:
   workflow_dispatch:  # Manual only — does not run automatically
+  pull_request:
+    branches:
+      - master
+      - develop

Otherwise, remove the dead code step (lines 65-93).

.github/workflows/setup.yml-371-374 (1)

371-374: Unreachable code: echo "::endgroup::" will never execute.

The exit 0 and exit 1 statements on lines 350 and 371 terminate the script before the ::endgroup:: echo can be reached.

Suggested fix - move endgroup before exit statements
            exit 0
          fi
-          
-          echo "::endgroup::"

The ::endgroup:: at line 374 can be safely removed since the ::group:: at line 287 will be implicitly closed when the step ends.

application/views/quote_templates/public/InvoicePlane_Web.php-215-216 (1)

215-216: Use $colspan for discount rows to keep column alignment.

The hardcoded colspan causes misalignment when item discounts are hidden.

🧩 Suggested 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: 267-268

.github/workflows/composer-update.yml-50-58 (1)

50-58: Composer audit output check always flags vulnerabilities.

The [ -s audit-report.json ] check is unreliable. Even when no vulnerabilities are found, composer audit --format=json outputs valid JSON with an empty advisories object (e.g., {"advisories": {}}), which is still a non-empty file. This causes the flag to always be set to true.

Parse the JSON instead to check if the advisories object actually contains findings.

Suggested fix
-          if [ -s audit-report.json ]; then
-            echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT
-          else
-            echo "vulnerabilities_found=false" >> $GITHUB_OUTPUT
-          fi
+          php -r '$data=json_decode(file_get_contents("audit-report.json"), true) ?: []; $has = !empty($data["advisories"]); echo "vulnerabilities_found=" . ($has ? "true" : "false") . PHP_EOL;' >> $GITHUB_OUTPUT
application/modules/quotes/controllers/Ajax.php-52-53 (1)

52-53: Ineffective statuses() call in modal_quote_lookups_select.

Same issue as above - the statuses() return value is discarded.

🐛 Remove the ineffective call
         if (!empty($quote_id)) {
             $this->mdl_quotes->is_related_quote($quote_id);
-            $this->mdl_quotes->statuses();
             $quote = $this->mdl_quotes->get()->result();

             echo json_encode($quote);
         }
application/modules/quotes/views/modal_quote_lookups.php-42-42 (1)

42-42: Fix ARIA attributes for accessibility.

The aria-labelledby attribute should reference the id of the element containing the modal's title, not the modal's own id. Also, the modal should have tabindex="-1" for proper focus management.

♿ Proposed fix
-<div id="modal-choose-quote" class="modal col-xs-12 col-sm-10 col-sm-offset-1" role="dialog" aria-labelledby="modal-choose-quote" aria-hidden="true">
+<div id="modal-choose-quote" class="modal col-xs-12 col-sm-10 col-sm-offset-1" role="dialog" aria-labelledby="modal-choose-quote-title" aria-hidden="true" tabindex="-1">
     <form class="modal-content">
         <div class="modal-header">
             <button type="button" class="close" data-dismiss="modal"><i class="fa fa-close"></i></button>
-            <h4 class="panel-title"><?php _trans('select_related_quote'); ?></h4>
+            <h4 class="panel-title" id="modal-choose-quote-title"><?php _trans('select_related_quote'); ?></h4>
         </div>
application/modules/quotes/controllers/Ajax.php-29-30 (1)

29-30: Ineffective statuses() call - return value not used.

The statuses() method returns an array but the return value is not assigned to anything. This call has no effect.

🐛 Remove the ineffective call
             $this->mdl_quotes->by_client($client_id);
             $this->mdl_quotes->related_is_open();
-            $this->mdl_quotes->statuses();

             $quotes = $this->mdl_quotes->get()->result();
🧹 Nitpick comments (11)
application/third_party/MX/Controller.php (1)

55-55: Simplify the no-op string replacement.

The str_replace('', '', get_class($this)) call replaces an empty string with an empty string, which is effectively a no-op. This makes the code less readable.

♻️ Simplify to direct assignment
-        $class = str_replace('', '', get_class($this));
+        $class = get_class($this);
.github/workflows/docker.yml (1)

23-27: Prefer docker compose or ensure docker-compose is installed.

Ubuntu runners have moved to Compose v2; the docker-compose binary may be missing. Consider switching to docker compose or adding an install step.

♻️ Proposed update
-      - name: Build and start containers (if docker-compose.yml is ready)
-        run: docker-compose up -d --build
+      - name: Build and start containers (if docker-compose.yml is ready)
+        run: docker compose up -d --build

-      - name: Check running services
-        run: docker-compose ps
+      - name: Check running services
+        run: docker compose ps
composer.json (1)

20-20: Consider using a caret constraint for pocketarc/codeigniter.

The fixed version "3.3" won't receive patch updates automatically, unlike other dependencies using caret constraints (e.g., ^2.6). Consider using "^3.3" to allow patch-level updates while maintaining compatibility.

Suggested change
-        "pocketarc/codeigniter": "3.3",
+        "pocketarc/codeigniter": "^3.3",
.github/workflows/pint.yml (2)

70-76: Parse error detection may have false positives.

Grepping for "!" in the output is too broad and may match legitimate content (e.g., file paths, output messages). Consider using a more specific pattern or relying solely on the exit code and "Parse error" string.

Suggested improvement
          # Check if there were parse errors
-          if grep -q "Parse error" pint_output.log || grep -q "!" pint_output.log; then
+          if grep -qE "Parse error|^\s+!" pint_output.log; then
            echo "parse_errors=true" >> $GITHUB_OUTPUT
            echo "WARNING: Parse errors detected in one or more files"
          else

Or alternatively, remove the "!" check entirely and rely on the more specific "Parse error" pattern.


93-107: Consider handling push failures gracefully.

If the git push fails (e.g., due to branch protection rules or concurrent updates), the workflow will fail without clear guidance. Consider adding error handling.

Suggested improvement
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add -A
          git commit -m "style: apply Laravel Pint fixes" --no-verify
-          git push
+          git push || {
+            echo "::warning::Failed to push changes. This may be due to branch protection rules or concurrent updates."
+            echo "Please pull the latest changes and run Pint locally."
+            exit 1
+          }
.github/workflows/setup.yml (1)

102-108: Minor: Missing newline before Step 1 comment.

There's a missing blank line between the initialization step output and the Step 1 comment, which slightly reduces readability.

          echo "errors_found=false" >> $GITHUB_OUTPUT
          echo "::notice::Starting setup with error tracking enabled"
+
       # Step 1: Yarn Install
.github/workflows/phpstan.yml (1)

50-54: Fail step could be more informative.

Consider outputting a summary of the error count or directing users to the artifacts for details.

Suggested improvement
      - name: Check for errors and fail if found
        if: steps.phpstan-analysis.outcome == 'failure'
        run: |
          echo "PHPStan found errors in the codebase"
+          echo "See the uploaded artifacts for detailed results"
+          echo "Download phpstan-report.md for a formatted summary"
          exit 1
.github/workflows/yarn-update.yml (2)

219-220: Consider enabling automatic branch deletion.

Setting delete-branch: false will leave automated branches (e.g., automated/yarn-update-*) in the repository after the PR is merged. Over time, this can lead to branch clutter.

💡 Suggested change
          branch: automated/yarn-update-${{ github.run_number }}
-          delete-branch: false
+          delete-branch: true

107-136: Security update logic relies on parsing audit output format.

The security update step parses audit-report.json using grep to extract vulnerable package names. This approach is brittle and may break if Yarn's audit JSON format changes. Additionally, upgrading vulnerable packages to --latest may introduce breaking changes.

Consider pinning to compatible versions rather than always using --latest:

              for pkg in $VULNERABLE_PACKAGES; do
                echo "  Upgrading $pkg..."
-                yarn upgrade "$pkg" --latest || true
+                yarn upgrade "$pkg" || true
              done
application/modules/quotes/models/Mdl_quotes.php (1)

345-352: Consider adding type hint for $quote_id parameter.

The method follows existing patterns in this file, but adding a type hint would improve type safety and documentation.

💡 Suggested improvement
     /**
+     * `@param` int $quote_id
      * `@return` $this
      */
-    public function is_related_quote($quote_id)
+    public function is_related_quote(int $quote_id)
     {
         $this->filter_where('ip_quotes.quote_id', $quote_id);
         return $this;
     }
application/modules/quotes/views/modal_quote_lookups.php (1)

13-14: Remove commented-out debug code.

These commented lines appear to be leftover from development and should be removed.

🧹 Suggested cleanup
-            //$.post("<?php echo site_url('quotes/ajax/process_product_selections'); ?>", {
-            //$.post("<?php echo site_url('quotes/ajax/modal_quote_lookups_select'); ?>", {
-
             $.post("<?php echo site_url('quotes/ajax/modal_quote_lookups_select'); ?>", {

Comment on lines +77 to +84
- name: Check for changes
id: check-changes
run: |
if git diff --quiet composer.lock; then
echo "changes_detected=false" >> $GITHUB_OUTPUT
else
echo "changes_detected=true" >> $GITHUB_OUTPUT
fi
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Detect composer.json changes when deciding to open a PR.

composer bump can update composer.json without a lockfile change; the current check would skip PR creation.

🛠️ Suggested fix
-          if git diff --quiet composer.lock; then
+          if git diff --quiet composer.lock composer.json; then
📝 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.

Suggested change
- name: Check for changes
id: check-changes
run: |
if git diff --quiet composer.lock; then
echo "changes_detected=false" >> $GITHUB_OUTPUT
else
echo "changes_detected=true" >> $GITHUB_OUTPUT
fi
- name: Check for changes
id: check-changes
run: |
if git diff --quiet composer.lock composer.json; then
echo "changes_detected=false" >> $GITHUB_OUTPUT
else
echo "changes_detected=true" >> $GITHUB_OUTPUT
fi
🤖 Prompt for AI Agents
In @.github/workflows/composer-update.yml around lines 77 - 84, The "Check for
changes" step (id: check-changes) only inspects composer.lock via `git diff
--quiet composer.lock` so it misses updates to composer.json; update the check
to detect changes to both composer.lock and composer.json (e.g., test diffs or
git status for composer.json composer.lock) and set GITHUB_OUTPUT
changes_detected accordingly so PRs are opened when either file was modified.

Comment on lines +1 to +105
name: Crowdin Translation Sync

on:
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: true
type: choice
options:
- upload-sources
- download-translations
- sync-bidirectional
default: 'download-translations'
schedule:
# Run weekly on Sundays at 2:00 AM UTC
- cron: '0 2 * * 0'

permissions:
contents: write
pull-requests: write

# Note: This workflow requires a Personal Access Token (PAT) to create pull requests.
# The default GITHUB_TOKEN has restricted permissions and cannot create PRs that trigger other workflows.
#
# To configure the required secret:
# 1. Create a Personal Access Token (classic) with 'repo' and 'workflow' scopes
# at https://github.com/settings/tokens
# 2. Add the token as a repository secret named 'PAT_TOKEN'
# at https://github.com/OWNER/REPO/settings/secrets/actions
#
# See: https://github.com/crowdin/github-action

jobs:
crowdin-sync:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Upload sources to Crowdin
if: github.event.inputs.action == 'upload-sources' || github.event.inputs.action == 'sync-bidirectional'
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: false
download_translations: false
localization_branch_name: master
config: 'crowdin.yml'
project_id: ${{ secrets.CROWDIN_PROJECT_ID }}
token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

- name: Download translations from Crowdin
if: github.event.inputs.action == 'download-translations' || github.event.inputs.action == 'sync-bidirectional' || github.event_name == 'schedule'
uses: crowdin/github-action@v2
with:
upload_sources: false
upload_translations: false
download_translations: true
localization_branch_name: master
create_pull_request: true
pull_request_title: 'chore(i18n): update translations from Crowdin'
pull_request_body: |
## Translation Update

This PR updates translations downloaded from Crowdin.

**Triggered by:** ${{ github.event_name }}
**Action:** ${{ github.event.inputs.action || 'download-translations' }}

### Review Checklist

- [ ] Review translation changes for accuracy
- [ ] Check for any formatting issues
- [ ] Verify no code is affected
- [ ] Test translations in UI if possible

---

*This PR was automatically created by the Crowdin Sync workflow.*
pull_request_labels: |
i18n
translations
crowdin
automated-pr
config: 'crowdin.yml'
project_id: ${{ secrets.CROWDIN_PROJECT_ID }}
token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}

- name: Workflow summary
run: |
echo "## Crowdin Sync Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Action:** ${{ github.event.inputs.action || 'download-translations' }}" >> $GITHUB_STEP_SUMMARY
echo "**Triggered by:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Next Steps" >> $GITHUB_STEP_SUMMARY
echo "- Review the created pull request (if any)" >> $GITHUB_STEP_SUMMARY
echo "- Verify translation changes" >> $GITHUB_STEP_SUMMARY
echo "- Merge when ready" >> $GITHUB_STEP_SUMMARY
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check available branches
echo "=== Available Branches ===" 
git branch -a 2>/dev/null || echo "No git branches available"

# Check localization_branch_name references in the workflow
echo -e "\n=== localization_branch_name in workflow ===" 
rg -n 'localization_branch_name' .github/workflows/crowdin-sync.yml

# Check for crowdin.yml or crowdin.yaml config file
echo -e "\n=== Crowdin config files ===" 
fd -a '(crowdin\.ya?ml|crowdin\.json)$' . -x cat {}

# Check default branch in repo config
echo -e "\n=== Repository Default Branch ===" 
cat .git/config 2>/dev/null | grep -A5 'branch' || echo "Git config not available"

# Check common branch name patterns in repository
echo -e "\n=== Branch name patterns in README/docs ===" 
fd -a 'README.md' . -x rg -i 'main branch|default branch|development|main|master' {} || echo "No README matches"

Repository: InvoicePlane/InvoicePlane

Length of output: 1265


Change localization_branch_name from master to development.

The repository's default branch is development, not master. The hardcoded localization_branch_name: master on lines 51 and 63 references a non-existent branch, which will cause the workflow to fail when attempting to create pull requests. Update both instances to localization_branch_name: development.

🤖 Prompt for AI Agents
In @.github/workflows/crowdin-sync.yml around lines 1 - 105, The workflow is
using the wrong branch name for Crowdin operations: update the two occurrences
of localization_branch_name currently set to "master" to "development" inside
the "Upload sources to Crowdin" and "Download translations from Crowdin" steps
(look for the localization_branch_name key in those steps and replace its
value), so Crowdin actions target the repository's default branch.

Comment thread .github/workflows/docker-phpmyadmin.yml
Comment thread .github/workflows/phpunit.yml Outdated
Comment thread .github/workflows/release.yml
Comment thread application/modules/quotes/views/view.php Outdated
Comment thread application/modules/setup/sql/041_1.7.0.sql
Comment thread application/views/quote_templates/public/InvoicePlane_Web.php Outdated
Comment on lines +888 to +894
// Ensure that string values for options.of are treated strictly as selectors
// and are not interpreted as HTML by the jQuery constructor.
if ( typeof options.of === "string" ) {
target = $( document ).find( options.of );
} else {
target = $( options.of );
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

CVE-2022-31160 jQuery UI XSS vulnerability

💡 Result:

Short summary — CVE-2022-31160 (jQuery UI)

  • What it is: a reflected/stored XSS in jQuery UI's checkboxradio widget. When a checkboxradio is initialized on an input inside a label, calling .checkboxradio("refresh") can incorrectly decode encoded HTML entities in the label text and thereby execute JavaScript. [1][2]

  • Affected versions: jquery-ui < 1.13.2. Patched in jQuery UI 1.13.2. [1][2]

  • Severity / impact: CVSSv3 ~6.1 (Medium); network-exploitable but requires user interaction in many scenarios. [1][3]

  • Mitigation / fix: upgrade to jQuery UI 1.13.2 (or later). As a workaround, wrap non-input contents of the label in a to prevent the unsafe decoding. [1][2][4]

References

  1. NVD / CVE-2022-31160.
  2. GitHub Advisory (jquery-ui) — GHSA-h6gj-6jjq-h8g9.
  3. Debian / Ubuntu security tracker entries.
  4. Vendor/third‑party advisories describing the mitigation and patch.

🌐 Web query:

jQuery UI 1.13.0 security patches CVE position widget

💡 Result:

Short answer — yes: jQuery UI 1.13.0 fixed multiple XSS issues (including the .position() "of" option). Specifically:

  • CVE-2021-41184 — XSS in the "of" option of the .position() util; fixed in 1.13.0. [1][2]
  • CVE-2021-41182 — XSS in Datepicker altField; fixed in 1.13.0. [1][3]
  • CVE-2021-41183 — XSS in Datepicker *Text options; fixed in 1.13.0. [1][3]

Note: a later XSS (CVE-2022-31160) affecting checkboxradio.refresh was fixed in jQuery UI 1.13.2. [4]

Sources:
[1] jquery/jquery-ui security advisories (GitHub).
[2] CVE-2021-41184 advisories / OSV / vendor pages.
[3] CVE-2021-41182 / CVE-2021-41183 entries (NVD/OSV).
[4] CVE-2022-31160 (package advisories / Rapid7 / OSV).


🌐 Web query:

jQuery UI 1.12.1 to 1.13.0 upgrade security fixes

💡 Result:

Summary — what changed (security fixes)

  • jQuery UI 1.13.0 fixed three XSS vulnerabilities present in 1.12.1:
    • CVE-2021-41182 — XSS via Datepicker altField. [1][3]
    • CVE-2021-41183 — XSS via various Datepicker "*Text" options (closeText, currentText, prev/nextText, buttonText, appendText). [1][3]
    • CVE-2021-41184 — XSS via the of option of the .position() utility. [1][6]

Recommendation

  • Upgrade any jQuery UI < 1.13.0 to 1.13.0 or later (patches 1.13.1/1.13.2 followed and included further security/regression fixes). [2][5][4]

Sources

  • jQuery UI 1.13.0 release notes / upgrade guide. [1][2]
  • Snyk vulnerability page for jquery-ui 1.12.1. [3]
  • Tenable advisory summarizing the three CVEs. [4]
  • jQuery UI blog posts for 1.13.x patch releases. [5]

Security fix for XSS vulnerability in .position() widget — verify alignment with official patches.

This change correctly mitigates CVE-2021-41184, a known jQuery UI XSS vulnerability where passing a string directly to $() in the .position() widget's of option could interpret it as HTML instead of a selector. Using $(document).find(options.of) ensures string inputs are treated strictly as selectors.

Regarding the CodeQL warning on line 893: this appears to be a false positive. The else branch only executes when options.of is not a string (e.g., DOM element, jQuery object, Event object, or window/document), all of which are safe to pass to $().

Consider upgrading to jQuery UI 1.13.0+ instead of maintaining manual patches to this vendored file, as 1.13.0 includes this fix natively.

🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 893-893: Unsafe jQuery plugin
Potential XSS vulnerability in the '$.fn.position' plugin.

🤖 Prompt for AI Agents
In `@assets/core/js/jquery-ui.js` around lines 888 - 894, The .position() widget
is vulnerable to XSS when a string passed to options.of is handed directly to
$() — change handling so that when options.of is a string you call
$(document).find(options.of) (ensuring strings are treated only as selectors)
and otherwise continue to pass non-string values (DOM element, jQuery object,
Event, window/document) directly to $(); update the branch around options.of and
target to reflect this secure pattern and consider replacing the vendored
assets/core/js/jquery-ui.js with jQuery UI 1.13.0+ which includes this fix
upstream.

Comment thread assets/core/js/scripts.js Outdated
@InvoicePlane InvoicePlane deleted a comment from coderabbitai Bot Jan 19, 2026
Copy link
Copy Markdown
Contributor

@nielsdrost7 nielsdrost7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot

In @.github/workflows/composer-update.yml:

  • Around line 77-84: The "Check for changes" step (id: check-changes) only
    inspects composer.lock via git diff --quiet composer.lock so it misses updates
    to composer.json; update the check to detect changes to both composer.lock and
    composer.json (e.g., test diffs or git status for composer.json composer.lock)
    and set GITHUB_OUTPUT changes_detected accordingly so PRs are opened when either
    file was modified.

In @.github/workflows/crowdin-sync.yml:

  • Around line 1-105: The workflow is using the wrong branch name for Crowdin
    operations: update the two occurrences of localization_branch_name currently set
    to "master" to "development" inside the "Upload sources to Crowdin" and
    "Download translations from Crowdin" steps (look for the
    localization_branch_name key in those steps and replace its value), so Crowdin
    actions target the repository's default branch.

In @.github/workflows/docker-phpmyadmin.yml:

  • Around line 1-5: The workflow's triggers were changed to only
    workflow_dispatch, removing automatic builds; update the top-level "on" block to
    include workflow_dispatch plus automatic triggers by adding a "push" event for
    the development branch and a "push" with "tags: ['v*..']" (i.e., restore
    push/branches and tag pushes) so the workflow runs both manually and
    automatically upon branch pushes and semver tags; ensure the "on" block contains
    workflow_dispatch, push.branches: [development], and push.tags: ['v*..'] to
    restore the original behavior.

In @.github/workflows/phpunit.yml:

  • Around line 1-49: The workflow uses Laravel-specific files and commands (steps
    named "Prepare Laravel environment" and "Run PHPUnit" that call php artisan and
    reference .env.testing.example) which don't exist in this CodeIgniter 3.3 repo;
    replace those steps with CodeIgniter-appropriate test setup: install PHPUnit via
    Composer (ensure composer.json includes phpunit), add or reference a
    phpunit.xml/phpunit.xml.dist suited to this project, remove php artisan
    key:generate/.env.testing.example usage, and change the Run PHPUnit step to
    invoke vendor/bin/phpunit (or the configured test command) and any necessary DB
    setup (create sqlite or mysql service and migration/seeding commands if you add
    CI test helpers). Ensure the workflow's "Setup PHP with Composer" step provides
    required PHP extensions and that the final test step runs the repository's
    actual test runner instead of php artisan.

In @.github/workflows/release.yml:

  • Line 427: Update the GitHub Actions step that references
    softprops/action-gh-release by changing the action version from v1 to v2
    (replace softprops/action-gh-release@v1 with softprops/action-gh-release@v2) so
    the workflow runs on Node 20; ensure any inputs or outputs used with that step
    remain compatible after the version bump and adjust them if the v2 action
    introduced any breaking changes.

In @.github/workflows/setup.yml:

  • Around line 196-223: The workflow step with id key_generate currently runs the
    Laravel command "php artisan key:generate", but this is a CodeIgniter 3 project;
    replace that call with a CI-appropriate key setup: remove "php artisan
    key:generate" and instead generate a random encryption key (for example via a
    short PHP one-liner or openssl) and write it into your CodeIgniter config (set
    $config['encryption_key'] in application/config/config.php or write to your .env
    if you use one); update the step id key_generate to echo success/failure exactly
    as before based on that command’s exit code. Also scan other workflow steps that
    call "php artisan migrate" or "php artisan db:seed" and replace them with your
    project's migration/seed approach (invoke your custom CLI controllers via "php
    index.php ControllerName method" or run your project-specific migration
    scripts).

In @.github/workflows/test-php.yml:

  • Around line 3-15: Update the GitHub Actions workflow triggers and matrix so CI
    runs on PRs and includes the project-required minimum PHP version: add
    pull_request to the top-level on: triggers (in addition to workflow_dispatch)
    and expand the strategy.matrix.php-versions array to include '8.1' alongside
    '8.2'; confirm the matrix key names operating-system and php-versions remain
    unchanged so the run job picks up the new entries.

In @.github/workflows/yarn-update.yml:

  • Around line 203-211: The workflow step with id updated-packages invokes the
    missing script generate-package-update-report.cjs; either add that script under
    the repository (implement the logic that generates updated-packages.txt and
    exits non-zero on errors) or change the step to point to the existing script
    path/name that produces updated-packages.txt; update the run block to call the
    correct script name and ensure the script writes updated-packages.txt before the
    echo to GITHUB_OUTPUT so the "Get updated packages" step succeeds.

In @application/helpers/file_security_helper.php:

  • Line 177: The code calls a non-existent mb_rtrim when building $fullPath in
    file_security_helper.php; replace the mb_rtrim usage with PHP's rtrim() so
    $fullPath = rtrim($baseDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR .
    $basename['filename']; (i.e., update the expression that constructs $fullPath to
    use rtrim instead of mb_rtrim).
  • Line 87: The code calls a non-existent mb_rtrim which will cause a fatal
    error; replace the mb_rtrim call with the standard rtrim when building
    $realBaseWithSep from $realBase and DIRECTORY_SEPARATOR (e.g., update the
    expression that sets $realBaseWithSep to use rtrim($realBase,
    DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR) because DIRECTORY_SEPARATOR is a
    single-byte ASCII char and multibyte handling is unnecessary.

In @application/modules/clients/Enums/ClientTitleEnum.php:

  • Around line 20-31: The tryFrom method currently uses array_search on
    self::getValues() and checks if ($searchResult), which skips the first enum
    (index 0) and returns the index rather than the matched string; change the
    lookup to perform a strict search (e.g., use array_search($value, $values, true)
    or use in_array with strict true) and when a match is found return the matched
    value string (the element from $values), not the numeric index; ensure tryFrom
    returns null (or the expected empty/false value) when no strict match exists.

In @application/modules/invoices/views/view.php:

  • Around line 479-498: The echoed input values for invoice_quote_number,
    invoice_work_order, and invoice_agreement are not escaped and can cause XSS;
    update the value outputs in the view to escape user content (e.g., wrap the
    properties with htmlsc() or htmlspecialchars()) when echoing inside the value
    attribute for the inputs in view.php (references: invoice_quote_number,
    invoice_work_order, invoice_agreement) so the rendered value attribute uses the
    escaped string.

In @application/modules/quotes/controllers/Ajax.php:

  • Around line 21-43: In modal_quote_lookups ensure $data is always defined
    before calling $this->layout->load_view to avoid the undefined variable when
    $client_id is empty: either initialize $data = array() (or with default keys
    'related_quotes','client_id','quote_statuses') before the if block, or add an
    early return/conditional so load_view is only called when $data is set;
    reference the modal_quote_lookups method, the $this->mdl_quotes->... calls, and
    the final $this->layout->load_view('quotes/modal_quote_lookups', $data) when
    applying the fix.
  • Around line 144-146: The POST-extracted variables $quote_work_order and
    $quote_agreement are never added to the record being saved; update the
    controller method in application/modules/quotes/controllers/Ajax.php to include
    these keys in the $db_array (e.g., 'quote_work_order' => $quote_work_order and
    'quote_agreement' => $quote_agreement) before the save/insert call so the values
    persist to the DB; ensure the $db_array used by the existing save/update
    function contains those two fields and run existing migrations/column names to
    match.

In @application/modules/quotes/views/modal_quote_lookups.php:

  • Around line 9-29: Validate that a quote is selected in the
    .select-items-confirm click handler by checking quote_id (from
    $("input[name='quote_id']:checked").val()) and aborting with a user-facing
    message if invalid; wrap the $.post callback parsing in a try/catch around
    JSON.parse(data) and handle parse or AJAX errors by logging and showing an error
    to the user; replace the for..in loop over items with a deterministic iteration
    (e.g., use items[0] or a for loop over numeric indices) so all fields are set
    correctly and move $('#modal-choose-quote').modal('hide') outside the loop so
    the modal is closed only after processing completes; ensure the error-handling
    branches do not attempt to use the parsed items when parsing fails.

In @application/modules/quotes/views/view.php:

  • Around line 349-362: The quote_work_order and quote_agreement values are
    echoed into input value attributes without escaping, enabling XSS; update the
    view (inputs with id "quote_work_order" and "quote_agreement" in view.php) to
    output escaped values using a safe HTML-escaping function (e.g.
    htmlspecialchars($quote->quote_work_order, ENT_QUOTES, 'UTF-8') and
    htmlspecialchars($quote->quote_agreement, ENT_QUOTES, 'UTF-8') or your
    framework's html_escape helper) and always render the value attribute (even when
    empty) to avoid malformed HTML.

In @application/modules/setup/sql/041_1.7.0.sql:

  • Line 1: The migration file 041_1.7.0.sql currently contains only a comment;
    add ALTER TABLE statements to create the missing columns used by the app: add
    client_number (e.g., VARCHAR(255) DEFAULT NULL) to ip_clients, add
    invoice_quote_number (TEXT DEFAULT NULL), invoice_work_order (VARCHAR(255)
    DEFAULT NULL) and invoice_agreement (VARCHAR(255) DEFAULT NULL) to ip_invoices,
    and add quote_work_order (VARCHAR(255) DEFAULT NULL) and quote_agreement
    (VARCHAR(255) DEFAULT NULL) to ip_quotes; ensure the statements are idempotent
    for your DB flavor (or wrapped in a safe conditional/transaction) and follow the
    repository’s naming and datatype conventions so functions referencing these
    fields (client_number, invoice_quote_number, invoice_work_order,
    invoice_agreement, quote_work_order, quote_agreement) will find the columns at
    runtime.

In @application/views/quote_templates/public/InvoicePlane_Web.php:

  • Around line 79-83: Escape the user-supplied VAT/tax ID and attachment name
    outputs to prevent stored XSS by replacing direct echoes of $quote->user_vat_id
    and $quote->user_tax_code (and any attachment name echoes around the other
    mentioned lines) with an HTML-escaped variant; use the project's escaping helper
    (e.g., html_escape()) or PHP's htmlspecialchars(..., ENT_QUOTES, 'UTF-8') when
    outputting these variables so all printed values are properly encoded before
    echoing.

In @assets/core/js/jquery-ui.js:

  • Around line 888-894: The .position() widget is vulnerable to XSS when a string
    passed to options.of is handed directly to $() — change handling so that when
    options.of is a string you call $(document).find(options.of) (ensuring strings
    are treated only as selectors) and otherwise continue to pass non-string values
    (DOM element, jQuery object, Event, window/document) directly to $(); update the
    branch around options.of and target to reflect this secure pattern and consider
    replacing the vendored assets/core/js/jquery-ui.js with jQuery UI 1.13.0+ which
    includes this fix upstream.

In @assets/core/js/scripts.js:

  • Around line 76-77: The preview currently uses .text() in
    update_email_template_preview(), which escapes HTML and shows raw tags; change
    it to write sanitized HTML into the iframe/body (use .html() on
    $('#email-template-preview').contents().find('body')) after running the template
    content through your sanitization function (e.g., sanitizeHtml or a safeSanitize
    helper) to prevent XSS while allowing tags to render; update references to
    $('.email-template-body').val() as the input to the sanitizer before injecting.

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 19, 2026

@nielsdrost7 I've opened a new pull request, #1394, to work on those changes. Once the pull request is ready, I'll request review from you.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 19, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8b234e6f-7896-456d-8ad9-6c22087e2a46

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds comprehensive GitHub Actions CI/CD infrastructure, implements quote-related features (work order and agreement fields), migrates from codeigniter/framework to pocketarc/codeigniter, and applies widespread code formatting improvements. It includes new database migrations, a quote lookup modal UI, security fixes for HTML sanitization, and configuration updates across build and deployment workflows.

Changes

Cohort / File(s) Summary
New CI/CD Workflows
.github/workflows/composer-update.yml, yarn-update.yml, crowdin-sync.yml, phpstan.yml, phpunit.yml, pint.yml, quickstart.yml, release.yml, setup.yml, docker.yml
Added 10 new GitHub Actions workflows implementing automated dependency updates (Composer, Yarn), static analysis (PHPStan), testing (PHPUnit), code formatting (Pint), production releases with versioning/packaging, internationalization sync (Crowdin), and smoke tests. Complex orchestration with conditional logic, artifact management, and PR creation.
Modified CI/CD Workflows
.github/workflows/docker-*.yml, test-frontend.yml, test-php.yml
Changed Docker workflows from automatic push triggers to manual dispatch; updated test workflows to support manual triggering and expanded PHP version matrix from 8.1 to [8.1, 8.2].
Workflow Documentation
.github/workflows/README.md, .github/scripts/generate-package-update-report.cjs
Added comprehensive workflow documentation and script for parsing Yarn lock diffs to generate update reports.
Quote Features
application/modules/quotes/controllers/Ajax.php, models/Mdl_quotes.php, views/modal_quote_lookups.php, views/view.php
Introduced quote work order and agreement fields, added modal UI for related quote selection with AJAX integration, and new query scopes (is_related_quote, related_is_open) for filtering.
Invoice Features
application/modules/invoices/views/view.php, partial_invoice_table.php
Added invoice quote number, work order, and agreement fields with UI controls; introduced related quote modal integration.
Client Features
application/modules/clients/controllers/Clients.php, models/Mdl_clients.php, Enums/ClientTitleEnum.php, views/partial_client_table.php, views/view.php, application/language/english/ip_lang.php, application/helpers/client_helper.php
Added client_number field, formatting helper, and new language strings; promoted enum constants from private to public.
Database Migrations
application/modules/setup/sql/038_1.6.1.sql, 041_1.7.0.sql
Added six new nullable VARCHAR/TEXT columns: client_number (ip_clients), invoice_quote_number/invoice_work_order/invoice_agreement (ip_invoices), quote_work_order/quote_agreement (ip_quotes).
Dependency Updates
composer.json, package.json, index.php
Migrated from codeigniter/framework to pocketarc/codeigniter 3.3; updated guzzlehttp/guzzle, moneyphp/money, phpmailer/phpmailer, laravel/pint, rector/rector; upgraded Sass from ^1.89.2 to ^1.97.
Frontend Security
assets/core/js/scripts.js, assets/core/js/jquery-ui.js
Implemented sanitizeHtml() function to prevent XSS in email template previews; fixed jQuery position selector resolution and escapeSelector regex for special characters.
Code Formatting & Helpers
application/helpers/{e-invoice,file_security,mailer,pager,pdf}_helper.php, application/libraries/MY_Form_validation.php, application/modules/{sessions,setup,upload,reports,settings}/...
Widespread whitespace normalization, spacing adjustments around operators, and docblock punctuation updates; refactored short-circuit assignments (e.g., strpos → str_starts_with, ternary condensing).
View Templates
application/views/{invoice,quote}_templates/{pdf,public}/*.php, application/modules/{guest,layout,email_templates}/views/...
HTML structural reflow, indentation consistency, layout adjustments for new fields; removed duplicate discount cells; improved client/user details rendering.
Miscellaneous
.gitignore
Ignored stan.txt and phpunit.txt output files.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • laodc
  • naui95

Poem

🐰 Workflows now dance with actions new,
Quotes and invoices get their due,
From CodeIgniter's framework old,
To Pocket Arc—the codebase bold!
With schemas grown and features bright,
This release hops into the night.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding new fields (client_number, quote_work_order, quote_agreement, invoice_quote_number, invoice_work_order, invoice_agreement) and associated functions across clients, quotes, and invoices modules.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 1022-add-extra-fields-and-functions-to-client-and-quotes-and-invoices

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
application/modules/sessions/controllers/Sessions.php (1)

451-556: Add explicit PHP version constraint to composer.json.

The codebase uses str_contains() and str_starts_with(), which require PHP 8.0+. While the Docker configuration specifies PHP 8.1, composer.json lacks an explicit PHP version requirement in the "require" section. This allows installations without the required runtime, causing fatal errors. Add "php": ">=8.1" to composer.json to enforce the minimum version requirement.

🤖 Fix all issues with AI agents
In @.github/scripts/generate-package-update-report.cjs:
- Around line 26-33: The current pattern in the package extraction block (where
updatedPackages is populated) is too loose and picks up non-package keys like
"__metadata"; update the parsing to only consider yarn.lock entry header lines
that end with a colon and then extract the package descriptor/name from that
header (i.e., match lines like 'package@version:'), rejecting keys without
trailing ':'; modify the two regex checks around the line.match(...) and the
descriptor extraction (the match(...) used to populate updatedPackages) to first
assert the line ends with ':' and then capture the package identifier (including
scoped names and potential slash) from the descriptor before adding to
updatedPackages.

In @.github/workflows/docker-publish.yml:
- Around line 3-4: The workflow currently only uses the workflow_dispatch
trigger, so automatic Docker builds on version tag pushes were removed; restore
the push.tags trigger by updating the workflow's triggers to include a push
event filtering tags (e.g., add a push: tags: ['v*'] entry alongside
workflow_dispatch) so that when version tags like "v1.7.0" are pushed the Docker
publish job runs automatically; modify the "on:" block in
.github/workflows/docker-publish.yml (where workflow_dispatch is declared) to
include the push.tags pattern.

In @.github/workflows/phpstan.yml:
- Around line 65-93: The "Comment on PR (if available)" step is unreachable
because the workflow only triggers on workflow_dispatch while the step checks
github.event_name == 'pull_request'; either remove this step entirely if PR
comments aren't needed, or update the workflow triggers to include pull_request
so the condition can be true — adjust the top-level "on:" entries to add
pull_request (or change the step's if to match an existing trigger) and keep the
step's existing fs/readFileSync and github.rest.issues.createComment logic
intact.

In @.github/workflows/pint.yml:
- Around line 14-18: Update the pull_request trigger filter under the workflow's
pull_request block so it includes the repository's actual default branch name;
replace or add the correct branch 'development' alongside 'master' and any other
branches listed (currently 'develop') so the Pint workflow runs for PRs
targeting 'development' — modify the branches array in the pull_request YAML
block to include 'development' (and remove 'develop' if that was a typo).
- Around line 93-101: The push step "Commit and Push Changes" should skip when
the PR comes from a fork; update its if condition to only run when changes exist
and the workflow is running in the same repository (not a fork). Replace the
current condition (steps.verify_diff.outputs.has_changes == 'true') with a
compound check such as: steps.verify_diff.outputs.has_changes == 'true' &&
(github.event.pull_request == null ||
github.event.pull_request.head.repo.full_name == github.repository) so the push
runs for non-PR runs or PRs originating from the same repo but is skipped for
forked PRs.

In @.github/workflows/quickstart.yml:
- Around line 59-68: The workflow currently runs Laravel-only commands (steps
named "Prepare Laravel environment", "Run database migrations", "Run smoke
tests") using `php artisan`, which don't exist in this CodeIgniter 3.x repo;
remove or replace those steps: either delete this workflow if no CI tests are
needed, or update the steps to CodeIgniter-compatible actions such as installing
PHP dependencies (composer install), copying the repository's CodeIgniter
env/example file into the runtime env, setting up any DB fixtures/migrations
using the project's migration script or SQL seed files, and running tests via
the project's configured test runner (e.g., phpunit) instead of `php artisan
test`; adjust step names ("Prepare Laravel environment", "Run database
migrations", "Run smoke tests") accordingly so they reflect CodeIgniter
commands.

In @.github/workflows/README.md:
- Around line 623-644: Remove the hard-coded line numbers in the README and
update the instructions to reference searchable identifiers instead: instruct
users to edit the release.yml keys (php-version, node-version, retention-days)
by locating those keys in release.yml and to modify the "Create release zip"
step by its step name ("Create release zip") when changing ZIP exclusions;
replace each outdated line-number instruction with guidance to find and edit the
specific key or step name so the docs remain correct across file edits.
- Line 40: Update the README line that currently says "Automatically runs on
every push to the `master` branch" to accurately describe the actual trigger
used by the workflow: reference the workflow file (release.yml) and state it
uses workflow_dispatch (manual trigger) and requires selecting a release type;
replace the sentence accordingly so it mentions manual dispatch and release type
selection rather than automatic pushes to master.

In @.github/workflows/setup.yml:
- Around line 34-51: Update the workflow input descriptions to remove "php
artisan" and reference CodeIgniter's CLI instead: replace the description string
'Run php artisan key:generate' with something like 'Run CodeIgniter CLI (php
spark) key generation' (or 'Run php spark key:generate'), change 'Run php
artisan migrate' (input run_migrate) to 'Run CodeIgniter migration (php spark
migrate)' and change 'Run php artisan db:seed' (input run_seed) to 'Run
CodeIgniter seeder (php spark db:seed)'; ensure you only update the description
values for these inputs so the dispatch UI labels are accurate for a CodeIgniter
workflow.

In @.github/workflows/test-frontend.yml:
- Around line 6-7: The workflow currently only uses the on: workflow_dispatch
trigger which disables automatic PR testing; update the on: section to include
pull_request (e.g., change from only workflow_dispatch to on: { pull_request:
{}, workflow_dispatch: {} }) so frontend tests run automatically on PRs as well
as allowing manual dispatch; ensure the pull_request entry is present and
configured with any branch filters you need.

In `@application/modules/guest/controllers/gateways/Stripe.php`:
- Line 122: The code is accessing nonexistent Checkout Session properties
($session->cancel, $session->cancellation_reason, $session->last_payment_error);
update the logic that builds the error/explanation string to use
$session->status or $session->payment_status instead and remove uses of
$session->cancel/cancellation_reason, and when you need detailed payment errors
expand or retrieve the session with 'payment_intent' and read
$session->payment_intent->last_payment_error as the source of payment failure
info; adjust the fallback plumbing so the expression that previously referenced
($session->cancel ? $session->cancellation_reason :
$session->last_payment_error) instead checks status/payment_status and, if
necessary, uses the expanded payment_intent->last_payment_error for detailed
messages.

In `@application/modules/invoices/views/view.php`:
- Around line 468-492: These three text inputs (invoice_quote_number,
invoice_work_order, invoice_agreement) are editable even when the invoice is
read-only; update their markup to emit the same read-only/disabled attribute
used by other invoice fields by conditionally adding readonly/disabled when the
invoice is read-only (use the same condition/variable other fields use, e.g.
$invoice->is_read_only or the existing invoice-status check), so the inputs are
non-editable in read-only state.

In `@application/modules/quotes/controllers/Ajax.php`:
- Around line 49-62: The modal_quote_lookups_select method currently returns
nothing when $quote_id is empty which breaks client-side JSON.parse; update
modal_quote_lookups_select to validate $quote_id and return a consistent JSON
error response (and appropriate HTTP status) instead of silence — e.g., when
$quote_id is empty respond with a JSON object like {"error":"quote_id required"}
using the same output mechanism used elsewhere (use
$this->output->set_content_type('application/json')->set_output(json_encode(...))
or equivalent) so callers parsing JSON won't fail; keep the existing logic that
loads $this->mdl_quotes and returns the quote when $quote_id is present.

In `@application/modules/quotes/views/modal_quote_lookups.php`:
- Around line 101-103: The current echo of
$quote_statuses[$quote->quote_status_id]['label'] can raise an undefined index
when $quote->quote_status_id is missing or not present in $quote_statuses;
update the view to check that isset($quote_statuses[$quote->quote_status_id])
(and optionally isset(['label'])) before accessing it and render a safe fallback
(e.g. a default label like "Unknown" or an empty string) when the lookup fails
so the template doesn't trigger notices; locate the usage in
modal_quote_lookups.php where $quote_statuses and $quote->quote_status_id are
referenced and add the conditional fallback logic there.

In `@application/modules/sessions/controllers/Sessions.php`:
- Around line 323-444: The current session-based rate limiting in
_is_ip_rate_limited_password_reset, _record_password_reset_attempt,
_is_email_rate_limited_password_reset and _record_email_password_reset_attempt
is bypassable by clearing cookies; replace session storage with a server-side
store (Redis or DB) keyed by IP/email with a TTL (e.g. keys like
password_reset:ip:{md5(ip)} and password_reset:email:{md5(email)}), implement
atomic increment or append-with-expiry and window-based trimming on read,
enforce counts against env-configured limits, persist attempts with expiry
instead of session->userdata/session->set_userdata, and update logging and tests
to reflect the new storage mechanism.

In `@application/modules/setup/sql/038_1.6.1.sql`:
- Around line 1-6: The migration duplicates columns already added in
041_1.7.0.sql causing "Duplicate column name" errors; update 038_1.6.1.sql to
avoid re-adding the same columns by either removing the six ALTER TABLE
statements for client_number (ip_clients), invoice_quote_number,
invoice_work_order, invoice_agreement (ip_invoices), and quote_work_order,
quote_agreement (ip_quotes), or replace each direct ALTER with conditional logic
that first checks for the column's existence (e.g., query
INFORMATION_SCHEMA/COLUMN_NAME or use CREATE/ALTER ... IF NOT EXISTS where
supported) and only runs the ALTER if the column is missing; ensure the chosen
fix preserves the intended data type/position consistency with 041_1.7.0.sql
(VARCHAR(255)/TEXT and AFTER positions) so sequential upgrades do not conflict.

In `@application/modules/setup/sql/041_1.7.0.sql`:
- Around line 1-17: The migrations create the same columns with conflicting
types—ensure a single canonical type and remove duplicate creations: update
041_1.7.0.sql to use the same types as 038_1.6.1.sql (set client_number,
invoice_quote_number, invoice_work_order, invoice_agreement, quote_work_order,
quote_agreement to VARCHAR(100) instead of VARCHAR(255)/TEXT) or conversely
consolidate by removing the duplicate column ALTERs from 038_1.6.1.sql and
standardizing on VARCHAR(255)/TEXT across both files; pick one schema, update
the ALTER TABLE statements in ip_clients/ip_invoices/ip_quotes (referencing
client_number, invoice_quote_number, invoice_work_order, invoice_agreement,
quote_work_order, quote_agreement) to that type, and delete the
duplicate/contradictory ALTER in the other migration so only one migration adds
each column.

In `@assets/core/js/scripts.js`:
- Around line 76-114: The anchor-href regex in sanitizeHtml incorrectly uses a
character class for &quot; which breaks matching; update the replacement that
handles <a> tags (the escaped variable replacement block) to use a non-greedy
capture like /&lt;a\s+href=&quot;(.*?)&quot;&gt;/gi (keeping the same href
validation logic and callback signature), so the entire href value between the
escaped &quot;...&quot; is captured reliably; ensure you update the
corresponding replace call on escaped (inside sanitizeHtml) to use that new
regex and preserve the existing URL validation checks.

In `@package.json`:
- Around line 38-40: package.json is pinning "sass": "^1.97" which introduces
breaking changes; before upgrading, search the codebase for deprecated/changed
Sass patterns and update them: locate any "@function type" definitions and
rename them, replace all "@import" usages with "@use" / "@forward" modules,
remove or replace "meta.feature-exists()" calls, audit any "@use"/"@forward ...
with" that depend on private variables and convert to public APIs, and scan for
nested rules/declarations whose emitted order may change and adjust
selectors/mixins accordingly; run the test build (and compare generated CSS)
after updating to validate outputs and adjust styles where order-dependent
behavior changed.
♻️ Duplicate comments (3)
assets/core/js/jquery-ui.js (1)

888-894: Security fix for XSS vulnerability correctly applied.

This change properly mitigates CVE-2021-41184 by using $(document).find(options.of) for string inputs, ensuring they're treated strictly as selectors rather than potentially interpreted as HTML. The else branch correctly handles non-string types (DOM elements, jQuery objects, Events, window/document) which are safe to pass directly to $().

As noted in previous reviews, consider upgrading to jQuery UI 1.13.0+ which includes this fix natively, reducing the maintenance burden of vendored patches.

application/views/quote_templates/public/InvoicePlane_Web.php (1)

114-123: Escape client VAT/tax IDs and attachment names to prevent stored XSS.

These fields can contain user-supplied values and should be HTML-escaped before output.

🛠️ Suggested fix
-                            echo ': ' . $quote->client_vat_id . '<br>';
+                            echo ': ' . htmlsc($quote->client_vat_id) . '<br>';
...
-                            echo ': ' . $quote->client_tax_code . '<br>';
+                            echo ': ' . htmlsc($quote->client_tax_code) . '<br>';
...
-                                            <td><?php echo $attachment['name']; ?></td>
+                                            <td><?php echo htmlsc($attachment['name']); ?></td>

Also applies to: 315-317

.github/workflows/composer-update.yml (1)

86-92: Smoke-test step still calls Laravel Artisan.

If this is CodeIgniter, artisan won’t exist and this step will fail or be skipped unintentionally. Please switch to the project’s actual test runner or guard the command.

🛠️ Safer guard (replace with project-specific command as needed)
-          php artisan key:generate --env=testing
-          php artisan test --configuration=phpunit.smoke.xml --env=testing
+          if [ -f artisan ]; then
+            php artisan key:generate --env=testing
+            php artisan test --configuration=phpunit.smoke.xml --env=testing
+          elif [ -x vendor/bin/phpunit ]; then
+            vendor/bin/phpunit --configuration=phpunit.smoke.xml
+          else
+            echo "::warning::No test runner found; skipping smoke tests."
+          fi
#!/bin/bash
# Verify whether Laravel's artisan or a phpunit runner exists
ls -la artisan 2>/dev/null || true
fd -a 'phpunit\.smoke\.xml|phpunit\.xml' .
fd -a 'phpunit' vendor/bin 2>/dev/null || true
🧹 Nitpick comments (9)
application/modules/settings/models/Mdl_versions.php (1)

44-44: Prefer explicit return null; for clarity.

Using return; implicitly returns null, but an explicit return null; would be more consistent with the docblock (@return string|null) and makes the intent clearer for maintainability.

✨ Suggested change
         if ($result->query->num_rows() === 0) {
-            return;
+            return null;
         }
.github/workflows/test-frontend.yml (1)

14-15: Node 14 and 16 are end-of-life."Production applications should only use Active LTS or Maintenance LTS releases." Node.js 18 maintenance LTS ends April 2025, Node.js 20 ends April 2026, and Node.js 22 ends April 2027. Node 14 and 16 reached EOL in April 2023 and September 2023 respectively.

Testing against EOL versions provides limited value and may produce false positives on deprecated APIs.

♻️ Suggested update to supported Node.js versions
     matrix:
       operating-system: [ubuntu-latest]
-      node: ['14', '16', '18']
+      node: ['20', '22']
application/helpers/client_helper.php (1)

49-56: Add null safety check for $client parameter.

Accessing $client->client_number without checking if $client is null could cause an error when called with invalid data. Also, consider using strict comparison or empty() for consistency with the rest of the codebase.

♻️ Suggested improvement
 function format_client_number($client)
 {
-    if ($client->client_number != "") {
+    if (!empty($client) && !empty($client->client_number)) {
         return $client->client_number;
     }
 
     return "00-00000";
 }
.github/workflows/docker.yml (2)

19-32: Consider adding a cleanup step to ensure containers are stopped.

The workflow builds and starts containers but doesn't clean them up. While GitHub-hosted runners are ephemeral, adding a cleanup step is a good practice for consistency and in case the workflow is ever run on self-hosted runners.

♻️ Suggested cleanup step
       - name: Check running services
         run: docker-compose ps
 
       `#-` name:
       #    Optional: Run health checks or tests
       #  run:
       #    echo "TODO: insert test commands after containers are up"
+
+      - name: Cleanup
+        if: always()
+        run: docker-compose down --volumes --remove-orphans

23-24: Use docker compose instead of the deprecated docker-compose command.

Docker Compose V2 is integrated into Docker CLI and the standalone docker-compose command is deprecated. Docker recommends using docker compose (with a space) for forward compatibility and to benefit from active maintenance. The docker-compose command continues to work through aliasing but will receive only critical and security fixes.

Consider updating line 24 from docker-compose up -d --build to docker compose up -d --build.

.github/workflows/phpstan.yml (1)

25-30: Consider capturing PHPStan exit code separately.

When PHPStan finds errors, it exits non-zero, but continue-on-error: true masks this. The subsequent "Check for errors" step (lines 50-54) correctly fails, but capturing the exit code explicitly would be cleaner and more debuggable.

Suggested improvement
      - name: Run PHPStan (JSON output)
        id: phpstan-analysis
        run: |
-          vendor/bin/phpstan analyse --memory-limit=1G --error-format=json > phpstan.json
-          cat phpstan.json
+          set +e
+          vendor/bin/phpstan analyse --memory-limit=1G --error-format=json > phpstan.json
+          echo "exit_code=$?" >> $GITHUB_OUTPUT
+          set -e
+          cat phpstan.json
        continue-on-error: true
application/modules/quotes/models/Mdl_quotes.php (1)

345-361: Use short array syntax for consistency.
The rest of the model already uses [].

♻️ Suggested refactor
-    $this->filter_where_in('quote_status_id', array(2, 3, 4));
+    $this->filter_where_in('quote_status_id', [2, 3, 4]);
application/modules/quotes/views/view.php (1)

347-359: XSS protection correctly applied.

The htmlsc() function properly escapes output values. The null coalescing (?? '') handles missing values gracefully.

For improved accessibility, consider adding for attributes to the labels to formally associate them with the inputs.

♿ Accessibility improvement
                             <div class="col-xs-12 col-md-4">
                                 <div class="quote-properties">
-                                    <label><?php _trans('work_order'); ?></label>
+                                    <label for="quote_work_order"><?php _trans('work_order'); ?></label>
                                     <input type="text" id="quote_work_order" class="form-control input-sm"
                                         value="<?php echo htmlsc($quote->quote_work_order ?? ''); ?>">

                                 </div>
                                 <div class="quote-properties">
-                                    <label><?php _trans('agreement'); ?></label>
+                                    <label for="quote_agreement"><?php _trans('agreement'); ?></label>
                                     <input type="text" id="quote_agreement" class="form-control input-sm"
                                         value="<?php echo htmlsc($quote->quote_agreement ?? ''); ?>">
                                 </div>
                             </div>
application/modules/quotes/controllers/Ajax.php (1)

21-47: Previous undefined $data issue resolved.

The $data array is now properly initialized with default values (lines 27-32) before conditional population.

However, line 37 calls $this->mdl_quotes->statuses() without using the return value - this call is redundant since statuses() is called again on line 40.

♻️ Remove redundant call
         if (!empty($client_id)) {
             $this->mdl_quotes->by_client($client_id);
             $this->mdl_quotes->related_is_open();
-            $this->mdl_quotes->statuses();

             $quotes = $this->mdl_quotes->get()->result();
             $quote_statuses = $this->mdl_quotes->statuses();

Comment thread .github/scripts/generate-package-update-report.cjs Outdated
Comment thread .github/workflows/docker-publish.yml
Comment thread .github/workflows/phpstan.yml Outdated
Comment thread .github/workflows/pint.yml Outdated
Comment thread .github/workflows/pint.yml Outdated
Comment on lines 323 to 444
/**
* Check if IP address has exceeded rate limit for password resets using session storage
* Check if IP address has exceeded rate limit for password resets using session storage.
*
* @param int $max_attempts Maximum attempts allowed per hour
* @param int $max_attempts Maximum attempts allowed per hour
* @param int $window_minutes Time window in minutes
*
* @return bool True if rate limited, false otherwise
*/
private function _is_ip_rate_limited_password_reset()
{
$max_attempts = env('PASSWORD_RESET_IP_MAX_ATTEMPTS', 5);
$max_attempts = env('PASSWORD_RESET_IP_MAX_ATTEMPTS', 5);
$window_minutes = env('PASSWORD_RESET_IP_WINDOW_MINUTES', 60);
$ip_address = $this->input->ip_address();

$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) {

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) {
$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;
}

/**
* Record a password reset attempt for the current IP
* Record a password reset attempt for the current IP.
*/
private function _record_password_reset_attempt()
{
$ip_address = $this->input->ip_address();
$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) {

if ( ! $attempts) {
$attempts = [];
}

// Add current timestamp
$attempts[] = time();

// Store back to session
$this->session->set_userdata($session_key, $attempts);
}

/**
* Check if email-based rate limit exceeded for password resets using session storage
* Check if email-based rate limit exceeded for password resets using session storage.
*
* @param string $email Email address to check
* @param int $max_attempts Maximum attempts allowed
* @param int $window_hours Time window in hours
* @param string $email Email address to check
* @param int $max_attempts Maximum attempts allowed
* @param int $window_hours Time window in hours
*
* @return bool True if rate limited, false otherwise
*/
private function _is_email_rate_limited_password_reset($email)
{
$max_attempts = env('PASSWORD_RESET_EMAIL_MAX_ATTEMPTS', 3);
$window_hours = env('PASSWORD_RESET_EMAIL_WINDOW_HOURS', 1);

$session_key = 'password_reset_email_' . md5($email);

// Get current attempts from session
$attempts = $this->session->userdata($session_key);
if (!$attempts) {

if ( ! $attempts) {
$attempts = [];
}

// Clean up old attempts outside the time window
$cutoff_time = time() - ($window_hours * 3600);
$attempts = array_filter($attempts, function($timestamp) use ($cutoff_time) {
$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_email_rate_limit_check') . ': ' . count($attempts) . ' attempts for email: ' . $email);

return true;
}

return false;
}

/**
* Record a password reset attempt for a specific email
* Record a password reset attempt for a specific email.
*
* @param string $email Email address
*/
private function _record_email_password_reset_attempt($email)
{
$session_key = 'password_reset_email_' . md5($email);

// Get current attempts from session
$attempts = $this->session->userdata($session_key);
if (!$attempts) {

if ( ! $attempts) {
$attempts = [];
}

// Add current timestamp
$attempts[] = time();

// Store back to session
$this->session->set_userdata($session_key, $attempts);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Session-scoped reset limits are bypassable.

Because attempts are stored in the session, a requester can clear cookies/start a new session and reset the counters. For password resets, this defeats rate limiting. Consider persisting attempts server-side (DB/cache) keyed by IP/email with TTL.

🤖 Prompt for AI Agents
In `@application/modules/sessions/controllers/Sessions.php` around lines 323 -
444, The current session-based rate limiting in
_is_ip_rate_limited_password_reset, _record_password_reset_attempt,
_is_email_rate_limited_password_reset and _record_email_password_reset_attempt
is bypassable by clearing cookies; replace session storage with a server-side
store (Redis or DB) keyed by IP/email with a TTL (e.g. keys like
password_reset:ip:{md5(ip)} and password_reset:email:{md5(email)}), implement
atomic increment or append-with-expiry and window-based trimming on read,
enforce counts against env-configured limits, persist attempts with expiry
instead of session->userdata/session->set_userdata, and update logging and tests
to reflect the new storage mechanism.

Comment thread application/modules/setup/sql/038_1.6.1.sql Outdated
Comment thread application/modules/setup/sql/041_1.7.0.sql Outdated
Comment thread assets/core/js/scripts.js Outdated
Comment thread package.json Outdated
@nielsdrost7 nielsdrost7 changed the base branch from development to prep/v172 January 20, 2026 07:45
@nielsdrost7 nielsdrost7 marked this pull request as draft January 20, 2026 08:47
Copilot AI and others added 8 commits April 16, 2026 15:50
…afety, dead code, DOM sanitizer

Agent-Logs-Url: https://github.com/InvoicePlane/InvoicePlane/sessions/bf2724e4-678b-4ed5-9c24-3a3758d28493

Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com>
…er entry point

Agent-Logs-Url: https://github.com/InvoicePlane/InvoicePlane/sessions/bf2724e4-678b-4ed5-9c24-3a3758d28493

Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com>
…ed as HTML'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…ate CHANGELOG and UPGRADE guide

Agent-Logs-Url: https://github.com/InvoicePlane/InvoicePlane/sessions/d48e22c6-b03a-42e2-86e9-941490936d14

Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com>
@nielsdrost7 nielsdrost7 force-pushed the 1022-add-extra-fields-and-functions-to-client-and-quotes-and-invoices branch 2 times, most recently from 113d55e to e8558ef Compare April 18, 2026 07:14
@nielsdrost7 nielsdrost7 force-pushed the 1022-add-extra-fields-and-functions-to-client-and-quotes-and-invoices branch from e8558ef to 5c41820 Compare April 18, 2026 07:24
@nielsdrost7
Copy link
Copy Markdown
Contributor

Hey @novafirst can you re-do your feature please?
During squashing only the addition of 1 single field in the clients table survived.

The code probably in Controllers. Models, etc, didn't survive and therefore the feature isn't complete.

My bad! my apologies.
Can you re-do please?

Base automatically changed from prep/v172 to develop April 25, 2026 09:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature Request Improvement or Feature Request Needs review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Add Client Number to Clients module and add Client Number, Quote, Work Order, and Agreement fields to Quotes and Invoices modules

4 participants