Browse Source

Merge pull request #403 from electerious/develop

Lychee 3.0.6
Tobias Reich 8 years ago
parent
commit
7879869a58

+ 1 - 1
README.md

@@ -37,7 +37,7 @@ In order to use the Dropbox import from your server, you need a valid drop-ins a
 
 ### Twitter Cards
 
-Lychee supports [Twitter Cards](https://dev.twitter.com/docs/cards) and [Open Graph](http://opengraphprotocol.org) for shared images (not albums). In order to use Twitter Cards you need to request an approval for your domain. Simply share an image with Lychee, copy its link and paste it in [Twitters Card Validator](https://dev.twitter.com/docs/cards/validation/validator).
+Lychee supports [Twitter Cards](https://dev.twitter.com/docs/cards) and [Open Graph](http://opengraphprotocol.org) for shared images ([not albums](https://github.com/electerious/Lychee/issues/384)). In order to use Twitter Cards you need to request an approval for your domain. Simply share an image with Lychee, copy its link and paste it in [Twitters Card Validator](https://dev.twitter.com/docs/cards/validation/validator).
 
 ### Imagick
 

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


+ 9 - 0
docs/Changelog.md

@@ -1,3 +1,12 @@
+## v3.0.6
+
+Released September 13, 2015
+
+- `Improved` Share photo now shares view.php link (#392)
+- `Fixed` Incorrect error messages for failed uploads (#393)
+- `Fixed` XSS issues and escaping problems
+- `Fixed` Broken "Download album" when album has an ampersand in the password (#356)
+
 ## v3.0.5
 
 Released August 9, 2015

+ 3 - 3
index.html

@@ -86,12 +86,12 @@
 			<a class="button button--right" id="button_trash" title="Delete">
 				<svg class="iconic"><use xlink:href="#trash"></use></svg>
 			</a>
-			<a class="button button--right button--info" id="button_info" title="About Photo">
-				<svg class="iconic"><use xlink:href="#info"></use></svg>
-			</a>
 			<a class="button button--right" id="button_move" title="Move">
 				<svg class="iconic"><use xlink:href="#folder"></use></svg>
 			</a>
+			<a class="button button--right button--info" id="button_info" title="About Photo">
+				<svg class="iconic"><use xlink:href="#info"></use></svg>
+			</a>
 			<a class="button_divider"></a>
 			<a class="button button--right button--eye" id="button_share" title="Share Photo">
 				<svg class="iconic"><use xlink:href="#eye"></use></svg>

+ 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);

+ 1 - 0
php/modules/Import.php

@@ -42,6 +42,7 @@ class Import extends Module {
 		$nameFile[0]['tmp_name']	= $path;
 		$nameFile[0]['error']		= 0;
 		$nameFile[0]['size']		= $size;
+		$nameFile[0]['error']		= UPLOAD_ERR_OK;
 
 		if (!$photo->add($nameFile, $albumID, $description, $tags, true)) return false;
 		return true;

+ 35 - 11
php/modules/Photo.php

@@ -88,6 +88,41 @@ class Photo extends Module {
 
 		foreach ($files as $file) {
 
+			# Check if file exceeds the upload_max_filesize directive
+			if ($file['error']===UPLOAD_ERR_INI_SIZE) {
+				Log::error($this->database, __METHOD__, __LINE__, 'The uploaded file exceeds the upload_max_filesize directive in php.ini');
+				if ($returnOnError===true) return false;
+				exit('Error: The uploaded file exceeds the upload_max_filesize directive in php.ini!');
+			}
+
+			# Check if file was only partially uploaded
+			if ($file['error']===UPLOAD_ERR_PARTIAL) {
+				Log::error($this->database, __METHOD__, __LINE__, 'The uploaded file was only partially uploaded');
+				if ($returnOnError===true) return false;
+				exit('Error: The uploaded file was only partially uploaded!');
+			}
+
+			# Check if writing file to disk failed
+			if ($file['error']===UPLOAD_ERR_CANT_WRITE) {
+				Log::error($this->database, __METHOD__, __LINE__, 'Failed to write photo to disk');
+				if ($returnOnError===true) return false;
+				exit('Error: Failed to write photo to disk!');
+			}
+
+			# Check if a extension stopped the file upload
+			if ($file['error']===UPLOAD_ERR_EXTENSION) {
+				Log::error($this->database, __METHOD__, __LINE__, 'A PHP extension stopped the file upload');
+				if ($returnOnError===true) return false;
+				exit('Error: A PHP extension stopped the file upload!');
+			}
+
+			# Check if the upload was successful
+			if ($file['error']!==UPLOAD_ERR_OK) {
+				Log::error($this->database, __METHOD__, __LINE__, 'Upload contains an error (' . $file['error'] . ')');
+				if ($returnOnError===true) return false;
+				exit('Error: Upload failed!');
+			}
+
 			# Verify extension
 			$extension = getExtension($file['name']);
 			if (!in_array(strtolower($extension), Photo::$validExtensions, true)) {
@@ -861,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);
@@ -894,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);
@@ -1087,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;

+ 7 - 7
src/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Lychee",
-  "version": "3.0.5",
+  "version": "3.0.6",
   "description": "Self-hosted photo-management done right.",
   "authors": "Tobias Reich <tobias@electerious.com>",
   "license": "MIT",
@@ -10,18 +10,18 @@
     "url": "https://github.com/electerious/Lychee.git"
   },
   "devDependencies": {
-    "basiccontext": "^3.3.0",
-    "basicmodal": "^3.1.1",
+    "basiccontext": "^3.3.1",
+    "basicmodal": "^3.1.2",
     "gulp": "^3.9.0",
-    "gulp-autoprefixer": "2.3.1",
-    "gulp-babel": "^5.2.0",
+    "gulp-autoprefixer": "3.0.1",
+    "gulp-babel": "^5.2.1",
     "gulp-concat": "^2.6.0",
     "gulp-inject": "^1.5.0",
     "gulp-load-plugins": "^1.0.0-rc",
-    "gulp-minify-css": "^1.2.0",
+    "gulp-minify-css": "^1.2.1",
     "gulp-rimraf": "^0.1.1",
     "gulp-sass": "^2.0.4",
-    "gulp-uglify": "^1.2.0",
+    "gulp-uglify": "^1.4.1",
     "jquery": "^2.1.4",
     "mousetrap": "^1.5.3"
   }

+ 9 - 16
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>`
 
 	}
 
@@ -247,7 +247,6 @@ album.setTitle = function(albumIDs) {
 		else if (albums.json) oldTitle = albums.getByID(albumIDs).title
 
 		if (!oldTitle) oldTitle = ''
-		oldTitle = oldTitle.replace(/'/g, '&apos;')
 
 	}
 
@@ -257,9 +256,6 @@ album.setTitle = function(albumIDs) {
 
 		basicModal.close()
 
-		// Remove html from input
-		newTitle = lychee.removeHTML(newTitle)
-
 		// Set title to Untitled when empty
 		newTitle = (newTitle==='') ? 'Untitled' : newTitle
 
@@ -296,10 +292,10 @@ album.setTitle = function(albumIDs) {
 
 	}
 
-	let input = `<input class='text' name='title' type='text' maxlength='50' placeholder='Title' value='${ 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,
@@ -327,9 +323,6 @@ album.setDescription = function(albumID) {
 
 		basicModal.close()
 
-		// Remove html from input
-		description = lychee.removeHTML(description)
-
 		if (visible.album()) {
 			album.json.description = description
 			view.album.description()
@@ -349,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='${ 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',
@@ -552,7 +545,7 @@ album.getArchive = function(albumID) {
 	if (location.href.indexOf('index.html')>0) link = location.href.replace(location.hash, '').replace('index.html', url)
 	else                                       link = location.href.replace(location.hash, '') + url
 
-	if (lychee.publicMode===true) link += `&password=${ password.value }`
+	if (lychee.publicMode===true) link += `&password=${ encodeURIComponent(password.value) }`
 
 	location.href = link
 
@@ -581,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-retina='false'>
-	               <img src='${ data.thumbs[1] }' width='200' height='200' alt='thumb' data-retina='false'>
-	               <img src='${ thumbPath }' width='200' height='200' alt='thumb' data-retina='${ 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 - 6
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) })
 
@@ -281,8 +281,6 @@ contextMenu.sharePhoto = function(photoID, e) {
 	let link      = photo.getViewLink(photoID),
 		iconClass = 'ionicons'
 
-	if (photo.json.public==='2') link = location.href
-
 	let items = [
 		{ type: 'item', title: `<input readonly id="link" value="${ link }">`, fn: () => {}, class: 'noHover' },
 		{ type: 'separator' },

+ 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
 

+ 49 - 18
src/scripts/lychee.js

@@ -6,8 +6,8 @@
 lychee = {
 
 	title           : document.title,
-	version         : '3.0.5',
-	version_code    : '030005',
+	version         : '3.0.6',
+	version_code    : '030006',
 
 	update_path     : 'http://lychee.electerious.com/version/index.php',
 	updateURL       : 'https://github.com/electerious/Lychee',
@@ -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({
@@ -312,15 +312,6 @@ lychee.animate = function(obj, animation) {
 
 }
 
-lychee.escapeHTML = function(s) {
-
-	return s.replace(/&/g, '&amp;')
-	        .replace(/"/g, '&quot;')
-	        .replace(/</g, '&lt;')
-	        .replace(/>/g, '&gt;')
-
-}
-
 lychee.retinize = function(path = '') {
 
 	let pixelRatio = window.devicePixelRatio,
@@ -385,14 +376,54 @@ lychee.getEventName = function() {
 
 }
 
-lychee.removeHTML = function(html = '') {
+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
+
+}
 
-	if (html==='') 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
+
+	})
 
-	let tmp = document.createElement('DIV')
-	tmp.innerHTML = html
+	// 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 (tmp.textContent || tmp.innerText)
+	return result
 
 }
 

+ 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 = `

+ 11 - 21
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>`
 
 	}
 
@@ -285,7 +285,6 @@ photo.setTitle = function(photoIDs) {
 		// Get old title if only one photo is selected
 		if (photo.json)      oldTitle = photo.json.title
 		else if (album.json) oldTitle = album.json.content[photoIDs].title
-		oldTitle = oldTitle.replace(/'/g, '&apos;')
 
 	}
 
@@ -295,9 +294,6 @@ photo.setTitle = function(photoIDs) {
 
 		let newTitle = data.title
 
-		// Remove html from input
-		newTitle = lychee.removeHTML(newTitle)
-
 		if (visible.photo()) {
 			photo.json.title = (newTitle==='' ? 'Untitled' : newTitle)
 			view.photo.title()
@@ -321,10 +317,10 @@ photo.setTitle = function(photoIDs) {
 
 	}
 
-	let input = `<input class='text' name='title' type='text' maxlength='50' placeholder='Title' value='${ 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,
@@ -465,7 +461,7 @@ photo.setPublic = function(photoID, e) {
 
 photo.setDescription = function(photoID) {
 
-	let oldDescription = photo.json.description.replace(/'/g, '&apos;')
+	let oldDescription = photo.json.description
 
 	const action = function(data) {
 
@@ -473,9 +469,6 @@ photo.setDescription = function(photoID) {
 
 		let description = data.description
 
-		// Remove html from input
-		description = lychee.removeHTML(description)
-
 		if (visible.photo()) {
 			photo.json.description = description
 			view.photo.description()
@@ -495,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='${ 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',
@@ -541,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,
@@ -571,9 +564,6 @@ photo.setTags = function(photoIDs, tags) {
 	tags = tags.replace(/(\ ,\ )|(\ ,)|(,\ )|(,{1,}\ {0,})|(,$|^,)/g, ',')
 	tags = tags.replace(/,$|^,|(\ ){0,}$/g, '')
 
-	// Remove html from input
-	tags = lychee.removeHTML(tags)
-
 	if (visible.photo()) {
 		photo.json.tags = tags
 		view.photo.tags()
@@ -684,7 +674,7 @@ photo.getArchive = function(photoID) {
 	if (location.href.indexOf('index.html')>0) link = location.href.replace(location.hash, '').replace('index.html', url)
 	else                                       link = location.href.replace(location.hash, '') + url
 
-	if (lychee.publicMode===true) link += '&password=' + password.value
+	if (lychee.publicMode===true) link += `&password=${ encodeURIComponent(password.value) }`
 
 	location.href = link
 

+ 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)
 

+ 3 - 3
src/styles/_content.scss

@@ -112,7 +112,7 @@
 	}
 
 	// No overlay for empty albums
-	.album img[data-retina='false'] + .overlay {
+	.album img[data-overlay='false'] + .overlay {
 		background: none;
 	}
 
@@ -154,8 +154,8 @@
 		filter: drop-shadow(0 1px 3px black(.4));
 	}
 
-	.album img[data-retina='false'] + .overlay h1,
-	.album img[data-retina='false'] + .overlay a  { text-shadow: none; }
+	.album img[data-overlay='false'] + .overlay h1,
+	.album img[data-overlay='false'] + .overlay a  { text-shadow: none; }
 
 	/* Badges ------------------------------------------------*/
 	.album .badges,

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