Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update check itemName #1748

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

strategy:
matrix:
node-version: [16.x, 18.x]
node-version: [18.x]
os: [ubuntu-latest]

steps:
Expand Down
9 changes: 8 additions & 1 deletion lib/handlers/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ async function patchHandler (req, res, next) {
({ path, contentType } = await ldp.resourceMapper.mapUrlToFile(
{ url: req, createIfNotExists: true, contentType: contentTypeForNew(req) }))
// check if a folder with same name exists
await ldp.checkItemName(req)
try {
await ldp.checkItemName(req)
} catch (err) {
return next(err)
}
resourceExists = false
}
const { url } = await ldp.resourceMapper.mapFileToUrl({ path, hostname: req.hostname })
Expand All @@ -65,6 +69,9 @@ async function patchHandler (req, res, next) {
patch.text = req.body ? req.body.toString() : ''
patch.uri = `${url}#patch-${hash(patch.text)}`
patch.contentType = getContentType(req.headers)
if (!patch.contentType) {
throw error(400, 'PATCH request requires a content-type via the Content-Type header')
}
debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType)
const parsePatch = PATCH_PARSERS[patch.contentType]
if (!parsePatch) {
Expand Down
51 changes: 46 additions & 5 deletions lib/handlers/put.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,17 @@ const { stringToStream } = require('../utils')
async function handler (req, res, next) {
debug(req.originalUrl)
res.header('MS-Author-Via', 'SPARQL')

const contentType = req.get('content-type')

// check whether a folder or resource with same name exists
try {
const ldp = req.app.locals.ldp
await ldp.checkItemName(req)
} catch (e) {
return next(e)
}
// check for valid rdf content for auxiliary resource and /profile/card
// in future we may check that /profile/card is a minimal valid WebID card
// TODO check that /profile/card is a minimal valid WebID card
if (isAuxiliary(req) || req.originalUrl === '/profile/card') {
if (contentType === 'text/turtle') {
return bodyParser.text({ type: () => true })(req, res, () => putValidRdf(req, res, next))
Expand All @@ -21,17 +28,51 @@ async function handler (req, res, next) {
return putStream(req, res, next)
}

// Verifies whether the user is allowed to perform Append PUT on the target
async function checkPermission (request, resourceExists) {
// If no ACL object was passed down, assume permissions are okay.
if (!request.acl) return Promise.resolve()
// At this point, we already assume append access,
// we might need to perform additional checks.
let modes = []
// acl:default Write is required for PUT when Resource Exists
if (resourceExists) modes = ['Write']
// if (resourceExists && request.originalUrl.endsWith('.acl')) modes = ['Control']
const { acl, session: { userId } } = request

const allowed = await Promise.all(modes.map(mode => acl.can(userId, mode, request.method, resourceExists)))
const allAllowed = allowed.reduce((memo, allowed) => memo && allowed, true)
if (!allAllowed) {
// check owner with Control
// const ldp = request.app.locals.ldp
// if (request.path.endsWith('.acl') && userId === await ldp.getOwner(request.hostname)) return Promise.resolve()

const errors = await Promise.all(modes.map(mode => acl.getError(userId, mode)))
const error = errors.filter(error => !!error)
.reduce((prevErr, err) => prevErr.status > err.status ? prevErr : err, { status: 0 })
return Promise.reject(error)
}
return Promise.resolve()
}

// TODO could be renamed as putResource (it now covers container and non-container)
async function putStream (req, res, next, stream = req) {
const ldp = req.app.locals.ldp
// try {
// Obtain details of the target resource
let resourceExists = true
try {
// First check if the file already exists
await ldp.resourceMapper.mapUrlToFile({ url: req })
} catch (err) {
resourceExists = false
}
try {
debug('test ' + req.get('content-type') + getContentType(req.headers))
if (!req.originalUrl.endsWith('.acl')) await checkPermission(req, resourceExists)
await ldp.put(req, stream, getContentType(req.headers))
debug('succeded putting the file/folder')
res.sendStatus(201)
return next()
} catch (err) {
debug('error putting the file/folder:' + err.message)
err.message = 'Can\'t write file/folder: ' + err.message
return next(err)
}
Expand Down
2 changes: 1 addition & 1 deletion lib/ldp-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function LdpMiddleware (corsSettings) {
router.get('/*', index, allow('Read'), header.addPermissions, get)
router.post('/*', allow('Append'), post)
router.patch('/*', allow('Append'), patch)
router.put('/*', allow('Write'), put)
router.put('/*', allow('Append'), put)
router.delete('/*', allow('Write'), del)

return router
Expand Down
68 changes: 25 additions & 43 deletions lib/ldp.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,37 +157,13 @@ class LDP {
if (container) {
extension = ''
}
// Check for file or folder with same name
let testName, fileName
try {
// check for defaulted slug in NSS POST (slug with extension)
fileName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension
fileName = container ? fileName : fileName + '/'
const { url: testUrl } = await this.resourceMapper.mapFileToUrl({ hostname, path: containerPath + fileName })
const { path: testPath } = await this.resourceMapper.mapUrlToFile({ url: testUrl })
testName = container ? fs.lstatSync(testPath).isFile() : fs.lstatSync(testPath).isDirectory()
} catch (err) { testName = false }

if (testName) {
throw error(404, 'Container and resource cannot have the same name in URI')
}

// TODO: possibly package this in ldp.post
let resourceUrl = await ldp.getAvailableUrl(hostname, containerPath, { slug, extension })
// always return a valid URL.
const resourceUrl = await ldp.getAvailableUrl(hostname, containerPath, { slug, extension, container })
debug.handlers('POST -- Will create at: ' + resourceUrl)
let originalUrl = resourceUrl

if (container) {
// Create directory by an LDP PUT to the container's .meta resource
resourceUrl = `${resourceUrl}${resourceUrl.endsWith('/') ? '' : '/'}` // ${ldp.suffixMeta}`
if (originalUrl && !originalUrl.endsWith('/')) {
originalUrl += '/'
}
}
// const { url: putUrl } = await this.resourceMapper.mapFileToUrl({ path: resourceUrl, hostname })

await ldp.put(resourceUrl, stream, contentType)
return URL.parse(originalUrl).path
return URL.parse(resourceUrl).path
}

isAuxResource (slug, extension) {
Expand Down Expand Up @@ -249,10 +225,6 @@ class LDP {
throw error(400, 'Resource with a $.ext is not allowed by the server')
}

// check if a folder or file with same name exists
// const urlItem = url.url || url
await this.checkItemName(url)

// First check if we are above quota
let isOverQuota
// Someone had a reason to make url actually a req sometimes but not
Expand Down Expand Up @@ -355,8 +327,9 @@ class LDP {
} catch (err) { }
}

// check whether a document (or container) has the same name as another document (or container)
async checkItemName (url) {
let testName
let testName, testPath
const { hostname, pathname } = this.resourceMapper._parseUrl(url) // (url.url || url)
let itemUrl = this.resourceMapper.resolveUrl(hostname, pathname)
const container = itemUrl.endsWith('/')
Expand All @@ -373,11 +346,11 @@ class LDP {
const { pathname } = this.resourceMapper._parseUrl(itemUrl) // (url.url || url)
// check not at root
if (pathname !== '/') {
await this.checkItemName(itemUrl)
return await this.checkItemName(itemUrl)
}
}
if (testName) {
throw error(404, 'Container and resource cannot have the same name in URI')
throw error(409, `${testPath}: Container and resource cannot have the same name in URI`)
}
}

Expand Down Expand Up @@ -606,17 +579,26 @@ class LDP {
}
}

async getAvailableUrl (hostname, containerURI, { slug = uuid.v1(), extension }) {
async getAvailableUrl (hostname, containerURI, { slug = uuid.v1(), extension, container }) {
let requestUrl = this.resourceMapper.resolveUrl(hostname, containerURI)
requestUrl = requestUrl.replace(/\/*$/, '/')
requestUrl = requestUrl.replace(/\/*$/, '/') // ??? what for

const { path: containerFilePath } = await this.resourceMapper.mapUrlToFile({ url: requestUrl })
let fileName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension
if (await promisify(fs.exists)(utilPath.join(containerFilePath, fileName))) {
fileName = `${uuid.v1()}-${fileName}`
}

return requestUrl + fileName
let itemName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension
try {
// check whether resource exists
const context = container ? '/' : ''
await this.resourceMapper.mapUrlToFile({ url: (requestUrl + itemName + context) })
itemName = `${uuid.v1()}-${itemName}`
} catch (e) {
try {
// check whether resource with same name exists
const context = !container ? '/' : ''
await this.resourceMapper.mapUrlToFile({ url: (requestUrl + itemName + context) })
itemName = `${uuid.v1()}-${itemName}`
} catch (e) {}
}
if (container) itemName += '/'
return requestUrl + itemName
}

getTrustedOrigins (req) {
Expand Down
Loading
Loading