fix: app icons saving (#2477)

This commit is contained in:
Daniel Salazar
2026-02-12 11:55:27 -08:00
committed by GitHub
parent 36b1499819
commit bfd8a4e16d
2 changed files with 61 additions and 0 deletions
@@ -20,6 +20,12 @@ import {
validate_url,
} from './lib/validation.js';
const APP_ICON_ENDPOINT_PATH_REGEX = /^\/?app-icon\/[^/?#]+\/[^/?#]+\/?$/;
const isAppIconEndpointPath = (value) => {
return typeof value === 'string' && APP_ICON_ENDPOINT_PATH_REGEX.test(value);
};
/**
* AppService contains an instance using the repository pattern
*/
@@ -385,6 +391,8 @@ export default class AppService extends BaseService {
if ( object.icon !== undefined && object.icon !== null ) {
if ( typeof object.icon === 'string' && object.icon.startsWith('data:') ) {
validate_image_base64(object.icon, { key: 'icon' });
} else if ( isAppIconEndpointPath(object.icon) ) {
// Allow existing relative app icon endpoint references.
} else {
validate_url(object.icon, { key: 'icon', maxlen: 3000 });
}
@@ -631,6 +639,8 @@ export default class AppService extends BaseService {
if ( object.icon !== undefined && object.icon !== null ) {
if ( typeof object.icon === 'string' && object.icon.startsWith('data:') ) {
validate_image_base64(object.icon, { key: 'icon' });
} else if ( isAppIconEndpointPath(object.icon) ) {
// Allow existing relative app icon endpoint references.
} else {
validate_url(object.icon, { key: 'icon', maxlen: 3000 });
}
@@ -806,6 +806,33 @@ describe('AppService', () => {
}));
});
it('should allow relative app-icon endpoint path for icon', async () => {
setupContextForWrite(createMockUserActor(1));
mockDb.read.mockResolvedValue([createMockAppRow()]);
validate_url.mockImplementation((_value, { key }) => {
if ( key === 'icon' ) {
throw new Error('icon should not be validated as a URL');
}
});
const crudQ = AppService.IMPLEMENTS['crud-q'];
await crudQ.create.call(appService, {
object: {
name: 'test-app',
title: 'Test',
index_url: 'https://example.com',
icon: '/app-icon/app-uid-123/64',
},
});
expect(mockEventService.emit).toHaveBeenCalledWith(
'app.new-icon',
expect.objectContaining({
data_url: '/app-icon/app-uid-123/64',
}));
expect(validate_url).toHaveBeenCalledWith('https://example.com', expect.objectContaining({ key: 'index_url' }));
});
it('should handle filetype_associations', async () => {
setupContextForWrite(createMockUserActor(1));
mockDb.read.mockResolvedValue([createMockAppRow()]);
@@ -1060,6 +1087,30 @@ describe('AppService', () => {
}));
});
it('should allow relative app-icon endpoint path when updating icon', async () => {
setupContextForWrite(createMockUserActor(1));
validate_url.mockImplementation((_value, { key }) => {
if ( key === 'icon' ) {
throw new Error('icon should not be validated as a URL');
}
});
const crudQ = AppService.IMPLEMENTS['crud-q'];
await crudQ.update.call(appService, {
object: {
uid: 'app-uid-123',
icon: '/app-icon/app-uid-123/64',
},
});
expect(mockEventService.emit).toHaveBeenCalledWith(
'app.new-icon',
expect.objectContaining({
app_uid: 'app-uid-123',
data_url: '/app-icon/app-uid-123/64',
}));
});
it('should emit app.rename event when name changes', async () => {
setupContextForWrite(createMockUserActor(1));