1/*
2 * Copyright © 2019-2020 A Bunch Tell LLC.
3 *
4 * This file is part of WriteFreely.
5 *
6 * WriteFreely is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License, included
8 * in the LICENSE file in this source code package.
9 */
10
11package writefreely
12
13import (
14	"github.com/writeas/web-core/log"
15	"io/ioutil"
16	"net/http"
17	"strings"
18	"sync"
19	"time"
20)
21
22// updatesCacheTime is the default interval between cache updates for new
23// software versions
24const defaultUpdatesCacheTime = 12 * time.Hour
25
26// updatesCache holds data about current and new releases of the writefreely
27// software
28type updatesCache struct {
29	mu             sync.Mutex
30	frequency      time.Duration
31	lastCheck      time.Time
32	latestVersion  string
33	currentVersion string
34	checkError     error
35}
36
37// CheckNow asks for the latest released version of writefreely and updates
38// the cache last checked time. If the version postdates the current 'latest'
39// the version value is replaced.
40func (uc *updatesCache) CheckNow() error {
41	if debugging {
42		log.Info("[update check] Checking for update now.")
43	}
44	uc.mu.Lock()
45	defer uc.mu.Unlock()
46	uc.lastCheck = time.Now()
47	latestRemote, err := newVersionCheck()
48	if err != nil {
49		log.Error("[update check] Failed: %v", err)
50		uc.checkError = err
51		return err
52	}
53	if CompareSemver(latestRemote, uc.latestVersion) == 1 {
54		uc.latestVersion = latestRemote
55	}
56	return nil
57}
58
59// AreAvailable updates the cache if the frequency duration has passed
60// then returns if the latest release is newer than the current running version.
61func (uc updatesCache) AreAvailable() bool {
62	if time.Since(uc.lastCheck) > uc.frequency {
63		uc.CheckNow()
64	}
65	return CompareSemver(uc.latestVersion, uc.currentVersion) == 1
66}
67
68// AreAvailableNoCheck returns if the latest release is newer than the current
69// running version.
70func (uc updatesCache) AreAvailableNoCheck() bool {
71	return CompareSemver(uc.latestVersion, uc.currentVersion) == 1
72}
73
74// LatestVersion returns the latest stored version available.
75func (uc updatesCache) LatestVersion() string {
76	return uc.latestVersion
77}
78
79func (uc updatesCache) ReleaseURL() string {
80	return "https://writefreely.org/releases/" + uc.latestVersion
81}
82
83// ReleaseNotesURL returns the full URL to the blog.writefreely.org release notes
84// for the latest version as stored in the cache.
85func (uc updatesCache) ReleaseNotesURL() string {
86	return wfReleaseNotesURL(uc.latestVersion)
87}
88
89func wfReleaseNotesURL(v string) string {
90	ver := strings.TrimPrefix(v, "v")
91	ver = strings.TrimSuffix(ver, ".0")
92	// hack until go 1.12 in build/travis
93	seg := strings.Split(ver, ".")
94	return "https://blog.writefreely.org/version-" + strings.Join(seg, "-")
95}
96
97// newUpdatesCache returns an initialized updates cache
98func newUpdatesCache(expiry time.Duration) *updatesCache {
99	cache := updatesCache{
100		frequency:      expiry,
101		currentVersion: "v" + softwareVer,
102	}
103	go cache.CheckNow()
104	return &cache
105}
106
107// InitUpdates initializes the updates cache, if the config value is set
108// It uses the defaultUpdatesCacheTime for the cache expiry
109func (app *App) InitUpdates() {
110	if app.cfg.App.UpdateChecks {
111		app.updates = newUpdatesCache(defaultUpdatesCacheTime)
112	}
113}
114
115func newVersionCheck() (string, error) {
116	res, err := http.Get("https://version.writefreely.org")
117	if debugging {
118		log.Info("[update check] GET https://version.writefreely.org")
119	}
120	// TODO: return error if statusCode != OK
121	if err == nil && res.StatusCode == http.StatusOK {
122		defer res.Body.Close()
123
124		body, err := ioutil.ReadAll(res.Body)
125		if err != nil {
126			return "", err
127		}
128		return string(body), nil
129	}
130	return "", err
131}
132