Fixed tons of XSS issues and escaping problems

pull/403/head
Tobias Reich 9 years ago
parent 9b833f89d1
commit 2e96f089a7

2
dist/main.css vendored

File diff suppressed because one or more lines are too long

12
dist/main.js vendored

File diff suppressed because one or more lines are too long

8
dist/view.js vendored

File diff suppressed because one or more lines are too long

@ -483,9 +483,6 @@ class Album extends Module {
# Call plugins # Call plugins
$this->plugins(__METHOD__, 0, func_get_args()); $this->plugins(__METHOD__, 0, func_get_args());
# Parse
if (strlen($title)>100) $title = substr($title, 0, 100);
# Execute query # Execute query
$query = Database::prepare($this->database, "UPDATE ? SET title = '?' WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $title, $this->albumIDs)); $query = Database::prepare($this->database, "UPDATE ? SET title = '?' WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $title, $this->albumIDs));
$result = $this->database->query($query); $result = $this->database->query($query);
@ -509,10 +506,6 @@ class Album extends Module {
# Call plugins # Call plugins
$this->plugins(__METHOD__, 0, func_get_args()); $this->plugins(__METHOD__, 0, func_get_args());
# Parse
$description = htmlentities($description, ENT_COMPAT | ENT_HTML401, 'UTF-8');
if (strlen($description)>1000) $description = substr($description, 0, 1000);
# Execute query # Execute query
$query = Database::prepare($this->database, "UPDATE ? SET description = '?' WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $description, $this->albumIDs)); $query = Database::prepare($this->database, "UPDATE ? SET description = '?' WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $description, $this->albumIDs));
$result = $this->database->query($query); $result = $this->database->query($query);

@ -896,9 +896,6 @@ class Photo extends Module {
# Call plugins # Call plugins
$this->plugins(__METHOD__, 0, func_get_args()); $this->plugins(__METHOD__, 0, func_get_args());
# Parse
if (strlen($title)>100) $title = substr($title, 0, 100);
# Set title # Set title
$query = Database::prepare($this->database, "UPDATE ? SET title = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $title, $this->photoIDs)); $query = Database::prepare($this->database, "UPDATE ? SET title = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $title, $this->photoIDs));
$result = $this->database->query($query); $result = $this->database->query($query);
@ -929,10 +926,6 @@ class Photo extends Module {
# Call plugins # Call plugins
$this->plugins(__METHOD__, 0, func_get_args()); $this->plugins(__METHOD__, 0, func_get_args());
# Parse
$description = htmlentities($description, ENT_COMPAT | ENT_HTML401, 'UTF-8');
if (strlen($description)>1000) $description = substr($description, 0, 1000);
# Set description # Set description
$query = Database::prepare($this->database, "UPDATE ? SET description = '?' WHERE id IN ('?')", array(LYCHEE_TABLE_PHOTOS, $description, $this->photoIDs)); $query = Database::prepare($this->database, "UPDATE ? SET description = '?' WHERE id IN ('?')", array(LYCHEE_TABLE_PHOTOS, $description, $this->photoIDs));
$result = $this->database->query($query); $result = $this->database->query($query);
@ -1122,10 +1115,6 @@ class Photo extends Module {
# Parse tags # Parse tags
$tags = preg_replace('/(\ ,\ )|(\ ,)|(,\ )|(,{1,}\ {0,})|(,$|^,)/', ',', $tags); $tags = preg_replace('/(\ ,\ )|(\ ,)|(,\ )|(,{1,}\ {0,})|(,$|^,)/', ',', $tags);
$tags = preg_replace('/,$|^,|(\ ){0,}$/', '', $tags); $tags = preg_replace('/,$|^,|(\ ){0,}$/', '', $tags);
if (strlen($tags)>1000) {
Log::notice($this->database, __METHOD__, __LINE__, 'Length of tags higher than 1000');
return false;
}
# Set tags # Set tags
$query = Database::prepare($this->database, "UPDATE ? SET tags = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $tags, $this->photoIDs)); $query = Database::prepare($this->database, "UPDATE ? SET tags = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $tags, $this->photoIDs));

@ -40,9 +40,9 @@ gulp.task('view--js', function() {
var stream = var stream =
gulp.src(paths.view.js) gulp.src(paths.view.js)
.pipe(plugins.babel())
.on('error', catchError)
.pipe(plugins.concat('_view--javascript.js', {newLine: "\n"})) .pipe(plugins.concat('_view--javascript.js', {newLine: "\n"}))
.pipe(plugins.babel({ compact: true }))
.on('error', catchError)
.pipe(gulp.dest('../dist/')); .pipe(gulp.dest('../dist/'));
return stream; return stream;
@ -109,9 +109,9 @@ gulp.task('main--js', function() {
var stream = var stream =
gulp.src(paths.main.js) gulp.src(paths.main.js)
.pipe(plugins.babel())
.on('error', catchError)
.pipe(plugins.concat('_main--javascript.js', {newLine: "\n"})) .pipe(plugins.concat('_main--javascript.js', {newLine: "\n"}))
.pipe(plugins.babel({ compact: true }))
.on('error', catchError)
.pipe(gulp.dest('../dist/')); .pipe(gulp.dest('../dist/'));
return stream; return stream;

@ -204,14 +204,14 @@ album.delete = function(albumIDs) {
if (album.json) albumTitle = album.json.title if (album.json) albumTitle = album.json.title
else if (albums.json) albumTitle = albums.getByID(albumIDs).title else if (albums.json) albumTitle = albums.getByID(albumIDs).title
msg = `<p>Are you sure you want to delete the album ${ albumTitle } and all of the photos it contains? This action can't be undone!</p>` msg = lychee.html`<p>Are you sure you want to delete the album '$${ albumTitle }' and all of the photos it contains? This action can't be undone!</p>`
} else { } else {
action.title = 'Delete Albums and Photos' action.title = 'Delete Albums and Photos'
cancel.title = 'Keep Albums' cancel.title = 'Keep Albums'
msg = `<p>Are you sure you want to delete all ${ albumIDs.length } selected albums and all of the photos they contain? This action can't be undone!</p>` msg = lychee.html`<p>Are you sure you want to delete all $${ albumIDs.length } selected albums and all of the photos they contain? This action can't be undone!</p>`
} }
@ -292,10 +292,10 @@ album.setTitle = function(albumIDs) {
} }
let input = `<input class='text' name='title' type='text' maxlength='50' placeholder='Title' value='${ lychee.escapeHTML(oldTitle) }'>` let input = lychee.html`<input class='text' name='title' type='text' maxlength='50' placeholder='Title' value='$${ oldTitle }'>`
if (albumIDs.length===1) msg = `<p>Enter a new title for this album: ${ input }</p>` if (albumIDs.length===1) msg = lychee.html`<p>Enter a new title for this album: ${ input }</p>`
else msg = `<p>Enter a title for all ${ albumIDs.length } selected albums: ${ input }</p>` else msg = lychee.html`<p>Enter a title for all $${ albumIDs.length } selected albums: ${ input }</p>`
basicModal.show({ basicModal.show({
body: msg, body: msg,
@ -342,7 +342,7 @@ album.setDescription = function(albumID) {
} }
basicModal.show({ basicModal.show({
body: `<p>Please enter a description for this album: <input class='text' name='description' type='text' maxlength='800' placeholder='Description' value='${ lychee.escapeHTML(oldDescription) }'></p>`, body: lychee.html`<p>Please enter a description for this album: <input class='text' name='description' type='text' maxlength='800' placeholder='Description' value='$${ oldDescription }'></p>`,
buttons: { buttons: {
action: { action: {
title: 'Set Description', title: 'Set Description',
@ -574,11 +574,11 @@ album.merge = function(albumIDs) {
if (!sTitle) sTitle = '' if (!sTitle) sTitle = ''
sTitle = sTitle.replace(/'/g, '&apos;') sTitle = sTitle.replace(/'/g, '&apos;')
msg = `<p>Are you sure you want to merge the album '${ sTitle }' into the album '${ title }'?</p>` msg = lychee.html`<p>Are you sure you want to merge the album '$${ sTitle }' into the album '$${ title }'?</p>`
} else { } else {
msg = `<p>Are you sure you want to merge all selected albums into the album '${ title }'?</p>` msg = lychee.html`<p>Are you sure you want to merge all selected albums into the album '$${ title }'?</p>`
} }

@ -7,54 +7,66 @@ build = {}
build.iconic = function(icon, classes = '') { build.iconic = function(icon, classes = '') {
return `<svg class='iconic ${ classes }'><use xlink:href='#${ icon }' /></svg>` let html = ''
html += lychee.html`<svg class='iconic $${ classes }'><use xlink:href='#$${ icon }' /></svg>`
return html
} }
build.divider = function(title) { build.divider = function(title) {
return `<div class='divider'><h1>${ title }</h1></div>` let html = ''
html += lychee.html`<div class='divider'><h1>$${ title }</h1></div>`
return html
} }
build.editIcon = function(id) { build.editIcon = function(id) {
return `<div id='${ id }' class='edit'>${ build.iconic('pencil') }</div>` let html = ''
html += lychee.html`<div id='$${ id }' class='edit'>${ build.iconic('pencil') }</div>`
return html
} }
build.multiselect = function(top, left) { build.multiselect = function(top, left) {
return `<div id='multiselect' style='top: ${ top }px; left: ${ left }px;'></div>` return lychee.html`<div id='multiselect' style='top: $${ top }px; left: $${ left }px;'></div>`
} }
build.album = function(data) { build.album = function(data) {
if (data==null) return '' let html = ''
let { path: thumbPath, hasRetina: thumbRetina } = lychee.retinize(data.thumbs[0]) let { path: thumbPath, hasRetina: thumbRetina } = lychee.retinize(data.thumbs[0])
let html = ` html += lychee.html`
<div class='album' data-id='${ data.id }'> <div class='album' data-id='$${ data.id }'>
<img src='${ data.thumbs[2] }' width='200' height='200' alt='thumb' data-overlay='false'> <img src='$${ data.thumbs[2] }' width='200' height='200' alt='thumb' data-overlay='false'>
<img src='${ data.thumbs[1] }' width='200' height='200' alt='thumb' data-overlay='false'> <img src='$${ data.thumbs[1] }' width='200' height='200' alt='thumb' data-overlay='false'>
<img src='${ thumbPath }' width='200' height='200' alt='thumb' data-overlay='${ thumbRetina }'> <img src='$${ thumbPath }' width='200' height='200' alt='thumb' data-overlay='$${ thumbRetina }'>
<div class='overlay'> <div class='overlay'>
<h1 title='${ data.title }'>${ data.title }</h1> <h1 title='$${ data.title }'>$${ data.title }</h1>
<a>${ data.sysdate }</a> <a>$${ data.sysdate }</a>
</div> </div>
` `
if (lychee.publicMode===false) { if (lychee.publicMode===false) {
html += ` html += lychee.html`
<div class='badges'> <div class='badges'>
<a class='badge ${ (data.star==='1' ? 'badge--visible' : '') } icn-star'>${ build.iconic('star') }</a> <a class='badge $${ (data.star==='1' ? 'badge--visible' : '') } icn-star'>${ build.iconic('star') }</a>
<a class='badge ${ (data.public==='1' ? 'badge--visible' : '') } icn-share'>${ build.iconic('eye') }</a> <a class='badge $${ (data.public==='1' ? 'badge--visible' : '') } icn-share'>${ build.iconic('eye') }</a>
<a class='badge ${ (data.unsorted==='1' ? 'badge--visible' : '') }'>${ build.iconic('list') }</a> <a class='badge $${ (data.unsorted==='1' ? 'badge--visible' : '') }'>${ build.iconic('list') }</a>
<a class='badge ${ (data.recent==='1' ? 'badge--visible' : '') }'>${ build.iconic('clock') }</a> <a class='badge $${ (data.recent==='1' ? 'badge--visible' : '') }'>${ build.iconic('clock') }</a>
<a class='badge ${ (data.password==='1' ? 'badge--visible' : '') }'>${ build.iconic('lock-locked') }</a> <a class='badge $${ (data.password==='1' ? 'badge--visible' : '') }'>${ build.iconic('lock-locked') }</a>
</div> </div>
` `
@ -68,34 +80,34 @@ build.album = function(data) {
build.photo = function(data) { build.photo = function(data) {
if (data==null) return '' let html = ''
let { path: thumbPath, hasRetina: thumbRetina } = lychee.retinize(data.thumbUrl) let { path: thumbPath, hasRetina: thumbRetina } = lychee.retinize(data.thumbUrl)
let html = ` html += lychee.html`
<div class='photo' data-album-id='${ data.album }' data-id='${ data.id }'> <div class='photo' data-album-id='$${ data.album }' data-id='$${ data.id }'>
<img src='${ thumbPath }' width='200' height='200' alt='thumb'> <img src='$${ thumbPath }' width='200' height='200' alt='thumb'>
<div class='overlay'> <div class='overlay'>
<h1 title='${ data.title }'>${ data.title }</h1> <h1 title='$${ data.title }'>$${ data.title }</h1>
` `
if (data.cameraDate==='1') html += `<a><span title='Camera Date'>${ build.iconic('camera-slr') }</span>${ data.sysdate }</a>` if (data.cameraDate==='1') html += lychee.html`<a><span title='Camera Date'>${ build.iconic('camera-slr') }</span>$${ data.sysdate }</a>`
else html += `<a>${ data.sysdate }</a>` else html += lychee.html`<a>$${ data.sysdate }</a>`
html += '</div>' html += `</div>`
if (lychee.publicMode===false) { if (lychee.publicMode===false) {
html += ` html += lychee.html`
<div class='badges'> <div class='badges'>
<a class='badge ${ (data.star==='1' ? 'badge--visible' : '') } icn-star'>${ build.iconic('star') }</a> <a class='badge $${ (data.star==='1' ? 'badge--visible' : '') } icn-star'>${ build.iconic('star') }</a>
<a class='badge ${ ((data.public==='1' && album.json.public!=='1') ? 'badge--visible' : '') } icn-share'>${ build.iconic('eye') }</a> <a class='badge $${ ((data.public==='1' && album.json.public!=='1') ? 'badge--visible' : '') } icn-share'>${ build.iconic('eye') }</a>
</div> </div>
` `
} }
html += '</div>' html += `</div>`
return html return html
@ -103,24 +115,22 @@ build.photo = function(data) {
build.imageview = function(data, size, visibleControls) { build.imageview = function(data, size, visibleControls) {
if (data==null) return ''
let html = '' let html = ''
if (size==='big') { if (size==='big') {
if (visibleControls===true) html += `<div id='image' style='background-image: url(${ data.url })'></div>` if (visibleControls===true) html += lychee.html`<div id='image' style='background-image: url($${ data.url })'></div>`
else html += `<div id='image' style='background-image: url(${ data.url });' class='full'></div>` else html += lychee.html`<div id='image' style='background-image: url($${ data.url });' class='full'></div>`
} else if (size==='medium') { } else if (size==='medium') {
if (visibleControls===true) html += `<div id='image' style='background-image: url(${ data.medium })'></div>` if (visibleControls===true) html += lychee.html`<div id='image' style='background-image: url($${ data.medium })'></div>`
else html += `<div id='image' style='background-image: url(${ data.medium });' class='full'></div>` else html += lychee.html`<div id='image' style='background-image: url($${ data.medium });' class='full'></div>`
} else if (size==='small') { } else if (size==='small') {
if (visibleControls===true) html += `<div id='image' class='small' style='background-image: url(${ data.url }); width: ${ data.width }px; height: ${ data.height }px; margin-top: -${ parseInt(data.height/2-20) }px; margin-left: -${ data.width/2 }px;'></div>` if (visibleControls===true) html += lychee.html`<div id='image' class='small' style='background-image: url($${ data.url }); width: $${ data.width }px; height: $${ data.height }px; margin-top: -$${ parseInt(data.height/2-20) }px; margin-left: -$${ data.width/2 }px;'></div>`
else html += `<div id='image' class='small' style='background-image: url(${ data.url }); width: ${ data.width }px; height: ${ data.height }px; margin-top: -${ parseInt(data.height/2) }px; margin-left: -${ data.width/2 }px;'></div>` else html += lychee.html`<div id='image' class='small' style='background-image: url($${ data.url }); width: $${ data.width }px; height: $${ data.height }px; margin-top: -$${ parseInt(data.height/2) }px; margin-left: -$${ data.width/2 }px;'></div>`
} }
@ -135,27 +145,29 @@ build.imageview = function(data, size, visibleControls) {
build.no_content = function(typ) { build.no_content = function(typ) {
let html = ` let html = ''
<div class='no_content fadeIn'>
${ build.iconic(typ) } html += `
` <div class='no_content fadeIn'>
${ build.iconic(typ) }
`
switch (typ) { switch (typ) {
case 'magnifying-glass': case 'magnifying-glass':
html += '<p>No results</p>' html += `<p>No results</p>`
break break
case 'eye': case 'eye':
html += '<p>No public albums</p>' html += `<p>No public albums</p>`
break break
case 'cog': case 'cog':
html += '<p>No configuration</p>' html += `<p>No configuration</p>`
break break
case 'question-mark': case 'question-mark':
html += '<p>Photo not found</p>' html += `<p>Photo not found</p>`
break break
} }
html += '</div>' html += `</div>`
return html return html
@ -163,10 +175,12 @@ build.no_content = function(typ) {
build.uploadModal = function(title, files) { build.uploadModal = function(title, files) {
let html = ` let html = ''
<h1>${ title }</h1>
<div class='rows'> html += lychee.html`
` <h1>$${ title }</h1>
<div class='rows'>
`
let i = 0 let i = 0
@ -176,9 +190,9 @@ build.uploadModal = function(title, files) {
if (file.name.length>40) file.name = file.name.substr(0, 17) + '...' + file.name.substr(file.name.length-20, 20) if (file.name.length>40) file.name = file.name.substr(0, 17) + '...' + file.name.substr(file.name.length-20, 20)
html += ` html += lychee.html`
<div class='row'> <div class='row'>
<a class='name'>${ lychee.escapeHTML(file.name) }</a> <a class='name'>$${ file.name }</a>
` `
if (file.supported===true) html += `<a class='status'></a>` if (file.supported===true) html += `<a class='status'></a>`
@ -193,7 +207,7 @@ build.uploadModal = function(title, files) {
} }
html += '</div>' html += `</div>`
return html return html
@ -208,7 +222,7 @@ build.tags = function(tags) {
tags = tags.split(',') tags = tags.split(',')
tags.forEach(function(tag, index, array) { tags.forEach(function(tag, index, array) {
html += `<a class='tag'>${ tag }<span data-index='${ index }'>${ build.iconic('x') }</span></a>` html += lychee.html`<a class='tag'>$${ tag }<span data-index='$${ index }'>${ build.iconic('x') }</span></a>`
}) })
} else { } else {

@ -101,7 +101,7 @@ contextMenu.albumTitle = function(albumID, e) {
if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg' if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg'
let title = `<img class='cover' width='16' height='16' src='${ this.thumbs[0] }'><div class='title'>${ this.title }</div>` let title = lychee.html`<img class='cover' width='16' height='16' src='$${ this.thumbs[0] }'><div class='title'>$${ this.title }</div>`
if (this.id!=albumID) items.push({ type: 'item', title, fn: () => lychee.goto(this.id) }) if (this.id!=albumID) items.push({ type: 'item', title, fn: () => lychee.goto(this.id) })
@ -131,7 +131,7 @@ contextMenu.mergeAlbum = function(albumID, e) {
if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg' if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg'
let title = `<img class='cover' width='16' height='16' src='${ this.thumbs[0] }'><div class='title'>${ this.title }</div>` let title = lychee.html`<img class='cover' width='16' height='16' src='$${ this.thumbs[0] }'><div class='title'>$${ this.title }</div>`
if (this.id!=albumID) items.push({ type: 'item', title, fn: () => album.merge([albumID, this.id]) }) if (this.id!=albumID) items.push({ type: 'item', title, fn: () => album.merge([albumID, this.id]) })
@ -206,7 +206,7 @@ contextMenu.photoTitle = function(albumID, photoID, e) {
// Generate list of albums // Generate list of albums
$.each(data.content, function(index) { $.each(data.content, function(index) {
let title = `<img class='cover' width='16' height='16' src='${ this.thumbUrl }'><div class='title'>${ this.title }</div>` let title = lychee.html`<img class='cover' width='16' height='16' src='$${ this.thumbUrl }'><div class='title'>$${ this.title }</div>`
if (this.id!=photoID) items.push({ type: 'item', title, fn: () => lychee.goto(albumID + '/' + this.id) }) if (this.id!=photoID) items.push({ type: 'item', title, fn: () => lychee.goto(albumID + '/' + this.id) })
@ -254,7 +254,7 @@ contextMenu.move = function(photoIDs, e) {
if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg' if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg'
let title = `<img class='cover' width='16' height='16' src='${ this.thumbs[0] }'><div class='title'>${ this.title }</div>` let title = lychee.html`<img class='cover' width='16' height='16' src='$${ this.thumbs[0] }'><div class='title'>$${ this.title }</div>`
if (this.id!=album.getID()) items.push({ type: 'item', title, fn: () => photo.setAlbum(photoIDs, this.id) }) if (this.id!=album.getID()) items.push({ type: 'item', title, fn: () => photo.setAlbum(photoIDs, this.id) })

@ -107,9 +107,10 @@ header.hide = function(e, delay = 500) {
header.setTitle = function(title = 'Untitled') { header.setTitle = function(title = 'Untitled') {
let $title = header.dom('#title') let $title = header.dom('#title'),
html = lychee.html`$${ title }${ build.iconic('caret-bottom') }`
$title.html(title + build.iconic('caret-bottom')) $title.html(html)
return true return true

@ -119,12 +119,12 @@ lychee.login = function(data) {
lychee.loginDialog = function() { lychee.loginDialog = function() {
let msg = ` let msg = lychee.html`
<p class='signIn'> <p class='signIn'>
<input class='text' name='username' autocomplete='username' type='text' value='' placeholder='username' autocapitalize='off' autocorrect='off'> <input class='text' name='username' autocomplete='username' type='text' value='' placeholder='username' autocapitalize='off' autocorrect='off'>
<input class='text' name='password' autocomplete='current-password' type='password' value='' placeholder='password'> <input class='text' name='password' autocomplete='current-password' type='password' value='' placeholder='password'>
</p> </p>
<p class='version'>Lychee ${ lychee.version }<span> &#8211; <a target='_blank' href='${ lychee.updateURL }'>Update available!</a><span></p> <p class='version'>Lychee $${ lychee.version }<span> &#8211; <a target='_blank' href='$${ lychee.updateURL }'>Update available!</a><span></p>
` `
basicModal.show({ basicModal.show({
@ -387,11 +387,46 @@ lychee.escapeHTML = function(html = '') {
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
.replace(/"/g, '&quot;') .replace(/"/g, '&quot;')
.replace(/'/g, '&#039;') .replace(/'/g, '&#039;')
.replace(/`/g, '&#96;')
return html return html
} }
lychee.html = function(literalSections, ...substs) {
// Use raw literal sections: we dont want
// backslashes (\n etc.) to be interpreted
let raw = literalSections.raw,
result = ''
substs.forEach((subst, i) => {
// Retrieve the literal section preceding
// the current substitution
let lit = raw[i]
// If the substitution is preceded by a dollar sign,
// we escape special characters in it
if (lit.slice(-1)==='$') {
subst = lychee.escapeHTML(subst)
lit = lit.slice(0, -1)
}
result += lit
result += subst
})
// Take care of last literal section
// (Never fails, because an empty template string
// produces one literal section, an empty string)
result += raw[raw.length-1]
return result
}
lychee.error = function(errorThrown, params, data) { lychee.error = function(errorThrown, params, data) {
console.error({ console.error({

@ -55,8 +55,10 @@ password.getDialog = function(albumID, callback) {
const action = (data) => password.get(albumID, callback, data.password) const action = (data) => password.get(albumID, callback, data.password)
const cancel = () => { const cancel = () => {
basicModal.close() basicModal.close()
if (visible.albums()===false) lychee.goto() if (visible.albums()===false) lychee.goto()
} }
let msg = ` let msg = `

@ -244,14 +244,14 @@ photo.delete = function(photoIDs) {
action.title = 'Delete Photo' action.title = 'Delete Photo'
cancel.title = 'Keep Photo' cancel.title = 'Keep Photo'
msg = `<p>Are you sure you want to delete the photo '${ photoTitle }'? This action can't be undone!</p>` msg = lychee.html`<p>Are you sure you want to delete the photo '$${ photoTitle }'? This action can't be undone!</p>`
} else { } else {
action.title = 'Delete Photo' action.title = 'Delete Photo'
cancel.title = 'Keep Photo' cancel.title = 'Keep Photo'
msg = `<p>Are you sure you want to delete all ${ photoIDs.length } selected photo? This action can't be undone!</p>` msg = lychee.html`<p>Are you sure you want to delete all $${ photoIDs.length } selected photo? This action can't be undone!</p>`
} }
@ -317,10 +317,10 @@ photo.setTitle = function(photoIDs) {
} }
let input = `<input class='text' name='title' type='text' maxlength='50' placeholder='Title' value='${ lychee.escapeHTML(oldTitle) }'>` let input = lychee.html`<input class='text' name='title' type='text' maxlength='50' placeholder='Title' value='$${ oldTitle }'>`
if (photoIDs.length===1) msg = `<p>Enter a new title for this photo: ${ input }</p>` if (photoIDs.length===1) msg = lychee.html`<p>Enter a new title for this photo: ${ input }</p>`
else msg = `<p>Enter a title for all ${ photoIDs.length } selected photos: ${ input }</p>` else msg = lychee.html`<p>Enter a title for all $${ photoIDs.length } selected photos: ${ input }</p>`
basicModal.show({ basicModal.show({
body: msg, body: msg,
@ -488,7 +488,7 @@ photo.setDescription = function(photoID) {
} }
basicModal.show({ basicModal.show({
body: `<p>Enter a description for this photo: <input class='text' name='description' type='text' maxlength='800' placeholder='Description' value='${ lychee.escapeHTML(oldDescription) }'></p>`, body: lychee.html`<p>Enter a description for this photo: <input class='text' name='description' type='text' maxlength='800' placeholder='Description' value='$${ oldDescription }'></p>`,
buttons: { buttons: {
action: { action: {
title: 'Set Description', title: 'Set Description',
@ -534,10 +534,10 @@ photo.editTags = function(photoIDs) {
} }
let input = `<input class='text' name='tags' type='text' maxlength='800' placeholder='Tags' value='${ oldTags }'>` let input = lychee.html`<input class='text' name='tags' type='text' maxlength='800' placeholder='Tags' value='$${ oldTags }'>`
if (photoIDs.length===1) msg = `<p>Enter your tags for this photo. You can add multiple tags by separating them with a comma: ${ input }</p>` if (photoIDs.length===1) msg = lychee.html`<p>Enter your tags for this photo. You can add multiple tags by separating them with a comma: ${ input }</p>`
else msg = `<p>Enter your tags for all ${ photoIDs.length } selected photos. Existing tags will be overwritten. You can add multiple tags by separating them with a comma: ${ input }</p>` else msg = lychee.html`<p>Enter your tags for all $${ photoIDs.length } selected photos. Existing tags will be overwritten. You can add multiple tags by separating them with a comma: ${ input }</p>`
basicModal.show({ basicModal.show({
body: msg, body: msg,

@ -404,10 +404,10 @@ settings.setDropboxKey = function(callback) {
} }
let msg = ` let msg = lychee.html`
<p> <p>
In order to import photos from your Dropbox, you need a valid drop-ins app key from <a href='https://www.dropbox.com/developers/apps/create'>their website</a>. Generate yourself a personal key and enter it below: In order to import photos from your Dropbox, you need a valid drop-ins app key from <a href='https://www.dropbox.com/developers/apps/create'>their website</a>. Generate yourself a personal key and enter it below:
<input class='text' name='key' type='text' placeholder='Dropbox API Key' value='${ lychee.dropboxKey }'> <input class='text' name='key' type='text' placeholder='Dropbox API Key' value='$${ lychee.dropboxKey }'>
</p> </p>
` `

@ -93,13 +93,17 @@ sidebar.setSelectable = function(selectable = true) {
} }
sidebar.changeAttr = function(attr, value = '-') { sidebar.changeAttr = function(attr, value = '-', dangerouslySetInnerHTML = false) {
if (attr==null || attr==='') return false if (attr==null || attr==='') return false
// Set a default for the value // Set a default for the value
if (value==null || value==='') value = '-' if (value==null || value==='') value = '-'
// Escape value
if (dangerouslySetInnerHTML===false) value = lychee.escapeHTML(value)
// Set new value
sidebar.dom('.attr_' + attr).html(value) sidebar.dom('.attr_' + attr).html(value)
return true return true
@ -339,14 +343,14 @@ sidebar.render = function(structure) {
if (value==='' || value==null) value = '-' if (value==='' || value==null) value = '-'
// Wrap span-element around value for easier selecting on change // Wrap span-element around value for easier selecting on change
value = `<span class='attr_${ row.title.toLowerCase() }'>${ value }</span>` value = lychee.html`<span class='attr_$${ row.title.toLowerCase() }'>$${ value }</span>`
// Add edit-icon to the value when editable // Add edit-icon to the value when editable
if (row.editable===true) value += ' ' + build.editIcon('edit_' + row.title.toLowerCase()) if (row.editable===true) value += ' ' + build.editIcon('edit_' + row.title.toLowerCase())
_html += ` _html += lychee.html`
<tr> <tr>
<td>${ row.title }</td> <td>$${ row.title }</td>
<td>${ value }</td> <td>${ value }</td>
</tr> </tr>
` `
@ -363,20 +367,19 @@ sidebar.render = function(structure) {
let renderTags = function(section) { let renderTags = function(section) {
let _html = '' let _html = '',
editable = ''
_html += ` // Add edit-icon to the value when editable
if (section.editable===true) editable = build.editIcon('edit_tags')
_html += lychee.html`
<div class='divider'> <div class='divider'>
<h1>${ section.title }</h1> <h1>$${ section.title }</h1>
</div> </div>
<div id='tags'> <div id='tags'>
<div class='attr_${ section.title.toLowerCase() }'>${ section.value }</div> <div class='attr_$${ section.title.toLowerCase() }'>${ section.value }</div>
` ${ editable }
// Add edit-icon to the value when editable
if (section.editable===true) _html += build.editIcon('edit_tags')
_html += `
</div> </div>
` `

@ -338,7 +338,7 @@ upload.start = {
} }
basicModal.show({ basicModal.show({
body: `<p>Please enter the direct link to a photo to import it: <input class='text' name='link' type='text' placeholder='http://' value='${ url }'></p>`, body: lychee.html`<p>Please enter the direct link to a photo to import it: <input class='text' name='link' type='text' placeholder='http://' value='$${ url }'></p>`,
buttons: { buttons: {
action: { action: {
title: 'Import', title: 'Import',
@ -444,7 +444,7 @@ upload.start = {
} }
basicModal.show({ basicModal.show({
body: `<p>This action will import all photos, folders and sub-folders which are located in the following directory. The <b>original files will be deleted</b> after the import when possible. <input class='text' name='path' type='text' maxlength='100' placeholder='Absolute path to directory' value='${ lychee.location }uploads/import/'></p>`, body: lychee.html`<p>This action will import all photos, folders and sub-folders which are located in the following directory. The <b>original files will be deleted</b> after the import when possible. <input class='text' name='path' type='text' maxlength='100' placeholder='Absolute path to directory' value='$${ lychee.location }uploads/import/'></p>`,
buttons: { buttons: {
action: { action: {
title: 'Import', title: 'Import',

@ -72,6 +72,8 @@ view.albums = {
let title = albums.getByID(albumID).title let title = albums.getByID(albumID).title
title = lychee.escapeHTML(title)
$('.album[data-id="' + albumID + '"] .overlay h1') $('.album[data-id="' + albumID + '"] .overlay h1')
.html(title) .html(title)
.attr('title', title) .attr('title', title)
@ -164,6 +166,8 @@ view.album = {
let title = album.json.content[photoID].title let title = album.json.content[photoID].title
title = lychee.escapeHTML(title)
$('.photo[data-id="' + photoID + '"] .overlay h1') $('.photo[data-id="' + photoID + '"] .overlay h1')
.html(title) .html(title)
.attr('title', title) .attr('title', title)
@ -199,7 +203,6 @@ view.album = {
if (!visible.albums()) { if (!visible.albums()) {
album.json.num-- album.json.num--
view.album.num() view.album.num()
view.album.title()
} }
}) })
@ -396,7 +399,7 @@ view.photo = {
tags: function() { tags: function() {
sidebar.changeAttr('tags', build.tags(photo.json.tags)) sidebar.changeAttr('tags', build.tags(photo.json.tags), true)
sidebar.bind() sidebar.bind()
}, },
@ -416,7 +419,7 @@ view.photo = {
let nextPhotoID = album.json.content[photo.getID()].nextPhoto, let nextPhotoID = album.json.content[photo.getID()].nextPhoto,
nextPhoto = album.json.content[nextPhotoID] nextPhoto = album.json.content[nextPhotoID]
$nextArrow.css('background-image', `linear-gradient(to bottom, rgba(0, 0, 0, .4), rgba(0, 0, 0, .4)), url("${ nextPhoto.thumbUrl }")`) $nextArrow.css('background-image', lychee.html`linear-gradient(to bottom, rgba(0, 0, 0, .4), rgba(0, 0, 0, .4)), url("$${ nextPhoto.thumbUrl }")`)
} }
@ -426,7 +429,7 @@ view.photo = {
let previousPhotoID = album.json.content[photo.getID()].previousPhoto, let previousPhotoID = album.json.content[photo.getID()].previousPhoto,
previousPhoto = album.json.content[previousPhotoID] previousPhoto = album.json.content[previousPhotoID]
$previousArrow.css('background-image', `linear-gradient(to bottom, rgba(0, 0, 0, .4), rgba(0, 0, 0, .4)), url("${ previousPhoto.thumbUrl }")`) $previousArrow.css('background-image', lychee.html`linear-gradient(to bottom, rgba(0, 0, 0, .4), rgba(0, 0, 0, .4)), url("$${ previousPhoto.thumbUrl }")`)
} }

@ -3,18 +3,74 @@
* @copyright 2015 by Tobias Reich * @copyright 2015 by Tobias Reich
*/ */
let lychee = { // Sub-implementation of Lychee -------------------------------------------------------------- //
content: $('#content'),
getEventName() {
let touchendSupport = (/Android|iPhone|iPad|iPod/i).test(navigator.userAgent || navigator.vendor || window.opera) && ('ontouchend' in document.documentElement), let lychee = {}
eventName = (touchendSupport===true ? 'touchend' : 'click')
return eventName lychee.content = $('#content')
lychee.getEventName = function() {
let touchendSupport = (/Android|iPhone|iPad|iPod/i).test(navigator.userAgent || navigator.vendor || window.opera) && ('ontouchend' in document.documentElement),
eventName = (touchendSupport===true ? 'touchend' : 'click')
return eventName
}
lychee.escapeHTML = function(html = '') {
// Ensure that html is a string
html += ''
// Escape all critical characters
html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
.replace(/`/g, '&#96;')
return html
}
lychee.html = function(literalSections, ...substs) {
// Use raw literal sections: we dont want
// backslashes (\n etc.) to be interpreted
let raw = literalSections.raw,
result = ''
substs.forEach((subst, i) => {
// Retrieve the literal section preceding
// the current substitution
let lit = raw[i]
// If the substitution is preceded by a dollar sign,
// we escape special characters in it
if (lit.slice(-1)==='$') {
subst = lychee.escapeHTML(subst)
lit = lit.slice(0, -1)
}
result += lit
result += subst
})
// Take care of last literal section
// (Never fails, because an empty template string
// produces one literal section, an empty string)
result += raw[raw.length-1]
return result
}
} }
// Main -------------------------------------------------------------- //
let loadingBar = { show() {}, hide() {} }, let loadingBar = { show() {}, hide() {} },
imageview = $('#imageview') imageview = $('#imageview')
@ -99,7 +155,7 @@ const loadPhotoInfo = function(photoID) {
// Set title // Set title
if (!data.title) data.title = 'Untitled' if (!data.title) data.title = 'Untitled'
document.title = 'Lychee - ' + data.title document.title = 'Lychee - ' + data.title
header.dom('#title').html(data.title) header.dom('#title').html(lychee.escapeHTML(data.title))
let size = getPhotoSize(data) let size = getPhotoSize(data)

Loading…
Cancel
Save