diff --git a/__tests__/files/file.spec.ts b/__tests__/files/file.spec.ts index a56c09c08..1a7aab923 100644 --- a/__tests__/files/file.spec.ts +++ b/__tests__/files/file.spec.ts @@ -81,7 +81,7 @@ describe('File creation', () => { // path checks expect(file.basename).toBe('picture.jpg') expect(file.extension).toBe('.jpg') - expect(file.dirname).toBe('https://domain.com/Photos') + expect(file.dirname).toBe('/Photos') expect(file.root).toBeNull() expect(file.isDavRessource).toBe(false) expect(file.permissions).toBe(Permission.READ) @@ -137,6 +137,18 @@ describe('File data change', () => { expect(file.mtime?.getDate()).toBe(new Date().getDate()) }) + test('Moving a file to an invalid destination throws', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + mtime: new Date(Date.UTC(2023, 0, 1, 0, 0, 0)), + }) + expect(() => { + file.move('ftp://cloud.domain.com/remote.php/dav/files/emma/Pictures/picture-old.jpg') + }).toThrowError('Invalid source format, only http(s) is supported') + }) + test('Moving a file to a different folder with root', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', diff --git a/__tests__/files/folder.spec.ts b/__tests__/files/folder.spec.ts index 893cf7d3f..39069f221 100644 --- a/__tests__/files/folder.spec.ts +++ b/__tests__/files/folder.spec.ts @@ -2,7 +2,7 @@ import { Folder } from '../../lib/files/folder' import { FileType } from '../../lib/files/fileType' import { Permission } from '../../lib/permissions' -describe('File creation', () => { +describe('Folder creation', () => { test('Valid dav folder', () => { const folder = new Folder({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/', @@ -70,7 +70,7 @@ describe('File creation', () => { // path checks expect(folder.basename).toBe('Photos') expect(folder.extension).toBeNull() - expect(folder.dirname).toBe('https://domain.com') + expect(folder.dirname).toBe('/') expect(folder.root).toBeNull() expect(folder.isDavRessource).toBe(false) expect(folder.permissions).toBe(Permission.READ) diff --git a/__tests__/files/node.spec.ts b/__tests__/files/node.spec.ts index 8eabb6935..3c712de07 100644 --- a/__tests__/files/node.spec.ts +++ b/__tests__/files/node.spec.ts @@ -80,8 +80,12 @@ describe('Sanity checks', () => { source: '/remote.php/dav/files/emma/Photos/picture.jpg', mime: 'image/jpeg', owner: 'emma' - })).toThrowError('Invalid source') - + })).toThrowError('Invalid source format, source must be a valid URL') + expect(() => new File({ + source: 'ftp://remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma' + })).toThrowError('Invalid source format, only http(s) is supported') }) test('Invalid mtime', () => { @@ -153,12 +157,27 @@ describe('Sanity checks', () => { owner: 'emma', root: true as unknown as string, })).toThrowError('Invalid root format') + expect(() => new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', mime: 'image/jpeg', owner: 'emma', root: 'https://cloud.domain.com/remote.php/dav/', })).toThrowError('Root must start with a leading slash') + + expect(() => new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + root: '/files/john', + })).toThrowError('Root must be part of the source') + + expect(() => new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + root: '/remote.php/dav/files/emma', + })).toThrowError('The root must be relative to the service. e.g /files/emma') }) }) @@ -213,43 +232,79 @@ describe('Dav service detection', () => { describe('Root and paths detection', () => { test('Unknown root', () => { - const file1 = new File({ + const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', mime: 'image/jpeg', owner: 'emma', }) - expect(file1.root).toBe('/files/emma/Photos') - expect(file1.dirname).toBe('/') + expect(file.root).toBe('/files/emma/Photos') + expect(file.dirname).toBe('/') }) test('Provided root dav service', () => { - const file1 = new File({ + const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', mime: 'image/jpeg', owner: 'emma', root: '/files/emma', }) - expect(file1.root).toBe('/files/emma') - expect(file1.dirname).toBe('/Photos') + expect(file.root).toBe('/files/emma') + expect(file.dirname).toBe('/Photos') }) test('Root with ending slash is removed', () => { - const file1 = new File({ + const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', mime: 'image/jpeg', owner: 'emma', root: '/files/emma/', }) - expect(file1.root).toBe('/files/emma') + expect(file.root).toBe('/files/emma') + expect(file.dirname).toBe('/Photos') + expect(file.path).toBe('/Photos/picture.jpg') }) test('Root and source are the same', () => { - const file1 = new File({ + const folder = new Folder({ source: 'https://cloud.domain.com/remote.php/dav/files/emma', + owner: 'emma', + root: '/files/emma', + }) + expect(folder.root).toBe('/files/emma') + expect(folder.dirname).toBe('/') + expect(folder.path).toBe('/') + }) + + test('Source contains a similar root path', () => { + const folder = new Folder({ + source: 'https://domain.com/remote.php/dav/files/emma/files/emma', + owner: 'emma', + root: '/files/emma', + }) + expect(folder.root).toBe('/files/emma') + expect(folder.dirname).toBe('/files') + expect(folder.path).toBe('/files/emma') + + const file = new File({ + source: 'https://domain.com/remote.php/dav/files/emma/files/emma.jpeg', mime: 'image/jpeg', owner: 'emma', root: '/files/emma', }) - expect(file1.dirname).toBe('/') + expect(file.root).toBe('/files/emma') + expect(file.dirname).toBe('/files') + expect(file.path).toBe('/files/emma.jpeg') + }) + + test('Non dav ressource with undefined root', () => { + const file = new File({ + source: 'https://domain.com/files/images/emma.jpeg', + mime: 'image/jpeg', + owner: 'emma', + }) + expect(file.isDavRessource).toBe(false) + expect(file.root).toBe(null) + expect(file.dirname).toBe('/files/images') + expect(file.path).toBe('/files/images/emma.jpeg') }) }) diff --git a/lib/files/node.ts b/lib/files/node.ts index ef3539235..3ab97d7f0 100644 --- a/lib/files/node.ts +++ b/lib/files/node.ts @@ -22,7 +22,7 @@ import { basename, extname, dirname } from 'path' import { Permission } from '../permissions' import { FileType } from './fileType' -import NodeData, { Attribute, validateData } from './nodeData' +import NodeData, { Attribute, isDavRessource, validateData } from './nodeData' export abstract class Node { @@ -32,7 +32,7 @@ export abstract class Node { constructor(data: NodeData, davService?: RegExp) { // Validate data - validateData(data) + validateData(data, davService || this._knownDavService) this._data = data @@ -88,9 +88,15 @@ export abstract class Node { */ get dirname(): string { if (this.root) { - return dirname(this.source.split(this.root).pop() || '/') + // Using replace would remove all part matching root + const firstMatch = this.source.indexOf(this.root) + return dirname(this.source.slice(firstMatch + this.root.length) || '/') } - return dirname(this.source) + + // This should always be a valid URL + // as this is tested in the constructor + const url = new URL(this.source) + return dirname(url.pathname) } /** @@ -160,7 +166,7 @@ export abstract class Node { * Is this a dav-related ressource ? */ get isDavRessource(): boolean { - return this.source.match(this._knownDavService) !== null + return isDavRessource(this.source, this._knownDavService) } /** @@ -184,7 +190,12 @@ export abstract class Node { /** * Get the absolute path of this object relative to the root */ - get path(): string|null { + get path(): string { + if (this.root) { + // Using replace would remove all part matching root + const firstMatch = this.source.indexOf(this.root) + return this.source.slice(firstMatch + this.root.length) || '/' + } return (this.dirname + '/' + this.basename).replace(/\/\//g, '/') } @@ -202,6 +213,7 @@ export abstract class Node { * e.g. https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg */ move(destination: string) { + validateData({ ...this._data, source: destination }, this._knownDavService) this._data.source = destination this._data.mtime = new Date() } diff --git a/lib/files/nodeData.ts b/lib/files/nodeData.ts index 830da3ba8..cd671d231 100644 --- a/lib/files/nodeData.ts +++ b/lib/files/nodeData.ts @@ -20,6 +20,7 @@ * */ +import { join } from "path" import { Permission } from "../permissions" export interface Attribute { [key: string]: any } @@ -62,11 +63,15 @@ export default interface NodeData { */ root?: string } + +export const isDavRessource = function(source: string, davService: RegExp): boolean { + return source.match(davService) !== null +} /** * Validate Node construct data */ -export const validateData = (data: NodeData) => { +export const validateData = (data: NodeData, davService: RegExp) => { if ('id' in data && (typeof data.id !== 'number' || data.id < 0)) { throw new Error('Invalid id type of value') } @@ -75,8 +80,14 @@ export const validateData = (data: NodeData) => { throw new Error('Missing mandatory source') } + try { + new URL(data.source) + } catch (e) { + throw new Error('Invalid source format, source must be a valid URL') + } + if (!data.source.startsWith('http')) { - throw new Error('Invalid source format') + throw new Error('Invalid source format, only http(s) is supported') } if ('mtime' in data && !(data.mtime instanceof Date)) { @@ -121,4 +132,15 @@ export const validateData = (data: NodeData) => { if (data.root && !data.root.startsWith('/')) { throw new Error('Root must start with a leading slash') } + + if (data.root && !data.source.includes(data.root)) { + throw new Error('Root must be part of the source') + } + + if (data.root && isDavRessource(data.source, davService)) { + const service = data.source.match(davService)![0] + if (!data.source.includes(join(service, data.root))) { + throw new Error('The root must be relative to the service. e.g /files/emma') + } + } }