Merge pull request #403 from electerious/develop

Lychee 3.0.6
This commit is contained in:
Tobias Reich 2015-09-13 14:45:03 +02:00
commit 7879869a58
24 changed files with 308 additions and 190 deletions

View File

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

BIN
dist/main.css vendored

Binary file not shown.

BIN
dist/main.js vendored

Binary file not shown.

BIN
dist/view.js vendored

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -204,14 +204,14 @@ album.delete = function(albumIDs) {
if (album.json) albumTitle = album.json.title
else if (albums.json) albumTitle = albums.getByID(albumIDs).title
msg = `<p>Are you sure you want to delete the album ${ albumTitle } and all of the photos it contains? This action can't be undone!</p>`
msg = lychee.html`<p>Are you sure you want to delete the album '$${ albumTitle }' and all of the photos it contains? This action can't be undone!</p>`
} else {
action.title = 'Delete Albums and Photos'
cancel.title = 'Keep Albums'
msg = `<p>Are you sure you want to delete all ${ albumIDs.length } selected albums and all of the photos they contain? This action can't be undone!</p>`
msg = lychee.html`<p>Are you sure you want to delete all $${ albumIDs.length } selected albums and all of the photos they contain? This action can't be undone!</p>`
}
@ -247,7 +247,6 @@ album.setTitle = function(albumIDs) {
else if (albums.json) oldTitle = albums.getByID(albumIDs).title
if (!oldTitle) oldTitle = ''
oldTitle = oldTitle.replace(/'/g, '&apos;')
}
@ -257,9 +256,6 @@ album.setTitle = function(albumIDs) {
basicModal.close()
// Remove html from input
newTitle = lychee.removeHTML(newTitle)
// Set title to Untitled when empty
newTitle = (newTitle==='') ? 'Untitled' : newTitle
@ -296,10 +292,10 @@ album.setTitle = function(albumIDs) {
}
let input = `<input class='text' name='title' type='text' maxlength='50' placeholder='Title' value='${ oldTitle }'>`
let input = lychee.html`<input class='text' name='title' type='text' maxlength='50' placeholder='Title' value='$${ oldTitle }'>`
if (albumIDs.length===1) msg = `<p>Enter a new title for this album: ${ input }</p>`
else msg = `<p>Enter a title for all ${ albumIDs.length } selected albums: ${ input }</p>`
if (albumIDs.length===1) msg = lychee.html`<p>Enter a new title for this album: ${ input }</p>`
else msg = lychee.html`<p>Enter a title for all $${ albumIDs.length } selected albums: ${ input }</p>`
basicModal.show({
body: msg,
@ -327,9 +323,6 @@ album.setDescription = function(albumID) {
basicModal.close()
// Remove html from input
description = lychee.removeHTML(description)
if (visible.album()) {
album.json.description = description
view.album.description()
@ -349,7 +342,7 @@ album.setDescription = function(albumID) {
}
basicModal.show({
body: `<p>Please enter a description for this album: <input class='text' name='description' type='text' maxlength='800' placeholder='Description' value='${ oldDescription }'></p>`,
body: lychee.html`<p>Please enter a description for this album: <input class='text' name='description' type='text' maxlength='800' placeholder='Description' value='$${ oldDescription }'></p>`,
buttons: {
action: {
title: 'Set Description',
@ -552,7 +545,7 @@ album.getArchive = function(albumID) {
if (location.href.indexOf('index.html')>0) link = location.href.replace(location.hash, '').replace('index.html', url)
else link = location.href.replace(location.hash, '') + url
if (lychee.publicMode===true) link += `&password=${ password.value }`
if (lychee.publicMode===true) link += `&password=${ encodeURIComponent(password.value) }`
location.href = link
@ -581,11 +574,11 @@ album.merge = function(albumIDs) {
if (!sTitle) sTitle = ''
sTitle = sTitle.replace(/'/g, '&apos;')
msg = `<p>Are you sure you want to merge the album '${ sTitle }' into the album '${ title }'?</p>`
msg = lychee.html`<p>Are you sure you want to merge the album '$${ sTitle }' into the album '$${ title }'?</p>`
} else {
msg = `<p>Are you sure you want to merge all selected albums into the album '${ title }'?</p>`
msg = lychee.html`<p>Are you sure you want to merge all selected albums into the album '$${ title }'?</p>`
}

View File

@ -7,54 +7,66 @@ build = {}
build.iconic = function(icon, classes = '') {
return `<svg class='iconic ${ classes }'><use xlink:href='#${ icon }' /></svg>`
let html = ''
html += lychee.html`<svg class='iconic $${ classes }'><use xlink:href='#$${ icon }' /></svg>`
return html
}
build.divider = function(title) {
return `<div class='divider'><h1>${ title }</h1></div>`
let html = ''
html += lychee.html`<div class='divider'><h1>$${ title }</h1></div>`
return html
}
build.editIcon = function(id) {
return `<div id='${ id }' class='edit'>${ build.iconic('pencil') }</div>`
let html = ''
html += lychee.html`<div id='$${ id }' class='edit'>${ build.iconic('pencil') }</div>`
return html
}
build.multiselect = function(top, left) {
return `<div id='multiselect' style='top: ${ top }px; left: ${ left }px;'></div>`
return lychee.html`<div id='multiselect' style='top: $${ top }px; left: $${ left }px;'></div>`
}
build.album = function(data) {
if (data==null) return ''
let html = ''
let { path: thumbPath, hasRetina: thumbRetina } = lychee.retinize(data.thumbs[0])
let html = `
<div class='album' data-id='${ data.id }'>
<img src='${ data.thumbs[2] }' width='200' height='200' alt='thumb' data-retina='false'>
<img src='${ data.thumbs[1] }' width='200' height='200' alt='thumb' data-retina='false'>
<img src='${ thumbPath }' width='200' height='200' alt='thumb' data-retina='${ thumbRetina }'>
<div class='overlay'>
<h1 title='${ data.title }'>${ data.title }</h1>
<a>${ data.sysdate }</a>
</div>
`
html += lychee.html`
<div class='album' data-id='$${ data.id }'>
<img src='$${ data.thumbs[2] }' width='200' height='200' alt='thumb' data-overlay='false'>
<img src='$${ data.thumbs[1] }' width='200' height='200' alt='thumb' data-overlay='false'>
<img src='$${ thumbPath }' width='200' height='200' alt='thumb' data-overlay='$${ thumbRetina }'>
<div class='overlay'>
<h1 title='$${ data.title }'>$${ data.title }</h1>
<a>$${ data.sysdate }</a>
</div>
`
if (lychee.publicMode===false) {
html += `
html += lychee.html`
<div class='badges'>
<a class='badge ${ (data.star==='1' ? 'badge--visible' : '') } icn-star'>${ build.iconic('star') }</a>
<a class='badge ${ (data.public==='1' ? 'badge--visible' : '') } icn-share'>${ build.iconic('eye') }</a>
<a class='badge ${ (data.unsorted==='1' ? 'badge--visible' : '') }'>${ build.iconic('list') }</a>
<a class='badge ${ (data.recent==='1' ? 'badge--visible' : '') }'>${ build.iconic('clock') }</a>
<a class='badge ${ (data.password==='1' ? 'badge--visible' : '') }'>${ build.iconic('lock-locked') }</a>
<a class='badge $${ (data.star==='1' ? 'badge--visible' : '') } icn-star'>${ build.iconic('star') }</a>
<a class='badge $${ (data.public==='1' ? 'badge--visible' : '') } icn-share'>${ build.iconic('eye') }</a>
<a class='badge $${ (data.unsorted==='1' ? 'badge--visible' : '') }'>${ build.iconic('list') }</a>
<a class='badge $${ (data.recent==='1' ? 'badge--visible' : '') }'>${ build.iconic('clock') }</a>
<a class='badge $${ (data.password==='1' ? 'badge--visible' : '') }'>${ build.iconic('lock-locked') }</a>
</div>
`
@ -68,34 +80,34 @@ build.album = function(data) {
build.photo = function(data) {
if (data==null) return ''
let html = ''
let { path: thumbPath, hasRetina: thumbRetina } = lychee.retinize(data.thumbUrl)
let html = `
<div class='photo' data-album-id='${ data.album }' data-id='${ data.id }'>
<img src='${ thumbPath }' width='200' height='200' alt='thumb'>
<div class='overlay'>
<h1 title='${ data.title }'>${ data.title }</h1>
`
html += lychee.html`
<div class='photo' data-album-id='$${ data.album }' data-id='$${ data.id }'>
<img src='$${ thumbPath }' width='200' height='200' alt='thumb'>
<div class='overlay'>
<h1 title='$${ data.title }'>$${ data.title }</h1>
`
if (data.cameraDate==='1') html += `<a><span title='Camera Date'>${ build.iconic('camera-slr') }</span>${ data.sysdate }</a>`
else html += `<a>${ data.sysdate }</a>`
if (data.cameraDate==='1') html += lychee.html`<a><span title='Camera Date'>${ build.iconic('camera-slr') }</span>$${ data.sysdate }</a>`
else html += lychee.html`<a>$${ data.sysdate }</a>`
html += '</div>'
html += `</div>`
if (lychee.publicMode===false) {
html += `
html += lychee.html`
<div class='badges'>
<a class='badge ${ (data.star==='1' ? 'badge--visible' : '') } icn-star'>${ build.iconic('star') }</a>
<a class='badge ${ ((data.public==='1' && album.json.public!=='1') ? 'badge--visible' : '') } icn-share'>${ build.iconic('eye') }</a>
<a class='badge $${ (data.star==='1' ? 'badge--visible' : '') } icn-star'>${ build.iconic('star') }</a>
<a class='badge $${ ((data.public==='1' && album.json.public!=='1') ? 'badge--visible' : '') } icn-share'>${ build.iconic('eye') }</a>
</div>
`
}
html += '</div>'
html += `</div>`
return html
@ -103,24 +115,22 @@ build.photo = function(data) {
build.imageview = function(data, size, visibleControls) {
if (data==null) return ''
let html = ''
if (size==='big') {
if (visibleControls===true) html += `<div id='image' style='background-image: url(${ data.url })'></div>`
else html += `<div id='image' style='background-image: url(${ data.url });' class='full'></div>`
if (visibleControls===true) html += lychee.html`<div id='image' style='background-image: url($${ data.url })'></div>`
else html += lychee.html`<div id='image' style='background-image: url($${ data.url });' class='full'></div>`
} else if (size==='medium') {
if (visibleControls===true) html += `<div id='image' style='background-image: url(${ data.medium })'></div>`
else html += `<div id='image' style='background-image: url(${ data.medium });' class='full'></div>`
if (visibleControls===true) html += lychee.html`<div id='image' style='background-image: url($${ data.medium })'></div>`
else html += lychee.html`<div id='image' style='background-image: url($${ data.medium });' class='full'></div>`
} else if (size==='small') {
if (visibleControls===true) html += `<div id='image' class='small' style='background-image: url(${ data.url }); width: ${ data.width }px; height: ${ data.height }px; margin-top: -${ parseInt(data.height/2-20) }px; margin-left: -${ data.width/2 }px;'></div>`
else html += `<div id='image' class='small' style='background-image: url(${ data.url }); width: ${ data.width }px; height: ${ data.height }px; margin-top: -${ parseInt(data.height/2) }px; margin-left: -${ data.width/2 }px;'></div>`
if (visibleControls===true) html += lychee.html`<div id='image' class='small' style='background-image: url($${ data.url }); width: $${ data.width }px; height: $${ data.height }px; margin-top: -$${ parseInt(data.height/2-20) }px; margin-left: -$${ data.width/2 }px;'></div>`
else html += lychee.html`<div id='image' class='small' style='background-image: url($${ data.url }); width: $${ data.width }px; height: $${ data.height }px; margin-top: -$${ parseInt(data.height/2) }px; margin-left: -$${ data.width/2 }px;'></div>`
}
@ -135,27 +145,29 @@ build.imageview = function(data, size, visibleControls) {
build.no_content = function(typ) {
let html = `
<div class='no_content fadeIn'>
${ build.iconic(typ) }
`
let html = ''
html += `
<div class='no_content fadeIn'>
${ build.iconic(typ) }
`
switch (typ) {
case 'magnifying-glass':
html += '<p>No results</p>'
html += `<p>No results</p>`
break
case 'eye':
html += '<p>No public albums</p>'
html += `<p>No public albums</p>`
break
case 'cog':
html += '<p>No configuration</p>'
html += `<p>No configuration</p>`
break
case 'question-mark':
html += '<p>Photo not found</p>'
html += `<p>Photo not found</p>`
break
}
html += '</div>'
html += `</div>`
return html
@ -163,10 +175,12 @@ build.no_content = function(typ) {
build.uploadModal = function(title, files) {
let html = `
<h1>${ title }</h1>
<div class='rows'>
`
let html = ''
html += lychee.html`
<h1>$${ title }</h1>
<div class='rows'>
`
let i = 0
@ -176,9 +190,9 @@ build.uploadModal = function(title, files) {
if (file.name.length>40) file.name = file.name.substr(0, 17) + '...' + file.name.substr(file.name.length-20, 20)
html += `
html += lychee.html`
<div class='row'>
<a class='name'>${ lychee.escapeHTML(file.name) }</a>
<a class='name'>$${ file.name }</a>
`
if (file.supported===true) html += `<a class='status'></a>`
@ -193,7 +207,7 @@ build.uploadModal = function(title, files) {
}
html += '</div>'
html += `</div>`
return html
@ -208,7 +222,7 @@ build.tags = function(tags) {
tags = tags.split(',')
tags.forEach(function(tag, index, array) {
html += `<a class='tag'>${ tag }<span data-index='${ index }'>${ build.iconic('x') }</span></a>`
html += lychee.html`<a class='tag'>$${ tag }<span data-index='$${ index }'>${ build.iconic('x') }</span></a>`
})
} else {

View File

@ -101,7 +101,7 @@ contextMenu.albumTitle = function(albumID, e) {
if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg'
let title = `<img class='cover' width='16' height='16' src='${ this.thumbs[0] }'><div class='title'>${ this.title }</div>`
let title = lychee.html`<img class='cover' width='16' height='16' src='$${ this.thumbs[0] }'><div class='title'>$${ this.title }</div>`
if (this.id!=albumID) items.push({ type: 'item', title, fn: () => lychee.goto(this.id) })
@ -131,7 +131,7 @@ contextMenu.mergeAlbum = function(albumID, e) {
if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg'
let title = `<img class='cover' width='16' height='16' src='${ this.thumbs[0] }'><div class='title'>${ this.title }</div>`
let title = lychee.html`<img class='cover' width='16' height='16' src='$${ this.thumbs[0] }'><div class='title'>$${ this.title }</div>`
if (this.id!=albumID) items.push({ type: 'item', title, fn: () => album.merge([albumID, this.id]) })
@ -206,7 +206,7 @@ contextMenu.photoTitle = function(albumID, photoID, e) {
// Generate list of albums
$.each(data.content, function(index) {
let title = `<img class='cover' width='16' height='16' src='${ this.thumbUrl }'><div class='title'>${ this.title }</div>`
let title = lychee.html`<img class='cover' width='16' height='16' src='$${ this.thumbUrl }'><div class='title'>$${ this.title }</div>`
if (this.id!=photoID) items.push({ type: 'item', title, fn: () => lychee.goto(albumID + '/' + this.id) })
@ -254,7 +254,7 @@ contextMenu.move = function(photoIDs, e) {
if (!this.thumbs[0]) this.thumbs[0] = 'src/images/no_cover.svg'
let title = `<img class='cover' width='16' height='16' src='${ this.thumbs[0] }'><div class='title'>${ this.title }</div>`
let title = lychee.html`<img class='cover' width='16' height='16' src='$${ this.thumbs[0] }'><div class='title'>$${ this.title }</div>`
if (this.id!=album.getID()) items.push({ type: 'item', title, fn: () => photo.setAlbum(photoIDs, this.id) })
@ -281,8 +281,6 @@ contextMenu.sharePhoto = function(photoID, e) {
let link = photo.getViewLink(photoID),
iconClass = 'ionicons'
if (photo.json.public==='2') link = location.href
let items = [
{ type: 'item', title: `<input readonly id="link" value="${ link }">`, fn: () => {}, class: 'noHover' },
{ type: 'separator' },

View File

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

View File

@ -6,8 +6,8 @@
lychee = {
title : document.title,
version : '3.0.5',
version_code : '030005',
version : '3.0.6',
version_code : '030006',
update_path : 'http://lychee.electerious.com/version/index.php',
updateURL : 'https://github.com/electerious/Lychee',
@ -119,12 +119,12 @@ lychee.login = function(data) {
lychee.loginDialog = function() {
let msg = `
let msg = lychee.html`
<p class='signIn'>
<input class='text' name='username' autocomplete='username' type='text' value='' placeholder='username' autocapitalize='off' autocorrect='off'>
<input class='text' name='password' autocomplete='current-password' type='password' value='' placeholder='password'>
</p>
<p class='version'>Lychee ${ lychee.version }<span> &#8211; <a target='_blank' href='${ lychee.updateURL }'>Update available!</a><span></p>
<p class='version'>Lychee $${ lychee.version }<span> &#8211; <a target='_blank' href='$${ lychee.updateURL }'>Update available!</a><span></p>
`
basicModal.show({
@ -312,15 +312,6 @@ lychee.animate = function(obj, animation) {
}
lychee.escapeHTML = function(s) {
return s.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}
lychee.retinize = function(path = '') {
let pixelRatio = window.devicePixelRatio,
@ -385,14 +376,54 @@ lychee.getEventName = function() {
}
lychee.removeHTML = function(html = '') {
lychee.escapeHTML = function(html = '') {
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
.replace(/`/g, '&#96;')
return (tmp.textContent || tmp.innerText)
return html
}
lychee.html = function(literalSections, ...substs) {
// Use raw literal sections: we dont 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
}

View File

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

View File

@ -244,14 +244,14 @@ photo.delete = function(photoIDs) {
action.title = 'Delete Photo'
cancel.title = 'Keep Photo'
msg = `<p>Are you sure you want to delete the photo '${ photoTitle }'? This action can't be undone!</p>`
msg = lychee.html`<p>Are you sure you want to delete the photo '$${ photoTitle }'? This action can't be undone!</p>`
} else {
action.title = 'Delete Photo'
cancel.title = 'Keep Photo'
msg = `<p>Are you sure you want to delete all ${ photoIDs.length } selected photo? This action can't be undone!</p>`
msg = lychee.html`<p>Are you sure you want to delete all $${ photoIDs.length } selected photo? This action can't be undone!</p>`
}
@ -285,7 +285,6 @@ photo.setTitle = function(photoIDs) {
// Get old title if only one photo is selected
if (photo.json) oldTitle = photo.json.title
else if (album.json) oldTitle = album.json.content[photoIDs].title
oldTitle = oldTitle.replace(/'/g, '&apos;')
}
@ -295,9 +294,6 @@ photo.setTitle = function(photoIDs) {
let newTitle = data.title
// Remove html from input
newTitle = lychee.removeHTML(newTitle)
if (visible.photo()) {
photo.json.title = (newTitle==='' ? 'Untitled' : newTitle)
view.photo.title()
@ -321,10 +317,10 @@ photo.setTitle = function(photoIDs) {
}
let input = `<input class='text' name='title' type='text' maxlength='50' placeholder='Title' value='${ oldTitle }'>`
let input = lychee.html`<input class='text' name='title' type='text' maxlength='50' placeholder='Title' value='$${ oldTitle }'>`
if (photoIDs.length===1) msg = `<p>Enter a new title for this photo: ${ input }</p>`
else msg = `<p>Enter a title for all ${ photoIDs.length } selected photos: ${ input }</p>`
if (photoIDs.length===1) msg = lychee.html`<p>Enter a new title for this photo: ${ input }</p>`
else msg = lychee.html`<p>Enter a title for all $${ photoIDs.length } selected photos: ${ input }</p>`
basicModal.show({
body: msg,
@ -465,7 +461,7 @@ photo.setPublic = function(photoID, e) {
photo.setDescription = function(photoID) {
let oldDescription = photo.json.description.replace(/'/g, '&apos;')
let oldDescription = photo.json.description
const action = function(data) {
@ -473,9 +469,6 @@ photo.setDescription = function(photoID) {
let description = data.description
// Remove html from input
description = lychee.removeHTML(description)
if (visible.photo()) {
photo.json.description = description
view.photo.description()
@ -495,7 +488,7 @@ photo.setDescription = function(photoID) {
}
basicModal.show({
body: `<p>Enter a description for this photo: <input class='text' name='description' type='text' maxlength='800' placeholder='Description' value='${ oldDescription }'></p>`,
body: lychee.html`<p>Enter a description for this photo: <input class='text' name='description' type='text' maxlength='800' placeholder='Description' value='$${ oldDescription }'></p>`,
buttons: {
action: {
title: 'Set Description',
@ -541,10 +534,10 @@ photo.editTags = function(photoIDs) {
}
let input = `<input class='text' name='tags' type='text' maxlength='800' placeholder='Tags' value='${ oldTags }'>`
let input = lychee.html`<input class='text' name='tags' type='text' maxlength='800' placeholder='Tags' value='$${ oldTags }'>`
if (photoIDs.length===1) msg = `<p>Enter your tags for this photo. You can add multiple tags by separating them with a comma: ${ input }</p>`
else msg = `<p>Enter your tags for all ${ photoIDs.length } selected photos. Existing tags will be overwritten. You can add multiple tags by separating them with a comma: ${ input }</p>`
if (photoIDs.length===1) msg = lychee.html`<p>Enter your tags for this photo. You can add multiple tags by separating them with a comma: ${ input }</p>`
else msg = lychee.html`<p>Enter your tags for all $${ photoIDs.length } selected photos. Existing tags will be overwritten. You can add multiple tags by separating them with a comma: ${ input }</p>`
basicModal.show({
body: msg,
@ -571,9 +564,6 @@ photo.setTags = function(photoIDs, tags) {
tags = tags.replace(/(\ ,\ )|(\ ,)|(,\ )|(,{1,}\ {0,})|(,$|^,)/g, ',')
tags = tags.replace(/,$|^,|(\ ){0,}$/g, '')
// Remove html from input
tags = lychee.removeHTML(tags)
if (visible.photo()) {
photo.json.tags = tags
view.photo.tags()
@ -684,7 +674,7 @@ photo.getArchive = function(photoID) {
if (location.href.indexOf('index.html')>0) link = location.href.replace(location.hash, '').replace('index.html', url)
else link = location.href.replace(location.hash, '') + url
if (lychee.publicMode===true) link += '&password=' + password.value
if (lychee.publicMode===true) link += `&password=${ encodeURIComponent(password.value) }`
location.href = link

View File

@ -404,10 +404,10 @@ settings.setDropboxKey = function(callback) {
}
let msg = `
let msg = lychee.html`
<p>
In order to import photos from your Dropbox, you need a valid drop-ins app key from <a href='https://www.dropbox.com/developers/apps/create'>their website</a>. Generate yourself a personal key and enter it below:
<input class='text' name='key' type='text' placeholder='Dropbox API Key' value='${ lychee.dropboxKey }'>
<input class='text' name='key' type='text' placeholder='Dropbox API Key' value='$${ lychee.dropboxKey }'>
</p>
`

View File

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

View File

@ -338,7 +338,7 @@ upload.start = {
}
basicModal.show({
body: `<p>Please enter the direct link to a photo to import it: <input class='text' name='link' type='text' placeholder='http://' value='${ url }'></p>`,
body: lychee.html`<p>Please enter the direct link to a photo to import it: <input class='text' name='link' type='text' placeholder='http://' value='$${ url }'></p>`,
buttons: {
action: {
title: 'Import',
@ -444,7 +444,7 @@ upload.start = {
}
basicModal.show({
body: `<p>This action will import all photos, folders and sub-folders which are located in the following directory. The <b>original files will be deleted</b> after the import when possible. <input class='text' name='path' type='text' maxlength='100' placeholder='Absolute path to directory' value='${ lychee.location }uploads/import/'></p>`,
body: lychee.html`<p>This action will import all photos, folders and sub-folders which are located in the following directory. The <b>original files will be deleted</b> after the import when possible. <input class='text' name='path' type='text' maxlength='100' placeholder='Absolute path to directory' value='$${ lychee.location }uploads/import/'></p>`,
buttons: {
action: {
title: 'Import',

View File

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

View File

@ -3,18 +3,74 @@
* @copyright 2015 by Tobias Reich
*/
let lychee = {
content: $('#content'),
getEventName() {
// Sub-implementation of Lychee -------------------------------------------------------------- //
let touchendSupport = (/Android|iPhone|iPad|iPod/i).test(navigator.userAgent || navigator.vendor || window.opera) && ('ontouchend' in document.documentElement),
eventName = (touchendSupport===true ? 'touchend' : 'click')
let lychee = {}
return eventName
lychee.content = $('#content')
lychee.getEventName = function() {
let touchendSupport = (/Android|iPhone|iPad|iPod/i).test(navigator.userAgent || navigator.vendor || window.opera) && ('ontouchend' in document.documentElement),
eventName = (touchendSupport===true ? 'touchend' : 'click')
return eventName
}
}
lychee.escapeHTML = function(html = '') {
// Ensure that html is a string
html += ''
// Escape all critical characters
html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
.replace(/`/g, '&#96;')
return html
}
lychee.html = function(literalSections, ...substs) {
// Use raw literal sections: we dont 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)

View File

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