Add bookmarklet

This commit is contained in:
Radhi Fadlillah 2018-06-06 21:48:46 +07:00
parent b1ae14e079
commit 4b939379e8
12 changed files with 355 additions and 25 deletions

File diff suppressed because one or more lines are too long

View file

@ -40,6 +40,7 @@ func NewServeCmd(db dt.Database, dataDir string) *cobra.Command {
router.GET("/login", hdl.serveLoginPage)
router.GET("/bookmark/:id", hdl.serveBookmarkCache)
router.GET("/thumb/:id", hdl.serveThumbnailImage)
router.GET("/submit", hdl.serveSubmitPage)
router.POST("/api/login", hdl.apiLogin)
router.GET("/api/bookmarks", hdl.apiGetBookmarks)

View file

@ -96,6 +96,11 @@ func (h *webHandler) apiGetTags(w http.ResponseWriter, r *http.Request, ps httpr
// apiInsertBookmark is handler for POST /api/bookmark
func (h *webHandler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Enable CORS for this endpoint
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
// Check token
err := h.checkAPIToken(r)
checkError(err)
@ -157,7 +162,14 @@ func (h *webHandler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, p
// Save bookmark to database
_, err = h.db.InsertBookmark(book)
checkError(err)
if err != nil {
fmt.Println(err)
book.ID = h.db.GetBookmarkID(book.URL)
book.Modified = time.Now().UTC().Format("2006-01-02 15:04:05")
fmt.Println(book.ID, book.Modified)
_, err = h.db.UpdateBookmarks(book)
checkError(err)
}
// Return new saved result
err = json.NewEncoder(w).Encode(&book)

View file

@ -34,6 +34,12 @@ func (h *webHandler) serveIndexPage(w http.ResponseWriter, r *http.Request, ps h
checkError(err)
}
// serveSubmitPage is handler for GET /submit
func (h *webHandler) serveSubmitPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
err := serveFile(w, "submit.html")
checkError(err)
}
// serveLoginPage is handler for GET /login
func (h *webHandler) serveLoginPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Check token

View file

@ -42,16 +42,21 @@ func newWebHandler(db dt.Database, dataDir string) (*webHandler, error) {
func (h *webHandler) checkToken(r *http.Request) error {
tokenCookie, err := r.Cookie("token")
if err != nil {
return fmt.Errorf("Token does not exist")
return fmt.Errorf("Token error: Token does not exist")
}
token, err := jwt.Parse(tokenCookie.Value, h.jwtKeyFunc)
if err != nil {
return err
return fmt.Errorf("Token error: %v", err)
}
claims := token.Claims.(jwt.MapClaims)
return claims.Valid()
err = claims.Valid()
if err != nil {
return fmt.Errorf("Token error: %v", err)
}
return nil
}
func (h *webHandler) checkAPIToken(r *http.Request) error {
@ -59,11 +64,17 @@ func (h *webHandler) checkAPIToken(r *http.Request) error {
request.AuthorizationHeaderExtractor,
h.jwtKeyFunc)
if err != nil {
return err
// Try to check in cookie
return h.checkToken(r)
}
claims := token.Claims.(jwt.MapClaims)
return claims.Valid()
err = claims.Valid()
if err != nil {
return fmt.Errorf("Token error: %v", err)
}
return nil
}
func (h *webHandler) jwtKeyFunc(token *jwt.Token) (interface{}, error) {

View file

@ -40,6 +40,9 @@ type Database interface {
// DeleteAccounts removes all record with matching usernames
DeleteAccounts(usernames ...string) error
// GetBookmarkID fetchs bookmark ID based by its url
GetBookmarkID(url string) int
}
func checkError(err error) {

View file

@ -324,7 +324,7 @@ func (db *SQLiteDatabase) SearchBookmarks(orderLatest bool, keyword string, tags
// Set order clause
if orderLatest {
query += ` ORDER BY id DESC`
query += ` ORDER BY modified DESC`
}
// Fetch bookmarks
@ -552,3 +552,10 @@ func (db *SQLiteDatabase) GetNewID(table string) (int, error) {
return tableID, nil
}
// GetBookmarkID fetchs bookmark ID based by its url
func (db *SQLiteDatabase) GetBookmarkID(url string) int {
var bookmarkID int
db.Get(&bookmarkID, `SELECT id FROM bookmark WHERE url = ?`, url)
return bookmarkID
}

File diff suppressed because one or more lines are too long

View file

@ -33,7 +33,8 @@
</a>
</yla-tooltip>
<yla-tooltip placement="right" content="Add new bookmark">
<a @click="showDialogAdd" v-show="!editMode && !loading">
<a :href="bookmarklet" @click="showDialogAdd" v-show="!editMode && !loading">
<span>+Shiori</span>
<i class="fas fa-plus fa-fw"></i>
</a>
</yla-tooltip>
@ -197,6 +198,7 @@
nightMode: false,
editMode: false,
selected: [],
bookmarklet: '',
dialogTags: {
visible: false,
loading: false,
@ -318,7 +320,8 @@
this.dialogTags.visible = false;
this.loadData();
},
showDialogAdd() {
showDialogAdd(e) {
e.preventDefault();
this.showDialog({
title: 'New Bookmark',
content: 'Create a new bookmark',
@ -643,6 +646,42 @@
this.listView = listView === '1';
this.nightMode = nightMode === '1';
// Create bookmarklet
var shioriURL = location.href.replace(/\/+$/g, ''),
baseBookmarklet = `(function () {
var shioriURL = '$SHIORI_URL',
bookmarkURL = location.href,
submitURL = shioriURL + '/submit?url=' + encodeURIComponent(bookmarkURL);
if (bookmarkURL.startsWith('https://') && !shioriURL.startsWith('https://')) {
window.open(submitURL, '_blank');
return;
}
var i = document.createElement('iframe');
i.src = submitURL;
i.frameBorder = '0';
i.allowTransparency = true;
i.style.position = 'fixed';
i.style.top = 0;
i.style.left = 0;
i.style.width = '100%';
i.style.height = '100%';
i.style.zIndex = 99999;
document.body.appendChild(i);
window.addEventListener('message', function onMessage(e) {
if (e.origin !== shioriURL) return;
if (e.data !== 'finished') return;
window.removeEventListener('message', onMessage);
document.body.removeChild(i);
});
}())`;
this.bookmarklet = 'javascript:' + baseBookmarklet
.replace('$SHIORI_URL', shioriURL)
.replace(/\s+/gm, ' ');
// Load data
this.loadData();
}

View file

@ -98,6 +98,12 @@ body {
display: block;
color: var(--colorSidebar);
flex-shrink: 0;
>span {
display: block;
height: 0;
line-height: 0;
overflow: hidden;
}
&:hover,
&:focus {
background-color: #232323;
@ -680,6 +686,22 @@ body {
}
}
#submit-page {
background-color: transparent;
.yla-dialog__header {
text-align: center;
}
&:not(.iframe) {
background-color: var(--bg);
.yla-dialog__overlay {
background-color: transparent;
.yla-dialog {
border: 1px solid var(--border);
}
}
}
}
.yla-tooltip {
@media (max-width: 800px) {
display: none;

View file

@ -83,12 +83,22 @@
timeout: 10000
})
.then(function (response) {
// Save token
var token = response.data;
Cookies.set('token', token, {
expires: this.rememberMe ? 7 : 1
});
location.href = '/';
// Set destination URL
var rx = /[&?]dst=([^&]+)(&|$)/g,
match = rx.exec(location.href);
if (match == null) {
location.href = '/';
} else {
var dst = match[1];
location.href = decodeURIComponent(dst);
}
})
.catch(function (error) {
var errorMsg = (error.response ? error.response.data : error.message).trim();

211
view/submit.html Normal file
View file

@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="/css/fontawesome.css">
<link rel="stylesheet" href="/css/source-sans-pro.css">
<link rel="stylesheet" href="/css/yla-dialog.css">
<link rel="stylesheet" href="/css/stylesheet.css">
<script src="/js/vue.js"></script>
<script src="/js/axios.js"></script>
<script src="/js/js-cookie.js"></script>
<script src="/js/component/yla-dialog.js"></script>
<script src="/js/page/base.js"></script>
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/res/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/res/apple-touch-icon-152x152.png" />
<link rel="icon" type="image/png" href="/res/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/res/favicon-16x16.png" sizes="16x16" />
<title>Submit New URL - Shiori - Bookmarks Manager</title>
</head>
<body>
<div id="submit-page" class="page" :class="{night: nightMode, iframe: inIframe}">
<yla-dialog v-bind="dialog"></yla-dialog>
</div>
<script>
// Create private function
function _inIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
// Register Vue component
Vue.component('yla-dialog', new YlaDialog());
// Prepare axios instance
var token = Cookies.get('token'),
rest = axios.create();
rest.defaults.timeout = 60000;
rest.defaults.headers.common['Authorization'] = 'Bearer ' + token;
var app = new Vue({
el: '#submit-page',
mixins: [new Base()],
data: {
targetURL: '',
nightMode: false,
inIframe: false,
},
methods: {
showDialogLogin() {
this.showDialog({
title: 'Login',
content: 'Please input username and password',
fields: [{
name: 'username',
label: 'Username',
}, {
name: 'password',
label: 'Password',
type: 'password'
}],
mainText: 'OK',
secondText: this.inIframe ? 'Cancel' : '',
mainClick: (data) => {
// Validate input
if (data.username.trim() === '') return;
// Send data
this.dialog.loading = true;
rest.post('/api/login', {
username: data.username.trim(),
password: data.password,
})
.then((response) => {
// Save token
var token = response.data;
Cookies.set('token', token, {
expires: this.rememberMe ? 7 : 1
});
this.showDialogAdd();
})
.catch((error) => {
var errorMsg = (error.response ? error.response.data : error.message).trim();
this.showErrorDialog(errorMsg);
this.dialog.mainClick = () => {
this.showDialogLogin();
}
});
},
secondClick: () => {
window.top.postMessage('finished', '*');
this.dialog.visible = false;
}
});
},
showDialogAdd() {
this.showDialog({
title: 'New Bookmark',
content: 'Create a new bookmark',
fields: [{
name: 'url',
label: 'Url, start with http://...',
value: this.targetURL,
}, {
name: 'title',
label: 'Custom title (optional)'
}, {
name: 'excerpt',
label: 'Custom excerpt (optional)',
type: 'area'
}, {
name: 'tags',
label: 'Comma separated tags (optional)'
}, ],
mainText: 'Save',
secondText: this.inIframe ? 'Cancel' : '',
mainClick: (data) => {
// Prepare tags
var tags = data.tags
.toLowerCase()
.replace(/\s+/g, ' ')
.split(/\s*,\s*/g)
.filter(tag => tag !== '')
.map(tag => {
return {
name: tag
};
});
// Validate input
if (data.url.trim() === '') return;
// Send data
this.dialog.loading = true;
rest.post('/api/bookmarks', {
url: data.url.trim(),
title: data.title.trim(),
excerpt: data.excerpt.trim(),
tags: tags
})
.then((response) => {
if (!this.inIframe) {
location.href = '/';
return;
}
this.showDialog({
title: 'New Bookmark',
content: 'The new bookmark has been saved successfully.',
mainText: 'OK',
mainClick: () => {
window.top.postMessage('finished', '*');
this.dialog.visible = false;
}
});
})
.catch((error) => {
var errorMsg = (error.response ? error.response.data : error.message).trim();
if (errorMsg.startsWith('Token error:')) {
this.showDialogLogin();
return;
}
this.showErrorDialog(errorMsg);
this.dialog.mainClick = () => {
this.showDialogAdd();
}
});
},
secondClick: () => {
window.top.postMessage('finished', '*');
this.dialog.visible = false;
}
});
}
},
mounted() {
// Check if in iframe
this.inIframe = _inIframe();
// Read config from local storage
var nightMode = localStorage.getItem('shiori-night-mode');
this.nightMode = nightMode === '1';
// Get target URL
var rxURL = /[&?]url=([^&]+)(&|$)/g,
match = rxURL.exec(location.href);
if (match != null) {
var dst = match[1];
this.targetURL = decodeURIComponent(dst);
} else {
this.targetURL = '';
}
// Show dialog
this.showDialogAdd();
}
});
</script>
</body>
</html>