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,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.20.0",
|
"version": "8.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
||||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|||||||
+37
@@ -40,6 +40,10 @@ class Receiver extends Writable {
|
|||||||
* extensions
|
* extensions
|
||||||
* @param {Boolean} [options.isServer=false] Specifies whether to operate in
|
* @param {Boolean} [options.isServer=false] Specifies whether to operate in
|
||||||
* client or server mode
|
* 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 {Number} [options.maxPayload=0] The maximum allowed message length
|
||||||
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
||||||
* not to skip UTF-8 validation for text and close messages
|
* 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._binaryType = options.binaryType || BINARY_TYPES[0];
|
||||||
this._extensions = options.extensions || {};
|
this._extensions = options.extensions || {};
|
||||||
this._isServer = !!options.isServer;
|
this._isServer = !!options.isServer;
|
||||||
|
this._maxBufferedChunks = options.maxBufferedChunks | 0;
|
||||||
|
this._maxFragments = options.maxFragments | 0;
|
||||||
this._maxPayload = options.maxPayload | 0;
|
this._maxPayload = options.maxPayload | 0;
|
||||||
this._skipUTF8Validation = !!options.skipUTF8Validation;
|
this._skipUTF8Validation = !!options.skipUTF8Validation;
|
||||||
this[kWebSocket] = undefined;
|
this[kWebSocket] = undefined;
|
||||||
@@ -71,6 +77,7 @@ class Receiver extends Writable {
|
|||||||
|
|
||||||
this._totalPayloadLength = 0;
|
this._totalPayloadLength = 0;
|
||||||
this._messageLength = 0;
|
this._messageLength = 0;
|
||||||
|
this._numFragments = 0;
|
||||||
this._fragments = [];
|
this._fragments = [];
|
||||||
|
|
||||||
this._errored = false;
|
this._errored = false;
|
||||||
@@ -89,6 +96,22 @@ class Receiver extends Writable {
|
|||||||
_write(chunk, encoding, cb) {
|
_write(chunk, encoding, cb) {
|
||||||
if (this._opcode === 0x08 && this._state == GET_INFO) return 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._bufferedBytes += chunk.length;
|
||||||
this._buffers.push(chunk);
|
this._buffers.push(chunk);
|
||||||
this.startLoop(cb);
|
this.startLoop(cb);
|
||||||
@@ -478,6 +501,19 @@ class Receiver extends Writable {
|
|||||||
return;
|
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) {
|
if (this._compressed) {
|
||||||
this._state = INFLATING;
|
this._state = INFLATING;
|
||||||
this.decompress(data, cb);
|
this.decompress(data, cb);
|
||||||
@@ -550,6 +586,7 @@ class Receiver extends Writable {
|
|||||||
this._totalPayloadLength = 0;
|
this._totalPayloadLength = 0;
|
||||||
this._messageLength = 0;
|
this._messageLength = 0;
|
||||||
this._fragmented = 0;
|
this._fragmented = 0;
|
||||||
|
this._numFragments = 0;
|
||||||
this._fragments = [];
|
this._fragments = [];
|
||||||
|
|
||||||
if (this._opcode === 2) {
|
if (this._opcode === 2) {
|
||||||
|
|||||||
+6
-1
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
const { Duplex } = require('stream');
|
const { Duplex } = require('stream');
|
||||||
const { randomFillSync } = require('crypto');
|
const { randomFillSync } = require('crypto');
|
||||||
|
const {
|
||||||
|
types: { isUint8Array }
|
||||||
|
} = require('util');
|
||||||
|
|
||||||
const PerMessageDeflate = require('./permessage-deflate');
|
const PerMessageDeflate = require('./permessage-deflate');
|
||||||
const { EMPTY_BUFFER, kWebSocket, NOOP } = require('./constants');
|
const { EMPTY_BUFFER, kWebSocket, NOOP } = require('./constants');
|
||||||
@@ -200,8 +203,10 @@ class Sender {
|
|||||||
|
|
||||||
if (typeof data === 'string') {
|
if (typeof data === 'string') {
|
||||||
buf.write(data, 2);
|
buf.write(data, 2);
|
||||||
} else {
|
} else if (isUint8Array(data)) {
|
||||||
buf.set(data, 2);
|
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
|
* called
|
||||||
* @param {Function} [options.handleProtocols] A hook to handle protocols
|
* @param {Function} [options.handleProtocols] A hook to handle protocols
|
||||||
* @param {String} [options.host] The hostname where to bind the server
|
* @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
|
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
||||||
* size
|
* size
|
||||||
* @param {Boolean} [options.noServer=false] Enable no server mode
|
* @param {Boolean} [options.noServer=false] Enable no server mode
|
||||||
@@ -65,6 +69,8 @@ class WebSocketServer extends EventEmitter {
|
|||||||
options = {
|
options = {
|
||||||
allowSynchronousEvents: true,
|
allowSynchronousEvents: true,
|
||||||
autoPong: true,
|
autoPong: true,
|
||||||
|
maxBufferedChunks: 256 * 1024,
|
||||||
|
maxFragments: 16 * 1024,
|
||||||
maxPayload: 100 * 1024 * 1024,
|
maxPayload: 100 * 1024 * 1024,
|
||||||
skipUTF8Validation: false,
|
skipUTF8Validation: false,
|
||||||
perMessageDeflate: false,
|
perMessageDeflate: false,
|
||||||
@@ -424,6 +430,8 @@ class WebSocketServer extends EventEmitter {
|
|||||||
|
|
||||||
ws.setSocket(socket, head, {
|
ws.setSocket(socket, head, {
|
||||||
allowSynchronousEvents: this.options.allowSynchronousEvents,
|
allowSynchronousEvents: this.options.allowSynchronousEvents,
|
||||||
|
maxBufferedChunks: this.options.maxBufferedChunks,
|
||||||
|
maxFragments: this.options.maxFragments,
|
||||||
maxPayload: this.options.maxPayload,
|
maxPayload: this.options.maxPayload,
|
||||||
skipUTF8Validation: this.options.skipUTF8Validation
|
skipUTF8Validation: this.options.skipUTF8Validation
|
||||||
});
|
});
|
||||||
|
|||||||
+14
@@ -201,6 +201,10 @@ class WebSocket extends EventEmitter {
|
|||||||
* multiple times in the same tick
|
* multiple times in the same tick
|
||||||
* @param {Function} [options.generateMask] The function used to generate the
|
* @param {Function} [options.generateMask] The function used to generate the
|
||||||
* masking key
|
* 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 {Number} [options.maxPayload=0] The maximum allowed message size
|
||||||
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
||||||
* not to skip UTF-8 validation for text and close messages
|
* not to skip UTF-8 validation for text and close messages
|
||||||
@@ -212,6 +216,8 @@ class WebSocket extends EventEmitter {
|
|||||||
binaryType: this.binaryType,
|
binaryType: this.binaryType,
|
||||||
extensions: this._extensions,
|
extensions: this._extensions,
|
||||||
isServer: this._isServer,
|
isServer: this._isServer,
|
||||||
|
maxBufferedChunks: options.maxBufferedChunks,
|
||||||
|
maxFragments: options.maxFragments,
|
||||||
maxPayload: options.maxPayload,
|
maxPayload: options.maxPayload,
|
||||||
skipUTF8Validation: options.skipUTF8Validation
|
skipUTF8Validation: options.skipUTF8Validation
|
||||||
});
|
});
|
||||||
@@ -640,6 +646,10 @@ module.exports = WebSocket;
|
|||||||
* masking key
|
* masking key
|
||||||
* @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
|
* @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
|
||||||
* handshake request
|
* 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
|
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
||||||
* size
|
* size
|
||||||
* @param {Number} [options.maxRedirects=10] The maximum number of redirects
|
* @param {Number} [options.maxRedirects=10] The maximum number of redirects
|
||||||
@@ -660,6 +670,8 @@ function initAsClient(websocket, address, protocols, options) {
|
|||||||
autoPong: true,
|
autoPong: true,
|
||||||
closeTimeout: CLOSE_TIMEOUT,
|
closeTimeout: CLOSE_TIMEOUT,
|
||||||
protocolVersion: protocolVersions[1],
|
protocolVersion: protocolVersions[1],
|
||||||
|
maxBufferedChunks: 256 * 1024,
|
||||||
|
maxFragments: 16 * 1024,
|
||||||
maxPayload: 100 * 1024 * 1024,
|
maxPayload: 100 * 1024 * 1024,
|
||||||
skipUTF8Validation: false,
|
skipUTF8Validation: false,
|
||||||
perMessageDeflate: true,
|
perMessageDeflate: true,
|
||||||
@@ -1017,6 +1029,8 @@ function initAsClient(websocket, address, protocols, options) {
|
|||||||
websocket.setSocket(socket, head, {
|
websocket.setSocket(socket, head, {
|
||||||
allowSynchronousEvents: opts.allowSynchronousEvents,
|
allowSynchronousEvents: opts.allowSynchronousEvents,
|
||||||
generateMask: opts.generateMask,
|
generateMask: opts.generateMask,
|
||||||
|
maxBufferedChunks: opts.maxBufferedChunks,
|
||||||
|
maxFragments: opts.maxFragments,
|
||||||
maxPayload: opts.maxPayload,
|
maxPayload: opts.maxPayload,
|
||||||
skipUTF8Validation: opts.skipUTF8Validation
|
skipUTF8Validation: opts.skipUTF8Validation
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ws",
|
"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",
|
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"HyBi",
|
"HyBi",
|
||||||
@@ -66,5 +66,9 @@
|
|||||||
"nyc": "^15.0.0",
|
"nyc": "^15.0.0",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"utf-8-validate": "^6.0.0"
|
"utf-8-validate": "^6.0.0"
|
||||||
|
},
|
||||||
|
"allowScripts": {
|
||||||
|
"bufferutil": true,
|
||||||
|
"utf-8-validate": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -627,10 +627,21 @@ class xlvaskUsageLogsRoute
|
|||||||
{
|
{
|
||||||
global $db;
|
global $db;
|
||||||
if ($this->hasPermission('list_xlvask_usage_orders_all')) {
|
if ($this->hasPermission('list_xlvask_usage_orders_all')) {
|
||||||
$result = $db->query(
|
$sqlWithSoftDelete = "SELECT DISTINCT HallId FROM plate_scanners
|
||||||
"SELECT DISTINCT HallId FROM plate_scanners
|
WHERE HallId IS NOT NULL AND TRIM(HallId) <> '' AND deleted_at IS NULL";
|
||||||
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(
|
$hallIds = $result === false ? [] : array_map(
|
||||||
static fn(array $row): string => trim((string)($row['HallId'] ?? '')),
|
static fn(array $row): string => trim((string)($row['HallId'] ?? '')),
|
||||||
$db->fetch_all($result)
|
$db->fetch_all($result)
|
||||||
|
|||||||
@@ -26,15 +26,26 @@ class XLVaskUsageHallScopeRouteHarness extends \routes\xlvaskUsageLogsRoute
|
|||||||
class XLVaskUsageHallScopeDbFake
|
class XLVaskUsageHallScopeDbFake
|
||||||
{
|
{
|
||||||
public int $queryCount = 0;
|
public int $queryCount = 0;
|
||||||
|
private bool $softDeleteQueryAttempted = false;
|
||||||
|
|
||||||
/** @param array<int,array{HallId:mixed}> $rows */
|
/** @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
|
public function query(string $sql): object
|
||||||
{
|
{
|
||||||
$this->queryCount++;
|
$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');
|
expect($sql)->toContain('SELECT DISTINCT HallId FROM plate_scanners');
|
||||||
|
|
||||||
return new stdClass();
|
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);
|
->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 {
|
it('keeps own-scope users limited to normalized group halls', function (): void {
|
||||||
$route = new XLVaskUsageHallScopeRouteHarness();
|
$route = new XLVaskUsageHallScopeRouteHarness();
|
||||||
$dbFake = new XLVaskUsageHallScopeDbFake([
|
$dbFake = new XLVaskUsageHallScopeDbFake([
|
||||||
|
|||||||
Reference in New Issue
Block a user