Browse Source

Fixed tons of XSS issues and escaping problems

Tobias Reich 8 years ago
parent
commit
2e96f089a7

File diff suppressed because it is too large
+ 0 - 0
dist/main.css


File diff suppressed because it is too large
+ 0 - 0
dist/main.js


File diff suppressed because it is too large
+ 0 - 0
dist/view.js


+ 0 - 7
php/modules/Album.php

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

+ 0 - 11
php/modules/Photo.php

@@ -896,9 +896,6 @@ class Photo extends Module {
 		# Call plugins
 		$this->plugins(__METHOD__, 0, func_get_args());
 
-		# Parse
-		if (strlen($title)>100) $title = substr($title, 0, 100);
-
 		# Set title
 		$query	= Database::prepare($this->database, "UPDATE ? SET title = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $title, $this->photoIDs));
 		$result	= $this->database->query($query);
@@ -929,10 +926,6 @@ class Photo extends Module {
 		# Call plugins
 		$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
 		$query	= Database::prepare($this->database, "UPDATE ? SET description = '?' WHERE id IN ('?')", array(LYCHEE_TABLE_PHOTOS, $description, $this->photoIDs));
 		$result	= $this->database->query($query);
@@ -1122,10 +1115,6 @@ class Photo extends Module {
 		# Parse tags
 		$tags = preg_replace('/(\ ,\ )|(\ ,)|(,\ )|(,{1,}\ {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
 		$query	= Database::prepare($this->database, "UPDATE ? SET tags = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $tags, $this->photoIDs));

+ 4 - 4
src/gulpfile.js

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

+ 8 - 8
src/scripts/album.js

@@ -204,14 +204,14 @@ album.delete = function(albumIDs) {
 		if (album.json)       albumTitle = album.json.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 {
 
 		action.title = 'Delete Albums and Photos'
 		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>`
-	else                     msg = `<p>Enter a title for all ${ albumIDs.length } selected albums: ${ input }</p>`
+	if (albumIDs.length===1) msg = lychee.html`<p>Enter a new title for this album: ${ input }</p>`
+	else                     msg = lychee.html`<p>Enter a title for all $${ albumIDs.length } selected albums: ${ input }</p>`
 
 	basicModal.show({
 		body: msg,
@@ -342,7 +342,7 @@ album.setDescription = function(albumID) {
 	}
 
 	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: {
 			action: {
 				title: 'Set Description',
@@ -574,11 +574,11 @@ album.merge = function(albumIDs) {
 		if (!sTitle) sTitle = ''
 		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 {
 
-		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>`
 
 	}
 

+ 74 - 60
src/scripts/build.js

@@ -7,54 +7,66 @@ build = {}
 
 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) {
 
-	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) {
 
-	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) {
 
-	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) {
 
-	if (data==null) return ''
+	let html = ''
 
 	let { path: thumbPath, hasRetina: thumbRetina } = lychee.retinize(data.thumbs[0])
 
-	let html = `
-	           <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[1] }' width='200' height='200' alt='thumb' data-overlay='false'>
-	               <img src='${ thumbPath }' width='200' height='200' alt='thumb' data-overlay='${ thumbRetina }'>
-	               <div class='overlay'>
-	                   <h1 title='${ data.title }'>${ data.title }</h1>
-	                   <a>${ data.sysdate }</a>
-	               </div>
-	           `
+	html += lychee.html`
+	        <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[1] }' width='200' height='200' alt='thumb' data-overlay='false'>
+	            <img src='$${ thumbPath }' width='200' height='200' alt='thumb' data-overlay='$${ thumbRetina }'>
+	            <div class='overlay'>
+	                <h1 title='$${ data.title }'>$${ data.title }</h1>
+	                <a>$${ data.sysdate }</a>
+	            </div>
+	        `
 
 	if (lychee.publicMode===false) {
 
-		html += `
+		html += lychee.html`
 		        <div class='badges'>
-		            <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.unsorted==='1' ? 'badge--visible' : '') }'>${ build.iconic('list') }</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.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.unsorted==='1' ? 'badge--visible' : '') }'>${ build.iconic('list') }</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>
 		        </div>
 		        `
 
@@ -68,34 +80,34 @@ build.album = function(data) {
 
 build.photo = function(data) {
 
-	if (data==null) return ''
+	let html = ''
 
 	let { path: thumbPath, hasRetina: thumbRetina } = lychee.retinize(data.thumbUrl)
 
-	let html = `
-	           <div class='photo' data-album-id='${ data.album }' data-id='${ data.id }'>
-	               <img src='${ thumbPath }' width='200' height='200' alt='thumb'>
-	               <div class='overlay'>
-	                   <h1 title='${ data.title }'>${ data.title }</h1>
-	           `
+	html += lychee.html`
+	        <div class='photo' data-album-id='$${ data.album }' data-id='$${ data.id }'>
+	            <img src='$${ thumbPath }' width='200' height='200' alt='thumb'>
+	            <div class='overlay'>
+	                <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>`
-	else                       html += `<a>${ 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 += lychee.html`<a>$${ data.sysdate }</a>`
 
-	html += '</div>'
+	html += `</div>`
 
 	if (lychee.publicMode===false) {
 
-		html += `
+		html += lychee.html`
 		        <div class='badges'>
-		            <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.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>
 		        </div>
 		        `
 
 	}
 
-	html += '</div>'
+	html += `</div>`
 
 	return html
 
@@ -103,24 +115,22 @@ build.photo = function(data) {
 
 build.imageview = function(data, size, visibleControls) {
 
-	if (data==null) return ''
-
 	let html = ''
 
 	if (size==='big') {
 
-		if (visibleControls===true) 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>`
+		if (visibleControls===true) html += lychee.html`<div id='image' style='background-image: url($${ data.url })'></div>`
+		else                        html += lychee.html`<div id='image' style='background-image: url($${ data.url });' class='full'></div>`
 
 	} else if (size==='medium') {
 
-		if (visibleControls===true) 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>`
+		if (visibleControls===true) html += lychee.html`<div id='image' style='background-image: url($${ data.medium })'></div>`
+		else                        html += lychee.html`<div id='image' style='background-image: url($${ data.medium });' class='full'></div>`
 
 	} 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>`
-		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>`
+		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 += 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) {
 
-	let html = `
-	           <div class='no_content fadeIn'>
-	               ${ build.iconic(typ) }
-	           `
+	let html = ''
+
+	html += `
+	        <div class='no_content fadeIn'>
+	            ${ build.iconic(typ) }
+	        `
 
 	switch (typ) {
 		case 'magnifying-glass':
-			html += '<p>No results</p>'
+			html += `<p>No results</p>`
 			break
 		case 'eye':
-			html += '<p>No public albums</p>'
+			html += `<p>No public albums</p>`
 			break
 		case 'cog':
-			html += '<p>No configuration</p>'
+			html += `<p>No configuration</p>`
 			break
 		case 'question-mark':
-			html += '<p>Photo not found</p>'
+			html += `<p>Photo not found</p>`
 			break
 	}
 
-	html += '</div>'
+	html += `</div>`
 
 	return html
 
@@ -163,10 +175,12 @@ build.no_content = function(typ) {
 
 build.uploadModal = function(title, files) {
 
-	let html = `
-	           <h1>${ title }</h1>
-	           <div class='rows'>
-	           `
+	let html = ''
+
+	html += lychee.html`
+	        <h1>$${ title }</h1>
+	        <div class='rows'>
+	        `
 
 	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)
 
-		html += `
+		html += lychee.html`
 		        <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>`
@@ -193,7 +207,7 @@ build.uploadModal = function(title, files) {
 
 	}
 
-	html +=	'</div>'
+	html +=	`</div>`
 
 	return html
 
@@ -208,7 +222,7 @@ build.tags = function(tags) {
 		tags = tags.split(',')
 
 		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 {

+ 4 - 4
src/scripts/contextMenu.js

@@ -101,7 +101,7 @@ contextMenu.albumTitle = function(albumID, e) {
 
 				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) })
 
@@ -131,7 +131,7 @@ contextMenu.mergeAlbum = function(albumID, e) {
 
 				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]) })
 
@@ -206,7 +206,7 @@ contextMenu.photoTitle = function(albumID, photoID, e) {
 		// Generate list of albums
 		$.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) })
 
@@ -254,7 +254,7 @@ contextMenu.move = function(photoIDs, e) {
 
 				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) })
 

+ 3 - 2
src/scripts/header.js

@@ -107,9 +107,10 @@ header.hide = function(e, delay = 500) {
 
 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
 

+ 37 - 2
src/scripts/lychee.js

@@ -119,12 +119,12 @@ lychee.login = function(data) {
 
 lychee.loginDialog = function() {
 
-	let msg = `
+	let msg = lychee.html`
 	          <p class='signIn'>
 	              <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'>
 	          </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({
@@ -387,11 +387,46 @@ lychee.escapeHTML = function(html = '') {
 	           .replace(/>/g, '&gt;')
 	           .replace(/"/g, '&quot;')
 	           .replace(/'/g, '&#039;')
+	           .replace(/`/g, '&#96;')
 
 	return html
 
 }
 
+lychee.html = function(literalSections, ...substs) {
+
+	// Use raw literal sections: we don’t 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) {
 
 	console.error({

+ 2 - 0
src/scripts/password.js

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

+ 9 - 9
src/scripts/photo.js

@@ -244,14 +244,14 @@ photo.delete = function(photoIDs) {
 		action.title = 'Delete 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 {
 
 		action.title = 'Delete 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>`
-	else                     msg = `<p>Enter a title for all ${ photoIDs.length } selected photos: ${ input }</p>`
+	if (photoIDs.length===1) msg = lychee.html`<p>Enter a new title for this photo: ${ input }</p>`
+	else                     msg = lychee.html`<p>Enter a title for all $${ photoIDs.length } selected photos: ${ input }</p>`
 
 	basicModal.show({
 		body: msg,
@@ -488,7 +488,7 @@ photo.setDescription = function(photoID) {
 	}
 
 	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: {
 			action: {
 				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>`
-	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>`
+	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 = 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({
 		body: msg,

+ 2 - 2
src/scripts/settings.js

@@ -404,10 +404,10 @@ settings.setDropboxKey = function(callback) {
 
 	}
 
-	let msg = `
+	let msg = lychee.html`
 	          <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:
-	              <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>
 	          `
 

+ 17 - 14
src/scripts/sidebar.js

@@ -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
 
 	// Set a default for the value
 	if (value==null || value==='') value = '-'
 
+	// Escape value
+	if (dangerouslySetInnerHTML===false) value = lychee.escapeHTML(value)
+
+	// Set new value
 	sidebar.dom('.attr_' + attr).html(value)
 
 	return true
@@ -339,14 +343,14 @@ sidebar.render = function(structure) {
 			if (value==='' || value==null) value = '-'
 
 			// 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
 			if (row.editable===true) value += ' ' + build.editIcon('edit_' + row.title.toLowerCase())
 
-			_html += `
+			_html += lychee.html`
 			         <tr>
-			             <td>${ row.title }</td>
+			             <td>$${ row.title }</td>
 			             <td>${ value }</td>
 			         </tr>
 			         `
@@ -363,20 +367,19 @@ sidebar.render = function(structure) {
 
 	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'>
-		             <h1>${ section.title }</h1>
+		             <h1>$${ section.title }</h1>
 		         </div>
 		         <div id='tags'>
-		             <div class='attr_${ section.title.toLowerCase() }'>${ section.value }</div>
-		         `
-
-		// Add edit-icon to the value when editable
-		if (section.editable===true) _html += build.editIcon('edit_tags')
-
-		_html += `
+		             <div class='attr_$${ section.title.toLowerCase() }'>${ section.value }</div>
+		             ${ editable }
 		         </div>
 		         `
 

+ 2 - 2
src/scripts/upload.js

@@ -338,7 +338,7 @@ upload.start = {
 		}
 
 		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: {
 				action: {
 					title: 'Import',
@@ -444,7 +444,7 @@ upload.start = {
 		}
 
 		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: {
 				action: {
 					title: 'Import',

+ 7 - 4
src/scripts/view.js

@@ -72,6 +72,8 @@ view.albums = {
 
 			let title = albums.getByID(albumID).title
 
+			title = lychee.escapeHTML(title)
+
 			$('.album[data-id="' + albumID + '"] .overlay h1')
 				.html(title)
 				.attr('title', title)
@@ -164,6 +166,8 @@ view.album = {
 
 			let title = album.json.content[photoID].title
 
+			title = lychee.escapeHTML(title)
+
 			$('.photo[data-id="' + photoID + '"] .overlay h1')
 				.html(title)
 				.attr('title', title)
@@ -199,7 +203,6 @@ view.album = {
 				if (!visible.albums()) {
 					album.json.num--
 					view.album.num()
-					view.album.title()
 				}
 			})
 
@@ -396,7 +399,7 @@ view.photo = {
 
 	tags: function() {
 
-		sidebar.changeAttr('tags', build.tags(photo.json.tags))
+		sidebar.changeAttr('tags', build.tags(photo.json.tags), true)
 		sidebar.bind()
 
 	},
@@ -416,7 +419,7 @@ view.photo = {
 			let nextPhotoID = album.json.content[photo.getID()].nextPhoto,
 			    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,
 			    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 }")`)
 
 		}
 

+ 64 - 8
src/scripts/view/main.js

@@ -3,18 +3,74 @@
  * @copyright   2015 by Tobias Reich
  */
 
-let lychee = {
-	content: $('#content'),
-	getEventName() {
+// Sub-implementation of Lychee -------------------------------------------------------------- //
 
-		let touchendSupport = (/Android|iPhone|iPad|iPod/i).test(navigator.userAgent || navigator.vendor || window.opera) && ('ontouchend' in document.documentElement),
-		    eventName       = (touchendSupport===true ? 'touchend' : 'click')
+let lychee = {}
 
-		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 don’t 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() {} },
     imageview  = $('#imageview')
 
@@ -99,7 +155,7 @@ const loadPhotoInfo = function(photoID) {
 		// Set title
 		if (!data.title) data.title = 'Untitled'
 		document.title = 'Lychee - ' + data.title
-		header.dom('#title').html(data.title)
+		header.dom('#title').html(lychee.escapeHTML(data.title))
 
 		let size = getPhotoSize(data)
 

Some files were not shown because too many files changed in this diff