Support project-linked-files originating from imported v1 projects

This commit is contained in:
Shane Kilkelly 2018-06-01 15:16:27 +01:00
parent baa321fa1a
commit 3baf0836bc
10 changed files with 200 additions and 51 deletions

View file

@ -1,7 +1,9 @@
AuthenticationController = require '../Authentication/AuthenticationController'
EditorController = require '../Editor/EditorController'
ProjectLocator = require '../Project/ProjectLocator'
Settings = require 'settings-sharelatex'
logger = require 'logger-sharelatex'
_ = require 'underscore'
module.exports = LinkedFilesController = {
Agents: {
@ -9,31 +11,91 @@ module.exports = LinkedFilesController = {
project_file: require('./ProjectFileAgent')
}
_getAgent: (provider) ->
if !LinkedFilesController.Agents.hasOwnProperty(provider)
return null
unless provider in Settings.enabledLinkedFileTypes
return null
LinkedFilesController.Agents[provider]
_getFileById: (project_id, file_id, callback=(err, file)->) ->
ProjectLocator.findElement {
project_id,
element_id: file_id,
type: 'file'
}, (err, file, path, parentFolder) ->
return callback(err) if err?
callback(null, file, path, parentFolder)
createLinkedFile: (req, res, next) ->
{project_id} = req.params
{name, provider, data, parent_folder_id} = req.body
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {project_id, name, provider, data, parent_folder_id, user_id}, 'create linked file request'
if !LinkedFilesController.Agents.hasOwnProperty(provider)
return res.send(400)
unless provider in Settings.enabledLinkedFileTypes
return res.send(400)
Agent = LinkedFilesController.Agents[provider]
Agent = LinkedFilesController._getAgent(provider)
if !Agent?
return res.sendStatus(400)
linkedFileData = Agent.sanitizeData(data)
linkedFileData.provider = provider
if !Agent.canCreate(linkedFileData)
return res.status(403).send('Cannot create linked file')
LinkedFilesController._doImport(
req, res, next, Agent, project_id, user_id,
parent_folder_id, name, linkedFileData
)
refreshLinkedFile: (req, res, next) ->
{project_id, file_id} = req.params
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {project_id, file_id, user_id}, 'refresh linked file request'
LinkedFilesController._getFileById project_id, file_id, (err, file, path, parentFolder) ->
return next(err) if err?
return res.sendStatus(404) if !file?
name = file.name
linkedFileData = file.linkedFileData
if !linkedFileData? || !linkedFileData?.provider?
return res.send(409)
provider = linkedFileData.provider
parent_folder_id = parentFolder._id
Agent = LinkedFilesController._getAgent(provider)
if !Agent?
return res.sendStatus(400)
LinkedFilesController._doImport(
req, res, next, Agent, project_id, user_id,
parent_folder_id, name, linkedFileData
)
_doImport: (req, res, next, Agent, project_id, user_id, parent_folder_id, name, linkedFileData) ->
Agent.checkAuth project_id, linkedFileData, user_id, (err, allowed) ->
return Agent.handleError(err, req, res, next) if err?
return res.sendStatus(403) if !allowed
Agent.decorateLinkedFileData linkedFileData, (err, newLinkedFileData) ->
return Agent.handleError(err) if err?
linkedFileData = newLinkedFileData
Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) ->
if error?
logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk'
return Agent.handleError(error, req, res, next)
EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error, file) ->
return next(error) if error?
res.json(new_file_id: file._id) # created
Agent.writeIncomingFileToDisk project_id,
linkedFileData,
user_id,
(error, fsPath) ->
if error?
logger.error(
{err: error, project_id, name, linkedFileData, parent_folder_id, user_id},
'error writing linked file to disk'
)
return Agent.handleError(error, req, res, next)
EditorController.upsertFile project_id,
parent_folder_id,
name,
fsPath,
linkedFileData,
"upload",
user_id,
(error, file) ->
return next(error) if error?
res.json(new_file_id: file._id) # created
}

View file

@ -9,3 +9,7 @@ module.exports =
AuthorizationMiddlewear.ensureUserCanWriteProjectContent,
LinkedFilesController.createLinkedFile
webRouter.post '/project/:project_id/linked_file/:file_id/refresh',
AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanWriteProjectContent,
LinkedFilesController.refreshLinkedFile

View file

@ -2,6 +2,7 @@ FileWriter = require('../../infrastructure/FileWriter')
AuthorizationManager = require('../Authorization/AuthorizationManager')
ProjectLocator = require('../Project/ProjectLocator')
ProjectGetter = require('../Project/ProjectGetter')
Project = require("../../models/Project").Project
DocstoreManager = require('../Docstore/DocstoreManager')
FileStoreHandler = require('../FileStore/FileStoreHandler')
FileWriter = require('../../infrastructure/FileWriter')
@ -41,9 +42,17 @@ ProjectNotFoundError = (message) ->
ProjectNotFoundError.prototype.__proto__ = Error.prototype
V1ProjectNotFoundError = (message) ->
error = new Error(message)
error.name = 'V1ProjectNotFound'
error.__proto__ = V1ProjectNotFoundError.prototype
return error
V1ProjectNotFoundError.prototype.__proto__ = Error.prototype
SourceFileNotFoundError = (message) ->
error = new Error(message)
error.name = 'BadData'
error.name = 'SourceFileNotFound'
error.__proto__ = SourceFileNotFoundError.prototype
return error
SourceFileNotFoundError.prototype.__proto__ = Error.prototype
@ -55,48 +64,71 @@ module.exports = ProjectFileAgent =
return _.pick(
data,
'source_project_id',
'v1_source_doc_id',
'source_entity_path'
)
_validate: (data) ->
return (
data.source_project_id? &&
(data.source_project_id? || data.v1_source_doc_id?) &&
data.source_entity_path?
)
canCreate: (data) ->
# Don't allow creation of linked-files with v1 doc ids
!data.v1_source_doc_id?
_getSourceProject: (data, callback=(err, project)->) ->
projection = {_id: 1, name: 1}
if data.v1_source_doc_id?
Project.findOne {'overleaf.id': data.v1_source_doc_id}, projection, (err, project) ->
return callback(err) if err?
if !project?
return callback(new V1ProjectNotFoundError())
callback(null, project)
else if data.source_project_id?
ProjectGetter.getProject data.source_project_id, projection, (err, project) ->
return callback(err) if err?
if !project?
return callback(new ProjectNotFoundError())
callback(null, project)
else
callback(new BadDataError('neither v1 nor v2 id present'))
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
callback = _.once(callback)
{ source_project_id } = data
return callback(new BadDataError()) if !source_project_id?
ProjectGetter.getProject source_project_id, (err, project) ->
@_getSourceProject data, (err, project) ->
return callback(err) if err?
return callback(new ProjectNotFoundError()) if !project?
callback(err, _.extend(data, {source_project_display_name: project.name}))
checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
callback = _.once(callback)
if !ProjectFileAgent._validate(data)
return callback(new BadDataError())
{source_project_id, source_entity_path} = data
AuthorizationManager.canUserReadProject current_user_id, source_project_id, null, (err, canRead) ->
@_getSourceProject data, (err, project) ->
return callback(err) if err?
callback(null, canRead)
AuthorizationManager.canUserReadProject current_user_id, project._id, null, (err, canRead) ->
return callback(err) if err?
callback(null, canRead)
writeIncomingFileToDisk:
(project_id, data, current_user_id, callback = (error, fsPath) ->) ->
callback = _.once(callback)
if !ProjectFileAgent._validate(data)
return callback(new BadDataError())
{source_project_id, source_entity_path} = data
ProjectLocator.findElementByPath {
project_id: source_project_id,
path: source_entity_path
}, (err, entity, type) ->
if err?
if err.toString().match(/^not found.*/)
err = new SourceFileNotFoundError()
return callback(err)
ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback
{ source_entity_path } = data
@_getSourceProject data, (err, project) ->
return callback(err) if err?
source_project_id = project._id
ProjectLocator.findElementByPath {
project_id: source_project_id,
path: source_entity_path
}, (err, entity, type) ->
if err?
if err.toString().match(/^not found.*/)
err = new SourceFileNotFoundError()
return callback(err)
ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback
_writeEntityToDisk: (project_id, entity_id, type, callback=(err, location)->) ->
callback = _.once(callback)
@ -122,6 +154,8 @@ module.exports = ProjectFileAgent =
res.status(404).send("Source file not found")
else if error instanceof ProjectNotFoundError
res.status(404).send("Project not found")
else if error instanceof V1ProjectNotFoundError
res.status(409).send("Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file")
else
next(error)
next()

View file

@ -27,6 +27,8 @@ module.exports = UrlAgent = {
url: @._prependHttpIfNeeded(data.url)
}
canCreate: (data) -> true
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
return callback(null, data)

View file

@ -1,5 +1,6 @@
_ = require("underscore")
module.exports = ProjectEditorHandler =
trackChangesAvailable: false

View file

@ -52,7 +52,10 @@ div.binary-file.full-size(
i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon
| Imported from
|
a(ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank")
a(ng-if='!openFile.linkedFileData.v1_source_doc_id'
ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank")
| {{ openFile.linkedFileData.source_project_display_name }}
span(ng-if='openFile.linkedFileData.v1_source_doc_id')
| {{ openFile.linkedFileData.source_project_display_name }}
| /{{ openFile.linkedFileData.source_entity_path.slice(1) }},
|

View file

@ -390,14 +390,11 @@ define [
refreshLinkedFile: (file) ->
parent_folder = @_findParentFolder(file)
data = file.linkedFileData
provider = data?.provider
return if !provider?
return @ide.$http.post "/project/#{@ide.project_id}/linked_file", {
name: file.name,
parent_folder_id: parent_folder?.id
provider,
data,
provider = file.linkedFileData?.provider
if !provider?
console.warn ">> no provider for #{file.name}", file
return
return @ide.$http.post "/project/#{@ide.project_id}/linked_file/#{file.id}/refresh", {
_csrf: window.csrfToken
}

View file

@ -94,7 +94,6 @@ describe "LinkedFiles", ->
}
done()
it 'should import a file from the source project', (done) ->
@owner.request.post {
url: "/project/#{@project_one_id}/linked_file",
@ -124,18 +123,13 @@ describe "LinkedFiles", ->
it 'should refresh the file', (done) ->
@owner.request.post {
url: "/project/#{@project_one_id}/linked_file",
json:
name: 'test-link.txt',
parent_folder_id: @project_one_root_folder_id,
provider: 'project_file',
data:
source_project_id: @project_two_id,
source_entity_path: "/#{@source_doc_name}",
url: "/project/#{@project_one_id}/linked_file/#{@existing_file_id}/refresh",
json: true
}, (error, response, body) =>
new_file_id = body.new_file_id
expect(new_file_id).to.exist
expect(new_file_id).to.not.equal @existing_file_id
@refreshed_file_id = new_file_id
@owner.getProject @project_one_id, (error, project) =>
return done(error) if error?
firstFile = project.rootFolder[0].fileRefs[0]
@ -143,6 +137,55 @@ describe "LinkedFiles", ->
expect(firstFile.name).to.equal('test-link.txt')
done()
it 'should not allow to create a linked-file with v1 id', (done) ->
@owner.request.post {
url: "/project/#{@project_one_id}/linked_file",
json:
name: 'test-link-should-not-work.txt',
parent_folder_id: @project_one_root_folder_id,
provider: 'project_file',
data:
v1_source_doc_id: 1234
source_entity_path: "/#{@source_doc_name}",
}, (error, response, body) =>
expect(response.statusCode).to.equal 403
expect(body).to.equal 'Cannot create linked file'
done()
describe "with a linked project_file from a v1 project that has not been imported", ->
before (done) ->
async.series [
(cb) =>
@owner.createProject 'plf-v1-test-one', {template: 'blank'}, (error, project_id) =>
@project_one_id = project_id
cb(error)
(cb) =>
@owner.getProject @project_one_id, (error, project) =>
@project_one = project
@project_one_root_folder_id = project.rootFolder[0]._id.toString()
@project_one.rootFolder[0].fileRefs.push {
linkedFileData: {
provider: "project_file",
v1_source_doc_id: 9999999, # We won't find this id in the database
source_entity_path: "example.jpeg"
},
_id: "abcd",
rev: 0,
created: new Date(),
name: "example.jpeg"
}
@owner.saveProject @project_one, cb
], done
it 'should refuse to refresh', (done) ->
@owner.request.post {
url: "/project/#{@project_one_id}/linked_file/abcd/refresh",
json: true
}, (error, response, body) =>
expect(response.statusCode).to.equal 409
expect(body).to.equal "Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file"
done()
describe "creating a URL based linked file", ->
before (done) ->
@owner.createProject "url-linked-files-project", {template: "blank"}, (error, project_id) =>

View file

@ -145,6 +145,9 @@ class User
getProject: (project_id, callback = (error, project)->) ->
db.projects.findOne {_id: ObjectId(project_id.toString())}, callback
saveProject: (project, callback=(error)->) ->
db.projects.update {_id: project._id}, project, callback
createProject: (name, options, callback = (error, oroject_id) ->) ->
if typeof options == "function"
callback = options

View file

@ -8,11 +8,11 @@ describe "EditorRealTimeController", ->
@rclient =
publish: sinon.stub()
@EditorRealTimeController = SandboxedModule.require modulePath, requires:
"../../infrastructure/RedisWrapper":
"../../infrastructure/RedisWrapper":
client: () => @rclient
"../../infrastructure/Server" : io: @io = {}
"settings-sharelatex":{redis:{}}
@room_id = "room-id"
@message = "message-to-editor"
@payload = ["argument one", 42]