Merge branch 'develop' of https://github.com/itflow-org/itflow into develop

This commit is contained in:
wrongecho 2025-11-18 13:08:17 +00:00
commit 169619c9b9
37 changed files with 425 additions and 233 deletions

View File

@ -2,7 +2,37 @@
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.
- Don't show Client Tickets Breadcrumb if no client is assigned to a ticket.
- Don't Show Contact or Assignment Tab in edit ticket if no Client is Assigned.
- Don't Show add contact, asset, vendor, watcher if not client is assigned to a ticket.
- Don't Show Public Comment & Email if contact email doesn't exist.
- Fixed IMAP Test whicn now uses RAW TCP Connection instead of the depracated php-imap extension.
- Fix Broken Link in Ticket Updates via Client Portal to agent.
### 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.
- Ticket Related Side Items UI Cleanup to use btn-tool class.
## [25.11] Stable
### Deprecation Notice:
- **Outdated CRON Scripts**: The following scripts are removed.

View File

@ -140,6 +140,7 @@
<?php if ($config_module_enable_itdoc) { ?>
<li class="nav-header">TEMPLATES</li>
<!-- 2025-11-16 JQ - Hide Contracts not yet ready
<li class="nav-item">
<a href="/admin/contract_template.php" class="nav-link <?php echo (basename($_SERVER['PHP_SELF']) == 'contract_template.php' ? 'active' : ''); ?>">
<i class="nav-icon fas fa-file-contract"></i>
@ -149,6 +150,7 @@
</p>
</a>
</li>
-->
<li class="nav-item">
<a href="/admin/project_template.php" class="nav-link <?php echo (in_array(basename($_SERVER['PHP_SELF']), ['project_template.php', 'project_template_details.php']) ? 'active' : ''); ?>">
<i class="nav-icon fas fa-project-diagram"></i>

View File

@ -12,11 +12,24 @@ $tag_type = intval($row['tag_type']);
$tag_color = nullable_htmlentities($row['tag_color']);
$tag_icon = nullable_htmlentities($row['tag_icon']);
// Generate the HTML form content using output buffering.
if ($tag_type == 1) {
$tag_type_display = "Client";
} elseif ( $tag_type == 2) {
$tag_type_display = "Location";
} elseif ( $tag_type == 3) {
$tag_type_display = "Contact";
} elseif ( $tag_type == 4) {
$tag_type_display = "Credential";
} elseif ( $tag_type == 5) {
$tag_type_display = "Asset";
} else {
$tag_type_display = "Unknown";
}
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-tag mr-2"></i>Editing tag: <strong><?php echo $tag_name; ?></strong></h5>
<h5 class="modal-title"><i class="fas fa-fw fa-tag mr-2"></i><?= $tag_type_display ?> Tag: <strong><?php echo $tag_name; ?></strong></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
@ -35,23 +48,6 @@ ob_start();
</div>
</div>
<div class="form-group">
<label>Type <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-th"></i></span>
</div>
<select class="form-control select2" name="type" required>
<option value="">- Type -</option>
<option value="1" <?php if ($tag_type == 1) { echo "selected"; } ?>>Client Tag</option>
<option value="2" <?php if ($tag_type == 2) { echo "selected"; } ?>>Location Tag</option>
<option value="3" <?php if ($tag_type == 3) { echo "selected"; } ?>>Contact Tag</option>
<option value="4" <?php if ($tag_type == 4) { echo "selected"; } ?>>Credential Tag</option>
<option value="5" <?php if ($tag_type == 5) { echo "selected"; } ?>>Asset Tag</option>
</select>
</div>
</div>
<div class="form-group">
<label>Color <strong class="text-danger">*</strong></label>
<div class="input-group">

View File

@ -106,12 +106,14 @@ $num_rows = mysqli_num_rows($sql);
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<div class="dropdown-divider"></div>
<!-- <a class="dropdown-item text-danger confirm-link" href="post.php?disable_payment_provider=--><?php //echo $provider_id; ?><!--&csrf_token=--><?php //echo $_SESSION['csrf_token'] ?><!--">-->
<!-- <i class="fas fa-fw fa-thumbs-down mr-2"></i>Disable-->
<!-- </a>-->
<!-- <a class="dropdown-item text-danger confirm-link" href="post.php?delete_payment_provider=--><?php //echo $provider_id; ?><!--&csrf_token=--><?php //echo $_SESSION['csrf_token'] ?><!--">-->
<!-- <i class="fas fa-fw fa-trash mr-2"></i>Delete-->
<!-- </a>-->
<a class="dropdown-item text-danger confirm-link" href="post.php?delete_payment_provider=<?= $provider_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-trash mr-2"></i><strong>Delete Provider and</strong>
<ul class="text-xs">
<li>Related Recurring Payments</li>
<li>Related Saved cards</li>
<li>Client Provider Relations</li>
</ul>
</a>
</div>
</div>
</td>

View File

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

View File

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

View File

@ -160,27 +160,128 @@ if (isset($_POST['test_email_smtp'])) {
}
if (isset($_POST['test_email_imap'])) {
validateCSRFToken($_POST['csrf_token']);
// Setup your IMAP connection parameters
$hostname = "{" . $config_imap_host . ":" . $config_imap_port . "/" . $config_imap_encryption . "/novalidate-cert}INBOX";
$username = $config_imap_username;
$password = $config_imap_password;
$host = $config_imap_host;
$port = (int) $config_imap_port;
$encryption = strtolower(trim($config_imap_encryption)); // e.g. "ssl", "tls", "none"
$username = $config_imap_username;
$password = $config_imap_password;
// Build remote socket (implicit SSL vs plain TCP)
$transport = 'tcp';
if ($encryption === 'ssl') {
$transport = 'ssl';
}
$remote_socket = $transport . '://' . $host . ':' . $port;
// Stream context (you can tighten these if you want strict validation)
$contextOptions = [];
if (in_array($encryption, ['ssl', 'tls'], true)) {
$contextOptions['ssl'] = [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
];
}
$context = stream_context_create($contextOptions);
try {
$inbox = @imap_open($hostname, $username, $password);
$errno = 0;
$errstr = '';
if ($inbox) {
imap_close($inbox);
// 10-second timeout, adjust as needed
$fp = @stream_socket_client(
$remote_socket,
$errno,
$errstr,
10,
STREAM_CLIENT_CONNECT,
$context
);
if (!$fp) {
throw new Exception("Could not connect to IMAP server: [$errno] $errstr");
}
stream_set_timeout($fp, 10);
// Read server greeting (IMAP servers send something like: * OK Dovecot ready)
$greeting = fgets($fp, 1024);
if ($greeting === false || strpos($greeting, '* OK') !== 0) {
fclose($fp);
throw new Exception("Invalid IMAP greeting: " . trim((string) $greeting));
}
// If you really want STARTTLS for "tls" (port 143), you can do it here
if ($encryption === 'tls' && stripos($greeting, 'STARTTLS') !== false) {
// Request STARTTLS
fwrite($fp, "A0001 STARTTLS\r\n");
$line = fgets($fp, 1024);
if ($line === false || stripos($line, 'A0001 OK') !== 0) {
fclose($fp);
throw new Exception("STARTTLS failed: " . trim((string) $line));
}
// Enable crypto on the stream
if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
fclose($fp);
throw new Exception("Unable to enable TLS encryption on IMAP connection.");
}
}
// --- Do LOGIN command ---
$tag = 'A0002';
// Simple quoting; this may fail with some special chars in username/password.
$loginCmd = sprintf(
"%s LOGIN \"%s\" \"%s\"\r\n",
$tag,
addcslashes($username, "\\\""),
addcslashes($password, "\\\"")
);
fwrite($fp, $loginCmd);
$success = false;
$errorLine = '';
while (!feof($fp)) {
$line = fgets($fp, 2048);
if ($line === false) {
break;
}
// Look for tagged response for our LOGIN
if (strpos($line, $tag . ' ') === 0) {
if (stripos($line, $tag . ' OK') === 0) {
$success = true;
} else {
$errorLine = trim($line);
}
break;
}
}
// Always logout / close
fwrite($fp, "A0003 LOGOUT\r\n");
fclose($fp);
if ($success) {
flash_alert("Connected successfully");
} else {
throw new Exception(imap_last_error());
if (!$errorLine) {
$errorLine = 'Unknown IMAP authentication error';
}
throw new Exception($errorLine);
}
} catch (Exception $e) {
flash_alert("<strong>IMAP connection failed:</strong> " . $e->getMessage(), 'error');
flash_alert("<strong>IMAP connection failed:</strong> " . htmlspecialchars($e->getMessage()), 'error');
}
redirect();
}

View File

@ -28,7 +28,7 @@ if (isset($_POST['edit_tag'])) {
$tag_id = intval($_POST['tag_id']);
mysqli_query($mysqli,"UPDATE tags SET tag_name = '$name', tag_type = $type, tag_color = '$color', tag_icon = '$icon' WHERE tag_id = $tag_id");
mysqli_query($mysqli,"UPDATE tags SET tag_name = '$name', tag_color = '$color', tag_icon = '$icon' WHERE tag_id = $tag_id");
logAction("Tag", "Edit", "$session_name edited tag $name", 0, $tag_id);

View File

@ -6,10 +6,31 @@ $order = "ASC";
require_once "includes/inc_all_admin.php";
if (isset($_GET['type'])) {
$type_filter = intval($_GET['type']);
} else {
$type_filter = 1;
}
if ($type_filter == 1) {
$tag_type_display = "Client";
} elseif ( $type_filter == 2) {
$tag_type_display = "Location";
} elseif ( $type_filter == 3) {
$tag_type_display = "Contact";
} elseif ( $type_filter == 4) {
$tag_type_display = "Credential";
} elseif ( $type_filter == 5) {
$tag_type_display = "Asset";
} else {
$tag_type_display = "Unknown";
}
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM tags
WHERE tag_name LIKE '%$q%'
AND tag_type = $type_filter
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
@ -19,9 +40,9 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-tags mr-2"></i>Tags</h3>
<h3 class="card-title mt-2"><i class="fas fa-fw fa-tags mr-2"></i><?= $tag_type_display ?> Tags</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/tag/tag_add.php"><i class="fas fa-plus mr-2"></i>New Tag</button>
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/tag/tag_add.php?type=<?= $type_filter ?>"><i class="fas fa-plus mr-2"></i>New <?= $tag_type_display ?> Tag</button>
</div>
</div>
@ -30,7 +51,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<div class="col-sm-4 mb-2">
<form autocomplete="off">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search Tags">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(nullable_htmlentities($q)); } ?>" placeholder="Search <?= $tag_type_display ?> Tags">
<div class="input-group-append">
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
@ -38,6 +59,45 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</form>
</div>
<div class="col-sm-8">
<div class="btn-group float-right">
<a href="?type=1"
class="btn <?php if ($type_filter == 1) {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Client</a>
<a href="?type=2"
class="btn <?php if ($type_filter == 2) {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Location</a>
<a href="?type=3"
class="btn <?php if ($type_filter == 3) {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Contact</a>
<a href="?type=4"
class="btn <?php if ($type_filter == 4) {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Credential</a>
<a href="?type=5"
class="btn <?php if ($type_filter == 5) {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>">Asset</a>
<a href="?<?= $url_query_strings_sort ?>&archived=1"
class="btn <?php if (isset($_GET['archived'])) {
echo 'btn-primary';
} else {
echo 'btn-default';
} ?>"><i
class="fas fa-fw fa-archive mr-2"></i>Archived</a>
</div>
</div>
</div>
@ -51,11 +111,6 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
Name <?php if ($sort == 'tag_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?php echo $url_query_strings_sort; ?>&sort=tag_type&order=<?php echo $disp; ?>">
Type <?php if ($sort == 'tag_type') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
@ -65,20 +120,6 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
while ($row = mysqli_fetch_array($sql)) {
$tag_id = intval($row['tag_id']);
$tag_name = nullable_htmlentities($row['tag_name']);
$tag_type = intval($row['tag_type']);
if ( $tag_type == 1) {
$tag_type_display = "Client Tag";
} elseif ( $tag_type == 2) {
$tag_type_display = "Location Tag";
} elseif ( $tag_type == 3) {
$tag_type_display = "Contact Tag";
} elseif ( $tag_type == 4) {
$tag_type_display = "Credential Tag";
} elseif ( $tag_type == 5) {
$tag_type_display = "Asset Tag";
} else {
$tag_type_display = "Unknown Tag";
}
$tag_color = nullable_htmlentities($row['tag_color']);
$tag_icon = nullable_htmlentities($row['tag_icon']);
@ -90,7 +131,6 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<span class='badge text-light p-2 mr-1' style="background-color: <?php echo $tag_color; ?>"><i class="fa fa-fw fa-<?php echo $tag_icon; ?> mr-2"></i><?php echo $tag_name; ?></span>
</a>
</td>
<td><?php echo $tag_type_display; ?></td>
<td>
<div class="dropdown dropleft text-center">
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
@ -119,8 +159,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</tbody>
</table>
</div>
<?php require_once "../includes/filter_footer.php";
?>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
</div>

View File

@ -133,9 +133,6 @@ $network_count = intval($row['network_count']);
//Other Count
$other_count = intval($row['other_count']);
//Rebuild URL
$url_query_strings_sort = http_build_query($get_copy);
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM assets

View File

@ -744,7 +744,7 @@ if ($user_config_dashboard_technical_enable == 1) {
href="ticket.php?ticket_id=<?= "$ticket_id$has_client" ?>"><?= "$ticket_prefix$ticket_number" ?>
</a>
</td>
<td><a href="ticket.php?ticket_id=<?= $ticket_id$has_client" ?>"><?= $ticket_subject ?></a></td>
<td><a href="ticket.php?ticket_id=<?= "$ticket_id$has_client" ?>"><?= $ticket_subject ?></a></td>
<td><a href="tickets.php?client_id=<?php echo $client_id; ?>"><strong><?php echo $client_name; ?></strong></a></td>
<td><?php echo $contact_display; ?></td>
<td><span class='p-2 badge badge-pill badge-<?php echo $ticket_priority_color; ?>'><?php echo $ticket_priority; ?></span></td>

View File

@ -23,9 +23,6 @@ if (!empty($q)) {
$query_snippet = ""; // empty
}
//Rebuild URL
$url_query_strings_sort = http_build_query($get_copy);
// Folder ID
$get_folder_id = 0;
if (!empty($_GET['folder_id'])) {

View File

@ -14,9 +14,6 @@ if (!empty($_GET['folder_id'])) {
$folder_id = 0;
}
//Rebuild URL
$url_query_strings_sort = http_build_query($get_copy);
// Folder ID
$get_folder_id = 0;
if (!empty($_GET['folder_id'])) {

View File

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

View File

@ -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;
");
?>
<tr>
@ -395,10 +402,8 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
<i class="fa fa-fw fa-credit-card mr-2"></i>Add Payment
</a>
<div class="dropdown-divider"></div>
<?php if ($invoice_status !== 'Partial' && $config_stripe_enable && $stripe_id && $stripe_pm) { ?>
<a class="dropdown-item confirm-link" href="post.php?add_payment_stripe&invoice_id=<?php echo $invoice_id; ?>&csrf_token=<?php echo $_SESSION['csrf_token']; ?>">
<i class="fa fa-fw fa-credit-card mr-2"></i>Pay via saved card
</a>
<?php if (mysqli_num_rows($sql_saved_payment_methods) > 0 && ($invoice_status === 'Sent' || $invoice_status === 'Viewed')) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/payment/payment_saved_method_add.php?id=<?= $invoice_id ?>"><i class="fas fa-fw fa-wallet mr-2"></i>Pay with Saved Card</a>
<div class="dropdown-divider"></div>
<?php } ?>
<?php } ?>

View File

@ -380,7 +380,7 @@ ob_start();
// Send a POST request to ajax.php as ajax.php with data contact_set_notes=true, contact_id=NUM, notes=NOTES
jQuery.post(
"../ajax.php",
"ajax.php",
{
asset_set_notes: 'TRUE',
asset_id: asset_id,

View File

@ -10,7 +10,7 @@ ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-clone mr-2"></i>Merge & close <strong><?= $count ?></strong>tickets</h5>
<h5 class="modal-title"><i class="fa fa-fw fa-clone mr-2"></i>Merge & close <strong><?= $count ?></strong> tickets</h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
@ -75,3 +75,5 @@ ob_start();
<!-- Ticket merge JS -->
<script src="/agent/js/ticket_merge.js"></script>
<?php require_once '../../../includes/modal_footer.php';

View File

@ -38,33 +38,35 @@ while ($row = mysqli_fetch_array($sql_additional_assets)) {
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fa fa-fw fa-life-ring mr-2"></i>Editing ticket: <strong><?php echo "$ticket_prefix$ticket_number"; ?></strong> - <?php echo $client_name; ?></h5>
<h5 class="modal-title"><i class="fa fa-fw fa-life-ring mr-2"></i>Ticket: <strong><?= "$ticket_prefix$ticket_number" ?></strong> - <?= $client_name ?></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="ticket_id" value="<?php echo $ticket_id; ?>">
<input type="hidden" name="ticket_id" value="<?= $ticket_id ?>">
<div class="modal-body">
<?php if ($client_id) { ?>
<ul class="nav nav-pills nav-justified mb-3">
<li class="nav-item">
<a class="nav-link active" data-toggle="pill" href="#pills-details<?php echo $ticket_id; ?>"><i class="fa fa-fw fa-life-ring mr-2"></i>Details</a>
<a class="nav-link active" data-toggle="pill" href="#pills-details"><i class="fa fa-fw fa-life-ring mr-2"></i>Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-contacts"><i class="fa fa-fw fa-users mr-2"></i>Contact</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-contacts<?php echo $ticket_id; ?>"><i class="fa fa-fw fa-users mr-2"></i>Contact</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href="#pills-assignment<?php echo $ticket_id; ?>"><i class="fa fa-fw fa-desktop mr-2"></i>Assignment</a>
<a class="nav-link" data-toggle="pill" href="#pills-assignment"><i class="fa fa-fw fa-desktop mr-2"></i>Assignment</a>
</li>
</ul>
<hr>
<?php } ?>
<div class="tab-content" <?php if (lookupUserPermission('module_support') <= 1) { echo 'inert'; } ?>>
<div class="tab-pane fade show active" id="pills-details<?php echo $ticket_id; ?>">
<div class="tab-pane fade show active" id="pills-details">
<div class="form-group">
<label>Subject <strong class="text-danger">*</strong></label>
@ -182,7 +184,9 @@ ob_start();
</div>
<div class="tab-pane fade" id="pills-contacts<?php echo $ticket_id; ?>">
<?php if ($client_id) { ?>
<div class="tab-pane fade" id="pills-contacts">
<div class="form-group">
<label>Contact</label>
@ -236,7 +240,7 @@ ob_start();
</div>
<div class="tab-pane fade" id="pills-assignment<?php echo $ticket_id; ?>">
<div class="tab-pane fade" id="pills-assignment">
<div class="form-group">
<label>Asset</label>
@ -372,7 +376,7 @@ ob_start();
while ($row = mysqli_fetch_array($sql_projects)) {
$project_id_select = intval($row['project_id']);
$project_name_select = nullable_htmlentities($row['project_name']); ?>
<option <?php if ($project_id == $project_id_select) { echo "selected"; } ?> value="<?php echo $project_id_select; ?>"><?php echo $project_name_select; ?></option>
<option <?php if ($project_id == $project_id_select) { echo "selected"; } ?> value="<?= $project_id_select ?>"><?= $project_name_select ?></option>
<?php } ?>
</select>
@ -380,13 +384,14 @@ ob_start();
</div>
</div>
<?php } // End client_id check ?>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="edit_ticket" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save</button>
<button type="submit" name="edit_ticket" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save changes</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div>

View File

@ -38,9 +38,6 @@ if (isset($_GET['account']) & !empty($_GET['account'])) {
$account_filter = '';
}
//Rebuild URL
$url_query_strings_sort = http_build_query($get_copy);
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM payments

View File

@ -9,9 +9,6 @@ require_once "includes/inc_all.php";
// Perms
enforceUserPermission('module_financial');
//Rebuild URL
$url_query_strings_sort = http_build_query($get_copy);
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM recurring_expenses

View File

@ -9,9 +9,6 @@ require_once "includes/inc_all.php";
// Perms
enforceUserPermission('module_financial');
//Rebuild URL
$url_query_strings_sort = http_build_query($get_copy);
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM revenues

View File

@ -39,7 +39,6 @@ if (isset($_GET['ticket_id'])) {
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
LEFT JOIN categories ON ticket_category = category_id
WHERE ticket_id = $ticket_id
LIMIT 1"
);
@ -358,9 +357,11 @@ if (isset($_GET['ticket_id'])) {
<li class="breadcrumb-item">
<a href="tickets.php">All Tickets</a>
</li>
<?php if ($client_url) { ?>
<li class="breadcrumb-item">
<a href="tickets.php?client_id=<?php echo $client_id; ?>"><?= $client_name ?> Tickets</a>
</li>
<?php } ?>
<li class="breadcrumb-item active"><?php echo "$ticket_prefix$ticket_number";?></li>
</ol>
@ -415,18 +416,13 @@ if (isset($_GET['ticket_id'])) {
<i class="fas fa-fw fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#"
data-modal-size = "lg"
data-modal-url="modals/ticket/ticket_edit.php?id=<?= $ticket_id ?>">
<i class="fas fa-fw fa-edit mr-2"></i>Edit
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/ticket/ticket_summary.php?ticket_id=<?= $ticket_id ?>" data-modal-size="lg">
<i class="fas fa-fw fa-lightbulb mr-2"></i>Summarize
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/ticket/ticket_merge.php?ticket_id=<?= $ticket_id ?>">
<i class="fas fa-fw fa-clone mr-2"></i>Merge
</a>
<?php if (empty($ticket_closed_at)) { ?>
<?php if (empty($ticket_closed_at) && $client_id) { ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item ajax-modal" href="#"
data-modal-url="modals/ticket/ticket_contact.php?id=<?= $ticket_id ?>">
@ -564,9 +560,14 @@ if (isset($_GET['ticket_id'])) {
<div class="card card-dark mb-3">
<div class="card-header bg-dark">
<h3 class="card-title">
<h5 class="card-title">
Ticket Details
</h3>
</h5>
<?php if (empty($ticket_closed_at)) { ?>
<div class="card-tools">
<button type="button" class="btn btn-tool ajax-modal" data-modal-url="modals/ticket/ticket_edit.php?id=<?= $ticket_id ?>" data-modal-size="lg"><i class="fas fa-edit"></i></button>
</div>
<?php } ?>
</div>
<div class="card-body prettyContent" id="ticketDetails">
@ -599,9 +600,11 @@ if (isset($_GET['ticket_id'])) {
<label class="btn btn-outline-dark active">
<input type="radio" name="public_reply_type" value="0" checked>Internal Note
</label>
<?php if ($contact_email) { ?>
<label class="btn btn-outline-info">
<input type="radio" name="public_reply_type" value="2">Public Comment & Email
</label>
<?php } ?>
<label class="btn btn-outline-info">
<input type="radio" name="public_reply_type" value="1">Public Comment
</label>
@ -807,16 +810,17 @@ if (isset($_GET['ticket_id'])) {
<div class="col-md-3">
<!-- Ticket details right card -->
<div class="card">
<div class="card <?php if(!$ticket_resolved_at) { echo "collapsed-card"; } ?>">
<div class="card-header">
<a class="text-reset text-decoration-none" href="#" data-toggle="collapse" data-target="#ticketDetailsCard">
<h5 class="card-title mt-1"><i class="fas fa-fw fa-life-ring mr-2"></i>Ticket Details</h5>
</a>
<h5 class="card-title"><i class="fas fa-fw fa-life-ring mr-2"></i>Ticket Details</h5>
<div class="card-tools">
<a class="fa fa-chevron-down" href="#" data-toggle="collapse" data-target="#ticketDetailsCard"></a>
<button type="button" class="btn btn-tool" data-card-widget="collapse">
<i class="fas fa-chevron-down"></i>
</button>
</div>
</div>
<div class="card-body collapse <?php echo !empty($ticket_resolved_at) ? 'show' : ''; ?>" id="ticketDetailsCard">
<div class="card-body">
<!-- Created -->
<div title="<?php echo $ticket_created_at; ?>">
@ -912,12 +916,12 @@ if (isset($_GET['ticket_id'])) {
<!-- Tasks Card -->
<?php if (empty($ticket_resolved_at) || (!empty($ticket_resolved_at) && $task_count > 0)) { ?>
<div class="card">
<div class="card-header py-2">
<h5 class="card-title"><i class="fas fa-fw fa-tasks mr-2 mt-2"></i>Tasks</h5>
<div class="card-header">
<h5 class="card-title"><i class="fas fa-fw fa-tasks mr-2"></i>Tasks</h5>
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<div class="card-tools">
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<div class="dropdown dropleft text-center">
<button class="btn btn-light text-secondary btn-sm" type="button" data-toggle="dropdown">
<button class="btn btn-tool" type="button" data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu">
@ -934,8 +938,8 @@ if (isset($_GET['ticket_id'])) {
</a>
</div>
</div>
<?php } ?>
</div>
<?php } ?>
</div>
<div class="card-body p-0">
@ -964,7 +968,7 @@ if (isset($_GET['ticket_id'])) {
$task_completion_estimate = intval($row['task_completion_estimate']);
$task_completed_at = nullable_htmlentities($row['task_completed_at']);
?>
<tr data-task-id="<?php echo $task_id; ?>">
<tr data-task-id="<?= $task_id ?>">
<td>
<?php if ($task_completed_at) { ?>
<i class="far fa-check-square text-success"></i>
@ -1022,16 +1026,16 @@ if (isset($_GET['ticket_id'])) {
<!-- Contact card -->
<?php if ($contact_id) { ?>
<div class="card">
<div class="card-header py-2">
<h5 class="card-title"><i class="fas fa-fw fa-user-check mr-2 mt-2"></i>Contact</h5>
<div class="card-header">
<h5 class="card-title"><i class="fas fa-fw fa-user-check mr-2"></i>Contact</h5>
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<div class="card-tools">
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<a class="btn btn-light text-secondary btn-sm ajax-modal" href="#"
data-modal-url="modals/ticket/ticket_contact.php?id=<?= $ticket_id ?>">
<i class="fas fa-edit"></i>
</a>
<?php } ?>
<a class="btn btn-tool ajax-modal" href="#"
data-modal-url="modals/ticket/ticket_contact.php?id=<?= $ticket_id ?>">
<i class="fas fa-edit"></i>
</a>
</div>
<?php } ?>
</div>
<div class="card-body">
@ -1073,23 +1077,19 @@ if (isset($_GET['ticket_id'])) {
<?php } ?>
<!-- End contact card -->
<!-- Ticket watchers card -->
<?php if (empty($ticket_closed_at) && mysqli_num_rows($sql_ticket_watchers) > 0) { ?>
<div class="card">
<div class="card-header py-2">
<h5 class="card-title"><i class="fas fa-fw fa-eye mr-2 mt-2"></i>Watchers</h5>
<div class="card-header">
<h5 class="card-title"><i class="fas fa-fw fa-eye mr-2"></i>Watchers</h5>
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<div class="card-tools">
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<a class="btn btn-light text-secondary btn-sm ajax-modal" href="#" data-modal-url="modals/ticket/ticket_add_watcher.php?ticket_id=<?= $ticket_id ?>">
<i class="fas fa-fw fa-plus"></i>
</a>
<?php } ?>
<a class="btn btn-tool ajax-modal" href="#" data-modal-url="modals/ticket/ticket_add_watcher.php?ticket_id=<?= $ticket_id ?>">
<i class="fas fa-fw fa-plus"></i>
</a>
</div>
<?php } ?>
</div>
<div class="card-body">
@ -1117,15 +1117,15 @@ if (isset($_GET['ticket_id'])) {
<!-- Asset card -->
<?php if ($asset_id) { ?>
<div class="card mb-3">
<div class="card-header py-2">
<h5 class="card-title"><i class="fas fa-fw fa-desktop mr-2 mt-2"></i>Assets</h5>
<div class="card-header">
<h5 class="card-title"><i class="fas fa-fw fa-desktop mr-2"></i>Assets</h5>
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<div class="card-tools">
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<a class="btn btn-light text-secondary btn-sm ajax-modal" href="#" data-modal-url="modals/ticket/ticket_edit_asset.php?id=<?= $ticket_id ?>">
<i class="fas fa-fw fa-edit"></i>
</a>
<?php } ?>
<a class="btn btn-tool ajax-modal" href="#" data-modal-url="modals/ticket/ticket_edit_asset.php?id=<?= $ticket_id ?>">
<i class="fas fa-fw fa-edit"></i>
</a>
</div>
<?php } ?>
</div>
<div class="card-body">
<div>
@ -1165,15 +1165,15 @@ if (isset($_GET['ticket_id'])) {
<!-- Vendor card -->
<?php if ($vendor_id) { ?>
<div class="card mb-3">
<div class="card-header py-2">
<h5 class="card-title"><i class="fas fa-fw fa-building mr-2 mt-2"></i>Vendor</h5>
<div class="card-header">
<h5 class="card-title"><i class="fas fa-fw fa-building mr-2"></i>Vendor</h5>
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<div class="card-tools">
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<a class="btn btn-light text-secondary btn-sm ajax-modal" href="#" data-modal-url="modals/ticket/ticket_edit_vendor.php?ticket_id=<?= $ticket_id ?>">
<i class="fas fa-fw fa-edit"></i>
</a>
<?php } ?>
<a class="btn btn-tool ajax-modal" href="#" data-modal-url="modals/ticket/ticket_edit_vendor.php?ticket_id=<?= $ticket_id ?>">
<i class="fas fa-fw fa-edit"></i>
</a>
</div>
<?php } ?>
</div>
<div class="card-body">
@ -1220,15 +1220,15 @@ if (isset($_GET['ticket_id'])) {
<!-- project card -->
<?php if ($project_id) { ?>
<div class="card">
<div class="card-header py-2">
<h5 class="card-title"><i class="fas fa-fw fa-project-diagram mr-2 mt-2"></i>Project</h5>
<div class="card-header">
<h5 class="card-title"><i class="fas fa-fw fa-project-diagram mr-2"></i>Project</h5>
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<div class="card-tools">
<?php if (empty($ticket_resolved_at) && lookupUserPermission("module_support") >= 2) { ?>
<button type="button" class="btn btn-light text-secondary btn-sm ajax-modal" data-modal-url="modals/ticket/ticket_edit_project.php?id=<?= $ticket_id ?>">
<i class="fas fa-edit"></i>
</button>
<?php } ?>
<button type="button" class="btn btn-tool ajax-modal" data-modal-url="modals/ticket/ticket_edit_project.php?id=<?= $ticket_id ?>">
<i class="fas fa-edit"></i>
</button>
</div>
<?php } ?>
</div>
<div class="card-body">
<div>

View File

@ -28,10 +28,6 @@ if (isset($_GET['account_to']) & !empty($_GET['account_to'])) {
$account_to_filter = '';
}
//Rebuild URL
$url_query_strings_sort = http_build_query($get_copy);
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS transfer_created_at, expense_date AS transfer_date, expense_amount AS transfer_amount, expense_account_id AS transfer_account_from, revenue_account_id AS transfer_account_to, transfer_expense_id, transfer_revenue_id , transfer_id, transfer_method, transfer_notes FROM transfers, expenses, revenues

View File

@ -15,9 +15,6 @@ if (isset($_GET['client_id'])) {
$client_url = '';
}
//Rebuild URL
$url_query_strings_sort = http_build_query($get_copy);
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS * FROM trips

View File

@ -49,7 +49,7 @@ if (isset($_POST['add_ticket'])) {
$details = removeEmoji($details);
$email_subject = "ITFlow - New Ticket - $client_name: $subject";
$email_body = "Hello, <br><br>This is a notification that a new ticket has been raised in ITFlow. <br>Client: $client_name<br>Priority: $priority<br>Link: https://$config_base_url/ticket.php?ticket_id=$ticket_id <br><br><b>$subject</b><br>$details";
$email_body = "Hello, <br><br>This is a notification that a new ticket has been raised in ITFlow. <br>Client: $client_name<br>Priority: $priority<br>Link: https://$config_base_url/agent/ticket.php?ticket_id=$ticket_id&client_id=$session_client_id <br><br><b>$subject</b><br>$details";
// Queue Mail
$data = [
@ -113,7 +113,7 @@ if (isset($_POST['add_ticket_comment'])) {
$tech_name = sanitizeInput($tech_details['user_name']);
$subject = "$config_app_name Ticket updated - [$config_ticket_prefix$ticket_number] $ticket_subject";
$body = "Hello $tech_name,<br><br>A new reply has been added to the below ticket, check ITFlow for full details.<br><br>Client: $client_name<br>Ticket: $config_ticket_prefix$ticket_number<br>Subject: $ticket_subject<br><br>https://$config_base_url/ticket.php?ticket_id=$ticket_id";
$body = "Hello $tech_name,<br><br>A new reply has been added to the below ticket, check ITFlow for full details.<br><br>Client: $client_name<br>Ticket: $config_ticket_prefix$ticket_number<br>Subject: $ticket_subject<br><br>https://$config_base_url/agent/ticket.php?ticket_id=$ticket_id&client_id=$session_client_id";
$data = [
[
@ -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']);

View File

@ -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");

12
plugins/composer.lock generated
View File

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

View File

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

View File

@ -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'),

View File

@ -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\\' =>

View File

@ -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": [
{

View File

@ -5,7 +5,7 @@
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => '981fb9585d0c76e8b9c31812d58dfdd5b56d6454',
'reference' => '612041635d962d37f2f400ba1974bec5456ccd1e',
'name' => '__root__',
'dev' => true,
),
@ -16,7 +16,7 @@
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => '981fb9585d0c76e8b9c31812d58dfdd5b56d6454',
'reference' => '612041635d962d37f2f400ba1974bec5456ccd1e',
'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(

View File

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

View File

@ -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);
}
}

View File

@ -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 ?? '']);
}
}

View File

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

View File

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