Merge remote-tracking branch 'upstream/rc' into fix-edge-zombie-consumer-cleanup

This commit is contained in:
Volodymyr Babak 2025-09-25 21:29:55 +03:00
commit 85cfe19d14
12 changed files with 94 additions and 8 deletions

View File

@ -300,6 +300,7 @@ public class ImageController extends BaseController {
tbImageService.putETag(cacheKey, descriptor.getEtag()); tbImageService.putETag(cacheKey, descriptor.getEtag());
var result = ResponseEntity.ok() var result = ResponseEntity.ok()
.header("Content-Type", descriptor.getMediaType()) .header("Content-Type", descriptor.getMediaType())
.header("Content-Security-Policy", "default-src 'none'")
.eTag(descriptor.getEtag()); .eTag(descriptor.getEtag());
if (!cacheKey.isPublic()) { if (!cacheKey.isPublic()) {
result result

View File

@ -238,7 +238,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
entitiesSortOrder = sortOrder; entitiesSortOrder = sortOrder;
} }
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters()); return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters());
} }
@Override @Override

View File

@ -519,6 +519,67 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
} }
@Test
public void testFindAlarmsWithEntityFilterAndLatestValues() throws Exception {
loginTenantAdmin();
List<Device> devices = new ArrayList<>();
List<String> temps = new ArrayList<>();
List<String> deviceNames = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setCustomerId(customerId);
device.setName("Device" + i);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
device = doPost("/api/device", device, Device.class);
devices.add(device);
deviceNames.add(device.getName());
int temp = i * 10;
temps.add(String.valueOf(temp));
JsonNode content = JacksonUtil.toJsonNode("{\"temperature\": " + temp + "}");
doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + device.getUuidId() + "/timeseries/SERVER_SCOPE", content)
.andExpect(status().isOk());
Thread.sleep(1);
}
for (int i = 0; i < devices.size(); i++) {
Alarm alarm = new Alarm();
alarm.setCustomerId(customerId);
alarm.setOriginator(devices.get(i).getId());
String type = "device alarm" + i;
alarm.setType(type);
alarm.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm, Alarm.class);
Thread.sleep(1);
}
AlarmDataPageLink pageLink = new AlarmDataPageLink();
pageLink.setPage(0);
pageLink.setPageSize(100);
pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "created_time")));
List<EntityKey> alarmFields = new ArrayList<>();
alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type"));
List<EntityKey> entityFields = new ArrayList<>();
entityFields.add(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
List<EntityKey> latestValues = new ArrayList<>();
latestValues.add(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
EntityTypeFilter deviceTypeFilter = new EntityTypeFilter();
deviceTypeFilter.setEntityType(EntityType.DEVICE);
AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields);
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10);
List<String> retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList();
assertThat(retrievedAlarmTemps).containsExactlyInAnyOrderElementsOf(temps);
List<String> retrievedDeviceNames = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).toList();
assertThat(retrievedDeviceNames).containsExactlyInAnyOrderElementsOf(deviceNames);
}
private void testCountAlarmsByQuery(List<Alarm> alarms) throws Exception { private void testCountAlarmsByQuery(List<Alarm> alarms) throws Exception {
AlarmCountQuery countQuery = new AlarmCountQuery(); AlarmCountQuery countQuery = new AlarmCountQuery();

View File

@ -37,7 +37,7 @@ public class UserFields extends AbstractEntityFields {
@Override @Override
public String getName() { public String getName() {
return super.getEmail(); return getEmail();
} }
public UserFields(UUID id, long createdTime, UUID tenantId, UUID customerId, public UserFields(UUID id, long createdTime, UUID tenantId, UUID customerId,

View File

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig; import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Data @Data
@ -40,9 +41,11 @@ public class MobileAppBundle extends BaseData<MobileAppBundleId> implements HasT
private TenantId tenantId; private TenantId tenantId;
@Schema(description = "Application bundle title. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "Application bundle title. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank @NotBlank
@NoXss
@Length(fieldName = "title") @Length(fieldName = "title")
private String title; private String title;
@Schema(description = "Application bundle description.") @Schema(description = "Application bundle description.")
@NoXss
@Length(fieldName = "description") @Length(fieldName = "description")
private String description; private String description;
@Schema(description = "Android application id") @Schema(description = "Android application id")

View File

@ -62,6 +62,7 @@ public class NotificationRule extends BaseData<NotificationRuleId> implements Ha
@Valid @Valid
private NotificationRuleRecipientsConfig recipientsConfig; private NotificationRuleRecipientsConfig recipientsConfig;
@Valid
private NotificationRuleConfig additionalConfig; private NotificationRuleConfig additionalConfig;
private NotificationRuleId externalId; private NotificationRuleId externalId;

View File

@ -16,12 +16,14 @@
package org.thingsboard.server.common.data.notification.rule; package org.thingsboard.server.common.data.notification.rule;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serializable; import java.io.Serializable;
@Data @Data
public class NotificationRuleConfig implements Serializable { public class NotificationRuleConfig implements Serializable {
@NoXss
private String description; private String description;
} }

View File

@ -30,6 +30,7 @@ import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { parseHttpErrorMessage } from '@core/utils'; import { parseHttpErrorMessage } from '@core/utils';
import { getInterceptorConfig } from './interceptor.util'; import { getInterceptorConfig } from './interceptor.util';
import { DomSanitizer } from '@angular/platform-browser';
const tmpHeaders = {}; const tmpHeaders = {};
@ -46,6 +47,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
private dialogService: DialogService, private dialogService: DialogService,
private translate: TranslateService, private translate: TranslateService,
private authService: AuthService, private authService: AuthService,
private sanitizer: DomSanitizer
) {} ) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
@ -129,7 +131,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
} }
if (unhandled && !ignoreErrors) { if (unhandled && !ignoreErrors) {
const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType); const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType, this.sanitizer);
this.showError(errorMessageWithTimeout.message, errorMessageWithTimeout.timeout); this.showError(errorMessageWithTimeout.message, errorMessageWithTimeout.timeout);
} }
return throwError(() => errorResponse); return throwError(() => errorResponse);

View File

@ -32,6 +32,8 @@ import {
isNotEmptyTbFunction, isNotEmptyTbFunction,
TbFunction TbFunction
} from '@shared/models/js-function.models'; } from '@shared/models/js-function.models';
import { DomSanitizer } from '@angular/platform-browser';
import { SecurityContext } from '@angular/core';
const varsRegex = /\${([^}]*)}/g; const varsRegex = /\${([^}]*)}/g;
@ -854,7 +856,7 @@ export function getEntityDetailsPageURL(id: string, entityType: EntityType): str
} }
export function parseHttpErrorMessage(errorResponse: HttpErrorResponse, export function parseHttpErrorMessage(errorResponse: HttpErrorResponse,
translate: TranslateService, responseType?: string): {message: string; timeout: number} { translate: TranslateService, responseType?: string, sanitizer?:DomSanitizer): {message: string; timeout: number} {
let error = null; let error = null;
let errorMessage: string; let errorMessage: string;
let timeout = 0; let timeout = 0;
@ -882,6 +884,9 @@ export function parseHttpErrorMessage(errorResponse: HttpErrorResponse,
errorText += errorKey ? translate.instant(errorKey) : errorResponse.statusText; errorText += errorKey ? translate.instant(errorKey) : errorResponse.statusText;
errorMessage = errorText; errorMessage = errorText;
} }
if(sanitizer) {
errorMessage = sanitizer.sanitize(SecurityContext.HTML,errorMessage);
}
return {message: errorMessage, timeout}; return {message: errorMessage, timeout};
} }

View File

@ -24,6 +24,7 @@ import {
NgZone, NgZone,
OnDestroy, OnDestroy,
OnInit, OnInit,
SecurityContext,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@ -53,6 +54,7 @@ import {
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { hidePageSizePixelValue } from '@shared/models/constants'; import { hidePageSizePixelValue } from '@shared/models/constants';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { DomSanitizer } from '@angular/platform-browser';
@Component({ @Component({
selector: 'tb-manage-widget-actions', selector: 'tb-manage-widget-actions',
@ -106,7 +108,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
private dialogs: DialogService, private dialogs: DialogService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private elementRef: ElementRef, private elementRef: ElementRef,
private zone: NgZone) { private zone: NgZone,
private sanitizer: DomSanitizer) {
super(); super();
const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC };
this.pageLink = new PageLink(10, 0, null, sortOrder); this.pageLink = new PageLink(10, 0, null, sortOrder);
@ -289,7 +292,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
} }
const title = this.translate.instant('widget-config.delete-action-title'); const title = this.translate.instant('widget-config.delete-action-title');
const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name}); const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name});
this.dialogs.confirm(title, content, const safeContent = this.sanitizer.sanitize(SecurityContext.HTML, content);
this.dialogs.confirm(title, safeContent,
this.translate.instant('action.no'), this.translate.instant('action.no'),
this.translate.instant('action.yes'), true).subscribe( this.translate.instant('action.yes'), true).subscribe(
(res) => { (res) => {

View File

@ -26,6 +26,7 @@ import {
OnInit, OnInit,
QueryList, QueryList,
Renderer2, Renderer2,
SecurityContext,
SkipSelf, SkipSelf,
ViewChild, ViewChild,
ViewChildren, ViewChildren,
@ -97,6 +98,7 @@ import { HttpStatusCode } from '@angular/common/http';
import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
import { EntityDebugSettings } from '@shared/models/entity.models'; import { EntityDebugSettings } from '@shared/models/entity.models';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
import { DomSanitizer } from '@angular/platform-browser';
@Component({ @Component({
selector: 'tb-rulechain-page', selector: 'tb-rulechain-page',
@ -273,6 +275,7 @@ export class RuleChainPageComponent extends PageComponent
private renderer: Renderer2, private renderer: Renderer2,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private changeDetector: ChangeDetectorRef, private changeDetector: ChangeDetectorRef,
private sanitizer:DomSanitizer,
public dialog: MatDialog, public dialog: MatDialog,
public dialogService: DialogService, public dialogService: DialogService,
public fb: FormBuilder) { public fb: FormBuilder) {
@ -1360,9 +1363,13 @@ export class RuleChainPageComponent extends PageComponent
name = node.name; name = node.name;
desc = this.translate.instant(ruleNodeTypeDescriptors.get(node.component.type).name) + ' - ' + node.component.name; desc = this.translate.instant(ruleNodeTypeDescriptors.get(node.component.type).name) + ' - ' + node.component.name;
if (node.additionalInfo) { if (node.additionalInfo) {
details = node.additionalInfo.description; details = this.sanitizer.sanitize(SecurityContext.HTML, node.additionalInfo.description);
} }
} }
name = this.sanitizer.sanitize(SecurityContext.HTML, name);
desc = this.sanitizer.sanitize(SecurityContext.HTML, desc);
let tooltipContent = '<div class="tb-rule-node-tooltip">' + let tooltipContent = '<div class="tb-rule-node-tooltip">' +
'<div id="tb-node-content">' + '<div id="tb-node-content">' +
'<div class="tb-node-title">' + name + '</div>' + '<div class="tb-node-title">' + name + '</div>' +

View File

@ -206,7 +206,7 @@ export const HelpLinks = {
mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/mobile-center/`, mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/mobile-center/`,
mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`, mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`,
calculatedField: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/calculated-fields/`, calculatedField: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/calculated-fields/`,
aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/ai-models`, aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/samples/analytics/ai-models/`,
timewindowSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/#time-window`, timewindowSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/#time-window`,
trendzSettings: `${helpBaseUrl}/docs/trendz/` trendzSettings: `${helpBaseUrl}/docs/trendz/`
} }