1use super::{DataManager, NAME_REGEX};
2use crate::model::{Entry, EntryMetadata};
3use oiseau::{PostgresRow, cache::Cache, execute, get, params, query_row};
4use serde_valid::Validate;
5use tetratto_core::{
6 auto_method,
7 model::{Error, Result},
8};
9use tetratto_shared::{hash::hash, unix_epoch_timestamp};
10
11impl DataManager {
12 pub(crate) fn get_entry_from_row(x: &PostgresRow) -> Entry {
14 Entry {
15 id: get!(x->0(i64)) as usize,
16 slug: get!(x->1(String)),
17 edit_code: get!(x->2(String)),
18 salt: get!(x->3(String)),
19 created: get!(x->4(i64)) as usize,
20 edited: get!(x->5(i64)) as usize,
21 content: get!(x->6(String)),
22 metadata: get!(x->7(String)),
23 last_edit_from: get!(x->8(String)),
24 modify_code: get!(x->9(String)),
25 views: get!(x->10(i64)) as usize,
26 }
27 }
28
29 auto_method!(get_entry_by_id(usize as i64)@get_entry_from_row -> "SELECT * FROM entries WHERE id = $1" --name="entry" --returns=Entry --cache-key-tmpl="fluf.entry:{}");
30 auto_method!(get_entry_by_slug(&str)@get_entry_from_row -> "SELECT * FROM entries WHERE slug = $1" --name="entry" --returns=Entry --cache-key-tmpl="fluf.entry:{}");
31
32 fn hash_passwords(metadata: &mut EntryMetadata) -> (bool, String) {
33 let do_update_metadata = (!metadata.option_view_password.is_empty()
35 || !metadata.option_source_password.is_empty())
36 && (!metadata.option_view_password.starts_with("h:")
37 || !metadata.option_source_password.starts_with("h:"));
38
39 if !metadata.option_view_password.is_empty()
40 && !metadata.option_view_password.starts_with("h:")
41 {
42 metadata.option_view_password =
43 format!("h:{}", hash(metadata.option_view_password.clone()));
44 }
45
46 if !metadata.option_source_password.is_empty()
47 && !metadata.option_source_password.starts_with("h:")
48 {
49 metadata.option_source_password =
50 format!("h:{}", hash(metadata.option_source_password.clone()));
51 }
52
53 if do_update_metadata {
54 if let Ok(x) = toml::to_string_pretty(&metadata) {
55 return (true, x);
56 };
57 }
58
59 (false, String::new())
60 }
61
62 pub async fn create_entry(&self, mut data: Entry) -> Result<Entry> {
67 if data.slug.trim().len() < 2 {
69 return Err(Error::DataTooShort("slug".to_string()));
70 } else if data.slug.len() > 128 {
71 return Err(Error::DataTooLong("slug".to_string()));
72 }
73
74 if data.content.len() < 2 {
75 return Err(Error::DataTooShort("content".to_string()));
76 }
77
78 if data.content.len() > 150_000 {
79 return Err(Error::DataTooLong("content".to_string()));
80 }
81
82 let regex = regex::RegexBuilder::new(NAME_REGEX)
84 .multi_line(true)
85 .build()
86 .unwrap();
87
88 if regex.captures(&data.slug).is_some() {
89 return Err(Error::MiscError(
90 "This slug contains invalid characters".to_string(),
91 ));
92 }
93
94 if self.get_entry_by_slug(&data.slug).await.is_ok() {
96 return Err(Error::MiscError("Slug is already in use".to_string()));
97 }
98
99 let mut metadata: EntryMetadata =
101 match toml::from_str(&EntryMetadata::ini_to_toml(&data.metadata)) {
102 Ok(x) => x,
103 Err(e) => return Err(Error::MiscError(e.to_string())),
104 };
105
106 if let Err(e) = metadata.validate() {
107 return Err(Error::MiscError(e.to_string()));
108 }
109
110 let (do_update_metadata, updated) = Self::hash_passwords(&mut metadata);
111 if do_update_metadata {
112 data.metadata = updated;
113 }
114
115 let conn = match self.0.connect().await {
117 Ok(c) => c,
118 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
119 };
120
121 let res = execute!(
122 &conn,
123 "INSERT INTO entries VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
124 params![
125 &(data.id as i64),
126 &data.slug,
127 &data.edit_code,
128 &data.salt,
129 &(data.created as i64),
130 &(data.edited as i64),
131 &data.content,
132 &data.metadata,
133 &data.last_edit_from,
134 &data.modify_code,
135 &(data.views as i64)
136 ]
137 );
138
139 if let Err(e) = res {
140 return Err(Error::DatabaseError(e.to_string()));
141 }
142
143 Ok(data)
144 }
145
146 pub async fn update_entry(
148 &self,
149 id: usize,
150 edit_code: String,
151 mut new_slug: String,
152 new_content: String,
153 mut new_metadata: String,
154 mut new_edit_code: String,
155 mut new_modify_code: String,
156 by_ip: String,
157 ) -> Result<String> {
158 if !new_slug.is_empty() {
160 if new_slug.trim().len() < 2 {
161 return Err(Error::DataTooShort("slug".to_string()));
162 } else if new_slug.len() > 128 {
163 return Err(Error::DataTooLong("slug".to_string()));
164 }
165 }
166
167 if new_content.len() < 2 {
168 return Err(Error::DataTooShort("content".to_string()));
169 }
170
171 if new_content.len() > 150_000 {
172 return Err(Error::DataTooLong("content".to_string()));
173 }
174
175 let regex = regex::RegexBuilder::new(NAME_REGEX)
177 .multi_line(true)
178 .build()
179 .unwrap();
180
181 if regex.captures(&new_slug).is_some() {
182 return Err(Error::MiscError(
183 "This slug contains invalid characters".to_string(),
184 ));
185 }
186
187 let mut metadata: EntryMetadata =
189 match toml::from_str(&EntryMetadata::ini_to_toml(&new_metadata)) {
190 Ok(x) => x,
191 Err(e) => return Err(Error::MiscError(e.to_string())),
192 };
193
194 if let Err(e) = metadata.validate() {
195 return Err(Error::MiscError(e.to_string()));
196 }
197
198 let (do_update_metadata, updated) = Self::hash_passwords(&mut metadata);
199 if do_update_metadata {
200 new_metadata = updated;
201 }
202
203 let entry = self.get_entry_by_id(id).await?;
205
206 let using_modify = hash(edit_code.clone() + &entry.salt) == entry.modify_code;
208
209 if !using_modify && edit_code != self.0.0.master_pass {
210 if !entry.check_password(edit_code) {
211 return Err(Error::NotAllowed);
212 }
213 }
214
215 self.cache_clear_entry(&entry).await;
217
218 if !using_modify {
220 if new_slug.is_empty() {
221 new_slug = entry.slug;
223 } else {
224 new_slug = new_slug.to_lowercase();
226 }
227
228 if !new_edit_code.is_empty() {
229 new_edit_code = hash(new_edit_code + &entry.salt);
230 } else {
231 new_edit_code = entry.edit_code;
233 }
234
235 if !new_modify_code.is_empty() {
236 new_modify_code = hash(new_modify_code + &entry.salt);
237 } else {
238 new_modify_code = entry.modify_code;
240 }
241 } else {
242 new_slug = entry.slug;
244 new_edit_code = entry.edit_code;
245 new_modify_code = entry.modify_code;
246 }
247
248 let conn = match self.0.connect().await {
250 Ok(c) => c,
251 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
252 };
253
254 let res = execute!(
255 &conn,
256 "UPDATE entries SET slug = $1, edit_code = $2, modify_code = $3, content = $4, metadata = $5, edited = $6, last_edit_from = $7 WHERE id = $8",
257 params![
258 &new_slug,
259 &new_edit_code,
260 &new_modify_code,
261 &new_content,
262 &new_metadata,
263 &(unix_epoch_timestamp() as i64),
264 &by_ip,
265 &(id as i64),
266 ]
267 );
268
269 if let Err(e) = res {
270 return Err(Error::DatabaseError(e.to_string()));
271 }
272
273 Ok(new_slug)
274 }
275
276 pub async fn delete_entry(&self, id: usize, edit_code: String) -> Result<()> {
278 let entry = self.get_entry_by_id(id).await?;
280
281 if edit_code != self.0.0.master_pass {
283 if !entry.check_password(edit_code) {
284 return Err(Error::NotAllowed);
285 }
286 }
287
288 let conn = match self.0.connect().await {
290 Ok(c) => c,
291 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
292 };
293
294 let res = execute!(
295 &conn,
296 "DELETE FROM entries WHERE id = $1",
297 params![&(id as i64)]
298 );
299
300 if let Err(e) = res {
301 return Err(Error::DatabaseError(e.to_string()));
302 }
303
304 self.cache_clear_entry(&entry).await;
305 Ok(())
306 }
307
308 pub async fn cache_clear_entry(&self, entry: &Entry) -> bool {
310 self.0.1.remove(format!("fluf.entry:{}", entry.id)).await
311 && self.0.1.remove(format!("fluf.entry:{}", entry.slug)).await
312 }
313
314 auto_method!(incr_entry_views()@get_entry_by_id -> "UPDATE entries SET views = views + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_entry --incr);
315 auto_method!(decr_entry_views()@get_entry_by_id -> "UPDATE entries SET views = views - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_entry --decr=views);
316}