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
 ### 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
 ### 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
 ## v3.0.5
 
 
 Released August 9, 2015
 Released August 9, 2015

+ 3 - 3
index.html

@@ -86,12 +86,12 @@
 			<a class="button button--right" id="button_trash" title="Delete">
 			<a class="button button--right" id="button_trash" title="Delete">
 				<svg class="iconic"><use xlink:href="#trash"></use></svg>
 				<svg class="iconic"><use xlink:href="#trash"></use></svg>
 			</a>
 			</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">
 			<a class="button button--right" id="button_move" title="Move">
 				<svg class="iconic"><use xlink:href="#folder"></use></svg>
 				<svg class="iconic"><use xlink:href="#folder"></use></svg>
 			</a>
 			</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_divider"></a>
 			<a class="button button--right button--eye" id="button_share" title="Share Photo">
 			<a class="button button--right button--eye" id="button_share" title="Share Photo">
 				<svg class="iconic"><use xlink:href="#eye"></use></svg>
 				<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
 		# 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);

+ 1 - 0
php/modules/Import.php

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

+ 35 - 11
php/modules/Photo.php

@@ -88,6 +88,41 @@ class Photo extends Module {
 
 
 		foreach ($files as $file) {
 		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
 			# Verify extension
 			$extension = getExtension($file['name']);
 			$extension = getExtension($file['name']);
 			if (!in_array(strtolower($extension), Photo::$validExtensions, true)) {
 			if (!in_array(strtolower($extension), Photo::$validExtensions, true)) {
@@ -861,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);
@@ -894,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);
@@ -1087,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));

+ 4 - 4
src/gulpfile.js

@@ -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;

+ 7 - 7
src/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "Lychee",
   "name": "Lychee",
-  "version": "3.0.5",
+  "version": "3.0.6",
   "description": "Self-hosted photo-management done right.",
   "description": "Self-hosted photo-management done right.",
   "authors": "Tobias Reich <tobias@electerious.com>",
   "authors": "Tobias Reich <tobias@electerious.com>",
   "license": "MIT",
   "license": "MIT",
@@ -10,18 +10,18 @@
     "url": "https://github.com/electerious/Lychee.git"
     "url": "https://github.com/electerious/Lychee.git"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "basiccontext": "^3.3.0",
-    "basicmodal": "^3.1.1",
+    "basiccontext": "^3.3.1",
+    "basicmodal": "^3.1.2",
     "gulp": "^3.9.0",
     "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-concat": "^2.6.0",
     "gulp-inject": "^1.5.0",
     "gulp-inject": "^1.5.0",
     "gulp-load-plugins": "^1.0.0-rc",
     "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-rimraf": "^0.1.1",
     "gulp-sass": "^2.0.4",
     "gulp-sass": "^2.0.4",
-    "gulp-uglify": "^1.2.0",
+    "gulp-uglify": "^1.4.1",
     "jquery": "^2.1.4",
     "jquery": "^2.1.4",
     "mousetrap": "^1.5.3"
     "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
 		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>`
 
 
 	}
 	}
 
 
@@ -247,7 +247,6 @@ album.setTitle = function(albumIDs) {
 		else if (albums.json) oldTitle = albums.getByID(albumIDs).title
 		else if (albums.json) oldTitle = albums.getByID(albumIDs).title
 
 
 		if (!oldTitle) oldTitle = ''
 		if (!oldTitle) oldTitle = ''
-		oldTitle = oldTitle.replace(/'/g, '&apos;')
 
 
 	}
 	}
 
 
@@ -257,9 +256,6 @@ album.setTitle = function(albumIDs) {
 
 
 		basicModal.close()
 		basicModal.close()
 
 
-		// Remove html from input
-		newTitle = lychee.removeHTML(newTitle)
-
 		// Set title to Untitled when empty
 		// Set title to Untitled when empty
 		newTitle = (newTitle==='') ? 'Untitled' : newTitle
 		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({
 	basicModal.show({
 		body: msg,
 		body: msg,
@@ -327,9 +323,6 @@ album.setDescription = function(albumID) {
 
 
 		basicModal.close()
 		basicModal.close()
 
 
-		// Remove html from input
-		description = lychee.removeHTML(description)
-
 		if (visible.album()) {
 		if (visible.album()) {
 			album.json.description = description
 			album.json.description = description
 			view.album.description()
 			view.album.description()
@@ -349,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='${ 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',
@@ -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)
 	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
 	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
 	location.href = link
 
 
@@ -581,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>`
 
 
 	}
 	}
 
 

+ 74 - 60
src/scripts/build.js

@@ -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 = `
-	           <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) {
 	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.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>
 		        </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 = `
-	           <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) {
 	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.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>
 		        </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>`
-		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') {
 	} 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') {
 	} 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) {
 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) {
 	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 = `
-	           <h1>${ title }</h1>
-	           <div class='rows'>
-	           `
+	let html = ''
+
+	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 {

+ 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'
 				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) })
 
 
@@ -281,8 +281,6 @@ contextMenu.sharePhoto = function(photoID, e) {
 	let link      = photo.getViewLink(photoID),
 	let link      = photo.getViewLink(photoID),
 		iconClass = 'ionicons'
 		iconClass = 'ionicons'
 
 
-	if (photo.json.public==='2') link = location.href
-
 	let items = [
 	let items = [
 		{ type: 'item', title: `<input readonly id="link" value="${ link }">`, fn: () => {}, class: 'noHover' },
 		{ type: 'item', title: `<input readonly id="link" value="${ link }">`, fn: () => {}, class: 'noHover' },
 		{ type: 'separator' },
 		{ type: 'separator' },

+ 3 - 2
src/scripts/header.js

@@ -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
 
 

+ 49 - 18
src/scripts/lychee.js

@@ -6,8 +6,8 @@
 lychee = {
 lychee = {
 
 
 	title           : document.title,
 	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',
 	update_path     : 'http://lychee.electerious.com/version/index.php',
 	updateURL       : 'https://github.com/electerious/Lychee',
 	updateURL       : 'https://github.com/electerious/Lychee',
@@ -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({
@@ -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 = '') {
 lychee.retinize = function(path = '') {
 
 
 	let pixelRatio = window.devicePixelRatio,
 	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 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 = `

+ 11 - 21
src/scripts/photo.js

@@ -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>`
 
 
 	}
 	}
 
 
@@ -285,7 +285,6 @@ photo.setTitle = function(photoIDs) {
 		// Get old title if only one photo is selected
 		// Get old title if only one photo is selected
 		if (photo.json)      oldTitle = photo.json.title
 		if (photo.json)      oldTitle = photo.json.title
 		else if (album.json) oldTitle = album.json.content[photoIDs].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
 		let newTitle = data.title
 
 
-		// Remove html from input
-		newTitle = lychee.removeHTML(newTitle)
-
 		if (visible.photo()) {
 		if (visible.photo()) {
 			photo.json.title = (newTitle==='' ? 'Untitled' : newTitle)
 			photo.json.title = (newTitle==='' ? 'Untitled' : newTitle)
 			view.photo.title()
 			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({
 	basicModal.show({
 		body: msg,
 		body: msg,
@@ -465,7 +461,7 @@ photo.setPublic = function(photoID, e) {
 
 
 photo.setDescription = function(photoID) {
 photo.setDescription = function(photoID) {
 
 
-	let oldDescription = photo.json.description.replace(/'/g, '&apos;')
+	let oldDescription = photo.json.description
 
 
 	const action = function(data) {
 	const action = function(data) {
 
 
@@ -473,9 +469,6 @@ photo.setDescription = function(photoID) {
 
 
 		let description = data.description
 		let description = data.description
 
 
-		// Remove html from input
-		description = lychee.removeHTML(description)
-
 		if (visible.photo()) {
 		if (visible.photo()) {
 			photo.json.description = description
 			photo.json.description = description
 			view.photo.description()
 			view.photo.description()
@@ -495,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='${ 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',
@@ -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({
 	basicModal.show({
 		body: msg,
 		body: msg,
@@ -571,9 +564,6 @@ photo.setTags = function(photoIDs, tags) {
 	tags = tags.replace(/(\ ,\ )|(\ ,)|(,\ )|(,{1,}\ {0,})|(,$|^,)/g, ',')
 	tags = tags.replace(/(\ ,\ )|(\ ,)|(,\ )|(,{1,}\ {0,})|(,$|^,)/g, ',')
 	tags = tags.replace(/,$|^,|(\ ){0,}$/g, '')
 	tags = tags.replace(/,$|^,|(\ ){0,}$/g, '')
 
 
-	// Remove html from input
-	tags = lychee.removeHTML(tags)
-
 	if (visible.photo()) {
 	if (visible.photo()) {
 		photo.json.tags = tags
 		photo.json.tags = tags
 		view.photo.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)
 	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
 	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
 	location.href = link
 
 

+ 2 - 2
src/scripts/settings.js

@@ -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>
 	          `
 	          `
 
 

+ 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
 	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>
-		         `
-
-		// 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>
 		         </div>
 		         `
 		         `
 
 

+ 2 - 2
src/scripts/upload.js

@@ -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',

+ 7 - 4
src/scripts/view.js

@@ -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 }")`)
 
 
 		}
 		}
 
 

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

@@ -3,18 +3,74 @@
  * @copyright   2015 by Tobias Reich
  * @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() {} },
 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)
 
 

+ 3 - 3
src/styles/_content.scss

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

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