Resolve issue causing crash when plate_scanners.deleted_at was missing. (#346)
Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
This commit is contained in:
co-authored by
Jeppe Bundgaard
parent
ab6c3ba5b6
commit
db9f589bf7
+3
-3
@@ -4,9 +4,9 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/ws": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"version": "8.21.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
||||
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
+37
@@ -40,6 +40,10 @@ class Receiver extends Writable {
|
||||
* extensions
|
||||
* @param {Boolean} [options.isServer=false] Specifies whether to operate in
|
||||
* client or server mode
|
||||
* @param {Number} [options.maxBufferedChunks=0] The maximum number of
|
||||
* buffered data chunks
|
||||
* @param {Number} [options.maxFragments=0] The maximum number of message
|
||||
* fragments
|
||||
* @param {Number} [options.maxPayload=0] The maximum allowed message length
|
||||
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
||||
* not to skip UTF-8 validation for text and close messages
|
||||
@@ -54,6 +58,8 @@ class Receiver extends Writable {
|
||||
this._binaryType = options.binaryType || BINARY_TYPES[0];
|
||||
this._extensions = options.extensions || {};
|
||||
this._isServer = !!options.isServer;
|
||||
this._maxBufferedChunks = options.maxBufferedChunks | 0;
|
||||
this._maxFragments = options.maxFragments | 0;
|
||||
this._maxPayload = options.maxPayload | 0;
|
||||
this._skipUTF8Validation = !!options.skipUTF8Validation;
|
||||
this[kWebSocket] = undefined;
|
||||
@@ -71,6 +77,7 @@ class Receiver extends Writable {
|
||||
|
||||
this._totalPayloadLength = 0;
|
||||
this._messageLength = 0;
|
||||
this._numFragments = 0;
|
||||
this._fragments = [];
|
||||
|
||||
this._errored = false;
|
||||
@@ -89,6 +96,22 @@ class Receiver extends Writable {
|
||||
_write(chunk, encoding, cb) {
|
||||
if (this._opcode === 0x08 && this._state == GET_INFO) return cb();
|
||||
|
||||
if (
|
||||
this._maxBufferedChunks > 0 &&
|
||||
this._buffers.length >= this._maxBufferedChunks
|
||||
) {
|
||||
cb(
|
||||
this.createError(
|
||||
RangeError,
|
||||
'Too many buffered chunks',
|
||||
false,
|
||||
1008,
|
||||
'WS_ERR_TOO_MANY_BUFFERED_PARTS'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this._bufferedBytes += chunk.length;
|
||||
this._buffers.push(chunk);
|
||||
this.startLoop(cb);
|
||||
@@ -478,6 +501,19 @@ class Receiver extends Writable {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._maxFragments > 0 && ++this._numFragments > this._maxFragments) {
|
||||
const error = this.createError(
|
||||
RangeError,
|
||||
'Too many message fragments',
|
||||
false,
|
||||
1008,
|
||||
'WS_ERR_TOO_MANY_BUFFERED_PARTS'
|
||||
);
|
||||
|
||||
cb(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._compressed) {
|
||||
this._state = INFLATING;
|
||||
this.decompress(data, cb);
|
||||
@@ -550,6 +586,7 @@ class Receiver extends Writable {
|
||||
this._totalPayloadLength = 0;
|
||||
this._messageLength = 0;
|
||||
this._fragmented = 0;
|
||||
this._numFragments = 0;
|
||||
this._fragments = [];
|
||||
|
||||
if (this._opcode === 2) {
|
||||
|
||||
+6
-1
@@ -4,6 +4,9 @@
|
||||
|
||||
const { Duplex } = require('stream');
|
||||
const { randomFillSync } = require('crypto');
|
||||
const {
|
||||
types: { isUint8Array }
|
||||
} = require('util');
|
||||
|
||||
const PerMessageDeflate = require('./permessage-deflate');
|
||||
const { EMPTY_BUFFER, kWebSocket, NOOP } = require('./constants');
|
||||
@@ -200,8 +203,10 @@ class Sender {
|
||||
|
||||
if (typeof data === 'string') {
|
||||
buf.write(data, 2);
|
||||
} else {
|
||||
} else if (isUint8Array(data)) {
|
||||
buf.set(data, 2);
|
||||
} else {
|
||||
throw new TypeError('Second argument must be a string or a Uint8Array');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -43,6 +43,10 @@ class WebSocketServer extends EventEmitter {
|
||||
* called
|
||||
* @param {Function} [options.handleProtocols] A hook to handle protocols
|
||||
* @param {String} [options.host] The hostname where to bind the server
|
||||
* @param {Number} [options.maxBufferedChunks=262144] The maximum number of
|
||||
* buffered data chunks
|
||||
* @param {Number} [options.maxFragments=16384] The maximum number of message
|
||||
* fragments
|
||||
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
||||
* size
|
||||
* @param {Boolean} [options.noServer=false] Enable no server mode
|
||||
@@ -65,6 +69,8 @@ class WebSocketServer extends EventEmitter {
|
||||
options = {
|
||||
allowSynchronousEvents: true,
|
||||
autoPong: true,
|
||||
maxBufferedChunks: 256 * 1024,
|
||||
maxFragments: 16 * 1024,
|
||||
maxPayload: 100 * 1024 * 1024,
|
||||
skipUTF8Validation: false,
|
||||
perMessageDeflate: false,
|
||||
@@ -424,6 +430,8 @@ class WebSocketServer extends EventEmitter {
|
||||
|
||||
ws.setSocket(socket, head, {
|
||||
allowSynchronousEvents: this.options.allowSynchronousEvents,
|
||||
maxBufferedChunks: this.options.maxBufferedChunks,
|
||||
maxFragments: this.options.maxFragments,
|
||||
maxPayload: this.options.maxPayload,
|
||||
skipUTF8Validation: this.options.skipUTF8Validation
|
||||
});
|
||||
|
||||
+14
@@ -201,6 +201,10 @@ class WebSocket extends EventEmitter {
|
||||
* multiple times in the same tick
|
||||
* @param {Function} [options.generateMask] The function used to generate the
|
||||
* masking key
|
||||
* @param {Number} [options.maxBufferedChunks=0] The maximum number of
|
||||
* buffered data chunks
|
||||
* @param {Number} [options.maxFragments=0] The maximum number of message
|
||||
* fragments
|
||||
* @param {Number} [options.maxPayload=0] The maximum allowed message size
|
||||
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
||||
* not to skip UTF-8 validation for text and close messages
|
||||
@@ -212,6 +216,8 @@ class WebSocket extends EventEmitter {
|
||||
binaryType: this.binaryType,
|
||||
extensions: this._extensions,
|
||||
isServer: this._isServer,
|
||||
maxBufferedChunks: options.maxBufferedChunks,
|
||||
maxFragments: options.maxFragments,
|
||||
maxPayload: options.maxPayload,
|
||||
skipUTF8Validation: options.skipUTF8Validation
|
||||
});
|
||||
@@ -640,6 +646,10 @@ module.exports = WebSocket;
|
||||
* masking key
|
||||
* @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
|
||||
* handshake request
|
||||
* @param {Number} [options.maxBufferedChunks=262144] The maximum number of
|
||||
* buffered data chunks
|
||||
* @param {Number} [options.maxFragments=16384] The maximum number of message
|
||||
* fragments
|
||||
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
||||
* size
|
||||
* @param {Number} [options.maxRedirects=10] The maximum number of redirects
|
||||
@@ -660,6 +670,8 @@ function initAsClient(websocket, address, protocols, options) {
|
||||
autoPong: true,
|
||||
closeTimeout: CLOSE_TIMEOUT,
|
||||
protocolVersion: protocolVersions[1],
|
||||
maxBufferedChunks: 256 * 1024,
|
||||
maxFragments: 16 * 1024,
|
||||
maxPayload: 100 * 1024 * 1024,
|
||||
skipUTF8Validation: false,
|
||||
perMessageDeflate: true,
|
||||
@@ -1017,6 +1029,8 @@ function initAsClient(websocket, address, protocols, options) {
|
||||
websocket.setSocket(socket, head, {
|
||||
allowSynchronousEvents: opts.allowSynchronousEvents,
|
||||
generateMask: opts.generateMask,
|
||||
maxBufferedChunks: opts.maxBufferedChunks,
|
||||
maxFragments: opts.maxFragments,
|
||||
maxPayload: opts.maxPayload,
|
||||
skipUTF8Validation: opts.skipUTF8Validation
|
||||
});
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ws",
|
||||
"version": "8.20.0",
|
||||
"version": "8.21.1",
|
||||
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
|
||||
"keywords": [
|
||||
"HyBi",
|
||||
@@ -66,5 +66,9 @@
|
||||
"nyc": "^15.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"utf-8-validate": "^6.0.0"
|
||||
},
|
||||
"allowScripts": {
|
||||
"bufferutil": true,
|
||||
"utf-8-validate": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,10 +627,21 @@ class xlvaskUsageLogsRoute
|
||||
{
|
||||
global $db;
|
||||
if ($this->hasPermission('list_xlvask_usage_orders_all')) {
|
||||
$result = $db->query(
|
||||
"SELECT DISTINCT HallId FROM plate_scanners
|
||||
WHERE HallId IS NOT NULL AND TRIM(HallId) <> '' AND deleted_at IS NULL"
|
||||
);
|
||||
$sqlWithSoftDelete = "SELECT DISTINCT HallId FROM plate_scanners
|
||||
WHERE HallId IS NOT NULL AND TRIM(HallId) <> '' AND deleted_at IS NULL";
|
||||
$sqlWithoutSoftDelete = "SELECT DISTINCT HallId FROM plate_scanners
|
||||
WHERE HallId IS NOT NULL AND TRIM(HallId) <> ''";
|
||||
try {
|
||||
$result = $db->query($sqlWithSoftDelete);
|
||||
} catch (\Throwable $throwable) {
|
||||
$message = (string)$throwable->getMessage();
|
||||
$missingDeletedAt = str_contains($message, "Unknown column 'deleted_at'")
|
||||
|| str_contains($message, "Unknown column `deleted_at`");
|
||||
if (!$missingDeletedAt) {
|
||||
throw $throwable;
|
||||
}
|
||||
$result = $db->query($sqlWithoutSoftDelete);
|
||||
}
|
||||
$hallIds = $result === false ? [] : array_map(
|
||||
static fn(array $row): string => trim((string)($row['HallId'] ?? '')),
|
||||
$db->fetch_all($result)
|
||||
|
||||
@@ -26,15 +26,26 @@ class XLVaskUsageHallScopeRouteHarness extends \routes\xlvaskUsageLogsRoute
|
||||
class XLVaskUsageHallScopeDbFake
|
||||
{
|
||||
public int $queryCount = 0;
|
||||
private bool $softDeleteQueryAttempted = false;
|
||||
|
||||
/** @param array<int,array{HallId:mixed}> $rows */
|
||||
public function __construct(private readonly array $rows)
|
||||
public function __construct(
|
||||
private readonly array $rows,
|
||||
private readonly bool $deletedAtColumnMissing = false
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function query(string $sql): object
|
||||
{
|
||||
$this->queryCount++;
|
||||
if ($this->deletedAtColumnMissing
|
||||
&& !$this->softDeleteQueryAttempted
|
||||
&& str_contains($sql, 'deleted_at IS NULL')) {
|
||||
$this->softDeleteQueryAttempted = true;
|
||||
throw new \Exception("Unknown column 'deleted_at' in 'WHERE'");
|
||||
}
|
||||
|
||||
expect($sql)->toContain('SELECT DISTINCT HallId FROM plate_scanners');
|
||||
|
||||
return new stdClass();
|
||||
@@ -116,6 +127,20 @@ it('uses every normalized scanner hall for users with all-scope permission', fun
|
||||
->and($dbFake->queryCount)->toBe(1);
|
||||
});
|
||||
|
||||
it('falls back when plate scanners table has no deleted_at column', function (): void {
|
||||
$route = new XLVaskUsageHallScopeRouteHarness();
|
||||
$route->hasAllPermission = true;
|
||||
$dbFake = new XLVaskUsageHallScopeDbFake([
|
||||
['HallId' => ' Hall-A '],
|
||||
['HallId' => 'Hall-B'],
|
||||
], true);
|
||||
|
||||
$hallIds = resolve_xlvask_usage_hall_scope($route, $dbFake, ['Own-Hall']);
|
||||
|
||||
expect($hallIds)->toBe(['Hall-A', 'Hall-B'])
|
||||
->and($dbFake->queryCount)->toBe(2);
|
||||
});
|
||||
|
||||
it('keeps own-scope users limited to normalized group halls', function (): void {
|
||||
$route = new XLVaskUsageHallScopeRouteHarness();
|
||||
$dbFake = new XLVaskUsageHallScopeDbFake([
|
||||
|
||||
Reference in New Issue
Block a user