Browse Source

Merge pull request #226 from electerious/v2.6.2

v2.6.2
Tobias Reich 9 years ago
parent
commit
6a6513737f
62 changed files with 904 additions and 310 deletions
  1. 10 1
      .htaccess
  2. 37 0
      CONTRIBUTING.md
  3. 1 0
      README.md
  4. 3 3
      assets/js/album.js
  5. 5 2
      assets/js/build.js
  6. 10 5
      assets/js/contextMenu.js
  7. 4 0
      assets/js/init.js
  8. 3 3
      assets/js/lychee.js
  9. 63 24
      assets/js/multiselect.js
  10. 20 0
      assets/js/photo.js
  11. 8 6
      assets/js/settings.js
  12. 2 1
      assets/js/upload.js
  13. 0 0
      assets/min/main.css
  14. 0 0
      assets/min/main.js
  15. 0 0
      assets/min/view.js
  16. 0 0
      assets/scss/animations.scss
  17. 0 0
      assets/scss/content.scss
  18. 0 0
      assets/scss/contextmenu.scss
  19. 0 0
      assets/scss/font.scss
  20. 0 0
      assets/scss/header.scss
  21. 0 0
      assets/scss/imageview.scss
  22. 0 0
      assets/scss/infobox.scss
  23. 0 0
      assets/scss/loading.scss
  24. 54 0
      assets/scss/main.scss
  25. 0 0
      assets/scss/mediaquery.scss
  26. 0 0
      assets/scss/message.scss
  27. 0 0
      assets/scss/misc.scss
  28. 0 0
      assets/scss/multiselect.scss
  29. 0 0
      assets/scss/reset.scss
  30. 0 0
      assets/scss/tooltip.scss
  31. 0 0
      assets/scss/upload.scss
  32. 2 1
      build/gulpfile.js
  33. 5 4
      build/package.json
  34. 14 0
      docs/Changelog.md
  35. 6 1
      docs/Keyboard Shortcuts.md
  36. 1 1
      docs/Plugins.md
  37. 7 1
      docs/Settings.md
  38. 12 3
      php/access/Admin.php
  39. 2 2
      php/access/Installation.php
  40. 5 5
      php/api.php
  41. 1 1
      php/database/albums_table.sql
  42. 1 1
      php/database/log_table.sql
  43. 1 1
      php/database/photos_table.sql
  44. 1 1
      php/database/settings_content.sql
  45. 1 1
      php/database/settings_table.sql
  46. 13 11
      php/database/update_020100.php
  47. 4 6
      php/database/update_020101.php
  48. 6 7
      php/database/update_020200.php
  49. 55 33
      php/database/update_020500.php
  50. 5 7
      php/database/update_020505.php
  51. 5 7
      php/database/update_020601.php
  52. 46 0
      php/database/update_020602.php
  53. 20 0
      php/define.php
  54. 69 41
      php/modules/Album.php
  55. 113 13
      php/modules/Database.php
  56. 1 7
      php/modules/Log.php
  57. 205 88
      php/modules/Photo.php
  58. 14 5
      php/modules/Settings.php
  59. 8 6
      php/modules/misc.php
  60. 41 5
      plugins/check/index.php
  61. 12 3
      plugins/displaylog/index.php
  62. 8 3
      view.php

+ 10 - 1
.htaccess

@@ -1,10 +1,19 @@
 IndexIgnore *
 
+# ---
 # Uncomment these lines to change PHP parameters if you are using the PHP Apache module
+# ---
 #<IfModule mod_php5.c>
 #	php_value max_execution_time 200
 #	php_value post_max_size 200M
 #	php_value upload_max_size 200M
 #	php_value upload_max_filesize 20M
 #	php_value max_file_uploads 100
-#</IfModule>
+#</IfModule>
+
+# ---
+# Uncomment these lines when you want to allow access to the Lychee API from different origins
+# ---
+#Header add Access-Control-Allow-Origin "*"
+#Header add Access-Control-Allow-Headers "origin, x-requested-with, content-type"
+#Header add Access-Control-Allow-Methods "PUT, GET, POST, DELETE, OPTIONS"

+ 37 - 0
CONTRIBUTING.md

@@ -0,0 +1,37 @@
+## How to report a bug
+
+Read the following before reporting a bug on GitHub:
+
+1. Update to the newest version of Lychee
+2. Update your Browser to the newest version
+2. Take a look in the [FAQ](https://github.com/electerious/Lychee/blob/master/docs/FAQ.md)
+3. Check if someone has [already reported](https://github.com/electerious/Lychee/issues) the same bug
+
+When reporting a bug on GitHub, make sure you include the following information:
+
+- Detailed description of the problem
+- How to reproduce the issue (step-by-step)
+- What you have already tried
+- Output of the diagnostics (`plugins/check/index.php`)
+- Browser and Browser version
+- Attach files when you have problems which specific photos
+
+## Coding Guidelines
+
+Check if there are branches newer than `master`. Always fork the newest available branch.
+
+Please follow the conventions already established in the code.
+
+- **Spacing**:<br>
+  Use tabs for indentation. No spaces.
+
+- **Naming**:<br>
+  Keep variable and method names concise and descriptive.
+
+- **Quotes**:<br>
+  Single-quoted strings are preferred to double-quoted strings
+  
+- **Comments**:<br>
+  Please use single-line comments to annotate significant additions. Use `#` for comments in PHP; `//` for comments in JS and CSS.
+  
+Merge you changes when the forked branch has been updated in the meanwhile. Make sure your code is 100% working before creating a Pull-Request on GitHub.

+ 1 - 0
readme.md → README.md

@@ -53,6 +53,7 @@ Here's a list of all available Plugins and Extensions:
 | Jekyll | Liquid tag for Jekyll sites that allows embedding Lychee albums | [More &#187;](https://gist.github.com/tobru/9171700) |
 | lychee-redirect | Redirect from an album-name to a Lychee-album | [More &#187;](https://github.com/electerious/lychee-redirect) |
 | lychee-watermark | Adds a second watermarked photo when uploading images | [More &#187;](https://github.com/electerious/lychee-watermark) |
+| lychee-rss | Creates a RSS-Feed out of your photos | [More &#187;](https://github.com/cternes/Lychee-RSS) |
 
 ## Troubleshooting
 

+ 3 - 3
assets/js/album.js

@@ -86,7 +86,7 @@ album = {
 
 	},
 
-	parse: function(photo) {
+	parse: function() {
 
 		if (!album.json.title) album.json.title = "Untitled";
 
@@ -143,7 +143,7 @@ album = {
 
 					if (visible.albums()) {
 
-						albumIDs.forEach(function(id, index, array) {
+						albumIDs.forEach(function(id) {
 							albums.json.num--;
 							view.albums.content.delete(id);
 						});
@@ -224,7 +224,7 @@ album = {
 
 				} else if (visible.albums()) {
 
-					albumIDs.forEach(function(id, index, array) {
+					albumIDs.forEach(function(id) {
 						albums.json.content[id].title = newTitle;
 						view.albums.content.title(id);
 					});

+ 5 - 2
assets/js/build.js

@@ -302,7 +302,8 @@ build = {
 			public,
 			editTitleHTML,
 			editDescriptionHTML,
-			infos;
+			infos,
+			exifHash = "";
 
 		infobox += "<div class='header'><h1>About</h1><a class='icon-remove-sign'></a></div>";
 		infobox += "<div class='wrapper'>";
@@ -337,7 +338,9 @@ build = {
 			["Tags", build.tags(photoJSON.tags, forView)]
 		];
 
-		if ((photoJSON.takestamp+photoJSON.make+photoJSON.model+photoJSON.shutter+photoJSON.aperture+photoJSON.focal+photoJSON.iso)!="0") {
+		exifHash = photoJSON.takestamp+photoJSON.make+photoJSON.model+photoJSON.shutter+photoJSON.aperture+photoJSON.focal+photoJSON.iso;
+
+		if (exifHash!="0"&&exifHash!=="null") {
 
 			infos = infos.concat([
 				["", "Camera"],

+ 10 - 5
assets/js/contextMenu.js

@@ -164,6 +164,7 @@ contextMenu = {
 			function() { photo.setStar([photoID]) },
 			function() { photo.editTags([photoID]) },
 			function() { photo.setTitle([photoID]) },
+			function() { photo.duplicate([photoID]) },
 			function() { contextMenu.move([photoID], e, "right") },
 			function() { photo.delete([photoID]) }
 		];
@@ -173,8 +174,9 @@ contextMenu = {
 			["<a class='icon-tags'></a> Tags", 1],
 			["separator", -1],
 			["<a class='icon-edit'></a> Rename", 2],
-			["<a class='icon-folder-open'></a> Move", 3],
-			["<a class='icon-trash'></a> Delete", 4]
+			["<a class='icon-copy'></a> Duplicate", 3],
+			["<a class='icon-folder-open'></a> Move", 4],
+			["<a class='icon-trash'></a> Delete", 5]
 		];
 
 		contextMenu.show(items, mouse_x, mouse_y, "right");
@@ -195,6 +197,7 @@ contextMenu = {
 			function() { photo.setStar(photoIDs) },
 			function() { photo.editTags(photoIDs) },
 			function() { photo.setTitle(photoIDs) },
+			function() { photo.duplicate(photoIDs) },
 			function() { contextMenu.move(photoIDs, e, "right") },
 			function() { photo.delete(photoIDs) }
 		];
@@ -204,8 +207,9 @@ contextMenu = {
 			["<a class='icon-tags'></a> Tag All", 1],
 			["separator", -1],
 			["<a class='icon-edit'></a> Rename All", 2],
-			["<a class='icon-folder-open'></a> Move All", 3],
-			["<a class='icon-trash'></a> Delete All", 4]
+			["<a class='icon-copy'></a> Duplicate All", 3],
+			["<a class='icon-folder-open'></a> Move All", 4],
+			["<a class='icon-trash'></a> Delete All", 5]
 		];
 
 		contextMenu.show(items, mouse_x, mouse_y, "right");
@@ -266,7 +270,8 @@ contextMenu = {
 
 		var mouse_x = e.pageX,
 			mouse_y = e.pageY,
-			items;
+			items,
+			link = "";
 
 		mouse_y -= $(document).scrollTop();
 

+ 4 - 0
assets/js/init.js

@@ -104,6 +104,10 @@ $(document).ready(function(){
 		.bind(['command+backspace', 'ctrl+backspace'], function() {
 			if (visible.photo()&&!visible.message()) photo.delete([photo.getID()]);
 			else if (visible.album()&&!visible.message()) album.delete([album.getID()]);
+		})
+		.bind(['command+a', 'ctrl+a'], function() {
+			if (visible.album()&&!visible.message()) multiselect.selectAll();
+			else if (visible.albums()&&!visible.message()) multiselect.selectAll();
 		});
 
 	Mousetrap.bindGlobal('enter', function() {

+ 3 - 3
assets/js/lychee.js

@@ -8,8 +8,8 @@
 var lychee = {
 
 	title: "",
-	version: "2.6.1",
-	version_code: "020601",
+	version: "2.6.2",
+	version_code: "020602",
 
 	api_path: "php/api.php",
 	update_path: "http://lychee.electerious.com/version/index.php",
@@ -149,7 +149,7 @@ var lychee = {
 
 	logout: function() {
 
-		lychee.api("logout", function(data) {
+		lychee.api("logout", function() {
 			window.location.reload();
 		});
 

+ 63 - 24
assets/js/multiselect.js

@@ -9,31 +9,70 @@ multiselect = {
 
 	position: {
 
-		top: null,
-		right: null,
-		bottom: null,
-		left: null
+		top:	null,
+		right:	null,
+		bottom:	null,
+		left:	null
 
 	},
 
 	show: function(e) {
 
-		if (mobileBrowser()) return false;
-		if (lychee.publicMode) return false;
-		if (visible.search()) return false;
-		if ($('.album:hover, .photo:hover').length!==0) return false;
-		if (visible.multiselect()) $('#multiselect').remove();
+		if (mobileBrowser())	return false;
+		if (lychee.publicMode)	return false;
+		if (visible.search())	return false;
+		if (visible.infobox())	return false;
+		if (!visible.albums()&&!visible.album)			return false;
+		if ($('.album:hover, .photo:hover').length!==0)	return false;
+		if (visible.multiselect())						$('#multiselect').remove();
 
-		multiselect.position.top = e.pageY;
-		multiselect.position.right = -1 * (e.pageX - $(document).width());
-		multiselect.position.bottom = -1 * (multiselect.position.top - $(window).height());
-		multiselect.position.left = e.pageX;
+		multiselect.position.top	= e.pageY;
+		multiselect.position.right	= -1 * (e.pageX - $(document).width());
+		multiselect.position.bottom	= -1 * (multiselect.position.top - $(window).height());
+		multiselect.position.left	= e.pageX;
 
 		$('body').append(build.multiselect(multiselect.position.top, multiselect.position.left));
 		$(document).on('mousemove', multiselect.resize);
 
 	},
 
+	selectAll: function() {
+
+		var e,
+			newWidth,
+			newHeight;
+
+		if (mobileBrowser())		return false;
+		if (lychee.publicMode)		return false;
+		if (visible.search())		return false;
+		if (visible.infobox())		return false;
+		if (!visible.albums()&&!visible.album)	return false;
+		if (visible.multiselect())	$('#multiselect').remove();
+
+		multiselect.position.top	= 70;
+		multiselect.position.right	= 40;
+		multiselect.position.bottom	= 90;
+		multiselect.position.left	= 20;
+
+		$('body').append(build.multiselect(multiselect.position.top, multiselect.position.left));
+
+		newWidth	= $(document).width() - multiselect.position.right + 2;
+		newHeight	= $(document).height() - multiselect.position.bottom;
+
+		$('#multiselect').css({
+			width: newWidth,
+			height: newHeight
+		});
+
+		e = {
+			pageX: $(document).width() - (multiselect.position.right / 2),
+			pageY: $(document).height() - multiselect.position.bottom
+		};
+
+		multiselect.getSelection(e);
+
+	},
+
 	resize: function(e) {
 
 		var mouse_x = e.pageX,
@@ -49,7 +88,7 @@ multiselect = {
 		if (mouse_y>=multiselect.position.top) {
 
 			// Do not leave the screen
-			newHeight = e.pageY - multiselect.position.top;
+			newHeight = mouse_y - multiselect.position.top;
 			if ((multiselect.position.top+newHeight)>=$(document).height())
 				newHeight -= (multiselect.position.top + newHeight) - $(document).height() + 2;
 
@@ -72,7 +111,7 @@ multiselect = {
 		if (mouse_x>=multiselect.position.left) {
 
 			// Do not leave the screen
-			newWidth = e.pageX - multiselect.position.left;
+			newWidth = mouse_x - multiselect.position.left;
 			if ((multiselect.position.left+newWidth)>=$(document).width())
 				newWidth -= (multiselect.position.left + newWidth) - $(document).width() + 2;
 
@@ -105,10 +144,10 @@ multiselect = {
 		if (!visible.multiselect()) return false;
 
 		return {
-			top: $('#multiselect').offset().top,
-			left: $('#multiselect').offset().left,
-			width: parseInt($('#multiselect').css('width').replace('px', '')),
-			height: parseInt($('#multiselect').css('height').replace('px', ''))
+			top:	$('#multiselect').offset().top,
+			left:	$('#multiselect').offset().left,
+			width:	parseInt($('#multiselect').css('width').replace('px', '')),
+			height:	parseInt($('#multiselect').css('height').replace('px', ''))
 		};
 
 	},
@@ -135,7 +174,7 @@ multiselect = {
 
 					id = $(this).data('id');
 
-					if (id!=='0'&&id!==0&&id!=='f'&&id!=='s'&&id!=='r'&&id!==null&id!==undefined) {
+					if (id!=='0'&&id!==0&&id!=='f'&&id!=='s'&&id!=='r'&&id!==null&&id!==undefined) {
 
 						ids.push(id);
 						$(this).addClass('active');
@@ -156,10 +195,10 @@ multiselect = {
 
 		multiselect.stopResize();
 
-		multiselect.position.top = null;
-		multiselect.position.right = null;
-		multiselect.position.bottom = null;
-		multiselect.position.left = null;
+		multiselect.position.top	= null;
+		multiselect.position.right	= null;
+		multiselect.position.bottom	= null;
+		multiselect.position.left	= null;
 
 		lychee.animate('#multiselect', 'fadeOut');
 		setTimeout(function() {

+ 20 - 0
assets/js/photo.js

@@ -117,6 +117,23 @@ photo = {
 
 	},
 
+	duplicate: function(photoIDs) {
+
+		var params;
+
+		if (!photoIDs) return false;
+		if (photoIDs instanceof Array===false) photoIDs = [photoIDs];
+
+		params = "duplicatePhoto&photoIDs=" + photoIDs;
+		lychee.api(params, function(data) {
+
+			if (data!==true) lychee.error(null, params, data);
+			else album.load(album.getID(), false);
+
+		});
+
+	},
+
 	delete: function(photoIDs) {
 
 		var params,
@@ -136,6 +153,9 @@ photo = {
 		buttons = [
 			["", function() {
 
+				var nextPhoto,
+					previousPhoto;
+
 				photoIDs.forEach(function(id, index, array) {
 
 					// Change reference for the next and previous photo

+ 8 - 6
assets/js/settings.js

@@ -13,21 +13,23 @@ var settings = {
 			dbUser,
 			dbPassword,
 			dbHost,
+			dbTablePrefix,
 			buttons,
 			params;
 
 		buttons = [
 			["Connect", function() {
 
-				dbHost = $(".message input.text#dbHost").val();
-				dbUser = $(".message input.text#dbUser").val();
-				dbPassword = $(".message input.text#dbPassword").val();
-				dbName = $(".message input.text#dbName").val();
+				dbHost			= $(".message input.text#dbHost").val();
+				dbUser			= $(".message input.text#dbUser").val();
+				dbPassword		= $(".message input.text#dbPassword").val();
+				dbName			= $(".message input.text#dbName").val();
+				dbTablePrefix	= $(".message input.text#dbTablePrefix").val();
 
 				if (dbHost.length<1) dbHost = "localhost";
 				if (dbName.length<1) dbName = "lychee";
 
-				params = "dbCreateConfig&dbName=" + escape(dbName) + "&dbUser=" + escape(dbUser) + "&dbPassword=" + escape(dbPassword) + "&dbHost=" + escape(dbHost);
+				params = "dbCreateConfig&dbName=" + escape(dbName) + "&dbUser=" + escape(dbUser) + "&dbPassword=" + escape(dbPassword) + "&dbHost=" + escape(dbHost) + "&dbTablePrefix=" + escape(dbTablePrefix);
 				lychee.api(params, function(data) {
 
 					if (data!==true) {
@@ -94,7 +96,7 @@ var settings = {
 			["", function() {}]
 		];
 
-		modal.show("Configuration", "Enter your database connection details below: <input id='dbHost' class='text less' type='text' placeholder='Host (optional)' value=''><input id='dbUser' class='text less' type='text' placeholder='Username' value=''><input id='dbPassword' class='text more' type='password' placeholder='Password' value=''><br>Lychee will create its own database. If required, you can enter the name of an existing database instead:<input id='dbName' class='text more' type='text' placeholder='Database (optional)' value=''>", buttons, -215, false);
+		modal.show("Configuration", "Enter your database connection details below: <input id='dbHost' class='text less' type='text' placeholder='Database Host (optional)' value=''><input id='dbUser' class='text less' type='text' placeholder='Database Username' value=''><input id='dbPassword' class='text more' type='password' placeholder='Database Password' value=''><br>Lychee will create its own database. If required, you can enter the name of an existing database instead:<input id='dbName' class='text less' type='text' placeholder='Database Name (optional)' value=''><input id='dbTablePrefix' class='text more' type='text' placeholder='Table prefix (optional)' value=''>", buttons, -235, false);
 
 	},
 

+ 2 - 1
assets/js/upload.js

@@ -12,7 +12,7 @@ upload = {
 		upload.close(true);
 		$("body").append(build.uploadModal(title, files));
 
-		if (callback!=null&&callback!=undefined) callback();
+		if (callback!==null&&callback!==undefined) callback();
 
 	},
 
@@ -101,6 +101,7 @@ upload = {
 
 					formData.append("function", "upload");
 					formData.append("albumID", albumID);
+					formData.append("tags", "");
 					formData.append(0, file);
 
 					xhr.open("POST", lychee.api_path);

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


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


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


+ 0 - 0
assets/css/animations.css → assets/scss/animations.scss


+ 0 - 0
assets/css/content.css → assets/scss/content.scss


+ 0 - 0
assets/css/contextmenu.css → assets/scss/contextmenu.scss


+ 0 - 0
assets/css/font.css → assets/scss/font.scss


+ 0 - 0
assets/css/header.css → assets/scss/header.scss


+ 0 - 0
assets/css/imageview.css → assets/scss/imageview.scss


+ 0 - 0
assets/css/infobox.css → assets/scss/infobox.scss


+ 0 - 0
assets/css/loading.css → assets/scss/loading.scss


+ 54 - 0
assets/scss/main.scss

@@ -0,0 +1,54 @@
+/**
+ * @author      Tobias Reich
+ * @copyright   2014 by Tobias Reich
+ */
+
+@import "reset";
+
+html,
+body {
+	min-height: 100%;
+}
+body {
+	background-color: #222;
+	font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;
+	font-size: 12px;
+	-webkit-font-smoothing: antialiased;
+	-moz-font-smoothing: antialiased;
+	-moz-osx-font-smoothing: grayscale;
+	font-smoothing: antialiased;
+}
+body.view {
+	background-color: #0f0f0f;
+}
+.center {
+	position: absolute;
+	left: 50%;
+	top:50%;
+}
+* {
+	-webkit-user-select: none;
+	-moz-user-select: none;
+	user-select: none;
+	transition: color .3s, opacity .3s ease-out, transform .3s ease-out, box-shadow .3s;
+}
+input {
+	-webkit-user-select: text !important;
+	-moz-user-select: text !important;
+	user-select: text !important;
+}
+
+@import "animations";
+@import "content";
+@import "contextmenu";
+@import "font";
+@import "header";
+@import "imageview";
+@import "infobox";
+@import "loading";
+@import "message";
+@import "misc";
+@import "multiselect";
+@import "tooltip";
+@import "upload";
+@import "mediaquery";

+ 0 - 0
assets/css/mediaquery.css → assets/scss/mediaquery.scss


+ 0 - 0
assets/css/message.css → assets/scss/message.scss


+ 0 - 0
assets/css/misc.css → assets/scss/misc.scss


+ 0 - 0
assets/css/multiselect.css → assets/scss/multiselect.scss


+ 0 - 0
assets/css/_reset.css → assets/scss/reset.scss


+ 0 - 0
assets/css/tooltip.css → assets/scss/tooltip.scss


+ 0 - 0
assets/css/upload.css → assets/scss/upload.scss


+ 2 - 1
build/gulpfile.js

@@ -19,7 +19,7 @@ paths = {
 		'../assets/js/*.js'
 	],
 	css: [
-		'../assets/css/*.css'
+		'../assets/scss/main.scss'
 	]
 }
 
@@ -44,6 +44,7 @@ gulp.task('js', function () {
 gulp.task('css', function () {
 
 	gulp.src(paths.css)
+		.pipe(plugins.sass())
 		.pipe(plugins.concat('main.css', {newLine: "\n"}))
 		.pipe(plugins.autoprefixer('last 4 versions', '> 5%'))
 		.pipe(plugins.minifyCss())

+ 5 - 4
build/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Lychee",
-  "version": "2.6.1",
+  "version": "2.6.2",
   "description": "Self-hosted photo-management done right.",
   "authors": "Tobias Reich <tobias.reich.ich@gmail.com>",
   "license": "MIT",
@@ -10,10 +10,11 @@
   },
   "devDependencies": {
     "gulp": "^3.8.7",
-    "gulp-autoprefixer": "0.0.8",
+    "gulp-autoprefixer": "0.0.10",
     "gulp-concat": "^2.3.4",
-    "gulp-load-plugins": "^0.5.3",
+    "gulp-load-plugins": "^0.6.0",
     "gulp-minify-css": "^0.3.7",
-    "gulp-uglify": "^0.3.1"
+    "gulp-sass": "^0.7.3",
+    "gulp-uglify": "^1.0.0"
   }
 }

+ 14 - 0
docs/Changelog.md

@@ -1,3 +1,17 @@
+## v2.6.2
+
+Released September ??, 2014
+
+- `New` Select all albums/photos with `cmd+a` or `ctrl+a`
+- `New` Detect duplicates and only save one file (#48)
+- `New` Duplicate photos (#186)
+- `New` Added contributing guide
+- `New` Database table prefix for multiple Lychee installations (#196)
+- `Improved` Use IPTC Title when Headline not available (#216)
+- `Improved` Diagnostics are showing system information
+- `Improved` Harden against SQL injection attacks (#38)
+- `Fixed` a problem with htmlentities and older PHP versions (#212)
+
 ## v2.6.1
 
 Released August 22, 2014

+ 6 - 1
docs/Keyboard Shortcuts.md

@@ -7,12 +7,15 @@ The following keys and shortcuts can be used in Lychee. Single char-shortcuts ar
 |:-----------|:------------|
 | `u` | Upload photo |
 | `enter` | Confirm Dialog |
-| `esc` or `cmd`+`up` | Close/Back |
+| `esc` | Close/Back |
+| `cmd`+`up` | Close/Back |
 
 ### Albums
 | Key | Action |
 |:-----------|:------------|
 | `s` or `f` | Search |
+| `cmd`+`a` | Select all albums |
+| `ctrl`+`a` | Select all albums |
 
 ### Album
 | Key | Action |
@@ -22,6 +25,8 @@ The following keys and shortcuts can be used in Lychee. Single char-shortcuts ar
 | `i` | Show information |
 | `cmd`+`backspace` | Delete album |
 | `ctrl`+`backspace` | Delete album |
+| `cmd`+`a` | Select all photos |
+| `ctrl`+`a` | Select all photos |
 
 ### Photo
 | Key | Action |

+ 1 - 1
docs/Plugins.md

@@ -70,7 +70,7 @@ $plugins->attach(new ExamplePlugin($database, $settings));
 
 Select the table `lychee_settings` and edit the value of `plugins` to the path of your plugin. The path must be relative from the `plugins/`-folder: `ExamplePlugin/index.php`.
 
-Divide multiple plugins with commas: `Plugin01/index.php,Plugin02/index.php`.
+Divide multiple plugins with semicolons: `Plugin01/index.php;Plugin02/index.php`.
 
 ### Available hooks
 

+ 7 - 1
docs/Settings.md

@@ -44,4 +44,10 @@ A typical part of a MySQL statement. This string will be appended to mostly ever
 
 This key is required to use the Dropbox import feature from your server. Lychee will ask you for this key, the first time you try to use the import. You can get your personal drop-ins app key from [their website](https://www.dropbox.com/developers/apps/create).
 
-	dropboxKey = Your personal App Key
+	dropboxKey = Your personal App Key
+	
+#### Imagick
+
+	imagick = [0|1]
+	
+If `1`, Lychee will use Imagick when available. Disable [Imagick](http://www.imagemagick.org) if you have problems or if you are using an outdated version. Lychee will use [GD](http://php.net/manual/en/book.image.php) when Imagick is disabled or not available.

+ 12 - 3
php/access/Admin.php

@@ -32,6 +32,7 @@ class Admin extends Access {
 			case 'setPhotoPublic':		$this->setPhotoPublic(); break;
 			case 'setPhotoAlbum':		$this->setPhotoAlbum(); break;
 			case 'setPhotoTags':		$this->setPhotoTags(); break;
+			case 'duplicatePhoto':		$this->duplicatePhoto(); break;
 			case 'deletePhoto':			$this->deletePhoto(); break;
 
 			# Add functions
@@ -119,7 +120,7 @@ class Admin extends Access {
 
 		Module::dependencies(isset($_POST['albumIDs']));
 		$album = new Album($this->database, $this->plugins, $this->settings, $_POST['albumIDs']);
-		echo $album->delete($_POST['albumIDs']);
+		echo $album->delete();
 
 	}
 
@@ -181,6 +182,14 @@ class Admin extends Access {
 
 	}
 
+	private function duplicatePhoto() {
+
+		Module::dependencies(isset($_POST['photoIDs']));
+		$photo = new Photo($this->database, $this->plugins, null, $_POST['photoIDs']);
+		echo $photo->duplicate();
+
+	}
+
 	private function deletePhoto() {
 
 		Module::dependencies(isset($_POST['photoIDs']));
@@ -193,9 +202,9 @@ class Admin extends Access {
 
 	private function upload() {
 
-		Module::dependencies(isset($_FILES, $_POST['albumID']));
+		Module::dependencies(isset($_FILES, $_POST['albumID'], $_POST['tags']));
 		$photo = new Photo($this->database, $this->plugins, $this->settings, null);
-		echo $photo->add($_FILES, $_POST['albumID']);
+		echo $photo->add($_FILES, $_POST['albumID'], '', $_POST['tags']);
 
 	}
 

+ 2 - 2
php/access/Installation.php

@@ -29,8 +29,8 @@ class Installation extends Access {
 
 	private function dbCreateConfig() {
 
-		Module::dependencies(isset($_POST['dbHost'], $_POST['dbUser'], $_POST['dbPassword'], $_POST['dbName']));
-		echo Database::createConfig($_POST['dbHost'], $_POST['dbUser'], $_POST['dbPassword'], $_POST['dbName']);
+		Module::dependencies(isset($_POST['dbHost'], $_POST['dbUser'], $_POST['dbPassword'], $_POST['dbName'], $_POST['dbTablePrefix']));
+		echo Database::createConfig($_POST['dbHost'], $_POST['dbUser'], $_POST['dbPassword'], $_POST['dbName'], $_POST['dbTablePrefix']);
 
 	}
 

+ 5 - 5
php/api.php

@@ -17,13 +17,9 @@ if (!empty($_POST['function'])||!empty($_GET['function'])) {
 	session_start();
 	date_default_timezone_set('UTC');
 
-	# Define globals
+	# Load required files
 	require(__DIR__ . '/define.php');
-
-	# Load autoload
 	require(__DIR__ . '/autoload.php');
-
-	# Load modules
 	require(__DIR__ . '/modules/misc.php');
 
 	if (file_exists(LYCHEE_CONFIG_FILE)) require(LYCHEE_CONFIG_FILE);
@@ -43,6 +39,10 @@ if (!empty($_POST['function'])||!empty($_GET['function'])) {
 
 	}
 
+	# Define the table prefix
+	if (!isset($dbTablePrefix)) $dbTablePrefix = '';
+	defineTablePrefix($dbTablePrefix);
+
 	# Connect to database
 	$database = Database::connect($dbHost, $dbUser, $dbPassword, $dbName);
 

+ 1 - 1
php/database/albums_table.sql

@@ -2,7 +2,7 @@
 # Version 2.5
 # ------------------------------------------------------------
 
-CREATE TABLE IF NOT EXISTS `lychee_albums` (
+CREATE TABLE IF NOT EXISTS `?` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `title` varchar(50) NOT NULL,
   `description` varchar(1000) DEFAULT '',

+ 1 - 1
php/database/log_table.sql

@@ -2,7 +2,7 @@
 # Version 2.5
 # ------------------------------------------------------------
 
-CREATE TABLE IF NOT EXISTS `lychee_log` (
+CREATE TABLE IF NOT EXISTS `?` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `time` int(11) NOT NULL,
   `type` varchar(11) NOT NULL,

+ 1 - 1
php/database/photos_table.sql

@@ -2,7 +2,7 @@
 # Version 2.5
 # ------------------------------------------------------------
 
-CREATE TABLE IF NOT EXISTS `lychee_photos` (
+CREATE TABLE IF NOT EXISTS `?` (
   `id` bigint(14) NOT NULL,
   `title` varchar(50) NOT NULL,
   `description` varchar(1000) DEFAULT '',

+ 1 - 1
php/database/settings_content.sql

@@ -2,7 +2,7 @@
 # Version 2.5
 # ------------------------------------------------------------
 
-INSERT INTO `lychee_settings` (`key`, `value`)
+INSERT INTO `?` (`key`, `value`)
 VALUES
   ('version',''),
   ('username',''),

+ 1 - 1
php/database/settings_table.sql

@@ -2,7 +2,7 @@
 # Version 2.5
 # ------------------------------------------------------------
 
-CREATE TABLE IF NOT EXISTS `lychee_settings` (
+CREATE TABLE IF NOT EXISTS `?` (
   `key` varchar(50) NOT NULL DEFAULT '',
   `value` varchar(200) DEFAULT ''
 ) ENGINE=MyISAM DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

+ 13 - 11
php/database/update_020100.php

@@ -6,36 +6,38 @@
 # @copyright	2014 by Tobias Reich
 ###
 
-if(!$database->query("SELECT `tags` FROM `lychee_photos` LIMIT 1;")) {
-	$result = $database->query("ALTER TABLE `lychee_photos` ADD `tags` VARCHAR( 1000 ) NULL DEFAULT ''");
+$query = Database::prepare($database, "SELECT `tags` FROM `?` LIMIT 1", array(LYCHEE_TABLE_PHOTOS));
+if(!$database->query($query)) {
+	$query = Database::prepare($database, "ALTER TABLE `?` ADD `tags` VARCHAR( 1000 ) NULL DEFAULT ''", array(LYCHEE_TABLE_PHOTOS));
+	$result = $database->query($query);
 	if (!$result) {
 		Log::error($database, 'update_020100', __LINE__, 'Could not update database (' . $database->error . ')');
 		return false;
 	}
 }
 
-$result = $database->query("SELECT `key` FROM `lychee_settings` WHERE `key` = 'dropboxKey' LIMIT 1;");
+$query	= Database::prepare($database, "SELECT `key` FROM `?` WHERE `key` = 'dropboxKey' LIMIT 1", array(LYCHEE_TABLE_SETTINGS));
+$result	= $database->query($query);
 if ($result->num_rows===0) {
-	$result = $database->query("INSERT INTO `lychee_settings` (`key`, `value`) VALUES ('dropboxKey', '')");
+	$query	= Database::prepare($database, "INSERT INTO `?` (`key`, `value`) VALUES ('dropboxKey', '')", array(LYCHEE_TABLE_SETTINGS));
+	$result	= $database->query($query);
 	if (!$result) {
 		Log::error($database, 'update_020100', __LINE__, 'Could not update database (' . $database->error . ')');
 		return false;
 	}
 }
 
-$result = $database->query("SELECT `key` FROM `lychee_settings` WHERE `key` = 'version' LIMIT 1;");
+$query	= Database::prepare($database, "SELECT `key` FROM `?` WHERE `key` = 'version' LIMIT 1", array(LYCHEE_TABLE_SETTINGS));
+$result	= $database->query($query);
 if ($result->num_rows===0) {
-	$result = $database->query("INSERT INTO `lychee_settings` (`key`, `value`) VALUES ('version', '020100')");
+	$query	= Database::prepare($database, "INSERT INTO `?` (`key`, `value`) VALUES ('version', '020100')", array(LYCHEE_TABLE_SETTINGS));
+	$result	= $database->query($query);
 	if (!$result) {
 		Log::error($database, 'update_020100', __LINE__, 'Could not update database (' . $database->error . ')');
 		return false;
 	}
 } else {
-	$result = $database->query("UPDATE lychee_settings SET value = '020100' WHERE `key` = 'version';");
-	if (!$result) {
-		Log::error($database, 'update_020100', __LINE__, 'Could not update database (' . $database->error . ')');
-		return false;
-	}
+	if (Database::setVersion($database, '020100')===false) return false;
 }
 
 ?>

+ 4 - 6
php/database/update_020101.php

@@ -6,16 +6,14 @@
 # @copyright	2014 by Tobias Reich
 ###
 
-$result = $database->query("ALTER TABLE `lychee_settings` CHANGE `value` `value` VARCHAR( 200 ) NULL DEFAULT ''");
+$query	= Database::prepare($database, "ALTER TABLE `?` CHANGE `value` `value` VARCHAR( 200 ) NULL DEFAULT ''", array(LYCHEE_TABLE_SETTINGS));
+$result	= $database->query($query);
 if (!$result) {
 	Log::error($database, 'update_020101', __LINE__, 'Could not update database (' . $database->error . ')');
 	return false;
 }
 
-$result = $database->query("UPDATE lychee_settings SET value = '020101' WHERE `key` = 'version';");
-if (!$result) {
-	Log::error($database, 'update_020101', __LINE__, 'Could not update database (' . $database->error . ')');
-	return false;
-}
+# Set version
+if (Database::setVersion($database, '020101')===false) return false;
 
 ?>

+ 6 - 7
php/database/update_020200.php

@@ -6,18 +6,17 @@
 # @copyright	2014 by Tobias Reich
 ###
 
-if (!$database->query("SELECT `visible` FROM `lychee_albums` LIMIT 1;")) {
-	$result = $database->query("ALTER TABLE `lychee_albums` ADD `visible` TINYINT(1) NOT NULL DEFAULT 1");
+$query = Database::prepare($database, "SELECT `visible` FROM `?` LIMIT 1", array(LYCHEE_TABLE_ALBUMS));
+if (!$database->query($query)) {
+	$query	= Database::prepare($database, "ALTER TABLE `?` ADD `visible` TINYINT(1) NOT NULL DEFAULT 1", array(LYCHEE_TABLE_ALBUMS));
+	$result	= $database->query($query);
 	if (!$result) {
 		Log::error($database, 'update_020200', __LINE__, 'Could not update database (' . $database->error . ')');
 		return false;
 	}
 }
 
-$result = $database->query("UPDATE lychee_settings SET value = '020200' WHERE `key` = 'version';");
-if (!$result) {
-	Log::error($database, 'update_020200', __LINE__, 'Could not update database (' . $database->error . ')');
-	return false;
-}
+# Set version
+if (Database::setVersion($database, '020200')===false) return false;
 
 ?>

+ 55 - 33
php/database/update_020500.php

@@ -7,9 +7,11 @@
 ###
 
 # Add `plugins`
-$result = $database->query("SELECT `key` FROM `lychee_settings` WHERE `key` = 'plugins' LIMIT 1;");
+$query	= Database::prepare($database, "SELECT `key` FROM `?` WHERE `key` = 'plugins' LIMIT 1", array(LYCHEE_TABLE_SETTINGS));
+$result	= $database->query($query);
 if ($result->num_rows===0) {
-	$result = $database->query("INSERT INTO `lychee_settings` (`key`, `value`) VALUES ('plugins', '')");
+	$query	= Database::prepare($database, "INSERT INTO `?` (`key`, `value`) VALUES ('plugins', '')", array(LYCHEE_TABLE_SETTINGS));
+	$result	= $database->query($query);
 	if (!$result) {
 		Log::error($database, 'update_020500', __LINE__, 'Could not update database (' . $database->error . ')');
 		return false;
@@ -17,8 +19,10 @@ if ($result->num_rows===0) {
 }
 
 # Add `takestamp`
-if (!$database->query("SELECT `takestamp` FROM `lychee_photos` LIMIT 1;")) {
-	$result = $database->query("ALTER TABLE `lychee_photos` ADD `takestamp` INT(11) DEFAULT NULL");
+$query = Database::prepare($database, "SELECT `takestamp` FROM `?` LIMIT 1;", array(LYCHEE_TABLE_PHOTOS));
+if (!$database->query($query)) {
+	$query	= Database::prepare($database, "ALTER TABLE `?` ADD `takestamp` INT(11) DEFAULT NULL", array(LYCHEE_TABLE_PHOTOS));
+	$result = $database->query($query);
 	if (!$result) {
 		Log::error($database, 'update_020500', __LINE__, 'Could not update database (' . $database->error . ')');
 		return false;
@@ -26,34 +30,46 @@ if (!$database->query("SELECT `takestamp` FROM `lychee_photos` LIMIT 1;")) {
 }
 
 # Convert to `takestamp`
-if ($database->query("SELECT `takedate`, `taketime` FROM `lychee_photos` LIMIT 1;")) {
-	$result = $database->query("SELECT `id`, `takedate`, `taketime` FROM `lychee_photos` WHERE `takedate` <> '' AND `taketime` <> '';");
+$query = Database::prepare($database, "SELECT `takedate`, `taketime` FROM `?` LIMIT 1;", array(LYCHEE_TABLE_PHOTOS));
+if ($database->query($query)) {
+	$query	= Database::prepare($database, "SELECT `id`, `takedate`, `taketime` FROM `?` WHERE `takedate` <> '' AND `taketime` <> ''", array(LYCHEE_TABLE_PHOTOS));
+	$result	= $database->query($query);
 	if (!$result) {
 		Log::error($database, 'update_020500', __LINE__, 'Could not update database (' . $database->error . ')');
 		return false;
 	}
 	while ($photo = $result->fetch_object()) {
-		$takestamp = strtotime($photo->takedate . $photo->taketime);
-		$database->query("UPDATE `lychee_photos` SET `takestamp` = '$takestamp' WHERE `id` = '$photo->id';");
+		$takestamp	= strtotime($photo->takedate . $photo->taketime);
+		$query		= Database::prepare($database, "UPDATE `?` SET `takestamp` = '?' WHERE `id` = '?'", array(LYCHEE_TABLE_PHOTOS, $takestamp, $photo->id));
+		$database->query($query);
 	}
-	$result = $database->query("ALTER TABLE `lychee_photos` DROP COLUMN `takedate`;");
-	$result = $database->query("ALTER TABLE `lychee_photos` DROP COLUMN `taketime`;");
+	$query	= Database::prepare($database, "ALTER TABLE `?` DROP COLUMN `takedate`;", array(LYCHEE_TABLE_PHOTOS));
+	$result	= $database->query($query);
+	$query	= Database::prepare($database, "ALTER TABLE `?` DROP COLUMN `taketime`", array(LYCHEE_TABLE_PHOTOS));
+	$result	= $database->query($query);
 }
 
 # Remove `import_name`
-if ($database->query("SELECT `import_name` FROM `lychee_photos` LIMIT 1;")) {
-	$result = $database->query("ALTER TABLE `lychee_photos` DROP COLUMN `import_name`;");
+$query = Database::prepare($database, "SELECT `import_name` FROM `?` LIMIT 1", array(LYCHEE_TABLE_PHOTOS));
+if ($database->query($query)) {
+	$query	= Database::prepare($database, "ALTER TABLE `?` DROP COLUMN `import_name`", array(LYCHEE_TABLE_PHOTOS));
+	$result	= $database->query($query);
 }
 
 # Remove `sysdate` and `systime`
-if ($database->query("SELECT `sysdate`, `systime` FROM `lychee_photos` LIMIT 1;")) {
-	$result = $database->query("ALTER TABLE `lychee_photos` DROP COLUMN `sysdate`;");
-	$result = $database->query("ALTER TABLE `lychee_photos` DROP COLUMN `systime`;");
+$query = Database::prepare($database, "SELECT `sysdate`, `systime` FROM `?` LIMIT 1", array(LYCHEE_TABLE_PHOTOS));
+if ($database->query($query)) {
+	$query	= Database::prepare($database, "ALTER TABLE `?` DROP COLUMN `sysdate`", array(LYCHEE_TABLE_PHOTOS));
+	$result	= $database->query($query);
+	$query	= Database::prepare($database, "ALTER TABLE `?` DROP COLUMN `systime`", array(LYCHEE_TABLE_PHOTOS));
+	$result	= $database->query($query);
 }
 
 # Add `sysstamp`
-if (!$database->query("SELECT `sysstamp` FROM `lychee_albums` LIMIT 1;")) {
-	$result = $database->query("ALTER TABLE `lychee_albums` ADD `sysstamp` INT(11) DEFAULT NULL");
+$query = Database::prepare($database, "SELECT `sysstamp` FROM `?` LIMIT 1", array(LYCHEE_TABLE_ALBUMS));
+if (!$database->query($query)) {
+	$query	= Database::prepare($database, "ALTER TABLE `?` ADD `sysstamp` INT(11) DEFAULT NULL", array(LYCHEE_TABLE_ALBUMS));
+	$result	= $database->query($query);
 	if (!$result) {
 		Log::error($database, 'update_020500', __LINE__, 'Could not update database (' . $database->error . ')');
 		return false;
@@ -61,17 +77,21 @@ if (!$database->query("SELECT `sysstamp` FROM `lychee_albums` LIMIT 1;")) {
 }
 
 # Convert to `sysstamp`
-if ($database->query("SELECT `sysdate` FROM `lychee_albums` LIMIT 1;")) {
-	$result = $database->query("SELECT `id`, `sysdate` FROM `lychee_albums`;");
+$query = Database::prepare($database, "SELECT `sysdate` FROM `?` LIMIT 1", array(LYCHEE_TABLE_ALBUMS));
+if ($database->query($query)) {
+	$query	= Database::prepare($database, "SELECT `id`, `sysdate` FROM `?`", array(LYCHEE_TABLE_ALBUMS));
+	$result	= $database->query($query);
 	if (!$result) {
 		Log::error($database, 'update_020500', __LINE__, 'Could not update database (' . $database->error . ')');
 		return false;
 	}
 	while ($album = $result->fetch_object()) {
-		$sysstamp = strtotime($album->sysdate);
-		$database->query("UPDATE `lychee_albums` SET `sysstamp` = '$sysstamp' WHERE `id` = '$album->id';");
+		$sysstamp	= strtotime($album->sysdate);
+		$query		= Database::prepare($database, "UPDATE `?` SET `sysstamp` = '?' WHERE `id` = '?'", array(LYCHEE_TABLE_ALBUMS, $sysstamp, $album->id));
+		$database->query($query);
 	}
-	$result = $database->query("ALTER TABLE `lychee_albums` DROP COLUMN `sysdate`;");
+	$query	= Database::prepare($database, "ALTER TABLE `?` DROP COLUMN `sysdate`", array(LYCHEE_TABLE_ALBUMS));
+	$result	= $database->query($query);
 }
 
 # Set character of database
@@ -82,52 +102,54 @@ if (!$result) {
 }
 
 # Set character
-$result = $database->query("ALTER TABLE `lychee_albums` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;");
+$query	= Database::prepare($database, "ALTER TABLE `?` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci", array(LYCHEE_TABLE_ALBUMS));
+$result	= $database->query($query);
 if (!$result) {
 	Log::error($database, 'update_020500', __LINE__, 'Could not update database (' . $database->error . ')');
 	return false;
 }
 
 # Set character
-$result = $database->query("ALTER TABLE `lychee_photos` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;");
+$query	= Database::prepare($database, "ALTER TABLE `?` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci", array(LYCHEE_TABLE_PHOTOS));
+$result	= $database->query($query);
 if (!$result) {
 	Log::error($database, 'update_020500', __LINE__, 'Could not update database (' . $database->error . ')');
 	return false;
 }
 
 # Set character
-$result = $database->query("ALTER TABLE `lychee_settings` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;");
+$query	= Database::prepare($database, "ALTER TABLE `?` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci", array(LYCHEE_TABLE_SETTINGS));
+$result	= $database->query($query);
 if (!$result) {
 	Log::error($database, 'update_020500', __LINE__, 'Could not update database (' . $database->error . ')');
 	return false;
 }
 
 # Set album password length to 100 (for longer hashes)
-$result = $database->query("ALTER TABLE `lychee_albums` CHANGE `password` `password` VARCHAR(100);");
+$query	= Database::prepare($database, "ALTER TABLE `?` CHANGE `password` `password` VARCHAR(100)", array(LYCHEE_TABLE_ALBUMS));
+$result	= $database->query($query);
 if (!$result) {
 	Log::error($database, 'update_020500', __LINE__, 'Could not update database (' . $database->error . ')');
 	return false;
 }
 
 # Set make length to 50
-$result = $database->query("ALTER TABLE `lychee_photos` CHANGE `make` `make` VARCHAR(50);");
+$query	= Database::prepare($database, "ALTER TABLE `?` CHANGE `make` `make` VARCHAR(50)", array(LYCHEE_TABLE_PHOTOS));
+$result	= $database->query($query);
 if (!$result) {
 	Log::error($database, 'update_020500', __LINE__, 'Could not update database (' . $database->error . ')');
 	return false;
 }
 
 # Reset sorting
-$result = $database->query("UPDATE lychee_settings SET value = 'ORDER BY takestamp DESC' WHERE `key` = 'sorting' AND `value` LIKE '%UNIX_TIMESTAMP%';");
+$query	= Database::prepare($database, "UPDATE ? SET value = 'ORDER BY takestamp DESC' WHERE `key` = 'sorting' AND `value` LIKE '%UNIX_TIMESTAMP%'", array(LYCHEE_TABLE_SETTINGS));
+$result	= $database->query($query);
 if (!$result) {
 	Log::error($database, 'update_020500', __LINE__, 'Could not update database (' . $database->error . ')');
 	return false;
 }
 
 # Set version
-$result = $database->query("UPDATE lychee_settings SET value = '020500' WHERE `key` = 'version';");
-if (!$result) {
-	Log::error($database, 'update_020500', __LINE__, 'Could not update database (' . $database->error . ')');
-	return false;
-}
+if (Database::setVersion($database, '020500')===false) return false;
 
 ?>

+ 5 - 7
php/database/update_020505.php

@@ -7,8 +7,10 @@
 ###
 
 # Add `checksum`
-if (!$database->query("SELECT `checksum` FROM `lychee_photos` LIMIT 1;")) {
-	$result = $database->query("ALTER TABLE `lychee_photos` ADD `checksum` VARCHAR(100) DEFAULT NULL");
+$query = Database::prepare($database, "SELECT `checksum` FROM `?` LIMIT 1", array(LYCHEE_TABLE_PHOTOS));
+if (!$database->query($query)) {
+	$query	= Database::prepare($database, "ALTER TABLE `?` ADD `checksum` VARCHAR(100) DEFAULT NULL", array(LYCHEE_TABLE_PHOTOS));
+	$result	= $database->query($query);
 	if (!$result) {
 		Log::error($database, 'update_020505', __LINE__, 'Could not update database (' . $database->error . ')');
 		return false;
@@ -16,10 +18,6 @@ if (!$database->query("SELECT `checksum` FROM `lychee_photos` LIMIT 1;")) {
 }
 
 # Set version
-$result = $database->query("UPDATE lychee_settings SET value = '020505' WHERE `key` = 'version';");
-if (!$result) {
-	Log::error($database, 'update_020505', __LINE__, 'Could not update database (' . $database->error . ')');
-	return false;
-}
+if (Database::setVersion($database, '020505')===false) return false;
 
 ?>

+ 5 - 7
php/database/update_020601.php

@@ -7,8 +7,10 @@
 ###
 
 # Add `downloadable`
-if (!$database->query("SELECT `downloadable` FROM `lychee_albums` LIMIT 1;")) {
-	$result = $database->query("ALTER TABLE `lychee_albums` ADD `downloadable` TINYINT(1) NOT NULL DEFAULT 1");
+$query = Database::prepare($database, "SELECT `downloadable` FROM `?` LIMIT 1", array(LYCHEE_TABLE_ALBUMS));
+if (!$database->query($query)) {
+	$query	= Database::prepare($database, "ALTER TABLE `?` ADD `downloadable` TINYINT(1) NOT NULL DEFAULT 1", array(LYCHEE_TABLE_ALBUMS));
+	$result	= $database->query($query);
 	if (!$result) {
 		Log::error($database, 'update_020601', __LINE__, 'Could not update database (' . $database->error . ')');
 		return false;
@@ -16,10 +18,6 @@ if (!$database->query("SELECT `downloadable` FROM `lychee_albums` LIMIT 1;")) {
 }
 
 # Set version
-$result = $database->query("UPDATE lychee_settings SET value = '020601' WHERE `key` = 'version';");
-if (!$result) {
-	Log::error($database, 'update_020601', __LINE__, 'Could not update database (' . $database->error . ')');
-	return false;
-}
+if (Database::setVersion($database, '020601')===false) return false;
 
 ?>

+ 46 - 0
php/database/update_020602.php

@@ -0,0 +1,46 @@
+<?php
+
+###
+# @name			Update to version 2.6.2
+# @author		Tobias Reich
+# @copyright	2014 by Tobias Reich
+###
+
+# Add a checksum
+$query	= Database::prepare($database, "SELECT `id`, `url` FROM `?` WHERE `checksum` IS NULL", array(LYCHEE_TABLE_PHOTOS));
+$result	= $database->query($query);
+if (!$result) {
+	Log::error($database, 'update_020602', __LINE__, 'Could not find photos without checksum (' . $database->error . ')');
+	return false;
+}
+while ($photo = $result->fetch_object()) {
+	$checksum = sha1_file(LYCHEE_UPLOADS_BIG . $photo->url);
+	if ($checksum!==false) {
+		$query			= Database::prepare($database, "UPDATE `?` SET `checksum` = '?' WHERE `id` = '?'", array(LYCHEE_TABLE_PHOTOS, $checksum, $photo->id));
+		$setChecksum	= $database->query($query);
+		if (!$setChecksum) {
+			Log::error($database, 'update_020602', __LINE__, 'Could not update checksum (' . $database->error . ')');
+			return false;
+		}
+	} else {
+		Log::error($database, 'update_020602', __LINE__, 'Could not calculate checksum for photo with id ' . $photo->id);
+		return false;
+	}
+}
+
+# Add Imagick
+$query	= Database::prepare($database, "SELECT `key` FROM `?` WHERE `key` = 'imagick' LIMIT 1", array(LYCHEE_TABLE_SETTINGS));
+$result	= $database->query($query);
+if ($result->num_rows===0) {
+	$query	= Database::prepare($database, "INSERT INTO `?` (`key`, `value`) VALUES ('imagick', '1')", array(LYCHEE_TABLE_SETTINGS));
+	$result	= $database->query($query);
+	if (!$result) {
+		Log::error($database, 'update_020100', __LINE__, 'Could not update database (' . $database->error . ')');
+		return false;
+	}
+}
+
+# Set version
+if (Database::setVersion($database, '020602')===false) return false;
+
+?>

+ 20 - 0
php/define.php

@@ -11,6 +11,7 @@ define('LYCHEE', substr(__DIR__, 0, -3));
 
 # Define dirs
 define('LYCHEE_DATA', LYCHEE . 'data/');
+define('LYCHEE_BUILD', LYCHEE . 'build/');
 define('LYCHEE_UPLOADS', LYCHEE . 'uploads/');
 define('LYCHEE_UPLOADS_BIG', LYCHEE_UPLOADS . 'big/');
 define('LYCHEE_UPLOADS_MEDIUM', LYCHEE_UPLOADS . 'medium/');
@@ -25,4 +26,23 @@ define('LYCHEE_CONFIG_FILE', LYCHEE_DATA . 'config.php');
 define('LYCHEE_URL_UPLOADS_THUMB', 'uploads/thumb/');
 define('LYCHEE_URL_UPLOADS_BIG', 'uploads/big/');
 
+function defineTablePrefix($dbTablePrefix) {
+
+	# This part is wrapped into a function, because it needs to be called
+	# after the config-file has been loaded. Other defines are also available
+	# before the config-file has been loaded.
+
+	# Parse table prefix
+	# Old users do not have the table prefix stored in their config-file
+	if (!isset($dbTablePrefix)||$dbTablePrefix==='') $dbTablePrefix = '';
+	else $dbTablePrefix .= '_';
+
+	# Define tables
+	define('LYCHEE_TABLE_ALBUMS', $dbTablePrefix . 'lychee_albums');
+	define('LYCHEE_TABLE_LOG', $dbTablePrefix . 'lychee_log');
+	define('LYCHEE_TABLE_PHOTOS', $dbTablePrefix . 'lychee_photos');
+	define('LYCHEE_TABLE_SETTINGS', $dbTablePrefix . 'lychee_settings');
+
+}
+
 ?>

+ 69 - 41
php/modules/Album.php

@@ -39,7 +39,8 @@ class Album extends Module {
 
 		# Database
 		$sysstamp	= time();
-		$result		= $this->database->query("INSERT INTO lychee_albums (title, sysstamp, public, visible) VALUES ('$title', '$sysstamp', '$public', '$visible');");
+		$query		= Database::prepare($this->database, "INSERT INTO ? (title, sysstamp, public, visible) VALUES ('?', '?', '?', '?')", array(LYCHEE_TABLE_ALBUMS, $title, $sysstamp, $public, $visible));
+		$result		= $this->database->query($query);
 
 		# Call plugins
 		$this->plugins(__METHOD__, 1, func_get_args());
@@ -64,26 +65,27 @@ class Album extends Module {
 		switch ($this->albumIDs) {
 
 			case 'f':	$return['public'] = false;
-						$query = "SELECT id, title, tags, public, star, album, thumbUrl, takestamp FROM lychee_photos WHERE star = 1 " . $this->settings['sorting'];
+						$query = Database::prepare($this->database, "SELECT id, title, tags, public, star, album, thumbUrl, takestamp FROM ? WHERE star = 1 " . $this->settings['sorting'], array(LYCHEE_TABLE_PHOTOS));
 						break;
 
 			case 's':	$return['public'] = false;
-						$query = "SELECT id, title, tags, public, star, album, thumbUrl, takestamp FROM lychee_photos WHERE public = 1 " . $this->settings['sorting'];
+						$query = Database::prepare($this->database, "SELECT id, title, tags, public, star, album, thumbUrl, takestamp FROM ? WHERE public = 1 " . $this->settings['sorting'], array(LYCHEE_TABLE_PHOTOS));
 						break;
 
 			case 'r':	$return['public'] = false;
-						$query = "SELECT id, title, tags, public, star, album, thumbUrl, takestamp FROM lychee_photos WHERE LEFT(id, 10) >= unix_timestamp(DATE_SUB(NOW(), INTERVAL 1 DAY)) " . $this->settings['sorting'];
+						$query = Database::prepare($this->database, "SELECT id, title, tags, public, star, album, thumbUrl, takestamp FROM ? WHERE LEFT(id, 10) >= unix_timestamp(DATE_SUB(NOW(), INTERVAL 1 DAY)) " . $this->settings['sorting'], array(LYCHEE_TABLE_PHOTOS));
 						break;
 
 			case '0':	$return['public'] = false;
-						$query = "SELECT id, title, tags, public, star, album, thumbUrl, takestamp FROM lychee_photos WHERE album = 0 " . $this->settings['sorting'];
+						$query = Database::prepare($this->database, "SELECT id, title, tags, public, star, album, thumbUrl, takestamp FROM ? WHERE album = 0 " . $this->settings['sorting'], array(LYCHEE_TABLE_PHOTOS));
 						break;
 
-			default:	$albums = $this->database->query("SELECT * FROM lychee_albums WHERE id = '$this->albumIDs' LIMIT 1;");
+			default:	$query	= Database::prepare($this->database, "SELECT * FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs));
+						$albums = $this->database->query($query);
 						$return = $albums->fetch_assoc();
 						$return['sysdate']		= date('d M. Y', $return['sysstamp']);
 						$return['password']		= ($return['password']=='' ? false : true);
-						$query = "SELECT id, title, tags, public, star, album, thumbUrl, takestamp FROM lychee_photos WHERE album = '$this->albumIDs' " . $this->settings['sorting'];
+						$query = Database::prepare($this->database, "SELECT id, title, tags, public, star, album, thumbUrl, takestamp FROM ? WHERE album = '?' " . $this->settings['sorting'], array(LYCHEE_TABLE_PHOTOS, $this->albumIDs));
 						break;
 
 		}
@@ -154,11 +156,15 @@ class Album extends Module {
 		if ($public===false) $return = $this->getSmartInfo();
 
 		# Albums query
-		$query = 'SELECT id, title, public, sysstamp, password FROM lychee_albums WHERE public = 1 AND visible <> 0';
-		if ($public===false) $query = 'SELECT id, title, public, sysstamp, password FROM lychee_albums';
+		$query = Database::prepare($this->database, 'SELECT id, title, public, sysstamp, password FROM ? WHERE public = 1 AND visible <> 0', array(LYCHEE_TABLE_ALBUMS));
+		if ($public===false) $query = Database::prepare($this->database, 'SELECT id, title, public, sysstamp, password FROM ?', array(LYCHEE_TABLE_ALBUMS));
 
 		# Execute query
-		$albums = $this->database->query($query) OR exit('Error: ' . $this->database->error);
+		$albums = $this->database->query($query);
+		if (!$albums) {
+			Log::error($database, __METHOD__, __LINE__, 'Could not get all albums (' . $database->error . ')');
+			exit('Error: ' . $this->database->error);
+		}
 
 		# For each album
 		while ($album = $albums->fetch_assoc()) {
@@ -171,7 +177,8 @@ class Album extends Module {
 			if (($public===true&&$album['password']===false)||($public===false)) {
 
 				# Execute query
-				$thumbs = $this->database->query("SELECT thumbUrl FROM lychee_photos WHERE album = '" . $album['id'] . "' ORDER BY star DESC, " . substr($this->settings['sorting'], 9) . " LIMIT 3");
+				$query	= Database::prepare($this->database, "SELECT thumbUrl FROM ? WHERE album = '?' ORDER BY star DESC, " . substr($this->settings['sorting'], 9) . " LIMIT 3", array(LYCHEE_TABLE_PHOTOS, $album['id']));
+				$thumbs	= $this->database->query($query);
 
 				# For each thumb
 				$k = 0;
@@ -203,7 +210,8 @@ class Album extends Module {
 		self::dependencies(isset($this->database, $this->settings));
 
 		# Unsorted
-		$unsorted	= $this->database->query("SELECT thumbUrl FROM lychee_photos WHERE album = 0 " . $this->settings['sorting']);
+		$query		= Database::prepare($this->database, 'SELECT thumbUrl FROM ? WHERE album = 0 ' . $this->settings['sorting'], array(LYCHEE_TABLE_PHOTOS));
+		$unsorted	= $this->database->query($query);
 		$i			= 0;
 		while($row = $unsorted->fetch_object()) {
 			if ($i<3) {
@@ -214,7 +222,8 @@ class Album extends Module {
 		$return['unsortedNum'] = $unsorted->num_rows;
 
 		# Public
-		$public	= $this->database->query("SELECT thumbUrl FROM lychee_photos WHERE public = 1 " . $this->settings['sorting']);
+		$query		= Database::prepare($this->database, 'SELECT thumbUrl FROM ? WHERE public = 1 ' . $this->settings['sorting'], array(LYCHEE_TABLE_PHOTOS));
+		$public		= $this->database->query($query);
 		$i			= 0;
 		while($row2 = $public->fetch_object()) {
 			if ($i<3) {
@@ -225,7 +234,8 @@ class Album extends Module {
 		$return['publicNum'] = $public->num_rows;
 
 		# Starred
-		$starred	= $this->database->query("SELECT thumbUrl FROM lychee_photos WHERE star = 1 " . $this->settings['sorting']);
+		$query		= Database::prepare($this->database, 'SELECT thumbUrl FROM ? WHERE star = 1 ' . $this->settings['sorting'], array(LYCHEE_TABLE_PHOTOS));
+		$starred	= $this->database->query($query);
 		$i			= 0;
 		while($row3 = $starred->fetch_object()) {
 			if ($i<3) {
@@ -236,7 +246,8 @@ class Album extends Module {
 		$return['starredNum'] = $starred->num_rows;
 
 		# Recent
-		$recent		= $this->database->query("SELECT thumbUrl FROM lychee_photos WHERE LEFT(id, 10) >= unix_timestamp(DATE_SUB(NOW(), INTERVAL 1 DAY)) " . $this->settings['sorting']);
+		$query		= Database::prepare($this->database, 'SELECT thumbUrl FROM ? WHERE LEFT(id, 10) >= unix_timestamp(DATE_SUB(NOW(), INTERVAL 1 DAY)) ' . $this->settings['sorting'], array(LYCHEE_TABLE_PHOTOS));
+		$recent		= $this->database->query($query);
 		$i			= 0;
 		while($row3 = $recent->fetch_object()) {
 			if ($i<3) {
@@ -267,27 +278,30 @@ class Album extends Module {
 		# Photos query
 		switch($this->albumIDs) {
 			case 's':
-				$photos = "SELECT title, url FROM lychee_photos WHERE public = '1';";
-				$zipTitle = 'Public';
+				$photos		= Database::prepare($this->database, 'SELECT title, url FROM ? WHERE public = 1', array(LYCHEE_TABLE_PHOTOS));
+				$zipTitle	= 'Public';
 				break;
 			case 'f':
-				$photos = "SELECT title, url FROM lychee_photos WHERE star = '1';";
-				$zipTitle = 'Starred';
+				$photos		= Database::prepare($this->database, 'SELECT title, url FROM ? WHERE star = 1', array(LYCHEE_TABLE_PHOTOS));
+				$zipTitle	= 'Starred';
 				break;
 			case 'r':
-				$photos = "SELECT title, url FROM lychee_photos WHERE LEFT(id, 10) >= unix_timestamp(DATE_SUB(NOW(), INTERVAL 1 DAY));";
-				$zipTitle = 'Recent';
+				$photos		= Database::prepare($this->database, 'SELECT title, url FROM ? WHERE LEFT(id, 10) >= unix_timestamp(DATE_SUB(NOW(), INTERVAL 1 DAY)) GROUP BY checksum', array(LYCHEE_TABLE_PHOTOS));
+				$zipTitle	= 'Recent';
 				break;
 			default:
-				$photos = "SELECT title, url FROM lychee_photos WHERE album = '$this->albumIDs';";
-				$zipTitle = 'Unsorted';
+				$photos		= Database::prepare($this->database, "SELECT title, url FROM ? WHERE album = '?'", array(LYCHEE_TABLE_PHOTOS, $this->albumIDs));
+				$zipTitle	= 'Unsorted';
 		}
 
 		# Set title
-		$album = $this->database->query("SELECT title FROM lychee_albums WHERE id = '$this->albumIDs' LIMIT 1;");
-		if ($this->albumIDs!=0&&is_numeric($this->albumIDs)) $zipTitle = $album->fetch_object()->title;
+		if ($this->albumIDs!=0&&is_numeric($this->albumIDs)) {
+			$query = Database::prepare($this->database, "SELECT title FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs));
+			$album = $this->database->query($query);
+			$zipTitle = $album->fetch_object()->title;
+		}
 
-		# Parse title
+		# Escape title
 		$zipTitle = str_replace($badChars, '', $zipTitle);
 
 		$filename = LYCHEE_DATA . $zipTitle . '.zip';
@@ -380,7 +394,8 @@ class Album extends Module {
 		if (strlen($title)>50) $title = substr($title, 0, 50);
 
 		# Execute query
-		$result = $this->database->query("UPDATE lychee_albums SET title = '$title' WHERE id IN ($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);
 
 		# Call plugins
 		$this->plugins(__METHOD__, 1, func_get_args());
@@ -402,11 +417,12 @@ class Album extends Module {
 		$this->plugins(__METHOD__, 0, func_get_args());
 
 		# Parse
-		$description = htmlentities($description);
+		$description = htmlentities($description, ENT_COMPAT | ENT_HTML401, 'UTF-8');
 		if (strlen($description)>1000) $description = substr($description, 0, 1000);
 
 		# Execute query
-		$result = $this->database->query("UPDATE lychee_albums SET description = '$description' WHERE id IN ($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);
 
 		# Call plugins
 		$this->plugins(__METHOD__, 1, func_get_args());
@@ -430,7 +446,8 @@ class Album extends Module {
 		if ($this->albumIDs==='0'||$this->albumIDs==='s'||$this->albumIDs==='f') return false;
 
 		# Execute query
-		$albums	= $this->database->query("SELECT public FROM lychee_albums WHERE id = '$this->albumIDs' LIMIT 1;");
+		$query	= Database::prepare($this->database, "SELECT public FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs));
+		$albums	= $this->database->query($query);
 		$album	= $albums->fetch_object();
 
 		# Call plugins
@@ -449,10 +466,11 @@ class Album extends Module {
 		# Call plugins
 		$this->plugins(__METHOD__, 0, func_get_args());
 
-		if ($this->albumIDs==='0'||$this->albumIDs==='s'||$this->albumIDs==='f') return false;
+		if ($this->albumIDs==='0'||$this->albumIDs==='s'||$this->albumIDs==='f'||$this->albumIDs==='r') return false;
 
 		# Execute query
-		$albums	= $this->database->query("SELECT downloadable FROM lychee_albums WHERE id = '$this->albumIDs' LIMIT 1;");
+		$query	= Database::prepare($this->database, "SELECT downloadable FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs));
+		$albums	= $this->database->query($query);
 		$album	= $albums->fetch_object();
 
 		# Call plugins
@@ -472,7 +490,8 @@ class Album extends Module {
 		$this->plugins(__METHOD__, 0, func_get_args());
 
 		# Get public
-		$albums	= $this->database->query("SELECT id, public FROM lychee_albums WHERE id IN ('$this->albumIDs');");
+		$query	= Database::prepare($this->database, "SELECT id, public FROM ? WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs));
+		$albums	= $this->database->query($query);
 
 		while ($album = $albums->fetch_object()) {
 
@@ -486,7 +505,8 @@ class Album extends Module {
 			$downloadable = ($downloadable==='true' ? 1 : 0);
 
 			# Set public
-			$result = $this->database->query("UPDATE lychee_albums SET public = '$public', visible = '$visible', downloadable = '$downloadable', password = NULL WHERE id = '$album->id';");
+			$query	= Database::prepare($this->database, "UPDATE ? SET public = '?', visible = '?', downloadable = '?', password = NULL WHERE id = '?'", array(LYCHEE_TABLE_ALBUMS, $public, $visible, $downloadable, $album->id));
+			$result	= $this->database->query($query);
 			if (!$result) {
 				Log::error($this->database, __METHOD__, __LINE__, $this->database->error);
 				return false;
@@ -494,7 +514,8 @@ class Album extends Module {
 
 			# Reset permissions for photos
 			if ($public===1) {
-				$result = $this->database->query("UPDATE lychee_photos SET public = 0 WHERE album = '$album->id';");
+				$query	= Database::prepare($this->database, "UPDATE ? SET public = 0 WHERE album = '?'", array(LYCHEE_TABLE_PHOTOS, $album->id));
+				$result	= $this->database->query($query);
 				if (!$result) {
 					Log::error($this->database, __METHOD__, __LINE__, $this->database->error);
 					return false;
@@ -527,12 +548,16 @@ class Album extends Module {
 			$password = get_hashed_password($password);
 
 			# Set hashed password
-			$result = $this->database->query("UPDATE lychee_albums SET password = '$password' WHERE id IN ('$this->albumIDs');");
+			# Do not prepare $password because it is hashed and save
+			# Preparing (escaping) the password would destroy the hash
+			$query	= Database::prepare($this->database, "UPDATE ? SET password = '$password' WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs));
+			$result	= $this->database->query($query);
 
 		} else {
 
 			# Unset password
-			$result = $this->database->query("UPDATE lychee_albums SET password = NULL WHERE id IN ('$this->albumIDs');");
+			$query	= Database::prepare($this->database, "UPDATE ? SET password = NULL WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs));
+			$result	= $this->database->query($query);
 
 		}
 
@@ -556,7 +581,8 @@ class Album extends Module {
 		$this->plugins(__METHOD__, 0, func_get_args());
 
 		# Execute query
-		$albums	= $this->database->query("SELECT password FROM lychee_albums WHERE id = '$this->albumIDs' LIMIT 1;");
+		$query	= Database::prepare($this->database, "SELECT password FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs));
+		$albums	= $this->database->query($query);
 		$album	= $albums->fetch_object();
 
 		# Call plugins
@@ -568,7 +594,7 @@ class Album extends Module {
 
 	}
 
-	public function delete($albumIDs) {
+	public function delete() {
 
 		# Check dependencies
 		self::dependencies(isset($this->database, $this->albumIDs));
@@ -580,7 +606,8 @@ class Album extends Module {
 		$error = false;
 
 		# Execute query
-		$photos = $this->database->query("SELECT id FROM lychee_photos WHERE album IN ($albumIDs);");
+		$query	= Database::prepare($this->database, "SELECT id FROM ? WHERE album IN (?)", array(LYCHEE_TABLE_PHOTOS, $this->albumIDs));
+		$photos = $this->database->query($query);
 
 		# For each album delete photo
 		while ($row = $photos->fetch_object()) {
@@ -591,7 +618,8 @@ class Album extends Module {
 		}
 
 		# Delete albums
-		$result = $this->database->query("DELETE FROM lychee_albums WHERE id IN ($albumIDs);");
+		$query	= Database::prepare($this->database, "DELETE FROM ? WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $this->albumIDs));
+		$result	= $this->database->query($query);
 
 		# Call plugins
 		$this->plugins(__METHOD__, 1, func_get_args());

+ 113 - 13
php/modules/Database.php

@@ -29,7 +29,8 @@ class Database extends Module {
 			if (!Database::createDatabase($database, $name)) exit('Error: Could not create database!');
 
 		# Check tables
-		if (!$database->query('SELECT * FROM lychee_photos, lychee_albums, lychee_settings, lychee_log LIMIT 0;'))
+		$query = Database::prepare($database, 'SELECT * FROM ?, ?, ?, ? LIMIT 0', array(LYCHEE_TABLE_PHOTOS, LYCHEE_TABLE_ALBUMS, LYCHEE_TABLE_SETTINGS, LYCHEE_TABLE_LOG));
+		if (!$database->query($query))
 			if (!Database::createTables($database)) exit('Error: Could not create tables!');
 
 		return $database;
@@ -48,7 +49,8 @@ class Database extends Module {
 			'020200', #2.2
 			'020500', #2.5
 			'020505', #2.5.5
-			'020601' #2.6.1
+			'020601', #2.6.1
+			'020602' #2.6.2
 		);
 
 		# For each update
@@ -65,7 +67,7 @@ class Database extends Module {
 
 	}
 
-	static function createConfig($host = 'localhost', $user, $password, $name = 'lychee') {
+	static function createConfig($host = 'localhost', $user, $password, $name = 'lychee', $prefix = '') {
 
 		# Check dependencies
 		Module::dependencies(isset($host, $user, $password, $name));
@@ -85,11 +87,18 @@ class Database extends Module {
 
 		}
 
+		# Escape data
+		$host		= mysqli_real_escape_string($database, $host);
+		$user		= mysqli_real_escape_string($database, $user);
+		$password	= mysqli_real_escape_string($database, $password);
+		$name		= mysqli_real_escape_string($database, $name);
+		$prefix		= mysqli_real_escape_string($database, $prefix);
+
 		# Save config.php
 $config = "<?php
 
 ###
-# @name		Configuration
+# @name			Configuration
 # @author		Tobias Reich
 # @copyright	2014 Tobias Reich
 ###
@@ -101,6 +110,7 @@ if(!defined('LYCHEE')) exit('Error: Direct access is not allowed!');
 \$dbUser = '$user'; # Username of the database
 \$dbPassword = '$password'; # Password of the database
 \$dbName = '$name'; # Database name
+\$dbTablePrefix = '$prefix'; # Table prefix
 
 ?>";
 
@@ -131,30 +141,36 @@ if(!defined('LYCHEE')) exit('Error: Direct access is not allowed!');
 		Module::dependencies(isset($database));
 
 		# Create log
-		if (!$database->query('SELECT * FROM lychee_log LIMIT 0;')) {
+		$exist = Database::prepare($database, 'SELECT * FROM ? LIMIT 0', array(LYCHEE_TABLE_LOG));
+		if (!$database->query($exist)) {
 
 			# Read file
 			$file	= __DIR__ . '/../database/log_table.sql';
 			$query	= @file_get_contents($file);
 
-			# Create table
 			if (!isset($query)||$query===false) return false;
+
+			# Create table
+			$query = Database::prepare($database, $query, array(LYCHEE_TABLE_LOG));
 			if (!$database->query($query)) return false;
 
 		}
 
 		# Create settings
-		if (!$database->query('SELECT * FROM lychee_settings LIMIT 0;')) {
+		$exist = Database::prepare($database, 'SELECT * FROM ? LIMIT 0', array(LYCHEE_TABLE_SETTINGS));
+		if (!$database->query($exist)) {
 
 			# Read file
 			$file	= __DIR__ . '/../database/settings_table.sql';
 			$query	= @file_get_contents($file);
 
-			# Create table
 			if (!isset($query)||$query===false) {
 				Log::error($database, __METHOD__, __LINE__, 'Could not load query for lychee_settings');
 				return false;
 			}
+
+			# Create table
+			$query = Database::prepare($database, $query, array(LYCHEE_TABLE_SETTINGS));
 			if (!$database->query($query)) {
 				Log::error($database, __METHOD__, __LINE__, $database->error);
 				return false;
@@ -164,11 +180,13 @@ if(!defined('LYCHEE')) exit('Error: Direct access is not allowed!');
 			$file	= __DIR__ . '/../database/settings_content.sql';
 			$query	= @file_get_contents($file);
 
-			# Add content
 			if (!isset($query)||$query===false) {
 				Log::error($database, __METHOD__, __LINE__, 'Could not load content-query for lychee_settings');
 				return false;
 			}
+
+			# Add content
+			$query = Database::prepare($database, $query, array(LYCHEE_TABLE_SETTINGS));
 			if (!$database->query($query)) {
 				Log::error($database, __METHOD__, __LINE__, $database->error);
 				return false;
@@ -177,17 +195,20 @@ if(!defined('LYCHEE')) exit('Error: Direct access is not allowed!');
 		}
 
 		# Create albums
-		if (!$database->query('SELECT * FROM lychee_albums LIMIT 0;')) {
+		$exist = Database::prepare($database, 'SELECT * FROM ? LIMIT 0', array(LYCHEE_TABLE_ALBUMS));
+		if (!$database->query($exist)) {
 
 			# Read file
 			$file	= __DIR__ . '/../database/albums_table.sql';
 			$query	= @file_get_contents($file);
 
-			# Create table
 			if (!isset($query)||$query===false) {
 				Log::error($database, __METHOD__, __LINE__, 'Could not load query for lychee_albums');
 				return false;
 			}
+
+			# Create table
+			$query = Database::prepare($database, $query, array(LYCHEE_TABLE_ALBUMS));
 			if (!$database->query($query)) {
 				Log::error($database, __METHOD__, __LINE__, $database->error);
 				return false;
@@ -196,17 +217,20 @@ if(!defined('LYCHEE')) exit('Error: Direct access is not allowed!');
 		}
 
 		# Create photos
-		if (!$database->query('SELECT * FROM lychee_photos LIMIT 0;')) {
+		$exist = Database::prepare($database, 'SELECT * FROM ? LIMIT 0', array(LYCHEE_TABLE_PHOTOS));
+		if (!$database->query($exist)) {
 
 			# Read file
 			$file	= __DIR__ . '/../database/photos_table.sql';
 			$query	= @file_get_contents($file);
 
-			# Create table
 			if (!isset($query)||$query===false) {
 				Log::error($database, __METHOD__, __LINE__, 'Could not load query for lychee_photos');
 				return false;
 			}
+
+			# Create table
+			$query = Database::prepare($database, $query, array(LYCHEE_TABLE_PHOTOS));
 			if (!$database->query($query)) {
 				Log::error($database, __METHOD__, __LINE__, $database->error);
 				return false;
@@ -218,6 +242,82 @@ if(!defined('LYCHEE')) exit('Error: Direct access is not allowed!');
 
 	}
 
+	static function setVersion($database, $version) {
+
+		$query	= Database::prepare($database, "UPDATE ? SET value = '?' WHERE `key` = 'version'", array(LYCHEE_TABLE_SETTINGS, $version));
+		$result = $database->query($query);
+		if (!$result) {
+			Log::error($database, __METHOD__, __LINE__, 'Could not update database (' . $database->error . ')');
+			return false;
+		}
+
+	}
+
+	static function prepare($database, $query, $data) {
+
+		# Check dependencies
+		Module::dependencies(isset($database, $query, $data));
+
+		# Count the number of placeholders and compare it with the number of arguments
+		# If it doesn't match, calculate the difference and skip this number of placeholders before starting the replacement
+		# This avoids problems with placeholders in user-input
+		# $skip = Number of placeholders which need to be skipped
+		$skip	= 0;
+		$num	= array(
+			'placeholder'	=> substr_count($query, '?'),
+			'data'			=> count($data)
+		);
+
+		if (($num['data']-$num['placeholder'])<0) Log::notice($database, __METHOD__, __LINE__, 'Could not completely prepare query. Query has more placeholders than values.');
+
+		foreach ($data as $value) {
+
+			# Escape
+			$value = mysqli_real_escape_string($database, $value);
+
+			# Recalculate number of placeholders
+			$num['placeholder'] = substr_count($query, '?');
+
+			# Calculate number of skips
+			if ($num['placeholder']>$num['data']) $skip = $num['placeholder'] - $num['data'];
+
+			if ($skip>0) {
+
+				# Need to skip $skip placeholders, because the user input contained placeholders
+				# Calculate a substring which does not contain the user placeholders
+				# 1 or -1 is the length of the placeholder (placeholder = ?)
+
+				$pos = -1;
+				for ($i=$skip; $i>0; $i--) $pos = strpos($query, '?', $pos + 1);
+				$pos++;
+
+				$temp	= substr($query, 0, $pos); # First part of $query
+				$query	= substr($query, $pos); # Last part of $query
+
+			}
+
+			# Replace
+			$query = preg_replace('/\?/', $value, $query, 1);
+
+			if ($skip>0) {
+
+				# Reassemble the parts of $query
+				$query = $temp . $query;
+
+			}
+
+			# Reset skip
+			$skip = 0;
+
+			# Decrease number of data elements
+			$num['data']--;
+
+		}
+
+		return $query;
+
+	}
+
 }
 
 ?>

+ 1 - 7
php/modules/Log.php

@@ -36,14 +36,8 @@ class Log extends Module {
 		# Get time
 		$sysstamp = time();
 
-		# Escape
-		$type		= mysqli_real_escape_string($database, $type);
-		$function	= mysqli_real_escape_string($database, $function);
-		$line		= mysqli_real_escape_string($database, $line);
-		$text		= mysqli_real_escape_string($database, $text);
-
 		# Save in database
-		$query	= "INSERT INTO lychee_log (time, type, function, line, text) VALUES ('$sysstamp', '$type', '$function', '$line', '$text');";
+		$query	= Database::prepare($database, "INSERT INTO ? (time, type, function, line, text) VALUES ('?', '?', '?', '?', '?')", array(LYCHEE_TABLE_LOG, $sysstamp, $type, $function, $line, $text));
 		$result	= $database->query($query);
 
 		if (!$result) return false;

+ 205 - 88
php/modules/Photo.php

@@ -96,74 +96,87 @@ class Photo extends Module {
 			$id = str_replace('.', '', microtime(true));
 			while(strlen($id)<14) $id .= 0;
 
+			# Set paths
 			$tmp_name	= $file['tmp_name'];
 			$photo_name	= md5($id) . $extension;
 			$path		= LYCHEE_UPLOADS_BIG . $photo_name;
 
-			# Import if not uploaded via web
-			if (!is_uploaded_file($tmp_name)) {
-				if (!@copy($tmp_name, $path)) {
-					Log::error($this->database, __METHOD__, __LINE__, 'Could not copy photo to uploads');
-					exit('Error: Could not copy photo to uploads!');
-				} else @unlink($tmp_name);
+			# Calculate checksum
+			$checksum = sha1_file($tmp_name);
+			if ($checksum===false) {
+				Log::error($this->database, __METHOD__, __LINE__, 'Could not calculate checksum for photo');
+				exit('Error: Could not calculate checksum for photo!');
+			}
+
+			# Check if image exists based on checksum
+			if ($checksum===false) {
+
+				$checksum	= '';
+				$exists		= false;
+
 			} else {
-				if (!@move_uploaded_file($tmp_name, $path)) {
-					Log::error($this->database, __METHOD__, __LINE__, 'Could not move photo to uploads');
-					exit('Error: Could not move photo to uploads!');
+
+				$exists = $this->exists($checksum);
+
+				if ($exists!==false) {
+					$photo_name	= $exists['photo_name'];
+					$path		= $exists['path'];
+					$path_thumb	= $exists['path_thumb'];
+					$exists		= true;
 				}
+
 			}
 
-			# Calculate checksum
-			$checksum = sha1_file($path);
-			if ($checksum===false) $checksum = '';
+			if ($exists===false) {
+
+				# Import if not uploaded via web
+				if (!is_uploaded_file($tmp_name)) {
+					if (!@copy($tmp_name, $path)) {
+						Log::error($this->database, __METHOD__, __LINE__, 'Could not copy photo to uploads');
+						exit('Error: Could not copy photo to uploads!');
+					} else @unlink($tmp_name);
+				} else {
+					if (!@move_uploaded_file($tmp_name, $path)) {
+						Log::error($this->database, __METHOD__, __LINE__, 'Could not move photo to uploads');
+						exit('Error: Could not move photo to uploads!');
+					}
+				}
+
+			}
 
 			# Read infos
 			$info = $this->getInfo($path);
 
 			# Use title of file if IPTC title missing
-			if ($info['title']==='') $info['title'] = mysqli_real_escape_string($this->database, substr(basename($file['name'], $extension), 0, 30));
+			if ($info['title']==='') $info['title'] = substr(basename($file['name'], $extension), 0, 30);
 
 			# Use description parameter if set
 			if ($description==='') $description = $info['description'];
 
-			# Set orientation based on EXIF data
-			if ($file['type']==='image/jpeg'&&isset($info['orientation'])&&$info['orientation']!==''&&isset($info['width'])&&isset($info['height'])) {
-				if (!$this->adjustFile($path, $info)) Log::notice($this->database, __METHOD__, __LINE__, 'Could not adjust photo (' . $info['title'] . ')');
-			}
+			if ($exists===false) {
+
+				# Set orientation based on EXIF data
+				if ($file['type']==='image/jpeg'&&isset($info['orientation'], $info['width'], $info['height'])&&$info['orientation']!=='') {
+					if (!$this->adjustFile($path, $info)) Log::notice($this->database, __METHOD__, __LINE__, 'Could not adjust photo (' . $info['title'] . ')');
+				}
 
-			# Set original date
-			if ($info['takestamp']!=='') @touch($path, $info['takestamp']);
+				# Set original date
+				if ($info['takestamp']!=='') @touch($path, $info['takestamp']);
+
+				# Create Thumb
+				if (!$this->createThumb($path, $photo_name)) {
+					Log::error($this->database, __METHOD__, __LINE__, 'Could not create thumbnail for photo');
+					exit('Error: Could not create thumbnail for photo!');
+				}
+
+				# Set thumb url
+				$path_thumb = md5($id) . '.jpeg';
 
-			# Create Thumb
-			if (!$this->createThumb($path, $photo_name)) {
-				Log::error($this->database, __METHOD__, __LINE__, 'Could not create thumbnail for photo');
-				exit('Error: Could not create thumbnail for photo!');
 			}
 
 			# Save to DB
-			$query = "INSERT INTO lychee_photos (id, title, url, description, tags, type, width, height, size, iso, aperture, make, model, shutter, focal, takestamp, thumbUrl, album, public, star, checksum)
-				VALUES (
-					'" . $id . "',
-					'" . $info['title'] . "',
-					'" . $photo_name . "',
-					'" . $description . "',
-					'" . $tags . "',
-					'" . $info['type'] . "',
-					'" . $info['width'] . "',
-					'" . $info['height'] . "',
-					'" . $info['size'] . "',
-					'" . $info['iso'] . "',
-					'" . $info['aperture'] . "',
-					'" . $info['make'] . "',
-					'" . $info['model'] . "',
-					'" . $info['shutter'] . "',
-					'" . $info['focal'] . "',
-					'" . $info['takestamp'] . "',
-					'" . md5($id) . ".jpeg',
-					'" . $albumID . "',
-					'" . $public . "',
-					'" . $star . "',
-					'" . $checksum . "');";
+			$values	= array(LYCHEE_TABLE_PHOTOS, $id, $info['title'], $photo_name, $description, $tags, $info['type'], $info['width'], $info['height'], $info['size'], $info['iso'], $info['aperture'], $info['make'], $info['model'], $info['shutter'], $info['focal'], $info['takestamp'], $path_thumb, $albumID, $public, $star, $checksum);
+			$query	= Database::prepare($this->database, "INSERT INTO ? (id, title, url, description, tags, type, width, height, size, iso, aperture, make, model, shutter, focal, takestamp, thumbUrl, album, public, star, checksum) VALUES ('?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?')", $values);
 			$result = $this->database->query($query);
 
 			if (!$result) {
@@ -180,6 +193,40 @@ class Photo extends Module {
 
 	}
 
+	private function exists($checksum, $photoID = null) {
+
+		# Check dependencies
+		self::dependencies(isset($this->database, $checksum));
+
+		# Exclude $photoID from select when $photoID is set
+		if (isset($photoID)) $query = Database::prepare($this->database, "SELECT id, url, thumbUrl FROM ? WHERE checksum = '?' AND id <> '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $checksum, $photoID));
+		else $query = Database::prepare($this->database, "SELECT id, url, thumbUrl FROM ? WHERE checksum = '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $checksum));
+
+		$result	= $this->database->query($query);
+
+		if (!$result) {
+			Log::error($this->database, __METHOD__, __LINE__, 'Could not check for existing photos with the same checksum');
+			return false;
+		}
+
+		if ($result->num_rows===1) {
+
+			$result = $result->fetch_object();
+
+			$return = array(
+				'photo_name'	=> $result->url,
+				'path'			=> LYCHEE_UPLOADS_BIG . $result->url,
+				'path_thumb'	=> $result->thumbUrl
+			);
+
+			return $return;
+
+		}
+
+		return false;
+
+	}
+
 	private function createThumb($url, $filename, $width = 200, $height = 200) {
 
 		# Check dependencies
@@ -194,7 +241,7 @@ class Photo extends Module {
 		$newUrl2x	= LYCHEE_UPLOADS_THUMB . $photoName[0] . '@2x.jpeg';
 
 		# create thumbnails with Imagick
-		if(extension_loaded('imagick')) {
+		if(extension_loaded('imagick')&&$this->settings['imagick']==='1') {
 
 			# Read image
 			$thumb = new Imagick();
@@ -264,7 +311,7 @@ class Photo extends Module {
 
 	}
 
-	private function adjustFile($path, $info) {
+	public function adjustFile($path, $info) {
 
 		# Check dependencies
 		self::dependencies(isset($path, $info));
@@ -272,7 +319,7 @@ class Photo extends Module {
 		# Call plugins
 		$this->plugins(__METHOD__, 0, func_get_args());
 
-		if (extension_loaded('imagick')) {
+		if (extension_loaded('imagick')&&$this->settings['imagick']==='1') {
 
 			$rotateImage = 0;
 
@@ -387,7 +434,8 @@ class Photo extends Module {
 		$this->plugins(__METHOD__, 0, func_get_args());
 
 		# Get photo
-		$photos	= $this->database->query("SELECT * FROM lychee_photos WHERE id = '$this->photoIDs' LIMIT 1;");
+		$query	= Database::prepare($this->database, "SELECT * FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
+		$photos	= $this->database->query($query);
 		$photo	= $photos->fetch_assoc();
 
 		# Parse photo
@@ -395,15 +443,19 @@ class Photo extends Module {
 		if (strlen($photo['takestamp'])>1) $photo['takedate'] = date('d M. Y', $photo['takestamp']);
 
 		# Parse url
-		$photo['url'] = LYCHEE_URL_UPLOADS_BIG . $photo['url'];
+		$photo['url']		= LYCHEE_URL_UPLOADS_BIG . $photo['url'];
+		$photo['thumbUrl']	= LYCHEE_URL_UPLOADS_THUMB . $photo['thumbUrl'];
 
 		if ($albumID!='false') {
 
+			# Show photo as public when parent album is public
+			# Check if parent album is available and not photo not unsorted
 			if ($photo['album']!=0) {
 
 				# Get album
-				$albums = $this->database->query("SELECT public FROM lychee_albums WHERE id = '" . $photo['album'] . " LIMIT 1';");
-				$album = $albums->fetch_assoc();
+				$query	= Database::prepare($this->database, "SELECT public FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_ALBUMS, $photo['album']));
+				$albums	= $this->database->query($query);
+				$album	= $albums->fetch_assoc();
 
 				# Parse album
 				$photo['public'] = ($album['public']=='1' ? '2' : $photo['public']);
@@ -422,7 +474,7 @@ class Photo extends Module {
 
 	}
 
-	private function getInfo($url) {
+	public function getInfo($url) {
 
 		# Check dependencies
 		self::dependencies(isset($this->database, $url));
@@ -459,6 +511,9 @@ class Photo extends Module {
 				$temp = @$iptcInfo['2#120'][0];
 				if (isset($temp)&&strlen($temp)>0) $return['description'] = $temp;
 
+				$temp = @$iptcInfo['2#005'][0];
+				if (isset($temp)&&strlen($temp)>0&&$return['title']==='') $return['title'] = $temp;
+
 			}
 
 		}
@@ -506,9 +561,6 @@ class Photo extends Module {
 
 		}
 
-		# Security
-		foreach(array_keys($return) as $key) $return[$key] = mysqli_real_escape_string($this->database, $return[$key]);
-
 		# Call plugins
 		$this->plugins(__METHOD__, 1, func_get_args());
 
@@ -525,7 +577,8 @@ class Photo extends Module {
 		$this->plugins(__METHOD__, 0, func_get_args());
 
 		# Get photo
-		$photos	= $this->database->query("SELECT title, url FROM lychee_photos WHERE id = '$this->photoIDs' LIMIT 1;");
+		$query	= Database::prepare($this->database, "SELECT title, url FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
+		$photos	= $this->database->query($query);
 		$photo	= $photos->fetch_object();
 
 		# Get extension
@@ -535,9 +588,18 @@ class Photo extends Module {
 			return false;
 		}
 
+		# Illicit chars
+		$badChars =	array_merge(
+						array_map('chr', range(0,31)),
+						array("<", ">", ":", '"', "/", "\\", "|", "?", "*")
+					);
+
 		# Parse title
 		if ($photo->title=='') $photo->title = 'Untitled';
 
+		# Escape title
+		$photo->title = str_replace($badChars, '', $photo->title);
+
 		# Set headers
 		header("Content-Type: application/octet-stream");
 		header("Content-Disposition: attachment; filename=\"" . $photo->title . $extension . "\"");
@@ -565,7 +627,8 @@ class Photo extends Module {
 		if (strlen($title)>50) $title = substr($title, 0, 50);
 
 		# Set title
-		$result = $this->database->query("UPDATE lychee_photos SET title = '$title' WHERE id IN ($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);
 
 		# Call plugins
 		$this->plugins(__METHOD__, 1, func_get_args());
@@ -587,11 +650,12 @@ class Photo extends Module {
 		$this->plugins(__METHOD__, 0, func_get_args());
 
 		# Parse
-		$description = htmlentities($description);
+		$description = htmlentities($description, ENT_COMPAT | ENT_HTML401, 'UTF-8');
 		if (strlen($description)>1000) $description = substr($description, 0, 1000);
 
 		# Set description
-		$result = $this->database->query("UPDATE lychee_photos SET description = '$description' WHERE id IN ('$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);
 
 		# Call plugins
 		$this->plugins(__METHOD__, 1, func_get_args());
@@ -616,7 +680,8 @@ class Photo extends Module {
 		$error	= false;
 
 		# Get photos
-		$photos	= $this->database->query("SELECT id, star FROM lychee_photos WHERE id IN ($this->photoIDs);");
+		$query	= Database::prepare($this->database, "SELECT id, star FROM ? WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
+		$photos	= $this->database->query($query);
 
 		# For each photo
 		while ($photo = $photos->fetch_object()) {
@@ -625,7 +690,8 @@ class Photo extends Module {
 			$star = ($photo->star==0 ? 1 : 0);
 
 			# Set star
-			$star = $this->database->query("UPDATE lychee_photos SET star = '$star' WHERE id = '$photo->id';");
+			$query	= Database::prepare($this->database, "UPDATE ? SET star = '?' WHERE id = '?'", array(LYCHEE_TABLE_PHOTOS, $star, $photo->id));
+			$star	= $this->database->query($query);
 			if (!$star) $error = true;
 
 		}
@@ -650,15 +716,16 @@ class Photo extends Module {
 		$this->plugins(__METHOD__, 0, func_get_args());
 
 		# Get photo
-		$photos	= $this->database->query("SELECT public, album FROM lychee_photos WHERE id = '$this->photoIDs' LIMIT 1;");
+		$query	= Database::prepare($this->database, "SELECT public, album FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
+		$photos	= $this->database->query($query);
 		$photo	= $photos->fetch_object();
 
 		# Check if public
 		if ($photo->public==1) return true;
 		else {
 			$album	= new Album($this->database, null, null, $photo->album);
-			$acP		= $album->checkPassword($password);
-			$agP		= $album->getPublic();
+			$acP	= $album->checkPassword($password);
+			$agP	= $album->getPublic();
 			if ($acP===true&&$agP===true) return true;
 		}
 
@@ -678,14 +745,16 @@ class Photo extends Module {
 		$this->plugins(__METHOD__, 0, func_get_args());
 
 		# Get public
-		$photos	= $this->database->query("SELECT public FROM lychee_photos WHERE id = '$this->photoIDs' LIMIT 1;");
+		$query	= Database::prepare($this->database, "SELECT public FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
+		$photos	= $this->database->query($query);
 		$photo	= $photos->fetch_object();
 
 		# Invert public
 		$public = ($photo->public==0 ? 1 : 0);
 
 		# Set public
-		$result = $this->database->query("UPDATE lychee_photos SET public = '$public' WHERE id = '$this->photoIDs';");
+		$query	= Database::prepare($this->database, "UPDATE ? SET public = '?' WHERE id = '?'", array(LYCHEE_TABLE_PHOTOS, $public, $this->photoIDs));
+		$result	= $this->database->query($query);
 
 		# Call plugins
 		$this->plugins(__METHOD__, 1, func_get_args());
@@ -707,7 +776,8 @@ class Photo extends Module {
 		$this->plugins(__METHOD__, 0, func_get_args());
 
 		# Set album
-		$result = $this->database->query("UPDATE lychee_photos SET album = '$albumID' WHERE id IN ($this->photoIDs);");
+		$query	= Database::prepare($this->database, "UPDATE ? SET album = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $albumID, $this->photoIDs));
+		$result	= $this->database->query($query);
 
 		# Call plugins
 		$this->plugins(__METHOD__, 1, func_get_args());
@@ -737,7 +807,8 @@ class Photo extends Module {
 		}
 
 		# Set tags
-		$result = $this->database->query("UPDATE lychee_photos SET tags = '$tags' WHERE id IN ($this->photoIDs);");
+		$query	= Database::prepare($this->database, "UPDATE ? SET tags = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $tags, $this->photoIDs));
+		$result	= $this->database->query($query);
 
 		# Call plugins
 		$this->plugins(__METHOD__, 1, func_get_args());
@@ -750,7 +821,7 @@ class Photo extends Module {
 
 	}
 
-	public function delete() {
+	public function duplicate() {
 
 		# Check dependencies
 		self::dependencies(isset($this->database, $this->photoIDs));
@@ -759,7 +830,8 @@ class Photo extends Module {
 		$this->plugins(__METHOD__, 0, func_get_args());
 
 		# Get photos
-		$photos = $this->database->query("SELECT id, url, thumbUrl FROM lychee_photos WHERE id IN ($this->photoIDs);");
+		$query	= Database::prepare($this->database, "SELECT id, checksum FROM ? WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
+		$photos	= $this->database->query($query);
 		if (!$photos) {
 			Log::error($this->database, __METHOD__, __LINE__, $this->database->error);
 			return false;
@@ -768,30 +840,75 @@ class Photo extends Module {
 		# For each photo
 		while ($photo = $photos->fetch_object()) {
 
-			# Get retina thumb url
-			$thumbUrl2x = explode(".", $photo->thumbUrl);
-			$thumbUrl2x = $thumbUrl2x[0] . '@2x.' . $thumbUrl2x[1];
+			# Generate id
+			$id = str_replace('.', '', microtime(true));
+			while(strlen($id)<14) $id .= 0;
 
-			# Delete big
-			if (file_exists(LYCHEE_UPLOADS_BIG . $photo->url)&&!unlink(LYCHEE_UPLOADS_BIG . $photo->url)) {
-				Log::error($this->database, __METHOD__, __LINE__, 'Could not delete photo in uploads/big/');
+			# Duplicate entry
+			$values		= array(LYCHEE_TABLE_PHOTOS, $id, LYCHEE_TABLE_PHOTOS, $photo->id);
+			$query		= Database::prepare($this->database, "INSERT INTO ? (id, title, url, description, tags, type, width, height, size, iso, aperture, make, model, shutter, focal, takestamp, thumbUrl, album, public, star, checksum) SELECT '?' AS id, title, url, description, tags, type, width, height, size, iso, aperture, make, model, shutter, focal, takestamp, thumbUrl, album, public, star, checksum FROM ? WHERE id = '?'", $values);
+			$duplicate	= $this->database->query($query);
+			if (!$duplicate) {
+				Log::error($this->database, __METHOD__, __LINE__, $this->database->error);
 				return false;
 			}
 
-			# Delete thumb
-			if (file_exists(LYCHEE_UPLOADS_THUMB . $photo->thumbUrl)&&!unlink(LYCHEE_UPLOADS_THUMB . $photo->thumbUrl)) {
-				Log::error($this->database, __METHOD__, __LINE__, 'Could not delete photo in uploads/thumb/');
-				return false;
-			}
+		}
+
+		return true;
+
+	}
+
+	public function delete() {
+
+		# Check dependencies
+		self::dependencies(isset($this->database, $this->photoIDs));
+
+		# Call plugins
+		$this->plugins(__METHOD__, 0, func_get_args());
+
+		# Get photos
+		$query	= Database::prepare($this->database, "SELECT id, url, thumbUrl, checksum FROM ? WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
+		$photos	= $this->database->query($query);
+		if (!$photos) {
+			Log::error($this->database, __METHOD__, __LINE__, $this->database->error);
+			return false;
+		}
+
+		# For each photo
+		while ($photo = $photos->fetch_object()) {
+
+			# Check if other photos are referring to this images
+			# If so, only delete the db entry
+			if ($this->exists($photo->checksum, $photo->id)===false) {
+
+				# Get retina thumb url
+				$thumbUrl2x = explode(".", $photo->thumbUrl);
+				$thumbUrl2x = $thumbUrl2x[0] . '@2x.' . $thumbUrl2x[1];
+
+				# Delete big
+				if (file_exists(LYCHEE_UPLOADS_BIG . $photo->url)&&!unlink(LYCHEE_UPLOADS_BIG . $photo->url)) {
+					Log::error($this->database, __METHOD__, __LINE__, 'Could not delete photo in uploads/big/');
+					return false;
+				}
+
+				# Delete thumb
+				if (file_exists(LYCHEE_UPLOADS_THUMB . $photo->thumbUrl)&&!unlink(LYCHEE_UPLOADS_THUMB . $photo->thumbUrl)) {
+					Log::error($this->database, __METHOD__, __LINE__, 'Could not delete photo in uploads/thumb/');
+					return false;
+				}
+
+				# Delete thumb@2x
+				if (file_exists(LYCHEE_UPLOADS_THUMB . $thumbUrl2x)&&!unlink(LYCHEE_UPLOADS_THUMB . $thumbUrl2x))	 {
+					Log::error($this->database, __METHOD__, __LINE__, 'Could not delete high-res photo in uploads/thumb/');
+					return false;
+				}
 
-			# Delete thumb@2x
-			if (file_exists(LYCHEE_UPLOADS_THUMB . $thumbUrl2x)&&!unlink(LYCHEE_UPLOADS_THUMB . $thumbUrl2x))	 {
-				Log::error($this->database, __METHOD__, __LINE__, 'Could not delete high-res photo in uploads/thumb/');
-				return false;
 			}
 
 			# Delete db entry
-			$delete = $this->database->query("DELETE FROM lychee_photos WHERE id = '$photo->id';");
+			$query	= Database::prepare($this->database, "DELETE FROM ? WHERE id = '?'", array(LYCHEE_TABLE_PHOTOS, $photo->id));
+			$delete	= $this->database->query($query);
 			if (!$delete) {
 				Log::error($this->database, __METHOD__, __LINE__, $this->database->error);
 				return false;

+ 14 - 5
php/modules/Settings.php

@@ -27,7 +27,8 @@ class Settings extends Module {
 		self::dependencies(isset($this->database));
 
 		# Execute query
-		$settings = $this->database->query('SELECT * FROM lychee_settings;');
+		$query		= Database::prepare($this->database, "SELECT * FROM ?", array(LYCHEE_TABLE_SETTINGS));
+		$settings	= $this->database->query($query);
 
 		# Add each to return
 		while ($setting = $settings->fetch_object()) $return[$setting->key] = $setting->value;
@@ -76,7 +77,8 @@ class Settings extends Module {
 		}
 
 		# Execute query
-		$result = $this->database->query("UPDATE lychee_settings SET value = '$username' WHERE `key` = 'username';");
+		$query	= Database::prepare($this->database, "UPDATE ? SET value = '?' WHERE `key` = 'username'", array(LYCHEE_TABLE_SETTINGS, $username));
+		$result	= $this->database->query($query);
 
 		if (!$result) {
 			Log::error($this->database, __METHOD__, __LINE__, $this->database->error);
@@ -94,7 +96,10 @@ class Settings extends Module {
 		$password = get_hashed_password($password);
 
 		# Execute query
-		$result = $this->database->query("UPDATE lychee_settings SET value = '$password' WHERE `key` = 'password';");
+		# Do not prepare $password because it is hashed and save
+		# Preparing (escaping) the password would destroy the hash
+		$query	= Database::prepare($this->database, "UPDATE ? SET value = '$password' WHERE `key` = 'password'", array(LYCHEE_TABLE_SETTINGS));
+		$result	= $this->database->query($query);
 
 		if (!$result) {
 			Log::error($this->database, __METHOD__, __LINE__, $this->database->error);
@@ -115,7 +120,8 @@ class Settings extends Module {
 		}
 
 		# Execute query
-		$result = $this->database->query("UPDATE lychee_settings SET value = '$key' WHERE `key` = 'dropboxKey';");
+		$query	= Database::prepare($this->database, "UPDATE ? SET value = '?' WHERE `key` = 'dropboxKey'", array(LYCHEE_TABLE_SETTINGS, $key));
+		$result = $this->database->query($query);
 
 		if (!$result) {
 			Log::error($this->database, __METHOD__, __LINE__, $this->database->error);
@@ -176,7 +182,10 @@ class Settings extends Module {
 		}
 
 		# Execute query
-		$result = $this->database->query("UPDATE lychee_settings SET value = '$sorting' WHERE `key` = 'sorting';");
+		# Do not prepare $sorting because it is a true statement
+		# Preparing (escaping) the sorting would destroy it
+		$query	= Database::prepare($this->database, "UPDATE ? SET value = '$sorting' WHERE `key` = 'sorting'", array(LYCHEE_TABLE_SETTINGS));
+		$result	= $this->database->query($query);
 
 		if (!$result) {
 			Log::error($this->database, __METHOD__, __LINE__, $this->database->error);

+ 8 - 6
php/modules/misc.php

@@ -16,7 +16,8 @@ function search($database, $settings, $term) {
 	$return['albums'] = '';
 
 	// Photos
-	$result = $database->query("SELECT id, title, tags, public, star, album, thumbUrl FROM lychee_photos WHERE title like '%$term%' OR description like '%$term%' OR tags like '%$term%';");
+	$query	= Database::prepare($database, "SELECT id, title, tags, public, star, album, thumbUrl FROM ? WHERE title LIKE '%?%' OR description LIKE '%%' OR tags LIKE '%?%'", array(LYCHEE_TABLE_PHOTOS, $term, $term, $term));
+	$result	= $database->query($query);
 	while($row = $result->fetch_assoc()) {
 		$return['photos'][$row['id']]				= $row;
 		$return['photos'][$row['id']]['thumbUrl']	= LYCHEE_URL_UPLOADS_THUMB . $row['thumbUrl'];
@@ -24,7 +25,8 @@ function search($database, $settings, $term) {
 	}
 
 	// Albums
-	$result = $database->query("SELECT id, title, public, sysstamp, password FROM lychee_albums WHERE title like '%$term%' OR description like '%$term%';");
+	$query	= Database::prepare($database, "SELECT id, title, public, sysstamp, password FROM ? WHERE title LIKE '%?%' OR description LIKE '%?%'", array(LYCHEE_TABLE_ALBUMS, $term, $term));
+	$result = $database->query($query);
 	$i		= 0;
 	while($row = $result->fetch_object()) {
 
@@ -36,7 +38,8 @@ function search($database, $settings, $term) {
 		$return['albums'][$row->id]['password']	= ($row->password=='' ? false : true);
 
 		// Thumbs
-		$result2	= $database->query("SELECT thumbUrl FROM lychee_photos WHERE album = '" . $row->id . "' " . $settings['sorting'] . " LIMIT 0, 3;");
+		$query		= Database::prepare($database, "SELECT thumbUrl FROM ? WHERE album = '?' " . $settings['sorting'] . " LIMIT 0, 3", array(LYCHEE_TABLE_PHOTOS, $row->id));
+		$result2	= $database->query($query);
 		$k			= 0;
 		while($row2 = $result2->fetch_object()){
 			$return['albums'][$row->id]["thumb$k"] = LYCHEE_URL_UPLOADS_THUMB . $row2->thumbUrl;
@@ -55,9 +58,8 @@ function getGraphHeader($database, $photoID) {
 
 	if (!isset($database, $photoID)) return false;
 
-	$photoID = mysqli_real_escape_string($database, $photoID);
-
-	$result	= $database->query("SELECT title, description, url FROM lychee_photos WHERE id = '$photoID';");
+	$query	= Database::prepare($database, "SELECT title, description, url FROM ? WHERE id = '?'", array(LYCHEE_TABLE_PHOTOS, $photoID));
+	$result	= $database->query($query);
 	$row	= $result->fetch_object();
 
 	$parseUrl	= parse_url("http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);

+ 41 - 5
plugins/check/index.php

@@ -25,6 +25,14 @@ $error = '';
 if (!file_exists(LYCHEE_CONFIG_FILE)) exit('Error 001: Configuration not found. Please install Lychee first.');
 require(LYCHEE_CONFIG_FILE);
 
+# Define the table prefix
+if (!isset($dbTablePrefix)) $dbTablePrefix = '';
+defineTablePrefix($dbTablePrefix);
+
+# Show separator
+echo('Diagnostics' . PHP_EOL);
+echo('-----------' . PHP_EOL);
+
 # Database
 $database = new mysqli($dbHost, $dbUser, $dbPassword, $dbName);
 if (mysqli_connect_errno()!=0) $error .= ('Error 100: ' . mysqli_connect_errno() . ': ' . mysqli_connect_error() . '' . PHP_EOL);
@@ -56,7 +64,8 @@ if (!isset($settings['password'])||$settings['password']=='')			$error .= ('Erro
 if (!isset($settings['thumbQuality'])||$settings['thumbQuality']=='')	$error .= ('Error 406: No or wrong property for thumbQuality in database' . PHP_EOL);
 if (!isset($settings['sorting'])||$settings['sorting']=='')				$error .= ('Error 407: Wrong property for sorting in database' . PHP_EOL);
 if (!isset($settings['plugins']))										$error .= ('Error 408: No property for plugins in database' . PHP_EOL);
-if (!isset($settings['checkForUpdates'])||($settings['checkForUpdates']!='0'&&$settings['checkForUpdates']!='1')) $error .= ('Error 409: No or wrong property for checkForUpdates in database' . PHP_EOL);
+if (!isset($settings['imagick'])||$settings['imagick']=='')				$error .= ('Error 409: No or wrong property for imagick in database' . PHP_EOL);
+if (!isset($settings['checkForUpdates'])||($settings['checkForUpdates']!='0'&&$settings['checkForUpdates']!='1')) $error .= ('Error 410: No or wrong property for checkForUpdates in database' . PHP_EOL);
 
 # Permissions
 if (hasPermissions(LYCHEE_UPLOADS_BIG)===false)			$error .= ('Error 500: Wrong permissions for \'uploads/big\' (777 required)' . PHP_EOL);
@@ -65,10 +74,6 @@ if (hasPermissions(LYCHEE_UPLOADS_IMPORT)===false)		$error .= ('Error 502: Wrong
 if (hasPermissions(LYCHEE_UPLOADS)===false)				$error .= ('Error 503: Wrong permissions for \'uploads/\' (777 required)' . PHP_EOL);
 if (hasPermissions(LYCHEE_DATA)===false)				$error .= ('Error 504: Wrong permissions for \'data/\' (777 required)' . PHP_EOL);
 
-# Output
-if ($error=='') echo('Everything is fine. Lychee should work without problems!' . PHP_EOL . PHP_EOL);
-else echo $error;
-
 # Check dropboxKey
 if (!$settings['dropboxKey']) echo('Warning: Dropbox import not working. No property for dropboxKey.' . PHP_EOL);
 
@@ -78,4 +83,35 @@ if (ini_get('max_execution_time')<200&&ini_set('upload_max_filesize', '20M')===f
 # Check mysql version
 if ($database->server_version<50500) echo('Warning: Lychee uses the GBK charset to avoid sql injections on your MySQL version. Please update to MySQL 5.5 or higher to enable UTF-8 support.' . PHP_EOL);
 
+# Output
+if ($error=='') echo('No critical problems found. Lychee should work without problems!' . PHP_EOL);
+else echo $error;
+
+# Show separator
+echo(PHP_EOL . PHP_EOL . 'System Information' . PHP_EOL);
+echo('------------------' . PHP_EOL);
+
+# Load json
+$json = file_get_contents(LYCHEE_BUILD . 'package.json');
+$json = json_decode($json, true);
+
+$imagick = extension_loaded('imagick');
+if ($imagick===false) $imagick = '-';
+
+if ($imagick===true) $imagickVersion = @Imagick::getVersion();
+if (!isset($imagickVersion)||$imagickVersion==='') $imagickVersion = '-';
+
+$gdVersion = gd_info();
+
+# Output system information
+echo('Lychee Version:  ' . $json['version'] . PHP_EOL);
+echo('DB Version:      ' . $settings['version'] . PHP_EOL);
+echo('System:          ' . PHP_OS . PHP_EOL);
+echo('PHP Version:     ' . floatval(phpversion()) . PHP_EOL);
+echo('MySQL Version:   ' . $database->server_version . PHP_EOL);
+echo('Imagick:         ' . $imagick . PHP_EOL);
+echo('Imagick Active:  ' . $settings['imagick'] . PHP_EOL);
+echo('Imagick Version: ' . $imagickVersion['versionNumber'] . PHP_EOL);
+echo('GD Version:      ' . $gdVersion['GD Version'] . PHP_EOL);
+
 ?>

+ 12 - 3
plugins/displaylog/index.php

@@ -22,6 +22,10 @@ header('content-type: text/plain');
 if (!file_exists(LYCHEE_CONFIG_FILE)) exit('Error 001: Configuration not found. Please install Lychee first.');
 require(LYCHEE_CONFIG_FILE);
 
+# Define the table prefix
+if (!isset($dbTablePrefix)) $dbTablePrefix = '';
+defineTablePrefix($dbTablePrefix);
+
 # Declare
 $result = '';
 
@@ -34,19 +38,24 @@ if (mysqli_connect_errno()!=0) {
 }
 
 # Result
-$result = $database->query('SELECT FROM_UNIXTIME(time), type, function, line, text FROM lychee_log;');
+$query	= Database::prepare($database, "SELECT FROM_UNIXTIME(time), type, function, line, text FROM ?", array(LYCHEE_TABLE_LOG));
+$result	= $database->query($query);
 
 # Output
-if ($result === FALSE) {
+if ($result->num_rows===0) {
+
 	echo('Everything looks fine, Lychee has not reported any problems!' . PHP_EOL . PHP_EOL);
+
 } else {
-	while ( $row = $result->fetch_row() ) {
+
+	while($row = $result->fetch_row()) {
 
 		# Encode result before printing
 		$row = array_map("htmlentities", $row);
 
 		# Format: time TZ - type - function(line) - text
 		printf ("%s %s - %s - %s (%s) \t- %s\n", $row[0], date_default_timezone_get(), $row[1], $row[2], $row[3], $row[4]);
+
 	}
 
 }

+ 8 - 3
view.php

@@ -24,10 +24,15 @@
 
 			if (isset($_GET['p'])&&$_GET['p']>0) {
 
-				require(__DIR__ . "/php/define.php");
+				# Load required files
+				require(__DIR__ . '/php/define.php');
+				require(__DIR__ . '/php/autoload.php');
+				require(__DIR__ . '/php/modules/misc.php');
 				require(LYCHEE_CONFIG_FILE);
-				require(LYCHEE . "php/autoload.php");
-				require(LYCHEE . "php/modules/misc.php");
+
+				# Define the table prefix
+				if (!isset($dbTablePrefix)) $dbTablePrefix = '';
+				defineTablePrefix($dbTablePrefix);
 
 				$database = Database::connect($dbHost, $dbUser, $dbPassword, $dbName);
 

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