diff --git a/README.md b/README.md index 7908538..437e06a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ In order to use the Dropbox import from your server, you need a valid drop-ins a ### Twitter Cards -Lychee supports [Twitter Cards](https://dev.twitter.com/docs/cards) and [Open Graph](http://opengraphprotocol.org) for shared images (not albums). In order to use Twitter Cards you need to request an approval for your domain. Simply share an image with Lychee, copy its link and paste it in [Twitters Card Validator](https://dev.twitter.com/docs/cards/validation/validator). +Lychee supports [Twitter Cards](https://dev.twitter.com/docs/cards) and [Open Graph](http://opengraphprotocol.org) for shared images ([not albums](https://github.com/electerious/Lychee/issues/384)). In order to use Twitter Cards you need to request an approval for your domain. Simply share an image with Lychee, copy its link and paste it in [Twitters Card Validator](https://dev.twitter.com/docs/cards/validation/validator). ### Imagick diff --git a/dist/main.css b/dist/main.css index f1aada5..66ae00c 100755 Binary files a/dist/main.css and b/dist/main.css differ diff --git a/dist/main.js b/dist/main.js index 9c066d8..dcd6c06 100644 Binary files a/dist/main.js and b/dist/main.js differ diff --git a/dist/view.js b/dist/view.js index 5df2562..55e4ffa 100644 Binary files a/dist/view.js and b/dist/view.js differ diff --git a/docs/Changelog.md b/docs/Changelog.md index 8bfd074..ffb192b 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,3 +1,12 @@ +## v3.0.6 + +Released September 13, 2015 + +- `Improved` Share photo now shares view.php link (#392) +- `Fixed` Incorrect error messages for failed uploads (#393) +- `Fixed` XSS issues and escaping problems +- `Fixed` Broken "Download album" when album has an ampersand in the password (#356) + ## v3.0.5 Released August 9, 2015 diff --git a/index.html b/index.html index a5088c2..4b591ac 100644 --- a/index.html +++ b/index.html @@ -86,12 +86,12 @@ - - - + + + diff --git a/php/modules/Album.php b/php/modules/Album.php index d91ac04..a04dcac 100644 --- a/php/modules/Album.php +++ b/php/modules/Album.php @@ -483,9 +483,6 @@ class Album extends Module { # Call plugins $this->plugins(__METHOD__, 0, func_get_args()); - # Parse - if (strlen($title)>100) $title = substr($title, 0, 100); - # Execute query $query = Database::prepare($this->database, "UPDATE ? SET title = '?' WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $title, $this->albumIDs)); $result = $this->database->query($query); @@ -509,10 +506,6 @@ class Album extends Module { # Call plugins $this->plugins(__METHOD__, 0, func_get_args()); - # Parse - $description = htmlentities($description, ENT_COMPAT | ENT_HTML401, 'UTF-8'); - if (strlen($description)>1000) $description = substr($description, 0, 1000); - # Execute query $query = Database::prepare($this->database, "UPDATE ? SET description = '?' WHERE id IN (?)", array(LYCHEE_TABLE_ALBUMS, $description, $this->albumIDs)); $result = $this->database->query($query); diff --git a/php/modules/Import.php b/php/modules/Import.php index 381e9b1..1872bbe 100644 --- a/php/modules/Import.php +++ b/php/modules/Import.php @@ -42,6 +42,7 @@ class Import extends Module { $nameFile[0]['tmp_name'] = $path; $nameFile[0]['error'] = 0; $nameFile[0]['size'] = $size; + $nameFile[0]['error'] = UPLOAD_ERR_OK; if (!$photo->add($nameFile, $albumID, $description, $tags, true)) return false; return true; diff --git a/php/modules/Photo.php b/php/modules/Photo.php index 402c42f..1035f21 100755 --- a/php/modules/Photo.php +++ b/php/modules/Photo.php @@ -88,6 +88,41 @@ class Photo extends Module { foreach ($files as $file) { + # Check if file exceeds the upload_max_filesize directive + if ($file['error']===UPLOAD_ERR_INI_SIZE) { + Log::error($this->database, __METHOD__, __LINE__, 'The uploaded file exceeds the upload_max_filesize directive in php.ini'); + if ($returnOnError===true) return false; + exit('Error: The uploaded file exceeds the upload_max_filesize directive in php.ini!'); + } + + # Check if file was only partially uploaded + if ($file['error']===UPLOAD_ERR_PARTIAL) { + Log::error($this->database, __METHOD__, __LINE__, 'The uploaded file was only partially uploaded'); + if ($returnOnError===true) return false; + exit('Error: The uploaded file was only partially uploaded!'); + } + + # Check if writing file to disk failed + if ($file['error']===UPLOAD_ERR_CANT_WRITE) { + Log::error($this->database, __METHOD__, __LINE__, 'Failed to write photo to disk'); + if ($returnOnError===true) return false; + exit('Error: Failed to write photo to disk!'); + } + + # Check if a extension stopped the file upload + if ($file['error']===UPLOAD_ERR_EXTENSION) { + Log::error($this->database, __METHOD__, __LINE__, 'A PHP extension stopped the file upload'); + if ($returnOnError===true) return false; + exit('Error: A PHP extension stopped the file upload!'); + } + + # Check if the upload was successful + if ($file['error']!==UPLOAD_ERR_OK) { + Log::error($this->database, __METHOD__, __LINE__, 'Upload contains an error (' . $file['error'] . ')'); + if ($returnOnError===true) return false; + exit('Error: Upload failed!'); + } + # Verify extension $extension = getExtension($file['name']); if (!in_array(strtolower($extension), Photo::$validExtensions, true)) { @@ -861,9 +896,6 @@ class Photo extends Module { # Call plugins $this->plugins(__METHOD__, 0, func_get_args()); - # Parse - if (strlen($title)>100) $title = substr($title, 0, 100); - # Set title $query = Database::prepare($this->database, "UPDATE ? SET title = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $title, $this->photoIDs)); $result = $this->database->query($query); @@ -894,10 +926,6 @@ class Photo extends Module { # Call plugins $this->plugins(__METHOD__, 0, func_get_args()); - # Parse - $description = htmlentities($description, ENT_COMPAT | ENT_HTML401, 'UTF-8'); - if (strlen($description)>1000) $description = substr($description, 0, 1000); - # Set description $query = Database::prepare($this->database, "UPDATE ? SET description = '?' WHERE id IN ('?')", array(LYCHEE_TABLE_PHOTOS, $description, $this->photoIDs)); $result = $this->database->query($query); @@ -1087,10 +1115,6 @@ class Photo extends Module { # Parse tags $tags = preg_replace('/(\ ,\ )|(\ ,)|(,\ )|(,{1,}\ {0,})|(,$|^,)/', ',', $tags); $tags = preg_replace('/,$|^,|(\ ){0,}$/', '', $tags); - if (strlen($tags)>1000) { - Log::notice($this->database, __METHOD__, __LINE__, 'Length of tags higher than 1000'); - return false; - } # Set tags $query = Database::prepare($this->database, "UPDATE ? SET tags = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $tags, $this->photoIDs)); diff --git a/src/gulpfile.js b/src/gulpfile.js index 8e70103..b313f03 100644 --- a/src/gulpfile.js +++ b/src/gulpfile.js @@ -40,9 +40,9 @@ gulp.task('view--js', function() { var stream = gulp.src(paths.view.js) - .pipe(plugins.babel()) - .on('error', catchError) .pipe(plugins.concat('_view--javascript.js', {newLine: "\n"})) + .pipe(plugins.babel({ compact: true })) + .on('error', catchError) .pipe(gulp.dest('../dist/')); return stream; @@ -109,9 +109,9 @@ gulp.task('main--js', function() { var stream = gulp.src(paths.main.js) - .pipe(plugins.babel()) - .on('error', catchError) .pipe(plugins.concat('_main--javascript.js', {newLine: "\n"})) + .pipe(plugins.babel({ compact: true })) + .on('error', catchError) .pipe(gulp.dest('../dist/')); return stream; diff --git a/src/package.json b/src/package.json index 231cd19..2ced175 100644 --- a/src/package.json +++ b/src/package.json @@ -1,6 +1,6 @@ { "name": "Lychee", - "version": "3.0.5", + "version": "3.0.6", "description": "Self-hosted photo-management done right.", "authors": "Tobias Reich ", "license": "MIT", @@ -10,18 +10,18 @@ "url": "https://github.com/electerious/Lychee.git" }, "devDependencies": { - "basiccontext": "^3.3.0", - "basicmodal": "^3.1.1", + "basiccontext": "^3.3.1", + "basicmodal": "^3.1.2", "gulp": "^3.9.0", - "gulp-autoprefixer": "2.3.1", - "gulp-babel": "^5.2.0", + "gulp-autoprefixer": "3.0.1", + "gulp-babel": "^5.2.1", "gulp-concat": "^2.6.0", "gulp-inject": "^1.5.0", "gulp-load-plugins": "^1.0.0-rc", - "gulp-minify-css": "^1.2.0", + "gulp-minify-css": "^1.2.1", "gulp-rimraf": "^0.1.1", "gulp-sass": "^2.0.4", - "gulp-uglify": "^1.2.0", + "gulp-uglify": "^1.4.1", "jquery": "^2.1.4", "mousetrap": "^1.5.3" } diff --git a/src/scripts/album.js b/src/scripts/album.js index b7ff84c..c813e3a 100644 --- a/src/scripts/album.js +++ b/src/scripts/album.js @@ -204,14 +204,14 @@ album.delete = function(albumIDs) { if (album.json) albumTitle = album.json.title else if (albums.json) albumTitle = albums.getByID(albumIDs).title - msg = `

Are you sure you want to delete the album ${ albumTitle } and all of the photos it contains? This action can't be undone!

` + msg = lychee.html`

Are you sure you want to delete the album '$${ albumTitle }' and all of the photos it contains? This action can't be undone!

` } else { action.title = 'Delete Albums and Photos' cancel.title = 'Keep Albums' - msg = `

Are you sure you want to delete all ${ albumIDs.length } selected albums and all of the photos they contain? This action can't be undone!

` + msg = lychee.html`

Are you sure you want to delete all $${ albumIDs.length } selected albums and all of the photos they contain? This action can't be undone!

` } @@ -247,7 +247,6 @@ album.setTitle = function(albumIDs) { else if (albums.json) oldTitle = albums.getByID(albumIDs).title if (!oldTitle) oldTitle = '' - oldTitle = oldTitle.replace(/'/g, ''') } @@ -257,9 +256,6 @@ album.setTitle = function(albumIDs) { basicModal.close() - // Remove html from input - newTitle = lychee.removeHTML(newTitle) - // Set title to Untitled when empty newTitle = (newTitle==='') ? 'Untitled' : newTitle @@ -296,10 +292,10 @@ album.setTitle = function(albumIDs) { } - let input = `` + let input = lychee.html`` - if (albumIDs.length===1) msg = `

Enter a new title for this album: ${ input }

` - else msg = `

Enter a title for all ${ albumIDs.length } selected albums: ${ input }

` + if (albumIDs.length===1) msg = lychee.html`

Enter a new title for this album: ${ input }

` + else msg = lychee.html`

Enter a title for all $${ albumIDs.length } selected albums: ${ input }

` basicModal.show({ body: msg, @@ -327,9 +323,6 @@ album.setDescription = function(albumID) { basicModal.close() - // Remove html from input - description = lychee.removeHTML(description) - if (visible.album()) { album.json.description = description view.album.description() @@ -349,7 +342,7 @@ album.setDescription = function(albumID) { } basicModal.show({ - body: `

Please enter a description for this album:

`, + body: lychee.html`

Please enter a description for this album:

`, buttons: { action: { title: 'Set Description', @@ -552,7 +545,7 @@ album.getArchive = function(albumID) { if (location.href.indexOf('index.html')>0) link = location.href.replace(location.hash, '').replace('index.html', url) else link = location.href.replace(location.hash, '') + url - if (lychee.publicMode===true) link += `&password=${ password.value }` + if (lychee.publicMode===true) link += `&password=${ encodeURIComponent(password.value) }` location.href = link @@ -581,11 +574,11 @@ album.merge = function(albumIDs) { if (!sTitle) sTitle = '' sTitle = sTitle.replace(/'/g, ''') - msg = `

Are you sure you want to merge the album '${ sTitle }' into the album '${ title }'?

` + msg = lychee.html`

Are you sure you want to merge the album '$${ sTitle }' into the album '$${ title }'?

` } else { - msg = `

Are you sure you want to merge all selected albums into the album '${ title }'?

` + msg = lychee.html`

Are you sure you want to merge all selected albums into the album '$${ title }'?

` } diff --git a/src/scripts/build.js b/src/scripts/build.js index 1da00dc..3e25391 100644 --- a/src/scripts/build.js +++ b/src/scripts/build.js @@ -7,54 +7,66 @@ build = {} build.iconic = function(icon, classes = '') { - return `` + let html = '' + + html += lychee.html`` + + return html } build.divider = function(title) { - return `

${ title }

` + let html = '' + + html += lychee.html`

$${ title }

` + + return html } build.editIcon = function(id) { - return `
${ build.iconic('pencil') }
` + let html = '' + + html += lychee.html`
${ build.iconic('pencil') }
` + + return html } build.multiselect = function(top, left) { - return `
` + return lychee.html`
` } build.album = function(data) { - if (data==null) return '' + let html = '' let { path: thumbPath, hasRetina: thumbRetina } = lychee.retinize(data.thumbs[0]) - let html = ` -
- thumb - thumb - thumb -
-

${ data.title }

-
${ data.sysdate } -
- ` + html += lychee.html` +
+ thumb + thumb + thumb +
+

$${ data.title }

+ $${ data.sysdate } +
+ ` if (lychee.publicMode===false) { - html += ` + html += lychee.html` ` @@ -68,34 +80,34 @@ build.album = function(data) { build.photo = function(data) { - if (data==null) return '' + let html = '' let { path: thumbPath, hasRetina: thumbRetina } = lychee.retinize(data.thumbUrl) - let html = ` -
- thumb -
-

${ data.title }

- ` + html += lychee.html` +
+ thumb +
+

$${ data.title }

+ ` - if (data.cameraDate==='1') html += `${ build.iconic('camera-slr') }${ data.sysdate }` - else html += `${ data.sysdate }` + if (data.cameraDate==='1') html += lychee.html`${ build.iconic('camera-slr') }$${ data.sysdate }` + else html += lychee.html`$${ data.sysdate }` - html += '
' + html += `
` if (lychee.publicMode===false) { - html += ` + html += lychee.html` ` } - html += '
' + html += `
` return html @@ -103,24 +115,22 @@ build.photo = function(data) { build.imageview = function(data, size, visibleControls) { - if (data==null) return '' - let html = '' if (size==='big') { - if (visibleControls===true) html += `
` - else html += `
` + if (visibleControls===true) html += lychee.html`
` + else html += lychee.html`
` } else if (size==='medium') { - if (visibleControls===true) html += `
` - else html += `
` + if (visibleControls===true) html += lychee.html`
` + else html += lychee.html`
` } else if (size==='small') { - if (visibleControls===true) html += `
` - else html += `
` + if (visibleControls===true) html += lychee.html`
` + else html += lychee.html`
` } @@ -135,27 +145,29 @@ build.imageview = function(data, size, visibleControls) { build.no_content = function(typ) { - let html = ` -
- ${ build.iconic(typ) } - ` + let html = '' + + html += ` +
+ ${ build.iconic(typ) } + ` switch (typ) { case 'magnifying-glass': - html += '

No results

' + html += `

No results

` break case 'eye': - html += '

No public albums

' + html += `

No public albums

` break case 'cog': - html += '

No configuration

' + html += `

No configuration

` break case 'question-mark': - html += '

Photo not found

' + html += `

Photo not found

` break } - html += '
' + html += `
` return html @@ -163,10 +175,12 @@ build.no_content = function(typ) { build.uploadModal = function(title, files) { - let html = ` -

${ title }

-
- ` + let html = '' + + html += lychee.html` +

$${ title }

+
+ ` let i = 0 @@ -176,9 +190,9 @@ build.uploadModal = function(title, files) { if (file.name.length>40) file.name = file.name.substr(0, 17) + '...' + file.name.substr(file.name.length-20, 20) - html += ` + html += lychee.html`
- ${ lychee.escapeHTML(file.name) } + $${ file.name } ` if (file.supported===true) html += `` @@ -193,7 +207,7 @@ build.uploadModal = function(title, files) { } - html += '
' + html += `
` return html @@ -208,7 +222,7 @@ build.tags = function(tags) { tags = tags.split(',') tags.forEach(function(tag, index, array) { - html += `${ tag }${ build.iconic('x') }` + html += lychee.html`$${ tag }${ build.iconic('x') }` }) } else { diff --git a/src/scripts/contextMenu.js b/src/scripts/contextMenu.js index f78d156..b387bfa 100644 --- a/src/scripts/contextMenu.js +++ b/src/scripts/contextMenu.js @@ -101,7 +101,7 @@ contextMenu.albumTitle = function(albumID, e) { if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg' - let title = `
${ this.title }
` + let title = lychee.html`
$${ this.title }
` if (this.id!=albumID) items.push({ type: 'item', title, fn: () => lychee.goto(this.id) }) @@ -131,7 +131,7 @@ contextMenu.mergeAlbum = function(albumID, e) { if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg' - let title = `
${ this.title }
` + let title = lychee.html`
$${ this.title }
` if (this.id!=albumID) items.push({ type: 'item', title, fn: () => album.merge([albumID, this.id]) }) @@ -206,7 +206,7 @@ contextMenu.photoTitle = function(albumID, photoID, e) { // Generate list of albums $.each(data.content, function(index) { - let title = `
${ this.title }
` + let title = lychee.html`
$${ this.title }
` if (this.id!=photoID) items.push({ type: 'item', title, fn: () => lychee.goto(albumID + '/' + this.id) }) @@ -254,7 +254,7 @@ contextMenu.move = function(photoIDs, e) { if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg' - let title = `
${ this.title }
` + let title = lychee.html`
$${ this.title }
` if (this.id!=album.getID()) items.push({ type: 'item', title, fn: () => photo.setAlbum(photoIDs, this.id) }) @@ -281,8 +281,6 @@ contextMenu.sharePhoto = function(photoID, e) { let link = photo.getViewLink(photoID), iconClass = 'ionicons' - if (photo.json.public==='2') link = location.href - let items = [ { type: 'item', title: ``, fn: () => {}, class: 'noHover' }, { type: 'separator' }, diff --git a/src/scripts/header.js b/src/scripts/header.js index 716a51c..e185fcc 100644 --- a/src/scripts/header.js +++ b/src/scripts/header.js @@ -107,9 +107,10 @@ header.hide = function(e, delay = 500) { header.setTitle = function(title = 'Untitled') { - let $title = header.dom('#title') + let $title = header.dom('#title'), + html = lychee.html`$${ title }${ build.iconic('caret-bottom') }` - $title.html(title + build.iconic('caret-bottom')) + $title.html(html) return true diff --git a/src/scripts/lychee.js b/src/scripts/lychee.js index a07743a..506a28c 100644 --- a/src/scripts/lychee.js +++ b/src/scripts/lychee.js @@ -6,8 +6,8 @@ lychee = { title : document.title, - version : '3.0.5', - version_code : '030005', + version : '3.0.6', + version_code : '030006', update_path : 'http://lychee.electerious.com/version/index.php', updateURL : 'https://github.com/electerious/Lychee', @@ -119,12 +119,12 @@ lychee.login = function(data) { lychee.loginDialog = function() { - let msg = ` + let msg = lychee.html` -

Lychee ${ lychee.version }Update available!

+

Lychee $${ lychee.version }Update available!

` basicModal.show({ @@ -312,15 +312,6 @@ lychee.animate = function(obj, animation) { } -lychee.escapeHTML = function(s) { - - return s.replace(/&/g, '&') - .replace(/"/g, '"') - .replace(//g, '>') - -} - lychee.retinize = function(path = '') { let pixelRatio = window.devicePixelRatio, @@ -385,14 +376,54 @@ lychee.getEventName = function() { } -lychee.removeHTML = function(html = '') { +lychee.escapeHTML = function(html = '') { - if (html==='') return html + // Ensure that html is a string + html += '' - let tmp = document.createElement('DIV') - tmp.innerHTML = html + // Escape all critical characters + html = html.replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/`/g, '`') - return (tmp.textContent || tmp.innerText) + return html + +} + +lychee.html = function(literalSections, ...substs) { + + // Use raw literal sections: we don’t want + // backslashes (\n etc.) to be interpreted + let raw = literalSections.raw, + result = '' + + substs.forEach((subst, i) => { + + // Retrieve the literal section preceding + // the current substitution + let lit = raw[i] + + // If the substitution is preceded by a dollar sign, + // we escape special characters in it + if (lit.slice(-1)==='$') { + subst = lychee.escapeHTML(subst) + lit = lit.slice(0, -1) + } + + result += lit + result += subst + + }) + + // Take care of last literal section + // (Never fails, because an empty template string + // produces one literal section, an empty string) + result += raw[raw.length-1] + + return result } diff --git a/src/scripts/password.js b/src/scripts/password.js index 7187d1f..6b28601 100644 --- a/src/scripts/password.js +++ b/src/scripts/password.js @@ -55,8 +55,10 @@ password.getDialog = function(albumID, callback) { const action = (data) => password.get(albumID, callback, data.password) const cancel = () => { + basicModal.close() if (visible.albums()===false) lychee.goto() + } let msg = ` diff --git a/src/scripts/photo.js b/src/scripts/photo.js index 4ebdc26..5770b5c 100644 --- a/src/scripts/photo.js +++ b/src/scripts/photo.js @@ -244,14 +244,14 @@ photo.delete = function(photoIDs) { action.title = 'Delete Photo' cancel.title = 'Keep Photo' - msg = `

Are you sure you want to delete the photo '${ photoTitle }'? This action can't be undone!

` + msg = lychee.html`

Are you sure you want to delete the photo '$${ photoTitle }'? This action can't be undone!

` } else { action.title = 'Delete Photo' cancel.title = 'Keep Photo' - msg = `

Are you sure you want to delete all ${ photoIDs.length } selected photo? This action can't be undone!

` + msg = lychee.html`

Are you sure you want to delete all $${ photoIDs.length } selected photo? This action can't be undone!

` } @@ -285,7 +285,6 @@ photo.setTitle = function(photoIDs) { // Get old title if only one photo is selected if (photo.json) oldTitle = photo.json.title else if (album.json) oldTitle = album.json.content[photoIDs].title - oldTitle = oldTitle.replace(/'/g, ''') } @@ -295,9 +294,6 @@ photo.setTitle = function(photoIDs) { let newTitle = data.title - // Remove html from input - newTitle = lychee.removeHTML(newTitle) - if (visible.photo()) { photo.json.title = (newTitle==='' ? 'Untitled' : newTitle) view.photo.title() @@ -321,10 +317,10 @@ photo.setTitle = function(photoIDs) { } - let input = `` + let input = lychee.html`` - if (photoIDs.length===1) msg = `

Enter a new title for this photo: ${ input }

` - else msg = `

Enter a title for all ${ photoIDs.length } selected photos: ${ input }

` + if (photoIDs.length===1) msg = lychee.html`

Enter a new title for this photo: ${ input }

` + else msg = lychee.html`

Enter a title for all $${ photoIDs.length } selected photos: ${ input }

` basicModal.show({ body: msg, @@ -465,7 +461,7 @@ photo.setPublic = function(photoID, e) { photo.setDescription = function(photoID) { - let oldDescription = photo.json.description.replace(/'/g, ''') + let oldDescription = photo.json.description const action = function(data) { @@ -473,9 +469,6 @@ photo.setDescription = function(photoID) { let description = data.description - // Remove html from input - description = lychee.removeHTML(description) - if (visible.photo()) { photo.json.description = description view.photo.description() @@ -495,7 +488,7 @@ photo.setDescription = function(photoID) { } basicModal.show({ - body: `

Enter a description for this photo:

`, + body: lychee.html`

Enter a description for this photo:

`, buttons: { action: { title: 'Set Description', @@ -541,10 +534,10 @@ photo.editTags = function(photoIDs) { } - let input = `` + let input = lychee.html`` - if (photoIDs.length===1) msg = `

Enter your tags for this photo. You can add multiple tags by separating them with a comma: ${ input }

` - else msg = `

Enter your tags for all ${ photoIDs.length } selected photos. Existing tags will be overwritten. You can add multiple tags by separating them with a comma: ${ input }

` + if (photoIDs.length===1) msg = lychee.html`

Enter your tags for this photo. You can add multiple tags by separating them with a comma: ${ input }

` + else msg = lychee.html`

Enter your tags for all $${ photoIDs.length } selected photos. Existing tags will be overwritten. You can add multiple tags by separating them with a comma: ${ input }

` basicModal.show({ body: msg, @@ -571,9 +564,6 @@ photo.setTags = function(photoIDs, tags) { tags = tags.replace(/(\ ,\ )|(\ ,)|(,\ )|(,{1,}\ {0,})|(,$|^,)/g, ',') tags = tags.replace(/,$|^,|(\ ){0,}$/g, '') - // Remove html from input - tags = lychee.removeHTML(tags) - if (visible.photo()) { photo.json.tags = tags view.photo.tags() @@ -684,7 +674,7 @@ photo.getArchive = function(photoID) { if (location.href.indexOf('index.html')>0) link = location.href.replace(location.hash, '').replace('index.html', url) else link = location.href.replace(location.hash, '') + url - if (lychee.publicMode===true) link += '&password=' + password.value + if (lychee.publicMode===true) link += `&password=${ encodeURIComponent(password.value) }` location.href = link diff --git a/src/scripts/settings.js b/src/scripts/settings.js index 89e054c..876ef8b 100644 --- a/src/scripts/settings.js +++ b/src/scripts/settings.js @@ -404,10 +404,10 @@ settings.setDropboxKey = function(callback) { } - let msg = ` + let msg = lychee.html`

In order to import photos from your Dropbox, you need a valid drop-ins app key from their website. Generate yourself a personal key and enter it below: - +

` diff --git a/src/scripts/sidebar.js b/src/scripts/sidebar.js index 9cbd473..d301ef6 100644 --- a/src/scripts/sidebar.js +++ b/src/scripts/sidebar.js @@ -93,13 +93,17 @@ sidebar.setSelectable = function(selectable = true) { } -sidebar.changeAttr = function(attr, value = '-') { +sidebar.changeAttr = function(attr, value = '-', dangerouslySetInnerHTML = false) { if (attr==null || attr==='') return false // Set a default for the value if (value==null || value==='') value = '-' + // Escape value + if (dangerouslySetInnerHTML===false) value = lychee.escapeHTML(value) + + // Set new value sidebar.dom('.attr_' + attr).html(value) return true @@ -339,14 +343,14 @@ sidebar.render = function(structure) { if (value==='' || value==null) value = '-' // Wrap span-element around value for easier selecting on change - value = `${ value }` + value = lychee.html`$${ value }` // Add edit-icon to the value when editable if (row.editable===true) value += ' ' + build.editIcon('edit_' + row.title.toLowerCase()) - _html += ` + _html += lychee.html` - ${ row.title } + $${ row.title } ${ value } ` @@ -363,20 +367,19 @@ sidebar.render = function(structure) { let renderTags = function(section) { - let _html = '' - - _html += ` -
-

${ section.title }

-
-
-
${ section.value }
- ` + let _html = '', + editable = '' // Add edit-icon to the value when editable - if (section.editable===true) _html += build.editIcon('edit_tags') + if (section.editable===true) editable = build.editIcon('edit_tags') - _html += ` + _html += lychee.html` +
+

$${ section.title }

+
+
+
${ section.value }
+ ${ editable }
` diff --git a/src/scripts/upload.js b/src/scripts/upload.js index e536a75..2d473af 100755 --- a/src/scripts/upload.js +++ b/src/scripts/upload.js @@ -338,7 +338,7 @@ upload.start = { } basicModal.show({ - body: `

Please enter the direct link to a photo to import it:

`, + body: lychee.html`

Please enter the direct link to a photo to import it:

`, buttons: { action: { title: 'Import', @@ -444,7 +444,7 @@ upload.start = { } basicModal.show({ - body: `

This action will import all photos, folders and sub-folders which are located in the following directory. The original files will be deleted after the import when possible.

`, + body: lychee.html`

This action will import all photos, folders and sub-folders which are located in the following directory. The original files will be deleted after the import when possible.

`, buttons: { action: { title: 'Import', diff --git a/src/scripts/view.js b/src/scripts/view.js index 1fed949..af63fb5 100644 --- a/src/scripts/view.js +++ b/src/scripts/view.js @@ -72,6 +72,8 @@ view.albums = { let title = albums.getByID(albumID).title + title = lychee.escapeHTML(title) + $('.album[data-id="' + albumID + '"] .overlay h1') .html(title) .attr('title', title) @@ -164,6 +166,8 @@ view.album = { let title = album.json.content[photoID].title + title = lychee.escapeHTML(title) + $('.photo[data-id="' + photoID + '"] .overlay h1') .html(title) .attr('title', title) @@ -199,7 +203,6 @@ view.album = { if (!visible.albums()) { album.json.num-- view.album.num() - view.album.title() } }) @@ -396,7 +399,7 @@ view.photo = { tags: function() { - sidebar.changeAttr('tags', build.tags(photo.json.tags)) + sidebar.changeAttr('tags', build.tags(photo.json.tags), true) sidebar.bind() }, @@ -416,7 +419,7 @@ view.photo = { let nextPhotoID = album.json.content[photo.getID()].nextPhoto, nextPhoto = album.json.content[nextPhotoID] - $nextArrow.css('background-image', `linear-gradient(to bottom, rgba(0, 0, 0, .4), rgba(0, 0, 0, .4)), url("${ nextPhoto.thumbUrl }")`) + $nextArrow.css('background-image', lychee.html`linear-gradient(to bottom, rgba(0, 0, 0, .4), rgba(0, 0, 0, .4)), url("$${ nextPhoto.thumbUrl }")`) } @@ -426,7 +429,7 @@ view.photo = { let previousPhotoID = album.json.content[photo.getID()].previousPhoto, previousPhoto = album.json.content[previousPhotoID] - $previousArrow.css('background-image', `linear-gradient(to bottom, rgba(0, 0, 0, .4), rgba(0, 0, 0, .4)), url("${ previousPhoto.thumbUrl }")`) + $previousArrow.css('background-image', lychee.html`linear-gradient(to bottom, rgba(0, 0, 0, .4), rgba(0, 0, 0, .4)), url("$${ previousPhoto.thumbUrl }")`) } diff --git a/src/scripts/view/main.js b/src/scripts/view/main.js index 8335c47..7e85922 100644 --- a/src/scripts/view/main.js +++ b/src/scripts/view/main.js @@ -3,18 +3,74 @@ * @copyright 2015 by Tobias Reich */ -let lychee = { - content: $('#content'), - getEventName() { +// Sub-implementation of Lychee -------------------------------------------------------------- // - let touchendSupport = (/Android|iPhone|iPad|iPod/i).test(navigator.userAgent || navigator.vendor || window.opera) && ('ontouchend' in document.documentElement), - eventName = (touchendSupport===true ? 'touchend' : 'click') +let lychee = {} - return eventName +lychee.content = $('#content') + +lychee.getEventName = function() { + + let touchendSupport = (/Android|iPhone|iPad|iPod/i).test(navigator.userAgent || navigator.vendor || window.opera) && ('ontouchend' in document.documentElement), + eventName = (touchendSupport===true ? 'touchend' : 'click') + + return eventName - } } +lychee.escapeHTML = function(html = '') { + + // Ensure that html is a string + html += '' + + // Escape all critical characters + html = html.replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/`/g, '`') + + return html + +} + +lychee.html = function(literalSections, ...substs) { + + // Use raw literal sections: we don’t want + // backslashes (\n etc.) to be interpreted + let raw = literalSections.raw, + result = '' + + substs.forEach((subst, i) => { + + // Retrieve the literal section preceding + // the current substitution + let lit = raw[i] + + // If the substitution is preceded by a dollar sign, + // we escape special characters in it + if (lit.slice(-1)==='$') { + subst = lychee.escapeHTML(subst) + lit = lit.slice(0, -1) + } + + result += lit + result += subst + + }) + + // Take care of last literal section + // (Never fails, because an empty template string + // produces one literal section, an empty string) + result += raw[raw.length-1] + + return result + +} + +// Main -------------------------------------------------------------- // + let loadingBar = { show() {}, hide() {} }, imageview = $('#imageview') @@ -99,7 +155,7 @@ const loadPhotoInfo = function(photoID) { // Set title if (!data.title) data.title = 'Untitled' document.title = 'Lychee - ' + data.title - header.dom('#title').html(data.title) + header.dom('#title').html(lychee.escapeHTML(data.title)) let size = getPhotoSize(data) diff --git a/src/styles/_content.scss b/src/styles/_content.scss index 520b4cd..099fb5e 100644 --- a/src/styles/_content.scss +++ b/src/styles/_content.scss @@ -112,7 +112,7 @@ } // No overlay for empty albums - .album img[data-retina='false'] + .overlay { + .album img[data-overlay='false'] + .overlay { background: none; } @@ -154,8 +154,8 @@ filter: drop-shadow(0 1px 3px black(.4)); } - .album img[data-retina='false'] + .overlay h1, - .album img[data-retina='false'] + .overlay a { text-shadow: none; } + .album img[data-overlay='false'] + .overlay h1, + .album img[data-overlay='false'] + .overlay a { text-shadow: none; } /* Badges ------------------------------------------------*/ .album .badges,