From efcc0fd5cb5c6d1b8fcb459252a4ef12fb5c095a Mon Sep 17 00:00:00 2001 From: johnnyq Date: Sun, 16 Nov 2025 15:33:45 -0500 Subject: [PATCH 01/23] Add Where clause to only accept saved payment by logged in session_client_id in Client Portal --- client/post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/post.php b/client/post.php index ce0f4a38..c61bff63 100644 --- a/client/post.php +++ b/client/post.php @@ -440,7 +440,7 @@ if (isset($_GET['add_payment_by_provider'])) { $sql = mysqli_query($mysqli,"SELECT * FROM invoices LEFT JOIN clients ON invoice_client_id = client_id LEFT JOIN contacts ON client_id = contact_client_id AND contact_primary = 1 - WHERE invoice_id = $invoice_id" + WHERE invoice_id = $invoice_id AND client_id = $session_client_id" ); $row = mysqli_fetch_array($sql); $invoice_number = intval($row['invoice_number']); From 612041635d962d37f2f400ba1974bec5456ccd1e Mon Sep 17 00:00:00 2001 From: johnnyq Date: Sun, 16 Nov 2025 15:49:11 -0500 Subject: [PATCH 02/23] Updated symfony/http-foundation from 7.3.3 to 7.3.7 --- plugins/composer.lock | 12 ++-- plugins/vendor/composer/autoload_files.php | 4 +- plugins/vendor/composer/autoload_psr4.php | 2 +- plugins/vendor/composer/autoload_static.php | 8 +-- plugins/vendor/composer/installed.json | 14 ++--- plugins/vendor/composer/installed.php | 10 ++-- .../http-foundation/BinaryFileResponse.php | 2 +- .../symfony/http-foundation/Request.php | 56 +++++++++++++++---- .../http-foundation/ResponseHeaderBag.php | 12 ++-- .../symfony/http-foundation/ServerEvent.php | 12 ++-- .../Storage/Handler/PdoSessionHandler.php | 2 +- 11 files changed, 82 insertions(+), 52 deletions(-) diff --git a/plugins/composer.lock b/plugins/composer.lock index b4b5bb03..4eebb831 100644 --- a/plugins/composer.lock +++ b/plugins/composer.lock @@ -893,16 +893,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.3.3", + "version": "v7.3.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00" + "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7475561ec27020196c49bb7c4f178d33d7d3dc00", - "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/db488a62f98f7a81d5746f05eea63a74e55bb7c4", + "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4", "shasum": "" }, "require": { @@ -952,7 +952,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.3" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.7" }, "funding": [ { @@ -972,7 +972,7 @@ "type": "tidelift" } ], - "time": "2025-08-20T08:04:18+00:00" + "time": "2025-11-08T16:41:12+00:00" }, { "name": "symfony/polyfill-mbstring", diff --git a/plugins/vendor/composer/autoload_files.php b/plugins/vendor/composer/autoload_files.php index a7d2f8fe..d4d56411 100644 --- a/plugins/vendor/composer/autoload_files.php +++ b/plugins/vendor/composer/autoload_files.php @@ -8,11 +8,11 @@ $baseDir = dirname($vendorDir); return array( '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', '662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php', - '606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php', '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', + '606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php', '2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php', - 'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php', '9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php', + 'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php', '23f09fe3194f8c2f70923f90d6702129' => $vendorDir . '/illuminate/collections/functions.php', '60799491728b879e74601d83e38b2cad' => $vendorDir . '/illuminate/collections/helpers.php', 'f625ee536139dfb962a398b200bdb2bd' => $vendorDir . '/illuminate/support/functions.php', diff --git a/plugins/vendor/composer/autoload_psr4.php b/plugins/vendor/composer/autoload_psr4.php index 0d2eba16..e18e7538 100644 --- a/plugins/vendor/composer/autoload_psr4.php +++ b/plugins/vendor/composer/autoload_psr4.php @@ -19,7 +19,7 @@ return array( 'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'), 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), 'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'), - 'Illuminate\\Support\\' => array($vendorDir . '/illuminate/macroable', $vendorDir . '/illuminate/conditionable', $vendorDir . '/illuminate/collections', $vendorDir . '/illuminate/support'), + 'Illuminate\\Support\\' => array($vendorDir . '/illuminate/collections', $vendorDir . '/illuminate/conditionable', $vendorDir . '/illuminate/macroable', $vendorDir . '/illuminate/support'), 'Illuminate\\Pagination\\' => array($vendorDir . '/illuminate/pagination'), 'Illuminate\\Contracts\\' => array($vendorDir . '/illuminate/contracts'), 'Doctrine\\Inflector\\' => array($vendorDir . '/doctrine/inflector/src'), diff --git a/plugins/vendor/composer/autoload_static.php b/plugins/vendor/composer/autoload_static.php index 00e27ea8..0c517441 100644 --- a/plugins/vendor/composer/autoload_static.php +++ b/plugins/vendor/composer/autoload_static.php @@ -9,11 +9,11 @@ class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e public static $files = array ( '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', '662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php', - '606a39d89246991a373564698c2d8383' => __DIR__ . '/..' . '/symfony/polyfill-php85/bootstrap.php', '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', + '606a39d89246991a373564698c2d8383' => __DIR__ . '/..' . '/symfony/polyfill-php85/bootstrap.php', '2203a247e6fda86070a5e4e07aed533a' => __DIR__ . '/..' . '/symfony/clock/Resources/now.php', - 'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php', '9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php', + 'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php', '23f09fe3194f8c2f70923f90d6702129' => __DIR__ . '/..' . '/illuminate/collections/functions.php', '60799491728b879e74601d83e38b2cad' => __DIR__ . '/..' . '/illuminate/collections/helpers.php', 'f625ee536139dfb962a398b200bdb2bd' => __DIR__ . '/..' . '/illuminate/support/functions.php', @@ -118,9 +118,9 @@ class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e ), 'Illuminate\\Support\\' => array ( - 0 => __DIR__ . '/..' . '/illuminate/macroable', + 0 => __DIR__ . '/..' . '/illuminate/collections', 1 => __DIR__ . '/..' . '/illuminate/conditionable', - 2 => __DIR__ . '/..' . '/illuminate/collections', + 2 => __DIR__ . '/..' . '/illuminate/macroable', 3 => __DIR__ . '/..' . '/illuminate/support', ), 'Illuminate\\Pagination\\' => diff --git a/plugins/vendor/composer/installed.json b/plugins/vendor/composer/installed.json index a4d12430..756eb787 100644 --- a/plugins/vendor/composer/installed.json +++ b/plugins/vendor/composer/installed.json @@ -929,17 +929,17 @@ }, { "name": "symfony/http-foundation", - "version": "v7.3.3", - "version_normalized": "7.3.3.0", + "version": "v7.3.7", + "version_normalized": "7.3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00" + "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7475561ec27020196c49bb7c4f178d33d7d3dc00", - "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/db488a62f98f7a81d5746f05eea63a74e55bb7c4", + "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4", "shasum": "" }, "require": { @@ -963,7 +963,7 @@ "symfony/mime": "^6.4|^7.0", "symfony/rate-limiter": "^6.4|^7.0" }, - "time": "2025-08-20T08:04:18+00:00", + "time": "2025-11-08T16:41:12+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -991,7 +991,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.3" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.7" }, "funding": [ { diff --git a/plugins/vendor/composer/installed.php b/plugins/vendor/composer/installed.php index b75cacc2..c0561b99 100644 --- a/plugins/vendor/composer/installed.php +++ b/plugins/vendor/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '981fb9585d0c76e8b9c31812d58dfdd5b56d6454', + 'reference' => 'efcc0fd5cb5c6d1b8fcb459252a4ef12fb5c095a', 'name' => '__root__', 'dev' => true, ), @@ -16,7 +16,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '981fb9585d0c76e8b9c31812d58dfdd5b56d6454', + 'reference' => 'efcc0fd5cb5c6d1b8fcb459252a4ef12fb5c095a', 'dev_requirement' => false, ), 'carbonphp/carbon-doctrine-types' => array( @@ -158,12 +158,12 @@ 'dev_requirement' => false, ), 'symfony/http-foundation' => array( - 'pretty_version' => 'v7.3.3', - 'version' => '7.3.3.0', + 'pretty_version' => 'v7.3.7', + 'version' => '7.3.7.0', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/http-foundation', 'aliases' => array(), - 'reference' => '7475561ec27020196c49bb7c4f178d33d7d3dc00', + 'reference' => 'db488a62f98f7a81d5746f05eea63a74e55bb7c4', 'dev_requirement' => false, ), 'symfony/polyfill-mbstring' => array( diff --git a/plugins/vendor/symfony/http-foundation/BinaryFileResponse.php b/plugins/vendor/symfony/http-foundation/BinaryFileResponse.php index a7358183..6a6679e0 100644 --- a/plugins/vendor/symfony/http-foundation/BinaryFileResponse.php +++ b/plugins/vendor/symfony/http-foundation/BinaryFileResponse.php @@ -164,7 +164,7 @@ class BinaryFileResponse extends Response for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) { $char = mb_substr($filename, $i, 1, $encoding); - if ('%' === $char || \ord($char) < 32 || \ord($char) > 126) { + if ('%' === $char || \ord($char[0]) < 32 || \ord($char[0]) > 126) { $filenameFallback .= '_'; } else { $filenameFallback .= $char; diff --git a/plugins/vendor/symfony/http-foundation/Request.php b/plugins/vendor/symfony/http-foundation/Request.php index dba930a2..3a609783 100644 --- a/plugins/vendor/symfony/http-foundation/Request.php +++ b/plugins/vendor/symfony/http-foundation/Request.php @@ -300,10 +300,21 @@ class Request $server['PATH_INFO'] = ''; $server['REQUEST_METHOD'] = strtoupper($method); + if (($i = strcspn($uri, ':/?#')) && ':' === ($uri[$i] ?? null) && (strspn($uri, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-.') !== $i || strcspn($uri, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'))) { + throw new BadRequestException('Invalid URI: Scheme is malformed.'); + } if (false === $components = parse_url(\strlen($uri) !== strcspn($uri, '?#') ? $uri : $uri.'#')) { throw new BadRequestException('Invalid URI.'); } + $part = ($components['user'] ?? '').':'.($components['pass'] ?? ''); + + if (':' !== $part && \strlen($part) !== strcspn($part, '[]')) { + throw new BadRequestException('Invalid URI: Userinfo is malformed.'); + } + if (($part = $components['host'] ?? '') && !self::isHostValid($part)) { + throw new BadRequestException('Invalid URI: Host is malformed.'); + } if (false !== ($i = strpos($uri, '\\')) && $i < strcspn($uri, '?#')) { throw new BadRequestException('Invalid URI: A URI cannot contain a backslash.'); } @@ -1091,10 +1102,8 @@ class Request // host is lowercase as per RFC 952/2181 $host = strtolower(preg_replace('/:\d+$/', '', trim($host))); - // as the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user) - // check that it does not contain forbidden characters (see RFC 952 and RFC 2181) - // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names - if ($host && '' !== preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host)) { + // the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user) + if ($host && !self::isHostValid($host)) { if (!$this->isHostValid) { return ''; } @@ -1236,15 +1245,22 @@ class Request static::initializeFormats(); } + $exactFormat = null; + $canonicalFormat = null; + foreach (static::$formats as $format => $mimeTypes) { - if (\in_array($mimeType, (array) $mimeTypes, true)) { - return $format; + if (\in_array($mimeType, $mimeTypes, true)) { + $exactFormat = $format; } - if (null !== $canonicalMimeType && \in_array($canonicalMimeType, (array) $mimeTypes, true)) { - return $format; + if (null !== $canonicalMimeType && \in_array($canonicalMimeType, $mimeTypes, true)) { + $canonicalFormat = $format; } } + if ($format = $exactFormat ?? $canonicalFormat) { + return $format; + } + return null; } @@ -1259,7 +1275,7 @@ class Request static::initializeFormats(); } - static::$formats[$format] = \is_array($mimeTypes) ? $mimeTypes : [$mimeTypes]; + static::$formats[$format ?? ''] = (array) $mimeTypes; } /** @@ -1892,9 +1908,8 @@ class Request } $pathInfo = substr($requestUri, \strlen($baseUrl)); - if ('' === $pathInfo) { - // If substr() returns false then PATH_INFO is set to an empty string - return '/'; + if ('' === $pathInfo || '/' !== $pathInfo[0]) { + return '/'.$pathInfo; } return $pathInfo; @@ -2101,4 +2116,21 @@ class Request return $this->isIisRewrite; } + + /** + * See https://url.spec.whatwg.org/. + */ + private static function isHostValid(string $host): bool + { + if ('[' === $host[0]) { + return ']' === $host[-1] && filter_var(substr($host, 1, -1), \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6); + } + + if (preg_match('/\.[0-9]++\.?$/D', $host)) { + return null !== filter_var($host, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4 | \FILTER_NULL_ON_FAILURE); + } + + // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names + return '' === preg_replace('/[-a-zA-Z0-9_]++\.?/', '', $host); + } } diff --git a/plugins/vendor/symfony/http-foundation/ResponseHeaderBag.php b/plugins/vendor/symfony/http-foundation/ResponseHeaderBag.php index b2bdb500..4e089fe6 100644 --- a/plugins/vendor/symfony/http-foundation/ResponseHeaderBag.php +++ b/plugins/vendor/symfony/http-foundation/ResponseHeaderBag.php @@ -159,7 +159,7 @@ class ResponseHeaderBag extends HeaderBag public function setCookie(Cookie $cookie): void { - $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie; + $this->cookies[$cookie->getDomain() ?? ''][$cookie->getPath()][$cookie->getName()] = $cookie; $this->headerNames['set-cookie'] = 'Set-Cookie'; } @@ -170,13 +170,13 @@ class ResponseHeaderBag extends HeaderBag { $path ??= '/'; - unset($this->cookies[$domain][$path][$name]); + unset($this->cookies[$domain ?? ''][$path][$name]); - if (empty($this->cookies[$domain][$path])) { - unset($this->cookies[$domain][$path]); + if (empty($this->cookies[$domain ?? ''][$path])) { + unset($this->cookies[$domain ?? ''][$path]); - if (empty($this->cookies[$domain])) { - unset($this->cookies[$domain]); + if (empty($this->cookies[$domain ?? ''])) { + unset($this->cookies[$domain ?? '']); } } diff --git a/plugins/vendor/symfony/http-foundation/ServerEvent.php b/plugins/vendor/symfony/http-foundation/ServerEvent.php index ea2b5c88..7597058b 100644 --- a/plugins/vendor/symfony/http-foundation/ServerEvent.php +++ b/plugins/vendor/symfony/http-foundation/ServerEvent.php @@ -132,14 +132,12 @@ class ServerEvent implements \IteratorAggregate } yield $head; - if ($this->data) { - if (is_iterable($this->data)) { - foreach ($this->data as $data) { - yield \sprintf('data: %s', $data)."\n"; - } - } else { - yield \sprintf('data: %s', $this->data)."\n"; + if (is_iterable($this->data)) { + foreach ($this->data as $data) { + yield \sprintf('data: %s', $data)."\n"; } + } elseif ('' !== $this->data) { + yield \sprintf('data: %s', $this->data)."\n"; } yield "\n"; diff --git a/plugins/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php b/plugins/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php index e2fb4f12..21765695 100644 --- a/plugins/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/plugins/vendor/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php @@ -219,7 +219,7 @@ class PdoSessionHandler extends AbstractSessionHandler $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true); break; case 'sqlsrv': - $table->addColumn($this->idCol, Types::TEXT)->setLength(128)->setNotnull(true); + $table->addColumn($this->idCol, Types::STRING)->setLength(128)->setNotnull(true); $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); $table->addColumn($this->lifetimeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); $table->addColumn($this->timeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); From 63141f357818c0a2c62d4a5e1f8d3653acbccb45 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Sun, 16 Nov 2025 16:00:57 -0500 Subject: [PATCH 03/23] Composer updates --- plugins/vendor/composer/installed.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/vendor/composer/installed.php b/plugins/vendor/composer/installed.php index c0561b99..6fb07b07 100644 --- a/plugins/vendor/composer/installed.php +++ b/plugins/vendor/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => 'efcc0fd5cb5c6d1b8fcb459252a4ef12fb5c095a', + 'reference' => '612041635d962d37f2f400ba1974bec5456ccd1e', 'name' => '__root__', 'dev' => true, ), @@ -16,7 +16,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => 'efcc0fd5cb5c6d1b8fcb459252a4ef12fb5c095a', + 'reference' => '612041635d962d37f2f400ba1974bec5456ccd1e', 'dev_requirement' => false, ), 'carbonphp/carbon-doctrine-types' => array( From aba5ed92710b768aad4d1b7bfec6ad3f35843718 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Sun, 16 Nov 2025 17:12:02 -0500 Subject: [PATCH 04/23] Add Back Delete Payment Provider, the db will cascade delete all related recurring payments, related saved cards and client payment provider relation --- admin/payment_provider.php | 9 +++------ admin/post/payment_provider.php | 5 +++++ admin/post/saved_payment_method.php | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/admin/payment_provider.php b/admin/payment_provider.php index 50c876aa..3ab7ecec 100644 --- a/admin/payment_provider.php +++ b/admin/payment_provider.php @@ -106,12 +106,9 @@ $num_rows = mysqli_num_rows($sql); Edit - - - - - - + + Delete
  • Recurring Payments
  • Saved cards
+
diff --git a/admin/post/payment_provider.php b/admin/post/payment_provider.php index 0ff98af7..ffa2da8a 100644 --- a/admin/post/payment_provider.php +++ b/admin/post/payment_provider.php @@ -101,6 +101,11 @@ if (isset($_GET['delete_payment_provider'])) { $provider_id = intval($_GET['delete_payment_provider']); + // When deleted it cascades deletes + // all Recurring paymentes related to payment provider + // Delete all Saved Cards related + // Delete Client Payment Provider Releation + $provider_name = sanitizeInput(getFieldById('payment_providers', $provider_id, 'provider_name')); // Delete provider diff --git a/admin/post/saved_payment_method.php b/admin/post/saved_payment_method.php index 7645b390..c4d0bfd6 100644 --- a/admin/post/saved_payment_method.php +++ b/admin/post/saved_payment_method.php @@ -42,7 +42,7 @@ if (isset($_GET['delete_saved_payment'])) { try { // Initialize stripe - require_once 'plugins/stripe-php/init.php'; + require_once '../plugins/stripe-php/init.php'; $stripe = new \Stripe\StripeClient($private_key); // Detach PM @@ -56,7 +56,7 @@ if (isset($_GET['delete_saved_payment'])) { } - // Remove payment method from ITFlow + // Remove payment method from ITFlow. This will also cascade delete related recurring payments setup mysqli_query($mysqli, "DELETE FROM client_saved_payment_methods WHERE saved_payment_id = $saved_payment_id"); // SQL Cascade delete will Remove All Associated Auto Payment Methods on recurring invoices in the recurring payments table. From cf0fa0024c748c0f6fe6004ac99492cb0724dc9b Mon Sep 17 00:00:00 2001 From: johnnyq Date: Sun, 16 Nov 2025 17:16:46 -0500 Subject: [PATCH 05/23] Update Wording on delete provider --- admin/payment_provider.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/admin/payment_provider.php b/admin/payment_provider.php index 3ab7ecec..7e4ef359 100644 --- a/admin/payment_provider.php +++ b/admin/payment_provider.php @@ -106,8 +106,13 @@ $num_rows = mysqli_num_rows($sql); Edit - - Delete
  • Recurring Payments
  • Saved cards
+
+ Delete Provider and +
    +
  • Related Recurring Payments
  • +
  • Related Saved cards
  • +
  • Client Provider Relations
  • +
From 96b8fcad3a7965e8573e3888056645eaf003a2bd Mon Sep 17 00:00:00 2001 From: johnnyq Date: Sun, 16 Nov 2025 17:34:37 -0500 Subject: [PATCH 06/23] Fix Pay With a Saved Card in Invoice Listing if Saved Cards are on files for that client --- agent/invoice.php | 4 ++-- agent/invoices.php | 17 +++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/agent/invoice.php b/agent/invoice.php index 039da741..710c04d6 100644 --- a/agent/invoice.php +++ b/agent/invoice.php @@ -18,8 +18,8 @@ if (isset($_GET['invoice_id'])) { $mysqli, "SELECT * FROM invoices LEFT JOIN clients ON invoice_client_id = client_id - LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1 - LEFT JOIN locations ON clients.client_id = locations.location_client_id AND location_primary = 1 + LEFT JOIN contacts ON client_id = contact_client_id AND contact_primary = 1 + LEFT JOIN locations ON client_id = location_client_id AND location_primary = 1 WHERE invoice_id = $invoice_id $access_permission_query LIMIT 1" diff --git a/agent/invoices.php b/agent/invoices.php index b547b47d..9cc3cf7c 100644 --- a/agent/invoices.php +++ b/agent/invoices.php @@ -344,8 +344,6 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); $recurring_invoice_display = "-"; } - - $now = time(); if (($invoice_status == "Sent" || $invoice_status == "Partial" || $invoice_status == "Viewed") && strtotime($invoice_due) + 86400 < $now) { @@ -356,6 +354,15 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); $invoice_badge_color = getInvoiceBadgeColor($invoice_status); + // Saved Payment Methods + $sql_saved_payment_methods = mysqli_query($mysqli, " + SELECT * FROM client_saved_payment_methods + LEFT JOIN payment_providers + ON client_saved_payment_methods.saved_payment_provider_id = payment_providers.payment_provider_id + WHERE saved_payment_client_id = $client_id + AND payment_provider_active = 1; + "); + ?> @@ -395,10 +402,8 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); Add Payment - - - Pay via saved card - + 0 && ($invoice_status === 'Sent' || $invoice_status === 'Viewed')) { ?> + Pay with Saved Card From a87b0b0447d2d17b9f38b87bad93005d592c4f90 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Sun, 16 Nov 2025 17:40:06 -0500 Subject: [PATCH 07/23] Fix regression in dashboard has client --- agent/dashboard.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/dashboard.php b/agent/dashboard.php index b07bce4f..9a8da89c 100644 --- a/agent/dashboard.php +++ b/agent/dashboard.php @@ -744,7 +744,7 @@ if ($user_config_dashboard_technical_enable == 1) { href="ticket.php?ticket_id="> - "> + "> From 47e647c71216fd208a25ff5b43f1e82cd5447233 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Sun, 16 Nov 2025 17:55:13 -0500 Subject: [PATCH 08/23] Update Changelog and bunp App Version --- CHANGELOG.md | 25 ++++++++++++++++++++++++- includes/app_version.php | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10370d33..6d4a1c8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,30 @@ This file documents all notable changes made to ITFlow. -## [25.11] Changelog +## [25.11.1] Maint Release + +### Fixes +- Fix broken edit Payment Method. +- Fix unable to delete Vendor Template. +- Fix Mail Queue link in flash alert for testing email and sending a quote. +- Add Show Category Type select if not defined. +- Add Show Product Type select if not defined. +- Fix add ticket watcher. +- Fix if Client isn't assigned to a ticket dont show client view. +- Fix missing session client id check when paying an invoice from client portal. +- Update Composer Webklex-IMAP library dependency symfony/http-foundation from 7.3.3 to 7.3.7 to fix security related issues. +- Add back delete Payment provider the database will handle cascade deletes to saved cards, recurring payments and client payment provider reference. + +### Added / Changed +- [Feature] Added Asset Tags. +- [Feature] Added Quick Add Links to most side bar navs example quickly add a client from sidebar. +- Migrate ticket template add to ajax modal. +- Add TOTP secret to Client Export PDF in Credential section. +- Add UserID on hover in users listing. +- Merge ticket now redirects to the new ticket details page. +- [Feature] Add Pay via saved card under invoice Listings. + +## [25.11] Stable ### Deprecation Notice: - **Outdated CRON Scripts**: The following scripts are removed. diff --git a/includes/app_version.php b/includes/app_version.php index a541d656..b2a09f25 100644 --- a/includes/app_version.php +++ b/includes/app_version.php @@ -5,4 +5,4 @@ * Update this file each time we merge develop into master. Format is YY.MM (add a .v if there is more than one release a month. */ -DEFINE("APP_VERSION", "25.11"); +DEFINE("APP_VERSION", "25.11.1"); From 29e1b56e783959befa245f9c351603f1b77df1ca Mon Sep 17 00:00:00 2001 From: johnnyq Date: Sun, 16 Nov 2025 17:58:07 -0500 Subject: [PATCH 09/23] Hide contract side nav as its not yet complete --- admin/includes/side_nav.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/admin/includes/side_nav.php b/admin/includes/side_nav.php index e2fedd98..e6c00510 100644 --- a/admin/includes/side_nav.php +++ b/admin/includes/side_nav.php @@ -140,6 +140,7 @@ + + + From b61dfac5692c632f39e8ef73d269331c4bf86314 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Sun, 16 Nov 2025 19:56:59 -0500 Subject: [PATCH 13/23] Ticket Details Checks, Dont display Add/edit relations if no cliet in selected, dont show relations in ticket edit if no client assigned to ticket, also dont display public and email response type if no contact_email exists --- agent/modals/ticket/ticket_edit.php | 35 ++++++++++++++++------------- agent/ticket.php | 4 +++- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/agent/modals/ticket/ticket_edit.php b/agent/modals/ticket/ticket_edit.php index cd2e5482..d2d34b30 100644 --- a/agent/modals/ticket/ticket_edit.php +++ b/agent/modals/ticket/ticket_edit.php @@ -38,33 +38,35 @@ while ($row = mysqli_fetch_array($sql_additional_assets)) { ob_start(); ?> - +