Skip to content

Conversation

@novafirst
Copy link

@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.

Added client_number to ip_clients table

Added to ip_invoices table:
invoice_quote_number,
invoice_work_order,
invoice_agreement
…e_agreement

Added new fields:

invoice_quote_number,
invoice_work_order,
invoice_agreement,

and resized css styles to accommodate extra fields
Added labels for:

client_number
quote_number
work_order
agreement
Added field client_number to list of quotes
Added entry for related_quote_number
Added related_quote_number instead of quote_number
Changed layout and added quote_work_order and quote_agreement text fields
Alter ip_quotes table to add quote_work_order and quote_agreement
In function validation_rules added

quote_work_order
quote_agreement
In function validation_rules added

invoice_quote_number
invoice_work_order
invoice_agreement
Added quote_work_order and quote_agreement fields
Added quote_work_order and quote_agreement fields
Added quote_work_order and quote_agreement fields
Added invoice_quote_number, invoice_work_order and invoice_agreement
Added invoice_quote_number, invoice_work_order and invoice_agreement
Added option to select related quote number
Added an option to select related quote in invoice module
Added an option to select related quote number in invoice module
Added an option to select related quote number in invoice module
Copy link

@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.

@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
Added a few options for newly created fields
@nielsdrost7
Copy link
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

@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.

Copy link

@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

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

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 on lines 1 to 5
name: Build & Push PHPMyAdmin Docker Image

on:
push:
branches:
- development
paths:
- 'resources/docker/phpmyadmin/Dockerfile'
push:
tags:
- 'v*.*.*'
workflow_dispatch:

Copy link

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

🏁 Script executed:

cat -n .github/workflows/docker-phpmyadmin.yml | head -20

Repository: InvoicePlane/InvoicePlane

Length of output: 602


🏁 Script executed:

git log --oneline -n 20 -- .github/workflows/docker-phpmyadmin.yml

Repository: InvoicePlane/InvoicePlane

Length of output: 177


🏁 Script executed:

git diff HEAD~5 HEAD -- .github/workflows/docker-phpmyadmin.yml

Repository: InvoicePlane/InvoicePlane

Length of output: 99


🏁 Script executed:

git log --all --oneline -- .github/workflows/docker-phpmyadmin.yml

Repository: InvoicePlane/InvoicePlane

Length of output: 198


🏁 Script executed:

git show a71ce175:.github/workflows/docker-phpmyadmin.yml 2>/dev/null | head -20

Repository: InvoicePlane/InvoicePlane

Length of output: 462


🏁 Script executed:

git log --all --format="%H %s" -- .github/workflows/docker-phpmyadmin.yml

Repository: InvoicePlane/InvoicePlane

Length of output: 262


🏁 Script executed:

git show 3879f70d:.github/workflows/docker-phpmyadmin.yml 2>/dev/null | head -20

Repository: InvoicePlane/InvoicePlane

Length of output: 411


🏁 Script executed:

git diff 3879f70d a71ce175 -- .github/workflows/docker-phpmyadmin.yml

Repository: InvoicePlane/InvoicePlane

Length of output: 551


Automatic image builds removed; workflow now manual-only. The push triggers on development branch and v*.*.* tags were replaced with workflow_dispatch, meaning builds won't trigger automatically on code changes or releases. If automatic builds are needed, restore the push and tag triggers alongside manual dispatch.

✅ Suggested trigger config (manual + automatic)
 on:
+  push:
+    branches:
+      - development
+    paths:
+      - 'resources/docker/phpmyadmin/Dockerfile'
+  push:
+    tags:
+      - 'v*.*.*'
   workflow_dispatch:
📝 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: Build & Push PHPMyAdmin Docker Image
on:
push:
branches:
- development
paths:
- 'resources/docker/phpmyadmin/Dockerfile'
push:
tags:
- 'v*.*.*'
workflow_dispatch:
name: Build & Push PHPMyAdmin Docker Image
on:
push:
branches:
- development
tags:
- 'v*.*.*'
paths:
- 'resources/docker/phpmyadmin/Dockerfile'
workflow_dispatch:
🤖 Prompt for AI Agents
In @.github/workflows/docker-phpmyadmin.yml around lines 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.

Comment on lines 1 to 49
name: Run PHPUnit Tests

on:
workflow_dispatch: # Manual only — does not run automatically

jobs:
phpunit:
runs-on: ubuntu-latest
permissions:
contents: read

# MySQL service commented out - tests use SQLite in-memory instead
# services:
# mysql:
# image: mysql:8
# env:
# MYSQL_ROOT_PASSWORD: root
# MYSQL_DATABASE: testing
# ports:
# - 3306:3306
# options: >-
# --health-cmd="mysqladmin ping --silent"
# --health-interval=10s
# --health-timeout=5s
# --health-retries=3

steps:
- uses: actions/checkout@v4

- name: Setup PHP with Composer
uses: ./.github/actions/setup-php-composer
with:
php-version: '8.3'
php-extensions: 'mbstring, bcmath, pdo_mysql'
composer-flags: '--no-progress --prefer-dist --optimize-autoloader'

- name: Prepare Laravel environment
run: |
cp .env.testing.example .env.testing
php artisan key:generate --env=testing
#- name: Run Laravel migrations
# run: php artisan migrate --force --env=testing

#- name: Run Laravel seeds (optional)
# run: php artisan db:seed --force --env=testing

- name: Run PHPUnit
run: php artisan test --env=testing
Copy link

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
# Verify Laravel artifacts expected by the workflow
echo "=== Checking for artisan file ==="
fd -a '^artisan$'

echo "=== Checking for .env.testing.example file ==="
fd -a '\.env\.testing\.example$'

echo "=== Checking for laravel/framework in composer.json ==="
rg -n '"laravel/framework"' composer.json

Repository: InvoicePlane/InvoicePlane

Length of output: 205


🏁 Script executed:

#!/bin/bash
# Identify the actual project structure
echo "=== Root directory files ==="
ls -la | head -20

echo "=== Checking for composer.json ==="
test -f composer.json && wc -l composer.json || echo "No composer.json"

echo "=== Checking for phpunit.xml ==="
fd -a 'phpunit\.xml'

echo "=== Checking for test directory ==="
fd -type d -name tests | head -5

echo "=== Checking for vendor/bin/phpunit ==="
test -f vendor/bin/phpunit && echo "phpunit found in vendor" || echo "No vendor/bin/phpunit"

echo "=== Looking at composer.json (first 50 lines) ==="
head -50 composer.json 2>/dev/null || echo "No composer.json"

Repository: InvoicePlane/InvoicePlane

Length of output: 3263


🏁 Script executed:

#!/bin/bash
# Check for test-related files and environment setup
echo "=== Looking for any test files or directories ==="
find . -type f \( -name "*.test.php" -o -name "*Test.php" -o -name "test.php" \) 2>/dev/null | head -10

echo "=== Checking for .env files ==="
find . -name ".env*" -type f 2>/dev/null

echo "=== Checking application directory structure ==="
ls -la application/ | head -20

echo "=== Looking for phpunit configuration anywhere ==="
find . -name "phpunit*" 2>/dev/null | head -10

echo "=== Checking if tests folder exists anywhere ==="
find . -type d -name "*test*" 2>/dev/null | head -10

Repository: InvoicePlane/InvoicePlane

Length of output: 1299


Rewrite phpunit.yml workflow—this project uses CodeIgniter, not Laravel.

The workflow assumes Laravel commands (php artisan, key:generate) and files (.env.testing.example), but this repository is a CodeIgniter 3.3 project with no test infrastructure. Commands like php artisan test, php artisan key:generate, and php artisan migrate do not exist in CodeIgniter. Either reconfigure this workflow for CodeIgniter testing or establish an appropriate testing framework and infrastructure for this project.

🤖 Prompt for AI Agents
In @.github/workflows/phpunit.yml around lines 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.


# Step 9: Create GitHub Draft Pre-Release
- name: Create GitHub Draft Pre-Release
uses: softprops/action-gh-release@v1
Copy link

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

🏁 Script executed:

# First, let's examine the workflow file around line 427
cat -n .github/workflows/release.yml | sed -n '420,435p'

Repository: InvoicePlane/InvoicePlane

Length of output: 664


🌐 Web query:

softprops/action-gh-release v1 deprecated runtime node

💡 Result:

Short answer: the v1 tag of softprops/action-gh-release runs on an older Node runtime (Node 16) and will trigger GitHub Actions deprecation warnings; upgrade your workflow to use the v2 release (which declares node20) to stop the warning.

What to do:

  • Replace uses: softprops/action-gh-release@v1 with softprops/action-gh-release@v2 (or newer v2.x). [1][2]
  • If you see other failures after upgrading, check the action changelog / issues for breaking changes or fixes. [3]

Sources:

  • action-gh-release repo (usage & releases). [1]
  • Example reporting of the Node.js 16 deprecation mentioning softprops/action-gh-release@v1. [2]
  • action-gh-release changelog noting v2 upgrade to node20. [3]

Upgrade softprops/action-gh-release from v1 to v2.

The v1 release uses Node 16, which GitHub Actions has deprecated. Upgrading to v2 (Node 20) prevents workflow failures.

🔧 Suggested fix
-        uses: softprops/action-gh-release@v1
+        uses: softprops/action-gh-release@v2
📝 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
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
🧰 Tools
🪛 actionlint (1.7.10)

427-427: the runner of "softprops/action-gh-release@v1" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
In @.github/workflows/release.yml at 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.

Comment on lines 349 to 362
<label><?php _trans('work_order'); ?></label>
<input type="text" id="quote_work_order" class="form-control input-sm"
<?php if ($quote->quote_work_order) : ?>
value="<?php echo $quote->quote_work_order; ?>"
<?php endif; ?>>

</div>
<div class="quote-properties">
<label><?php _trans('agreement'); ?></label>
<input type="text" id="quote_agreement" class="form-control input-sm"
<?php if ($quote->quote_agreement) : ?>
value="<?php echo $quote->quote_agreement; ?>"
<?php endif; ?>>
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing XSS protection on output values.

The quote_work_order and quote_agreement values are output without HTML escaping. If these values contain malicious content, it could lead to XSS attacks.

🔒 Proposed fix
                                    <input type="text" id="quote_work_order" class="form-control input-sm"
                                        <?php if ($quote->quote_work_order) : ?>
-                                            value="<?php echo $quote->quote_work_order; ?>"
+                                            value="<?php _htmlsc($quote->quote_work_order); ?>"
                                        <?php endif; ?>>

                                </div>
                                <div class="quote-properties">
                                    <label><?php _trans('agreement'); ?></label>
                                    <input type="text" id="quote_agreement" class="form-control input-sm"
                                        <?php if ($quote->quote_agreement) : ?>
-                                            value="<?php echo $quote->quote_agreement; ?>"
+                                            value="<?php _htmlsc($quote->quote_agreement); ?>"
                                        <?php endif; ?>>
🤖 Prompt for AI Agents
In `@application/modules/quotes/views/view.php` around lines 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.

@@ -0,0 +1 @@
# Added for versioning
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Missing database schema changes for new fields.

This SQL migration file contains only a comment but no actual schema modifications. Based on the PR objectives, this file should include ALTER TABLE statements to add the following fields:

Clients table:

  • client_number

Invoices table:

  • invoice_quote_number (text field)
  • invoice_work_order (imported from quote when associated)
  • invoice_agreement (imported from quote when associated)

Quotes table:

  • quote_work_order
  • quote_agreement

Without these schema changes, the application code expecting these fields will fail at runtime with database errors.

📝 Example schema changes needed
# Added for versioning

# Add client_number to clients table
ALTER TABLE `ip_clients` ADD COLUMN `client_number` VARCHAR(255) DEFAULT NULL AFTER `client_id`;

# Add new fields to invoices table
ALTER TABLE `ip_invoices` ADD COLUMN `invoice_quote_number` TEXT DEFAULT NULL;
ALTER TABLE `ip_invoices` ADD COLUMN `invoice_work_order` VARCHAR(255) DEFAULT NULL;
ALTER TABLE `ip_invoices` ADD COLUMN `invoice_agreement` VARCHAR(255) DEFAULT NULL;

# Add new fields to quotes table
ALTER TABLE `ip_quotes` ADD COLUMN `quote_work_order` VARCHAR(255) DEFAULT NULL;
ALTER TABLE `ip_quotes` ADD COLUMN `quote_agreement` VARCHAR(255) DEFAULT NULL;

Note: Column names, data types, and positions should be verified against the actual application code and existing schema conventions.

🤖 Prompt for AI Agents
In `@application/modules/setup/sql/041_1.7.0.sql` at 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.

Comment on lines 79 to 83
if ($quote->user_vat_id) {
echo lang('vat_id_short') . ': ' . $quote->user_vat_id . '<br>';
}
if ($quote->user_tax_code) {
echo lang('tax_code_short') . ': ' . $quote->user_tax_code . '<br>';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Escape VAT/tax IDs and attachment names in the public view.

These fields can contain user-supplied values; rendering them unescaped risks stored XSS.

🛠️ Suggested fix
-                            echo lang('vat_id_short') . ': ' . $quote->user_vat_id . '<br>';
+                            echo lang('vat_id_short') . ': ' . htmlsc($quote->user_vat_id) . '<br>';
...
-                            echo lang('tax_code_short') . ': ' . $quote->user_tax_code . '<br>';
+                            echo lang('tax_code_short') . ': ' . htmlsc($quote->user_tax_code) . '<br>';
...
-                            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: 116-122, 316-316

🤖 Prompt for AI Agents
In `@application/views/quote_templates/public/InvoicePlane_Web.php` around lines
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.

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

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 on lines 76 to 77
function update_email_template_preview() {
$('#email-template-preview').contents().find("body").html($('.email-template-body').val());
$('#email-template-preview').contents().find("body").text($('.email-template-body').val());
Copy link

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

🏁 Script executed:

# Check git diff to see the original code before the change
cd assets/core/js && git diff HEAD scripts.js | head -100

Repository: InvoicePlane/InvoicePlane

Length of output: 51


🏁 Script executed:

# Also check the broader context of the change
git log --oneline -20 -- assets/core/js/scripts.js

Repository: InvoicePlane/InvoicePlane

Length of output: 177


🏁 Script executed:

# Get the full function to understand context better
git show HEAD:assets/core/js/scripts.js | sed -n '76,85p'

Repository: InvoicePlane/InvoicePlane

Length of output: 472


🏁 Script executed:

# Check if there's a specific commit that changed this function
git log -p --follow -S "update_email_template_preview" -- assets/core/js/scripts.js | head -200

Repository: InvoicePlane/InvoicePlane

Length of output: 51


🏁 Script executed:

# Use git blame to find when .text() was introduced
git blame assets/core/js/scripts.js | grep -A2 -B2 "update_email_template_preview"

Repository: InvoicePlane/InvoicePlane

Length of output: 3304


🏁 Script executed:

# Search for any previous versions of this function in git history
git log --all -p -- assets/core/js/scripts.js | grep -A10 -B5 "update_email_template_preview" | head -150

Repository: InvoicePlane/InvoicePlane

Length of output: 5635


🏁 Script executed:

# Check for related security issues or PRs
git log --all --oneline | grep -i "email\|template\|security\|xss" | head -20

Repository: InvoicePlane/InvoicePlane

Length of output: 51


🏁 Script executed:

# Look for any comments or TODOs related to email template in the file
grep -n -B3 -A3 "email.template" assets/core/js/scripts.js

Repository: InvoicePlane/InvoicePlane

Length of output: 2660


HTML email preview will show raw tags instead of rendering.

Using .text() escapes HTML, so the preview breaks—users will see plain-text markup like <b>text</b> instead of rendered output. If the intent was security hardening, sanitize the input first then render with .html().

🔧 Suggested fix
-function update_email_template_preview() {
-    $('#email-template-preview').contents().find("body").text($('.email-template-body').val());
-}
+function update_email_template_preview() {
+    const html = $('.email-template-body').val();
+    // If needed, sanitize here to prevent XSS.
+    $('#email-template-preview').contents().find("body").html(html);
+}
📝 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
function update_email_template_preview() {
$('#email-template-preview').contents().find("body").html($('.email-template-body').val());
$('#email-template-preview').contents().find("body").text($('.email-template-body').val());
function update_email_template_preview() {
const html = $('.email-template-body').val();
// If needed, sanitize here to prevent XSS.
$('#email-template-preview').contents().find("body").html(html);
}
🤖 Prompt for AI Agents
In `@assets/core/js/scripts.js` around lines 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.

@InvoicePlane InvoicePlane deleted a comment from coderabbitai bot Jan 19, 2026
Copy link
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
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.

…ity issues (#1394)

* Initial plan

* Fix GitHub workflow issues: triggers, commands, and scripts

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

* Fix PHP security issues: XSS, undefined variables, and logic bugs

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

* Fix JavaScript XSS vulnerability in email template preview

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

* Address code review feedback: improve URL validation and data handling

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nielsdrost7 <47660417+nielsdrost7@users.noreply.github.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

📝 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 docstrings

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

@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 on lines +26 to +33
// Look for package name lines (they typically start with package names after +/-)
if (line.match(/^[-+]"?[@a-zA-Z0-9_-]+/)) {
// Extract package name
const match = line.match(/^[-+]"?(@?[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)?)/);
if (match && match[1]) {
updatedPackages.add(match[1]);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tighten yarn.lock entry parsing to avoid false positives (e.g., "__metadata").

The current regex can capture non-package keys in Yarn v2+/v3 lockfiles, which pollutes the report. Prefer matching only entry header lines that end with : and extracting the package name from the descriptor.

🔧 Proposed fix (more precise entry parsing)
-        if (line.match(/^[-+]"?[`@a-zA-Z0-9_-`]+/)) {
-            // Extract package name
-            const match = line.match(/^[-+]"?(@?[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)?)/);
-            if (match && match[1]) {
-                updatedPackages.add(match[1]);
-            }
-        }
+        const entryMatch = line.match(/^[-+]"?(.+?)":$/);
+        if (entryMatch) {
+            const descriptor = entryMatch[1];
+            const nameMatch = descriptor.match(/^(@?[^@]+(?:\/[^@]+)?)@/);
+            if (nameMatch && nameMatch[1]) {
+                updatedPackages.add(nameMatch[1]);
+            }
+        }
🤖 Prompt for AI Agents
In @.github/scripts/generate-package-update-report.cjs around lines 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.

Comment on lines 3 to 4
on:
push:
tags: [ 'v*.*.*' ]
workflow_dispatch:
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Automatic Docker image builds removed for releases.

Removing the push.tags trigger means Docker images won't automatically build and publish when version tags (e.g., v1.7.0) are pushed. This requires manual workflow dispatch for every release.

If automatic release builds are desired, consider restoring the tag trigger:

🔧 Suggested fix to restore automatic release builds
 on:
   workflow_dispatch:
+  push:
+    tags:
+      - 'v*.*.*'
📝 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
on:
push:
tags: [ 'v*.*.*' ]
workflow_dispatch:
on:
workflow_dispatch:
push:
tags:
- 'v*.*.*'
🤖 Prompt for AI Agents
In @.github/workflows/docker-publish.yml around lines 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.

Comment on lines 65 to 93
- name: Comment on PR (if available)
if: github.event_name == 'pull_request' && always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
try {
if (!fs.existsSync('phpstan-report.md')) {
console.log('PHPStan report file "phpstan-report.md" not found. Skipping PR comment.');
return;
}
const report = fs.readFileSync('phpstan-report.md', 'utf8');
if (!report || report.trim().length === 0) {
console.log('PHPStan report is empty. Skipping PR comment.');
return;
}
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: report
});
} catch (error) {
console.log('Failed to create PHPStan PR comment:', error && error.message ? error.message : error);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

PR comment step will never execute.

The workflow only triggers on workflow_dispatch (line 4), but this step's condition checks for github.event_name == 'pull_request' (line 66). This condition will always be false, making this step unreachable dead code.

Either:

  1. Remove this step if PR comments aren't needed, or
  2. Add pull_request to the workflow triggers if you want PR comments
Option: Add pull_request trigger
 on:
   workflow_dispatch:  # Manual only — does not run automatically
+  pull_request:
+    paths:
+      - '**.php'
🤖 Prompt for AI Agents
In @.github/workflows/phpstan.yml around lines 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.

Comment on lines 14 to 18
pull_request:
branches:
- master
- develop

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/pint.yml | head -25

Repository: InvoicePlane/InvoicePlane

Length of output: 681


🏁 Script executed:

git branch -a 2>/dev/null || echo "No branches info available"
git for-each-ref --format='%(refname:short)' refs/heads/ 2>/dev/null || echo "Unable to list branches"

Repository: InvoicePlane/InvoicePlane

Length of output: 233


Add development branch to PR trigger filters; workflow will not run on PRs targeting the default branch.

The repository's default branch is development (not develop), but the workflow only filters for master and develop. This means Pint checks will be skipped on PRs targeting the main development branch.

Suggested fix
   pull_request:
     branches:
       - master
       - develop
+      - development
📝 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
pull_request:
branches:
- master
- develop
pull_request:
branches:
- master
- develop
- development
🤖 Prompt for AI Agents
In @.github/workflows/pint.yml around lines 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).

Comment on lines 93 to 101
- name: Commit and Push Changes
if: steps.verify_diff.outputs.has_changes == 'true'
run: |
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
Copy link

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

🏁 Script executed:

cat -n .github/workflows/pint.yml

Repository: InvoicePlane/InvoicePlane

Length of output: 5216


Guard auto-push to prevent failures on forked PRs.

Forked pull requests have limited token permissions and cannot push to the base repository. Add a condition to skip the push step for forked PRs.

✅ Add fork check to push condition
      - name: Commit and Push Changes
-       if: steps.verify_diff.outputs.has_changes == 'true'
+       if: steps.verify_diff.outputs.has_changes == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
        run: |
          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
📝 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: Commit and Push Changes
if: steps.verify_diff.outputs.has_changes == 'true'
run: |
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
- name: Commit and Push Changes
if: steps.verify_diff.outputs.has_changes == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
run: |
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
🤖 Prompt for AI Agents
In @.github/workflows/pint.yml around lines 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.

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

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 on lines +1 to +6
ALTER TABLE `ip_clients` ADD client_number VARCHAR(100) NULL DEFAULT NULL AFTER `client_id`;
ALTER TABLE `ip_invoices` ADD invoice_quote_number VARCHAR(100) NULL DEFAULT NULL AFTER `invoice_id`;
ALTER TABLE `ip_invoices` ADD invoice_work_order VARCHAR(100) NULL DEFAULT NULL AFTER `invoice_quote_number`;
ALTER TABLE `ip_invoices` ADD invoice_agreement VARCHAR(100) NULL DEFAULT NULL AFTER `invoice_work_order`;
ALTER TABLE `ip_quotes` ADD quote_work_order VARCHAR(100) NULL DEFAULT NULL AFTER `quote_id`;
ALTER TABLE `ip_quotes` ADD quote_agreement VARCHAR(100) NULL DEFAULT NULL AFTER `quote_work_order`;
Copy link

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
echo "=== Checking 038_1.6.1.sql ==="
if [ -f application/modules/setup/sql/038_1.6.1.sql ]; then
  cat -n application/modules/setup/sql/038_1.6.1.sql
else
  echo "File not found at application/modules/setup/sql/038_1.6.1.sql"
fi

echo -e "\n=== Checking 041_1.7.0.sql ==="
if [ -f application/modules/setup/sql/041_1.7.0.sql ]; then
  cat -n application/modules/setup/sql/041_1.7.0.sql
else
  echo "File not found at application/modules/setup/sql/041_1.7.0.sql"
fi

Repository: InvoicePlane/InvoicePlane

Length of output: 1735


Critical: Duplicate column definitions will cause sequential upgrade failures.

This migration (038_1.6.1.sql) adds six columns that are also defined in 041_1.7.0.sql:

  • client_number on ip_clients
  • invoice_quote_number, invoice_work_order, invoice_agreement on ip_invoices
  • quote_work_order, quote_agreement on ip_quotes

When users upgrade sequentially through both versions, 041_1.7.0.sql will fail with "Duplicate column name" errors. Additionally, the column definitions conflict between migrations:

  • Data types differ: 038 uses VARCHAR(100), while 041 uses VARCHAR(255) or TEXT
  • AFTER positions differ: e.g., invoice_quote_number is positioned AFTER invoice_id in 038 but AFTER invoice_number in 041

Remove the duplicate definitions from one migration file, or use conditional logic to skip columns already present.

🤖 Prompt for AI Agents
In `@application/modules/setup/sql/038_1.6.1.sql` around lines 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.

Comment on lines +1 to +17
# Added for versioning and new fields for client_number, invoice fields, and quote fields

# Add client_number to ip_clients
ALTER TABLE `ip_clients`
ADD COLUMN client_number VARCHAR(255) DEFAULT NULL AFTER `client_id`;

# Add invoice_quote_number, invoice_work_order, and invoice_agreement to ip_invoices
ALTER TABLE `ip_invoices`
ADD COLUMN invoice_quote_number TEXT DEFAULT NULL AFTER `invoice_number`,
ADD COLUMN invoice_work_order VARCHAR(255) DEFAULT NULL AFTER `invoice_quote_number`,
ADD COLUMN invoice_agreement VARCHAR(255) DEFAULT NULL AFTER `invoice_work_order`;

# Add quote_work_order and quote_agreement to ip_quotes
ALTER TABLE `ip_quotes`
ADD COLUMN quote_work_order VARCHAR(255) DEFAULT NULL AFTER `quote_number`,
ADD COLUMN quote_agreement VARCHAR(255) DEFAULT NULL AFTER `quote_work_order`;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Schema changes added, but data type inconsistencies with 038_1.6.1.sql.

The migration now includes the required ALTER TABLE statements. However, there are data type mismatches with 038_1.6.1.sql:

Column 038_1.6.1.sql 041_1.7.0.sql
client_number VARCHAR(100) VARCHAR(255)
invoice_quote_number VARCHAR(100) TEXT
invoice_work_order VARCHAR(100) VARCHAR(255)
invoice_agreement VARCHAR(100) VARCHAR(255)
quote_work_order VARCHAR(100) VARCHAR(255)
quote_agreement VARCHAR(100) VARCHAR(255)

These inconsistencies, combined with the duplicate column issue flagged in 038_1.6.1.sql, indicate these migrations need consolidation. Align on a single data type per column and remove duplicates.

🤖 Prompt for AI Agents
In `@application/modules/setup/sql/041_1.7.0.sql` around lines 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.

Comment on lines +76 to +114
// Sanitize HTML content for safe rendering in email template preview
function sanitizeHtml(html) {
// Create a temporary div to parse HTML
var temp = document.createElement('div');
temp.textContent = html; // This escapes all HTML
var escaped = temp.innerHTML;

// Allowed safe tags that should render as HTML
var allowedTags = {
'b': true, 'strong': true, 'i': true, 'em': true, 'u': true,
'p': true, 'br': true, 'h1': true, 'h2': true, 'h3': true, 'h4': true,
'ul': true, 'ol': true, 'li': true, 'a': true, 'span': true, 'div': true,
'table': true, 'tr': true, 'td': true, 'th': true, 'thead': true, 'tbody': true
};

// Replace escaped versions of allowed tags back to HTML
// But only for closing tags and simple opening tags without attributes
escaped = escaped.replace(/&lt;(\/?)(b|strong|i|em|u|p|br|h1|h2|h3|h4|ul|ol|li|span|div|table|tr|td|th|thead|tbody)&gt;/gi,
function(match, slash, tag) {
if (allowedTags[tag.toLowerCase()]) {
return '<' + slash + tag + '>';
}
return match;
});

// Handle <a> tags specially to allow href but sanitize it
escaped = escaped.replace(/&lt;a\s+href=&quot;([^&quot;]+)&quot;&gt;/gi, function(match, href) {
// Only allow http, https, and mailto URLs with proper validation
// Ensure javascript: or data: URIs are not allowed
if (href.match(/^https?:\/\/[a-zA-Z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/i) ||
href.match(/^mailto:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/i)) {
return '<a href="' + href + '">';
}
return '&lt;a href="' + href + '"&gt;';
});
escaped = escaped.replace(/&lt;\/a&gt;/gi, '</a>');

return escaped;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Good sanitization implementation, but verify the escaped quote matching in the <a> tag regex.

The sanitization approach is sound—escaping everything first, then selectively re-enabling safe tags. However, the regex on line 102 may have issues matching escaped quotes:

When textContent escapes HTML, quotes become &quot;. The regex pattern &quot;([^&quot;]+)&quot; is interpreted as &quot;([^&]+)quot; because [^&quot;] is a character class excluding &, q, u, o, t, ; individually, not the literal string.

🔧 Suggested fix for escaped quote matching
-    escaped = escaped.replace(/&lt;a\s+href=&quot;([^&quot;]+)&quot;&gt;/gi, function(match, href) {
+    escaped = escaped.replace(/&lt;a\s+href=&quot;([^&]+?)&quot;&gt;/gi, function(match, href) {

Or use a more explicit non-greedy pattern:

-    escaped = escaped.replace(/&lt;a\s+href=&quot;([^&quot;]+)&quot;&gt;/gi, function(match, href) {
+    escaped = escaped.replace(/&lt;a\s+href=&quot;(.+?)&quot;&gt;/gi, function(match, href) {
🤖 Prompt for AI Agents
In `@assets/core/js/scripts.js` around lines 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.

Comment on lines 38 to 40
"postcss": "8.4",
"sass": "^1.89.2",
"sass": "^1.97",
"select2": "4.1.0-rc.0",
Copy link

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

🏁 Script executed:

# Find and inspect package.json to see the complete dependencies
git ls-files | grep -E '(package\.json|Gruntfile|grunt)' | head -20

Repository: InvoicePlane/InvoicePlane

Length of output: 94


🏁 Script executed:

# Read package.json to understand the full dependency set
cat -n package.json | grep -A 30 -B 5 "sass"

Repository: InvoicePlane/InvoicePlane

Length of output: 1054


🌐 Web query:

grunt-sass 3.1 Dart Sass 1.97 compatibility breaking changes

💡 Result:

Short answer: grunt-sass itself is just a Grunt wrapper that calls whatever Sass implementation you pass in, so “breaking” behavior with Dart Sass 1.97 comes from Dart Sass 1.97’s language/CLI changes — not from grunt-sass. See the grunt-sass README that you must pass the Sass implementation to options. [1]

Relevant Dart Sass 1.97+ changes to watch for (from the Dart Sass changelog and releases):

  • Declarations, childless at‑rules, and comments are now emitted in source order even when interleaved with nested rules (breaking change / obsoletes mixed-decls). [2]
  • The function name type() is reserved for the plain CSS function; @function named type now errors and calls parse as special function strings. [2]
  • Configuring private variables via @use ... with, @forward ... with, and meta.load-css(..., $with: ...) is deprecated (encourages module encapsulation). [2]
  • Deprecation/removal of @import (migration to @use/@forward was announced and @import will be removed in Dart Sass 3.0). This may surface as deprecation warnings now and break when you move to later Dart Sass majors. [3]
  • New color-space support (display-p3-linear) and related parsing/serialization changes (feature additions that can affect output). [4]

If your project uses grunt-sass + Dart Sass 1.97:

  • Expect any of the above Sass-language/serialization changes to affect compilation output or cause errors in previously-valid @function/@use/@import patterns. These are Dart Sass changes — update your Sass code (replace @import with @use/@forward, avoid @function type, adjust private-variable expectations, and check nested-rule ordering) to remain compatible. [2][3][4]

Sources:
[1] grunt-sass README/npm (usage and implementation option).
[2] Dart Sass changelog / releases (breaking changes: mixed-decls ordering; reserved type() name; private variable deprecation).
[3] Dart Sass deprecation/@import removal discussion.
[4] Dart Sass 1.97 release notes (display-p3-linear support).


🌐 Web query:

Sass 1.97 release notes breaking changes deprecations

💡 Result:

Here are the breaking changes and deprecations called out in the Dart Sass 1.97 release notes:

  • Declarations, childless at-rules, and comments are now emitted in the order they appear in source even when interleaved with nested rules — this is a breaking change and it obsoletes the previous mixed-decls deprecation. [1]
  • The function name type() is now reserved for the plain CSS function: @function definitions named type will error, and calls parse as special function strings. [1][2]
  • Configuring private variables via @use ... with, @forward ... with, and meta.load-css(..., $with: ...) is deprecated (private variables are now treated as fully encapsulated). [1]
  • meta.feature-exists() has been deprecated (to be removed in Dart Sass 2.0.0); use other detection functions or remove usages. [3]
  • Miscellaneous bugfix: @extend rules loaded through mixed @import/@use could fail to apply — fixed. [1]
  • CLI: in --watch mode, the source map is deleted when its source file is deleted. [1]

Sources: Dart Sass release notes and breaking-change docs.
[1] Dart Sass release (1.97) changelog. [2] Breaking change: type() function. [3] Breaking change: meta.feature-exists().


Audit Sass code for breaking changes in 1.97 before upgrading.

grunt-sass 3.1 will work with Sass 1.97, but Dart Sass 1.97 introduces breaking changes that require code updates:

  • Nested rules/declarations now emit in source order (may change CSS output)
  • @function type is reserved; rename any existing @function type definitions
  • @use/@forward ... with for private variables is deprecated
  • @import is deprecated (removes in Sass 3.0); migrate to @use/@forward
  • meta.feature-exists() is deprecated

Review your Sass codebase for these patterns and update before merging.

🤖 Prompt for AI Agents
In `@package.json` around lines 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.

@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
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

3 participants