shiori/internal/webserver/handler-api.go
2019-05-29 20:08:58 +07:00

305 lines
7.3 KiB
Go

package webserver
import (
"encoding/json"
"fmt"
"math"
"net/http"
nurl "net/url"
"os"
"path"
fp "path/filepath"
"strconv"
"strings"
"time"
"github.com/go-shiori/go-readability"
"github.com/go-shiori/shiori/internal/database"
"github.com/go-shiori/shiori/internal/model"
"github.com/gofrs/uuid"
"github.com/julienschmidt/httprouter"
"golang.org/x/crypto/bcrypt"
)
// apiLogin is handler for POST /api/login
func (h *handler) apiLogin(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Decode request
var request model.LoginRequest
err := json.NewDecoder(r.Body).Decode(&request)
checkError(err)
// Prepare function to generate session
genSession := func(expTime time.Duration) {
// Create session ID
sessionID, err := uuid.NewV4()
checkError(err)
// Save session ID to cache
strSessionID := sessionID.String()
h.SessionCache.Set(strSessionID, request.Username, expTime)
// Save user's session IDs to cache as well
// useful for mass logout
sessionIDs := []string{strSessionID}
if val, found := h.UserCache.Get(request.Username); found {
sessionIDs = val.([]string)
sessionIDs = append(sessionIDs, strSessionID)
}
h.UserCache.Set(request.Username, sessionIDs, -1)
// Return session ID to user in cookies
http.SetCookie(w, &http.Cookie{
Name: "session-id",
Value: strSessionID,
Path: "/",
Expires: time.Now().Add(expTime),
})
w.Header().Set("Content-Type", "text/plain")
fmt.Fprint(w, strSessionID)
}
// Check if user's database is empty.
// If database still empty, and user uses default account, let him in.
accounts, err := h.DB.GetAccounts("")
checkError(err)
if len(accounts) == 0 && request.Username == "shiori" && request.Password == "gopher" {
genSession(time.Hour)
return
}
// Get account data from database
account, exist := h.DB.GetAccount(request.Username)
if !exist {
panic(fmt.Errorf("username doesn't exist"))
}
// Compare password with database
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(request.Password))
if err != nil {
panic(fmt.Errorf("username and password don't match"))
}
// Calculate expiration time
expTime := time.Hour
if request.Remember > 0 {
expTime = time.Duration(request.Remember) * time.Hour
} else {
expTime = -1
}
// Create session
genSession(expTime)
}
// apiLogout is handler for POST /api/logout
func (h *handler) apiLogout(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get session ID
sessionID, err := r.Cookie("session-id")
if err != nil {
if err == http.ErrNoCookie {
panic(fmt.Errorf("session is expired"))
} else {
panic(err)
}
}
h.SessionCache.Delete(sessionID.Value)
fmt.Fprint(w, 1)
}
// apiGetBookmarks is handler for GET /api/bookmarks
func (h *handler) apiGetBookmarks(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Make sure session still valid
err := h.validateSession(r)
checkError(err)
// Get URL queries
keyword := r.URL.Query().Get("keyword")
strTags := r.URL.Query().Get("tags")
strPage := r.URL.Query().Get("page")
tags := strings.Split(strTags, ",")
if len(tags) == 1 && tags[0] == "" {
tags = []string{}
}
page, _ := strconv.Atoi(strPage)
if page < 1 {
page = 1
}
// Prepare filter for database
searchOptions := database.GetBookmarksOptions{
Tags: tags,
Keyword: keyword,
Limit: 30,
Offset: (page - 1) * 30,
OrderLatest: true,
}
// Calculate max page
nBookmarks, err := h.DB.GetBookmarksCount(searchOptions)
checkError(err)
maxPage := int(math.Ceil(float64(nBookmarks) / 30))
// Fetch all matching bookmarks
bookmarks, err := h.DB.GetBookmarks(searchOptions)
checkError(err)
// Get image URL for each bookmark
for i := range bookmarks {
strID := strconv.Itoa(bookmarks[i].ID)
imgPath := fp.Join(h.DataDir, "thumb", strID)
imgURL := path.Join("/", "thumb", strID)
if fileExists(imgPath) {
bookmarks[i].ImageURL = imgURL
}
}
// Return JSON response
resp := map[string]interface{}{
"page": page,
"maxPage": maxPage,
"bookmarks": bookmarks,
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(&resp)
checkError(err)
}
// apiGetTags is handler for GET /api/tags
func (h *handler) apiGetTags(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Make sure session still valid
err := h.validateSession(r)
checkError(err)
// Fetch all tags
tags, err := h.DB.GetTags()
checkError(err)
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(&tags)
checkError(err)
}
// apiInsertBookmark is handler for POST /api/bookmark
func (h *handler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Make sure session still valid
err := h.validateSession(r)
checkError(err)
// Decode request
book := model.Bookmark{}
err = json.NewDecoder(r.Body).Decode(&book)
checkError(err)
// Clean up URL by removing its fragment and UTM parameters
tmp, err := nurl.Parse(book.URL)
if err != nil || tmp.Scheme == "" || tmp.Hostname() == "" {
panic(fmt.Errorf("URL is not valid"))
}
tmp.Fragment = ""
clearUTMParams(tmp)
book.URL = tmp.String()
// Create bookmark ID
book.ID, err = h.DB.CreateNewID("bookmark")
if err != nil {
panic(fmt.Errorf("failed to create ID: %v", err))
}
// Fetch data from internet
var imageURLs []string
func() {
resp, err := httpClient.Get(book.URL)
if err != nil {
return
}
defer resp.Body.Close()
article, err := readability.FromReader(resp.Body, book.URL)
if err != nil {
return
}
book.Author = article.Byline
book.Content = article.TextContent
book.HTML = article.Content
// If title and excerpt doesnt have submitted value, use from article
if book.Title == "" {
book.Title = article.Title
}
if book.Excerpt == "" {
book.Excerpt = article.Excerpt
}
// Get image URL
if article.Image != "" {
imageURLs = append(imageURLs, article.Image)
}
if article.Favicon != "" {
imageURLs = append(imageURLs, article.Favicon)
}
}()
// Make sure title is not empty
if book.Title == "" {
book.Title = book.URL
}
// Save bookmark to database
results, err := h.DB.SaveBookmarks(book)
if err != nil || len(results) == 0 {
panic(fmt.Errorf("failed to save bookmark: %v", err))
}
book = results[0]
// Save article image to local disk
imgPath := fp.Join(h.DataDir, "thumb", fmt.Sprintf("%d", book.ID))
for _, imageURL := range imageURLs {
err = downloadBookImage(imageURL, imgPath, time.Minute)
if err == nil {
strID := strconv.Itoa(book.ID)
book.ImageURL = path.Join("/", "thumb", strID)
break
}
}
// Return the new bookmark
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(&book)
checkError(err)
}
// apiDeleteBookmarks is handler for DELETE /api/bookmark
func (h *handler) apiDeleteBookmark(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Make sure session still valid
err := h.validateSession(r)
checkError(err)
// Decode request
ids := []int{}
err = json.NewDecoder(r.Body).Decode(&ids)
checkError(err)
// Delete bookmarks
err = h.DB.DeleteBookmarks(ids...)
checkError(err)
// Delete thumbnail image from local disk
for _, id := range ids {
strID := strconv.Itoa(id)
imgPath := fp.Join(h.DataDir, "thumb", strID)
os.Remove(imgPath)
}
fmt.Fprint(w, 1)
}