Skip to content

Commit

Permalink
enhancement: The abe type 'file' is now usable. + we can configure au…
Browse files Browse the repository at this point in the history
…thorized extensions and mimetypes in abe.json + fix big security hole on file save
  • Loading branch information
gregorybesson committed Dec 20, 2016
1 parent f56d735 commit 9b6507f
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 40 deletions.
19 changes: 15 additions & 4 deletions src/cli/cms/editor/handlebars/printInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,20 @@ export function createInputRich(attributes, inputClass, params) {
}

export function createInputFile(attributes, inputClass, params) {
return `<input class="form-control" ${attributes} name="${params.key}" type="file" />
<span class="percent"></span>
<input type="text" ${attributes} class="${inputClass} hidden" />`

return `<div class="input-group file-upload">
<div class="input-group-addon image">
<span class="glyphicon glyphicon-file" aria-hidden="true"></span>
</div>
<input type="text" ${attributes} class="${inputClass} file-input" />
<div class="upload-wrapper">
<input class="form-control" ${attributes} name="${params.key}" type="file" title="upload a file"/>
<span class="percent">
<span class="glyphicon glyphicon-upload" aria-hidden="true"></span>
</span>
</div>
</div>
<div class="input-error"></div>`
}

export function createInputTextarea(attributes, inputClass, params) {
Expand All @@ -157,7 +168,7 @@ export function createInputImage(attributes, inputClass, params) {
<div class="input-group-addon image">
<span class="glyphicon glyphicon-picture" aria-hidden="true"></span>
</div>
<input type="text" ${attributes} class="${inputClass} image-input" />
<input type="text" ${attributes} class="${inputClass} file-input" />
<div class="upload-wrapper">
<input class="form-control" ${attributes} name="${params.key}" type="file" title="upload an image"/>
<span class="percent">
Expand Down
80 changes: 52 additions & 28 deletions src/cli/cms/media/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,47 +88,56 @@ export function generateThumbnail(file) {

export function saveFile(req) {
var p = new Promise((resolve) => {
var folderFilePath = createMediaFolder(req)
var resp = {success: 1}
var filePath
req.pipe(req.busboy)
req.busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
var ext = path.extname(filename).toLowerCase()
var slug = createMediaSlug(filename, ext)
var mediaType = getMediaType(ext)

var folderFilePath = createMediaFolder(mediaType)
var hasSentHeader = false

filePath = path.join(folderFilePath, slug)
resp['filePath'] = path.join('/' + config.upload.image, slug)
resp['filePath'] = path.join('/' + mediaType, slug)

var returnErr = function (msg) {
file.on('limit', function() {
hasSentHeader = true
file.resume()
resolve({error: 1, response: msg})
}

file.on('limit', function() {
returnErr('file is too big')
resolve({error: 1, response: 'file is too big'})
return
})

var isValid = isValidMedia(mimetype, ext)
if(isValid.error) returnErr(isValid.error)
if(isValid.error){
hasSentHeader = true
file.resume()
resolve({error: 1, response: isValid.error})
return
}

var fstream = fse.createWriteStream(filePath)
fstream.on('finish', function() {
if(hasSentHeader) return
if(/\.(jpg|png|gif|svg)/.test(filePath)) resp = abeExtend.hooks.instance.trigger('afterSaveImage', resp, req)

var thumbPromise = generateThumbnail(filePath)
thumbPromise.then(function (thumbResp) {
resp.thumbnail = thumbResp.thumb
if(req.query.input.indexOf('data-size') > -1){
var thumbsSizes = cmsData.regex.getAttr(req.query.input, 'data-size').split(',')
cropAndSaveFiles(thumbsSizes, filePath, resp).then(function (resp) {
resolve(resp)
})
}
else resolve(resp)
})

resp = abeExtend.hooks.instance.trigger('afterSaveImage', resp, req)

if(mediaType === 'image') {
var thumbPromise = generateThumbnail(filePath)
thumbPromise.then(function (thumbResp) {
resp.thumbnail = thumbResp.thumb
if(req.query.input.indexOf('data-size') > -1){
var thumbsSizes = cmsData.regex.getAttr(req.query.input, 'data-size').split(',')
cropAndSaveFiles(thumbsSizes, filePath, resp).then(function (resp) {
resolve(resp)
})
}
else resolve(resp)
})
} else {
resolve(resp)
}
})
file.pipe(fstream)
})
Expand All @@ -138,23 +147,38 @@ export function saveFile(req) {
}

export function isValidMedia(mimetype, ext) {
var allowedExtensions = ['.gif', '.jpg', '.jpeg', '.png', '.svg', '.mp4']
var allowedMimetype = ['image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', 'video/mp4']
var allowedExtensions = config.upload.extensions
var allowedMimetypes = config.upload.mimetypes

var error = false
if (allowedMimetype.indexOf(mimetype) < 0) error = 'unauthorized file'
if (allowedMimetypes.indexOf(mimetype) < 0) error = 'unauthorized file'
else if (allowedExtensions.indexOf(ext) < 0) error = 'not a valid asset'

return {error: error}
}

export function getMediaType(ext) {
let type = 'document'

if(/\.(jpg|jpeg|png|gif|svg)/.test(ext)){
type = 'image'
} else if(/\.(mov|avi|mp4)/.test(ext)) {
type = 'video'
} else if(/\.(mp3|wav)/.test(ext)){
type = 'sound'
}

return type
}

export function createMediaSlug(filename, ext) {
var filenameNoExt = path.basename(filename, ext)
return limax(filenameNoExt, {separateNumbers: false}) + '-' + coreUtils.random.generateUniqueIdentifier(2) + ext
}

export function createMediaFolder(req) {
var folderWebPath = '/' + config.upload.image
folderWebPath = abeExtend.hooks.instance.trigger('beforeSaveImage', folderWebPath, req)
export function createMediaFolder(mediaType) {
var folderWebPath = '/' + mediaType
folderWebPath = abeExtend.hooks.instance.trigger('beforeSaveImage', folderWebPath)
var folderFilePath = path.join(config.root, config.publish.url, folderWebPath)
mkdirp.sync(folderFilePath)

Expand Down
7 changes: 6 additions & 1 deletion src/cli/core/config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
},
"upload": {
"image": "image",
"fileSizelimit": 10485760
"video": "video",
"document": "document",
"document": "sound",
"fileSizelimit": 10485760,
"extensions":[".gif", ".jpg", ".jpeg", ".png", ".svg", ".mp4"],
"mimetypes": ["image/gif", "image/jpeg", "image/png", "image/svg+xml", "video/mp4"]
},
"data": {
"url": "data"
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ var hooks = {
afterSave: (obj, abe) => {
return obj
},
beforeSaveImage: (folderWebPath, req, abe) => {
beforeSaveImage: (folderWebPath, abe) => {
return folderWebPath
},
afterSaveImage: (resp, abe) => {
Expand Down
8 changes: 5 additions & 3 deletions src/server/public/abejs/scripts/modules/EditorFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export default class EditorFiles {
}

rebind() {
let files = [].slice.call(document.querySelectorAll('.img-upload input[type="file"]'))
let files = [].slice.call(document.querySelectorAll(
'.img-upload input[type="file"], .file-upload input[type="file"]')
)

Array.prototype.forEach.call(files, (file) => {
file.removeEventListener('change', this._handleChangeFiles)
Expand Down Expand Up @@ -59,7 +61,7 @@ export default class EditorFiles {
percent.innerHTML = percentHtml
return
}
var input = parentTarget.querySelector('input.image-input')
var input = parentTarget.querySelector('input.file-input')
input.value = resp.filePath
if(resp.thumbs){
var parent = input.parentNode
Expand All @@ -72,7 +74,7 @@ export default class EditorFiles {
var inputThumbs = document.createElement('input')
inputThumbs.classList.add('form-control')
inputThumbs.classList.add('form-abe')
inputThumbs.classList.add('image-input')
inputThumbs.classList.add('file-input')
inputThumbs.id = thumdID
inputThumbs.setAttribute('data-id', thumdID)
inputThumbs.value = thumb.name
Expand Down
4 changes: 2 additions & 2 deletions src/server/public/abejs/scripts/utils/img-picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ window.ImagePickerUpload = {
<div class="input-group-addon image">
<span class="glyphicon glyphicon-picture" aria-hidden="true"></span>
</div>
<input type="text" id="wysiwyg-image-text" name="wysiwyg-image-text" placeholder="image or video (.mp4)" value="" class="form-control form-abe image-input">
<input type="text" id="wysiwyg-image-text" name="wysiwyg-image-text" placeholder="image or video (.mp4)" value="" class="form-control form-abe file-input">
<div class="upload-wrapper">
<input class="form-control" id="wysiwyg-image-upload" name="wysiwyg-image-upload" value="" type="file" title="upload an image">
<span class="percent">
Expand Down Expand Up @@ -61,7 +61,7 @@ window.ImagePickerUpload = {
percent.innerHTML = percentHtml
return
}
var input = parentTarget.querySelector('input.image-input')
var input = parentTarget.querySelector('input.file-input')
input.value = resp.filePath
input.focus()
input.blur()
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe('Abe', function() {
.assert.title('Abe')
.click("//div[@class='btns']/button[3]")
.pause(2000)
//.assert.containsText("//div[@class='display-status']/span", "publish")
//.assert.containsText("//div[@class='display-status']/span", "publish") // don't pass in Travis
.url('http://localhost:3003/abe/editor')
.waitForElementVisible('//body')
.assert.cssClassPresent("//table[@id='navigation-list']/tbody/tr[1]/td[6]/a", "label-published");
Expand Down

0 comments on commit 9b6507f

Please sign in to comment.