1// SPDX-FileCopyrightText: 2022 Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: BSD-2-Clause
4
5package main
6
7import (
8 "errors"
9 "fmt"
10 "log"
11 "net/url"
12 "sort"
13 "strings"
14 "text/template"
15
16 "github.com/dgraph-io/badger/v3"
17)
18
19// Create shortens a given URL with an optional name. If a name is provided,
20// that name will be used. Otherwise, 4-character string will be generated and
21// used instead.
22func (m model) create(name string, url string) string {
23 err := m.database.Update(func(txn *badger.Txn) error {
24 return txn.Set([]byte(name), []byte(url))
25 })
26 if err != nil {
27 // TODO: return an error instead so it can be handled like an error
28 return fmt.Sprint(err)
29 }
30
31 log.Println("\"" + url + "\" mapped to \"" + name + "\"")
32 return fmt.Sprint("URL mapped to ", name, "\n")
33}
34
35// read() accepts a start variable for the first link in a set (implying reverse
36// iteration), an end variable for the last link in a set (implying forward
37// iteration), and a count variable for the number of requested entries. The
38// requested set is returned as JSON.
39func (m model) read(start string, end string, count int) (map[string]string, error) {
40 links := make(map[string]string)
41
42 if !(count > 0) {
43 return nil, errors.New("Count parameter is required")
44 }
45
46 err := m.database.View(func(txn *badger.Txn) error {
47 opts := badger.DefaultIteratorOptions
48 opts.PrefetchSize = 20
49 if start != "" && end == "" {
50 // Start value provided, iterate backwards
51 opts.Reverse = true
52 }
53 iterator := txn.NewIterator(opts)
54 defer iterator.Close()
55 if start == "" && end == "" {
56 iterator.Rewind()
57 } else if start != "" && end == "" {
58 // Set position to "start"
59 iterator.Seek([]byte(start))
60 // If "start" exists, move to next value
61 if iterator.Valid() {
62 iterator.Next()
63 }
64 } else if start == "" && end != "" {
65 // Start value provided, iterate forwards
66 // Set position to "end"
67 iterator.Seek([]byte(end))
68 // If "end" exists, move to next value
69 if iterator.Valid() {
70 iterator.Next()
71 }
72 } else {
73 return errors.New("Only provide start OR end parameters, not both")
74 }
75 for i := 0; i < count; i++ {
76 if iterator.Valid() {
77 err := iterator.Item().Value(func(v []byte) error {
78 links[string(iterator.Item().Key())] = string(v)
79 return nil
80 })
81 if err != nil {
82 return err
83 }
84 iterator.Next()
85 }
86 }
87 return nil
88 })
89 if err != nil {
90 return nil, err
91 }
92
93 return links, nil
94}
95
96// Update modifies a shortened link
97func (m model) update(name string, url string, oldName string) string {
98 m.create(name, url)
99 if name != oldName {
100 m.delete(oldName)
101 }
102 return fmt.Sprint("\"" + url + "\" mapped to \"" + name + "\"")
103}
104
105// Delete removes a shortened URL from the database given its name.
106func (m model) delete(name string) string {
107 err := m.database.Update(func(txn *badger.Txn) error {
108 return txn.Delete([]byte(name))
109 })
110 if err != nil {
111 return fmt.Sprint(err)
112 }
113
114 log.Println("\"" + name + "\" has been deleted")
115 return fmt.Sprint("\"" + name + "\" has been deleted\n")
116}
117
118// nameExists returns true if the provided name is already stored in the
119// database and false if it's not.
120func (m model) nameExists(name string) bool {
121 err := m.database.View(func(txn *badger.Txn) error {
122 _, err := txn.Get([]byte(name))
123 if err != nil {
124 return err
125 }
126 return nil
127 })
128 return err == nil
129}
130
131// unauthenticated serves the unauthenticated home page to the visitor
132func unauthenticated() string {
133 home, err := templates.ReadFile("templates/home_unauthenticated.html")
134 if err != nil {
135 log.Fatalln(err)
136 }
137 return string(home)
138}
139
140// authenticated serves the authenticated dashboard to the user
141func (m model) authenticated(links map[string]string) string {
142 dash, err := templates.ReadFile("templates/home_authenticated.html")
143 if err != nil {
144 log.Fatalln(err)
145 }
146 tmpl, err := template.New("authenticated").Parse(string(dash))
147 if err != nil {
148 log.Println(err)
149 }
150
151 // Maps have no order so we're making an array containing the keys
152 linksArray := make([]string, 0, len(links))
153 for k := range links {
154 linksArray = append(linksArray, k)
155 }
156 // Then sorting them
157 sort.Sort(sort.StringSlice(linksArray))
158
159 // And ranging through the keys to pull ordered values from the map
160 var table string
161 for _, k := range linksArray {
162 v := links[k]
163 table = table + fmt.Sprintf(`<tr>
164 <td><p>%s</p></td>
165 <td><p>%s</p></td>
166 <td><a class="button" href="/?action=edit&name=%s&url=%s">Edit</a><a class="button" href="/?action=delete&name=%s">Delete</a></td>
167</tr>`, k, v, k, url.QueryEscape(string(v)), k)
168 }
169
170 page := new(strings.Builder)
171 err = tmpl.Execute(page, table)
172 if err != nil {
173 log.Println(err)
174 }
175 return page.String()
176}
177
178type EditFields struct {
179 URL string
180 Name string
181}
182
183// update serves the editing interface to the user
184func (m model) edit(name string, url string) string {
185 fields := EditFields{url, name}
186 editPage, err := templates.ReadFile("templates/edit.html")
187 if err != nil {
188 log.Fatalln(err)
189 }
190 tmpl, err := template.New("editPage").Parse(string(editPage))
191 if err != nil {
192 log.Println(err)
193 }
194 page := new(strings.Builder)
195 err = tmpl.Execute(page, fields)
196 if err != nil {
197 log.Println(err)
198 }
199 return page.String()
200}