From b153fa8b3cd2e48bbe13326695f11d2013427ebc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= <fred@miniflux.net>
Date: Mon, 18 Dec 2017 20:52:46 -0800
Subject: [PATCH] Add Wallabag integration

---
 http/client.go                         |  86 ++++++++++++++----
 integration/instapaper/instapaper.go   |   2 +-
 integration/integration.go             |  30 +++++--
 integration/pinboard/pinboard.go       |   2 +-
 integration/wallabag/wallabag.go       | 116 +++++++++++++++++++++++++
 locale/translations.go                 |  12 ++-
 locale/translations/fr_FR.json         |   8 +-
 model/integration.go                   |   6 ++
 scheduler/scheduler.go                 |   2 +-
 server/template/html/integrations.html |  22 +++++
 server/template/views.go               |  26 +++++-
 server/ui/controller/integrations.go   |   6 ++
 server/ui/form/integration.go          |  18 ++++
 sql/schema_version_11.sql              |   6 ++
 sql/sql.go                             |   9 +-
 storage/integration.go                 |  30 ++++++-
 storage/migration.go                   |   2 +-
 17 files changed, 347 insertions(+), 36 deletions(-)
 create mode 100644 integration/wallabag/wallabag.go
 create mode 100644 sql/schema_version_11.sql

diff --git a/http/client.go b/http/client.go
index 9524dd67..304a9cc5 100644
--- a/http/client.go
+++ b/http/client.go
@@ -5,10 +5,14 @@
 package http
 
 import (
+	"bytes"
 	"crypto/tls"
+	"encoding/json"
 	"fmt"
+	"io"
 	"net/http"
 	"net/url"
+	"strings"
 	"time"
 
 	"github.com/miniflux/miniflux/helper"
@@ -21,20 +25,59 @@ const requestTimeout = 300
 
 // Client is a HTTP Client :)
 type Client struct {
-	url                string
-	etagHeader         string
-	lastModifiedHeader string
-	username           string
-	password           string
-	Insecure           bool
+	url                 string
+	etagHeader          string
+	lastModifiedHeader  string
+	authorizationHeader string
+	username            string
+	password            string
+	Insecure            bool
 }
 
 // Get execute a GET HTTP request.
 func (c *Client) Get() (*Response, error) {
 	defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[HttpClient:Get] url=%s", c.url))
 
+	request, err := c.buildRequest(http.MethodGet, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return c.executeRequest(request)
+}
+
+// PostForm execute a POST HTTP request with form values.
+func (c *Client) PostForm(values url.Values) (*Response, error) {
+	request, err := c.buildRequest(http.MethodPost, strings.NewReader(values.Encode()))
+	if err != nil {
+		return nil, err
+	}
+
+	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+	return c.executeRequest(request)
+}
+
+// PostJSON execute a POST HTTP request with JSON payload.
+func (c *Client) PostJSON(data interface{}) (*Response, error) {
+	b, err := json.Marshal(data)
+	if err != nil {
+		return nil, err
+	}
+
+	request, err := c.buildRequest(http.MethodPost, bytes.NewReader(b))
+	if err != nil {
+		return nil, err
+	}
+
+	request.Header.Add("Content-Type", "application/json")
+	return c.executeRequest(request)
+}
+
+func (c *Client) executeRequest(request *http.Request) (*Response, error) {
+	defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[HttpClient] url=%s", c.url))
+
 	client := c.buildClient()
-	resp, err := client.Do(c.buildRequest())
+	resp, err := client.Do(request)
 	if err != nil {
 		return nil, err
 	}
@@ -48,7 +91,8 @@ func (c *Client) Get() (*Response, error) {
 		ContentType:  resp.Header.Get("Content-Type"),
 	}
 
-	logger.Debug("[HttpClient:Get] OriginalURL=%s, StatusCode=%d, ETag=%s, LastModified=%s, EffectiveURL=%s",
+	logger.Debug("[HttpClient:%s] OriginalURL=%s, StatusCode=%d, ETag=%s, LastModified=%s, EffectiveURL=%s",
+		request.Method,
 		c.url,
 		response.StatusCode,
 		response.ETag,
@@ -59,19 +103,18 @@ func (c *Client) Get() (*Response, error) {
 	return response, err
 }
 
-func (c *Client) buildRequest() *http.Request {
-	link, _ := url.Parse(c.url)
-	request := &http.Request{
-		URL:    link,
-		Method: http.MethodGet,
-		Header: c.buildHeaders(),
+func (c *Client) buildRequest(method string, body io.Reader) (*http.Request, error) {
+	request, err := http.NewRequest(method, c.url, body)
+	if err != nil {
+		return nil, err
 	}
 
 	if c.username != "" && c.password != "" {
 		request.SetBasicAuth(c.username, c.password)
 	}
 
-	return request
+	request.Header = c.buildHeaders()
+	return request, nil
 }
 
 func (c *Client) buildClient() http.Client {
@@ -88,7 +131,7 @@ func (c *Client) buildClient() http.Client {
 func (c *Client) buildHeaders() http.Header {
 	headers := make(http.Header)
 	headers.Add("User-Agent", userAgent)
-	headers.Add("Accept", "text/html,application/xhtml+xml,application/xml,application/json")
+	headers.Add("Accept", "text/html,application/xhtml+xml,application/xml,application/json,image/*")
 
 	if c.etagHeader != "" {
 		headers.Add("If-None-Match", c.etagHeader)
@@ -98,6 +141,10 @@ func (c *Client) buildHeaders() http.Header {
 		headers.Add("If-Modified-Since", c.lastModifiedHeader)
 	}
 
+	if c.authorizationHeader != "" {
+		headers.Add("Authorization", c.authorizationHeader)
+	}
+
 	return headers
 }
 
@@ -106,11 +153,16 @@ func NewClient(url string) *Client {
 	return &Client{url: url, Insecure: false}
 }
 
-// NewClientWithCredentials returns a new HTTP client that require authentication.
+// NewClientWithCredentials returns a new HTTP client that requires authentication.
 func NewClientWithCredentials(url, username, password string) *Client {
 	return &Client{url: url, Insecure: false, username: username, password: password}
 }
 
+// NewClientWithAuthorization returns a new client with a custom authorization header.
+func NewClientWithAuthorization(url, authorization string) *Client {
+	return &Client{url: url, Insecure: false, authorizationHeader: authorization}
+}
+
 // NewClientWithCacheHeaders returns a new HTTP client that send cache headers.
 func NewClientWithCacheHeaders(url, etagHeader, lastModifiedHeader string) *Client {
 	return &Client{url: url, etagHeader: etagHeader, lastModifiedHeader: lastModifiedHeader, Insecure: false}
diff --git a/integration/instapaper/instapaper.go b/integration/instapaper/instapaper.go
index 51c5e05e..33a25353 100644
--- a/integration/instapaper/instapaper.go
+++ b/integration/instapaper/instapaper.go
@@ -27,7 +27,7 @@ func (c *Client) AddURL(link, title string) error {
 	client := http.NewClientWithCredentials(apiURL, c.username, c.password)
 	response, err := client.Get()
 	if response.HasServerFailure() {
-		return fmt.Errorf("unable to send bookmark to instapaper, status=%d", response.StatusCode)
+		return fmt.Errorf("instapaper: unable to send url, status=%d", response.StatusCode)
 	}
 
 	return err
diff --git a/integration/integration.go b/integration/integration.go
index 18975e97..1468a2b5 100644
--- a/integration/integration.go
+++ b/integration/integration.go
@@ -7,6 +7,7 @@ package integration
 import (
 	"github.com/miniflux/miniflux/integration/instapaper"
 	"github.com/miniflux/miniflux/integration/pinboard"
+	"github.com/miniflux/miniflux/integration/wallabag"
 	"github.com/miniflux/miniflux/logger"
 	"github.com/miniflux/miniflux/model"
 )
@@ -15,17 +16,36 @@ import (
 func SendEntry(entry *model.Entry, integration *model.Integration) {
 	if integration.PinboardEnabled {
 		client := pinboard.NewClient(integration.PinboardToken)
-		err := client.AddBookmark(entry.URL, entry.Title, integration.PinboardTags, integration.PinboardMarkAsUnread)
+		err := client.AddBookmark(
+			entry.URL,
+			entry.Title,
+			integration.PinboardTags,
+			integration.PinboardMarkAsUnread,
+		)
+
 		if err != nil {
-			logger.Error("[Pinboard] %v", err)
+			logger.Error("[Integration] %v", err)
 		}
 	}
 
 	if integration.InstapaperEnabled {
 		client := instapaper.NewClient(integration.InstapaperUsername, integration.InstapaperPassword)
-		err := client.AddURL(entry.URL, entry.Title)
-		if err != nil {
-			logger.Error("[Instapaper] %v", err)
+		if err := client.AddURL(entry.URL, entry.Title); err != nil {
+			logger.Error("[Integration] %v", err)
+		}
+	}
+
+	if integration.WallabagEnabled {
+		client := wallabag.NewClient(
+			integration.WallabagURL,
+			integration.WallabagClientID,
+			integration.WallabagClientSecret,
+			integration.WallabagUsername,
+			integration.WallabagPassword,
+		)
+
+		if err := client.AddEntry(entry.URL, entry.Title); err != nil {
+			logger.Error("[Integration] %v", err)
 		}
 	}
 }
diff --git a/integration/pinboard/pinboard.go b/integration/pinboard/pinboard.go
index 2e1bbd74..bad65b1e 100644
--- a/integration/pinboard/pinboard.go
+++ b/integration/pinboard/pinboard.go
@@ -33,7 +33,7 @@ func (c *Client) AddBookmark(link, title, tags string, markAsUnread bool) error
 	client := http.NewClient("https://api.pinboard.in/v1/posts/add?" + values.Encode())
 	response, err := client.Get()
 	if response.HasServerFailure() {
-		return fmt.Errorf("unable to send bookmark to pinboard, status=%d", response.StatusCode)
+		return fmt.Errorf("pinboard: unable to send bookmark, status=%d", response.StatusCode)
 	}
 
 	return err
diff --git a/integration/wallabag/wallabag.go b/integration/wallabag/wallabag.go
new file mode 100644
index 00000000..fbb100a8
--- /dev/null
+++ b/integration/wallabag/wallabag.go
@@ -0,0 +1,116 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package wallabag
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/url"
+
+	"github.com/miniflux/miniflux/http"
+)
+
+// Client represents a Wallabag client.
+type Client struct {
+	baseURL      string
+	clientID     string
+	clientSecret string
+	username     string
+	password     string
+}
+
+// AddEntry sends a link to Wallabag.
+func (c *Client) AddEntry(link, title string) error {
+	accessToken, err := c.getAccessToken()
+	if err != nil {
+		return err
+	}
+
+	return c.createEntry(accessToken, link, title)
+}
+
+func (c *Client) createEntry(accessToken, link, title string) error {
+	endpoint, err := getAPIEndpoint(c.baseURL, "/api/entries.json")
+	if err != nil {
+		return fmt.Errorf("wallbag: unable to get entries endpoint: %v", err)
+	}
+
+	client := http.NewClientWithAuthorization(endpoint, "Bearer "+accessToken)
+	response, err := client.PostJSON(map[string]string{"url": link, "title": title})
+	if err != nil {
+		return fmt.Errorf("wallabag: unable to post entry: %v", err)
+	}
+
+	if response.HasServerFailure() {
+		return fmt.Errorf("wallabag: request failed, status=%d", response.StatusCode)
+	}
+
+	return nil
+}
+
+func (c *Client) getAccessToken() (string, error) {
+	values := url.Values{}
+	values.Add("grant_type", "password")
+	values.Add("client_id", c.clientID)
+	values.Add("client_secret", c.clientSecret)
+	values.Add("username", c.username)
+	values.Add("password", c.password)
+
+	endpoint, err := getAPIEndpoint(c.baseURL, "/oauth/v2/token")
+	if err != nil {
+		return "", fmt.Errorf("wallbag: unable to get token endpoint: %v", err)
+	}
+
+	client := http.NewClient(endpoint)
+	response, err := client.PostForm(values)
+	if err != nil {
+		return "", fmt.Errorf("wallabag: unable to get access token: %v", err)
+	}
+
+	if response.HasServerFailure() {
+		return "", fmt.Errorf("wallabag: request failed, status=%d", response.StatusCode)
+	}
+
+	token, err := decodeTokenResponse(response.Body)
+	if err != nil {
+		return "", err
+	}
+
+	return token.AccessToken, nil
+}
+
+// NewClient returns a new Wallabag client.
+func NewClient(baseURL, clientID, clientSecret, username, password string) *Client {
+	return &Client{baseURL, clientID, clientSecret, username, password}
+}
+
+func getAPIEndpoint(baseURL, path string) (string, error) {
+	u, err := url.Parse(baseURL)
+	if err != nil {
+		return "", fmt.Errorf("wallabag: invalid API endpoint: %v", err)
+	}
+	u.Path = path
+	return u.String(), nil
+}
+
+type tokenResponse struct {
+	AccessToken  string `json:"access_token"`
+	Expires      int    `json:"expires_in"`
+	RefreshToken string `json:"refresh_token"`
+	Scope        string `json:"scope"`
+	TokenType    string `json:"token_type"`
+}
+
+func decodeTokenResponse(body io.Reader) (*tokenResponse, error) {
+	var token tokenResponse
+
+	decoder := json.NewDecoder(body)
+	if err := decoder.Decode(&token); err != nil {
+		return nil, fmt.Errorf("wallabag: unable to decode token response: %v", err)
+	}
+
+	return &token, nil
+}
diff --git a/locale/translations.go b/locale/translations.go
index 8f5f4eda..7a82d051 100644
--- a/locale/translations.go
+++ b/locale/translations.go
@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2017-12-16 17:48:32.323083386 -0800 PST m=+0.056720065
+// 2017-12-18 18:49:32.159555255 -0800 PST m=+0.041213049
 
 package locale
 
@@ -171,12 +171,18 @@ var translations = map[string]string{
     "Scraper Rules": "Règles pour récupérer le contenu original",
     "Rewrite Rules": "Règles de réécriture",
     "Preferences saved!": "Préférences sauvegardées !",
-    "Your external account is now linked !": "Votre compte externe est maintenant associé !"
+    "Your external account is now linked !": "Votre compte externe est maintenant associé !",
+    "Save articles to Wallabag": "Sauvegarder les articles vers Wallabag",
+    "Wallabag API Endpoint": "URL de l'API de Wallabag",
+    "Wallabag Client ID": "Identifiant du client Wallabag",
+    "Wallabag Client Secret": "Clé secrète du client Wallabag",
+    "Wallabag Username": "Nom d'utilisateur de Wallabag",
+    "Wallabag Password": "Mot de passe de Wallabag"
 }
 `,
 }
 
 var translationsChecksums = map[string]string{
 	"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
-	"fr_FR": "f52a6503ee61d1103adb280c242d438a89936b34d147d29c2502cec8b2cc9ff9",
+	"fr_FR": "3a71dbf4fcdb488acdaf43530e521a0c17a28ef637fbd60b204e468afb0dbe09",
 }
diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json
index 5015f2ae..1a5cbd6b 100644
--- a/locale/translations/fr_FR.json
+++ b/locale/translations/fr_FR.json
@@ -155,5 +155,11 @@
     "Scraper Rules": "Règles pour récupérer le contenu original",
     "Rewrite Rules": "Règles de réécriture",
     "Preferences saved!": "Préférences sauvegardées !",
-    "Your external account is now linked !": "Votre compte externe est maintenant associé !"
+    "Your external account is now linked !": "Votre compte externe est maintenant associé !",
+    "Save articles to Wallabag": "Sauvegarder les articles vers Wallabag",
+    "Wallabag API Endpoint": "URL de l'API de Wallabag",
+    "Wallabag Client ID": "Identifiant du client Wallabag",
+    "Wallabag Client Secret": "Clé secrète du client Wallabag",
+    "Wallabag Username": "Nom d'utilisateur de Wallabag",
+    "Wallabag Password": "Mot de passe de Wallabag"
 }
diff --git a/model/integration.go b/model/integration.go
index d8ca2798..5ddaef27 100644
--- a/model/integration.go
+++ b/model/integration.go
@@ -18,4 +18,10 @@ type Integration struct {
 	FeverUsername        string
 	FeverPassword        string
 	FeverToken           string
+	WallabagEnabled      bool
+	WallabagURL          string
+	WallabagClientID     string
+	WallabagClientSecret string
+	WallabagUsername     string
+	WallabagPassword     string
 }
diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go
index cc3de6aa..90011cb9 100644
--- a/scheduler/scheduler.go
+++ b/scheduler/scheduler.go
@@ -34,7 +34,7 @@ func NewSessionScheduler(store *storage.Storage, frequency int) {
 		for _ = range c {
 			nbSessions := store.CleanOldSessions()
 			nbUserSessions := store.CleanOldUserSessions()
-			logger.Debug("[SessionScheduler] cleaned %d sessions and %d user sessions", nbSessions, nbUserSessions)
+			logger.Info("[SessionScheduler] cleaned %d sessions and %d user sessions", nbSessions, nbUserSessions)
 		}
 	}()
 }
diff --git a/server/template/html/integrations.html b/server/template/html/integrations.html
index adc5a1bd..5005d688 100644
--- a/server/template/html/integrations.html
+++ b/server/template/html/integrations.html
@@ -71,6 +71,28 @@
         <input type="password" name="instapaper_password" id="form-instapaper-password" value="{{ .form.InstapaperPassword }}">
     </div>
 
+    <h3>Wallabag</h3>
+    <div class="form-section">
+        <label>
+            <input type="checkbox" name="wallabag_enabled" value="1" {{ if .form.WallabagEnabled }}checked{{ end }}> {{ t "Save articles to Wallabag" }}
+        </label>
+
+        <label for="form-wallabag-url">{{ t "Wallabag API Endpoint" }}</label>
+        <input type="url" name="wallabag_url" id="form-wallabag-url" value="{{ .form.WallabagURL }}" placeholder="http://v2.wallabag.org/">
+
+        <label for="form-wallabag-client-id">{{ t "Wallabag Client ID" }}</label>
+        <input type="text" name="wallabag_client_id" id="form-wallabag-client-id" value="{{ .form.WallabagClientID }}">
+
+        <label for="form-wallabag-client-secret">{{ t "Wallabag Client Secret" }}</label>
+        <input type="password" name="wallabag_client_secret" id="form-wallabag-client-secret" value="{{ .form.WallabagClientSecret }}">
+
+        <label for="form-wallabag-username">{{ t "Wallabag Username" }}</label>
+        <input type="text" name="wallabag_username" id="form-wallabag-username" value="{{ .form.WallabagUsername }}">
+
+        <label for="form-wallabag-password">{{ t "Wallabag Password" }}</label>
+        <input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}">
+    </div>
+
     <div class="buttons">
         <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
     </div>
diff --git a/server/template/views.go b/server/template/views.go
index f455c2f9..571727e8 100644
--- a/server/template/views.go
+++ b/server/template/views.go
@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2017-12-15 18:49:24.044316922 -0800 PST m=+0.016912794
+// 2017-12-18 18:49:32.144679579 -0800 PST m=+0.026337373
 
 package template
 
@@ -882,6 +882,28 @@ var templateViewsMap = map[string]string{
         <input type="password" name="instapaper_password" id="form-instapaper-password" value="{{ .form.InstapaperPassword }}">
     </div>
 
+    <h3>Wallabag</h3>
+    <div class="form-section">
+        <label>
+            <input type="checkbox" name="wallabag_enabled" value="1" {{ if .form.WallabagEnabled }}checked{{ end }}> {{ t "Save articles to Wallabag" }}
+        </label>
+
+        <label for="form-wallabag-url">{{ t "Wallabag API Endpoint" }}</label>
+        <input type="url" name="wallabag_url" id="form-wallabag-url" value="{{ .form.WallabagURL }}" placeholder="http://v2.wallabag.org/">
+
+        <label for="form-wallabag-client-id">{{ t "Wallabag Client ID" }}</label>
+        <input type="text" name="wallabag_client_id" id="form-wallabag-client-id" value="{{ .form.WallabagClientID }}">
+
+        <label for="form-wallabag-client-secret">{{ t "Wallabag Client Secret" }}</label>
+        <input type="password" name="wallabag_client_secret" id="form-wallabag-client-secret" value="{{ .form.WallabagClientSecret }}">
+
+        <label for="form-wallabag-username">{{ t "Wallabag Username" }}</label>
+        <input type="text" name="wallabag_username" id="form-wallabag-username" value="{{ .form.WallabagUsername }}">
+
+        <label for="form-wallabag-password">{{ t "Wallabag Password" }}</label>
+        <input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}">
+    </div>
+
     <div class="buttons">
         <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
     </div>
@@ -1201,7 +1223,7 @@ var templateViewsMapChecksums = map[string]string{
 	"feeds":               "c22af39b42ba9ca69ea0914ca789303ec2c5b484abcd4eaa49016e365381257c",
 	"history":             "9a67599a5d8d67ef958e3f07da339b749f42892667547c9e60a54477e8d32a56",
 	"import":              "73b5112e20bfd232bf73334544186ea419505936bc237d481517a8622901878f",
-	"integrations":        "30249eefa4e2da62051447537ee5c4ed3dad377656fec3080e0e96c3c697c672",
+	"integrations":        "3c14d7de904911aad7f3ebec6d1a20b50843287f58125c526e167f429f3d455d",
 	"login":               "04f3ce79bfa5753f69e0d956c2a8999c0da549c7925634a3e8134975da0b0e0f",
 	"sessions":            "878dbe8f8ea783b44130c495814179519fa5c3aa2666ac87508f94d58dd008bf",
 	"settings":            "ea2505b9d0a6d6bb594dba87a92079de19baa6d494f0651693a7685489fb7de9",
diff --git a/server/ui/controller/integrations.go b/server/ui/controller/integrations.go
index babcfb74..99971b07 100644
--- a/server/ui/controller/integrations.go
+++ b/server/ui/controller/integrations.go
@@ -40,6 +40,12 @@ func (c *Controller) ShowIntegrations(ctx *core.Context, request *core.Request,
 			FeverEnabled:         integration.FeverEnabled,
 			FeverUsername:        integration.FeverUsername,
 			FeverPassword:        integration.FeverPassword,
+			WallabagEnabled:      integration.WallabagEnabled,
+			WallabagURL:          integration.WallabagURL,
+			WallabagClientID:     integration.WallabagClientID,
+			WallabagClientSecret: integration.WallabagClientSecret,
+			WallabagUsername:     integration.WallabagUsername,
+			WallabagPassword:     integration.WallabagPassword,
 		},
 	}))
 }
diff --git a/server/ui/form/integration.go b/server/ui/form/integration.go
index 209b8423..8cc6d356 100644
--- a/server/ui/form/integration.go
+++ b/server/ui/form/integration.go
@@ -22,6 +22,12 @@ type IntegrationForm struct {
 	FeverEnabled         bool
 	FeverUsername        string
 	FeverPassword        string
+	WallabagEnabled      bool
+	WallabagURL          string
+	WallabagClientID     string
+	WallabagClientSecret string
+	WallabagUsername     string
+	WallabagPassword     string
 }
 
 // Merge copy form values to the model.
@@ -36,6 +42,12 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 	integration.FeverEnabled = i.FeverEnabled
 	integration.FeverUsername = i.FeverUsername
 	integration.FeverPassword = i.FeverPassword
+	integration.WallabagEnabled = i.WallabagEnabled
+	integration.WallabagURL = i.WallabagURL
+	integration.WallabagClientID = i.WallabagClientID
+	integration.WallabagClientSecret = i.WallabagClientSecret
+	integration.WallabagUsername = i.WallabagUsername
+	integration.WallabagPassword = i.WallabagPassword
 }
 
 // NewIntegrationForm returns a new AuthForm.
@@ -51,5 +63,11 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
 		FeverEnabled:         r.FormValue("fever_enabled") == "1",
 		FeverUsername:        r.FormValue("fever_username"),
 		FeverPassword:        r.FormValue("fever_password"),
+		WallabagEnabled:      r.FormValue("wallabag_enabled") == "1",
+		WallabagURL:          r.FormValue("wallabag_url"),
+		WallabagClientID:     r.FormValue("wallabag_client_id"),
+		WallabagClientSecret: r.FormValue("wallabag_client_secret"),
+		WallabagUsername:     r.FormValue("wallabag_username"),
+		WallabagPassword:     r.FormValue("wallabag_password"),
 	}
 }
diff --git a/sql/schema_version_11.sql b/sql/schema_version_11.sql
new file mode 100644
index 00000000..79f08d78
--- /dev/null
+++ b/sql/schema_version_11.sql
@@ -0,0 +1,6 @@
+alter table integrations add column wallabag_enabled bool default 'f';
+alter table integrations add column wallabag_url text default '';
+alter table integrations add column wallabag_client_id text default '';
+alter table integrations add column wallabag_client_secret text default '';
+alter table integrations add column wallabag_username text default '';
+alter table integrations add column wallabag_password text default '';
\ No newline at end of file
diff --git a/sql/sql.go b/sql/sql.go
index b0262438..3cbfe117 100644
--- a/sql/sql.go
+++ b/sql/sql.go
@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2017-12-16 17:48:32.268871258 -0800 PST m=+0.002507937
+// 2017-12-18 18:49:32.121198779 -0800 PST m=+0.002856573
 
 package sql
 
@@ -116,6 +116,12 @@ create table sessions (
     created_at timestamp with time zone not null default now(),
     primary key(id)
 );`,
+	"schema_version_11": `alter table integrations add column wallabag_enabled bool default 'f';
+alter table integrations add column wallabag_url text default '';
+alter table integrations add column wallabag_client_id text default '';
+alter table integrations add column wallabag_client_secret text default '';
+alter table integrations add column wallabag_username text default '';
+alter table integrations add column wallabag_password text default '';`,
 	"schema_version_2": `create extension if not exists hstore;
 alter table users add column extra hstore;
 create index users_extra_idx on users using gin(extra);
@@ -157,6 +163,7 @@ alter table users add column entry_direction entry_sorting_direction default 'as
 var SqlMapChecksums = map[string]string{
 	"schema_version_1":  "7be580fc8a93db5da54b2f6e87019803c33b0b0c28482c7af80cef873bdac4e2",
 	"schema_version_10": "8faf15ddeff7c8cc305e66218face11ed92b97df2bdc2d0d7944d61441656795",
+	"schema_version_11": "dc5bbc302e01e425b49c48ddcd8e29e3ab2bb8e73a6cd1858a6ba9fbec0b5243",
 	"schema_version_2":  "e8e9ff32478df04fcddad10a34cba2e8bb1e67e7977b5bd6cdc4c31ec94282b4",
 	"schema_version_3":  "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
 	"schema_version_4":  "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
diff --git a/storage/integration.go b/storage/integration.go
index 6d89a68e..779ca817 100644
--- a/storage/integration.go
+++ b/storage/integration.go
@@ -47,7 +47,13 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 			fever_enabled,
 			fever_username,
 			fever_password,
-			fever_token
+			fever_token,
+			wallabag_enabled,
+			wallabag_url,
+			wallabag_client_id,
+			wallabag_client_secret,
+			wallabag_username,
+			wallabag_password
 		FROM integrations
 		WHERE user_id=$1
 	`
@@ -65,6 +71,12 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 		&integration.FeverUsername,
 		&integration.FeverPassword,
 		&integration.FeverToken,
+		&integration.WallabagEnabled,
+		&integration.WallabagURL,
+		&integration.WallabagClientID,
+		&integration.WallabagClientSecret,
+		&integration.WallabagUsername,
+		&integration.WallabagPassword,
 	)
 	switch {
 	case err == sql.ErrNoRows:
@@ -90,8 +102,14 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 			fever_enabled=$8,
 			fever_username=$9,
 			fever_password=$10,
-			fever_token=$11
-		WHERE user_id=$12
+			fever_token=$11,
+			wallabag_enabled=$12,
+			wallabag_url=$13,
+			wallabag_client_id=$14,
+			wallabag_client_secret=$15,
+			wallabag_username=$16,
+			wallabag_password=$17
+		WHERE user_id=$18
 	`
 	_, err := s.db.Exec(
 		query,
@@ -106,6 +124,12 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 		integration.FeverUsername,
 		integration.FeverPassword,
 		integration.FeverToken,
+		integration.WallabagEnabled,
+		integration.WallabagURL,
+		integration.WallabagClientID,
+		integration.WallabagClientSecret,
+		integration.WallabagUsername,
+		integration.WallabagPassword,
 		integration.UserID,
 	)
 
diff --git a/storage/migration.go b/storage/migration.go
index d29c76db..04083303 100644
--- a/storage/migration.go
+++ b/storage/migration.go
@@ -12,7 +12,7 @@ import (
 	"github.com/miniflux/miniflux/sql"
 )
 
-const schemaVersion = 10
+const schemaVersion = 11
 
 // Migrate run database migrations.
 func (s *Storage) Migrate() {
-- 
GitLab