phemios_core/
article_store.rs1use crate::{federation::Source, network::Client, private_documents};
3use serde_json::{Value, json};
4use std::path::PathBuf;
5pub struct Store {
7 document: private_documents::Store,
8}
9impl Store {
10 pub fn new(root: PathBuf) -> Self {
12 Self {
13 document: private_documents::Store::new(root, private_documents::Kind::Articles),
14 }
15 }
16 fn document(&self, owner: &Value) -> Result<Value, String> {
17 let value = self
18 .document
19 .read(owner)?
20 .unwrap_or_else(|| json!({"articles":[]}));
21 validate(&value)?;
22 Ok(value)
23 }
24 pub(crate) fn prepare_restore(
26 &self,
27 source: Source,
28 client: &Client,
29 imported: &Value,
30 ) -> Result<Value, String> {
31 writable(client)?;
32 validate(imported)?;
33 let owner = private_documents::owner(source, client)?;
34 let mut value = self.document(&owner)?;
35 let incoming = imported["articles"]
36 .as_array()
37 .ok_or("Articles enregistrés illisibles.")?;
38 let held = value["articles"]
39 .as_array_mut()
40 .ok_or("Articles enregistrés illisibles.")?;
41 for row in incoming {
42 if !held.iter().any(|existing| {
43 existing["language"] == row["language"] && existing["pageId"] == row["pageId"]
44 }) {
45 held.push(row.clone());
46 }
47 }
48 validate(&value)?;
49 self.document.preflight(&owner, &value)?;
50 Ok(value)
51 }
52 pub(crate) fn commit_restore(
54 &self,
55 source: Source,
56 client: &Client,
57 prepared: Value,
58 ) -> Result<(), String> {
59 writable(client)?;
60 validate(&prepared)?;
61 self.document
62 .write(&private_documents::owner(source, client)?, prepared)
63 }
64 pub fn read(&self, source: Source, client: &Client) -> Result<Value, String> {
66 let value = self.document(&private_documents::owner(source, client)?)?;
67 Ok(json!({"version":1,"articles":value["articles"]}))
68 }
69 pub fn update(
71 &self,
72 source: Source,
73 client: &Client,
74 action: &str,
75 payload: &Value,
76 ) -> Result<Value, String> {
77 writable(client)?;
78 identity(payload)?;
79 let owner = private_documents::owner(source, client)?;
80 let mut value = self.document(&owner)?;
81 let rows = value["articles"]
82 .as_array_mut()
83 .ok_or("Articles enregistrés illisibles.")?;
84 let same = |row: &Value| {
85 row["language"] == payload["language"] && row["pageId"] == payload["pageId"]
86 };
87 match action {
88 "save" => {
89 article(payload)?;
90 if let Some(row) = rows.iter_mut().find(|row| same(row)) {
91 *row = payload.clone();
92 } else {
93 rows.push(payload.clone());
94 }
95 }
96 "remove" => rows.retain(|row| !same(row)),
97 _ => return Err("Action article invalide.".into()),
98 }
99 validate(&value)?;
100 self.document.write(&owner, value)?;
101 self.read(source, client)
102 }
103}
104fn writable(client: &Client) -> Result<(), String> {
105 if matches!(
106 client.account()["account"]["role"].as_str(),
107 Some("user" | "admin")
108 ) {
109 Ok(())
110 } else {
111 Err("Ce compte ne peut pas modifier ses articles.".into())
112 }
113}
114
115fn identity(value: &Value) -> Result<(), String> {
116 if !matches!(value["language"].as_str(), Some("fr" | "en")) || !positive(&value["pageId"]) {
117 return Err("Article enregistré invalide.".into());
118 }
119 Ok(())
120}
121fn positive(value: &Value) -> bool {
122 value
123 .as_u64()
124 .is_some_and(|n| n > 0 && n <= 9_007_199_254_740_991)
125}
126fn article(value: &Value) -> Result<(), String> {
127 identity(value)?;
128 if let Some(links) = value.get("externalLinks") {
129 crate::encyclopedia::resources::validate(links)?;
130 }
131 let title = value["title"].as_str().is_some_and(|s| {
132 !s.trim().is_empty() && s.encode_utf16().count() <= 2048 && !s.chars().any(char::is_control)
133 });
134 let text = value["text"].as_str().is_some_and(|s| {
135 !s.trim().is_empty()
136 && s.len() <= 512 * 1024
137 && !s
138 .chars()
139 .any(|c| c.is_control() && !matches!(c, '\n' | '\r' | '\t'))
140 });
141 if !positive(&value["revision"])
142 || !value["retrievedAt"]
143 .as_u64()
144 .is_some_and(|n| n > 0 && n <= 8_640_000_000_000_000)
145 || !title
146 || !text
147 {
148 return Err("Article enregistré invalide.".into());
149 }
150 Ok(())
151}
152pub(crate) fn validate(value: &Value) -> Result<(), String> {
153 let rows = value["articles"]
154 .as_array()
155 .filter(|rows| rows.len() <= 40)
156 .ok_or("Articles enregistrés illisibles.")?;
157 if serde_json::to_vec(rows)
158 .map_err(|_| "Articles enregistrés illisibles.")?
159 .len()
160 > 3 * 1024 * 1024
161 {
162 return Err("Limite de stockage des articles atteinte.".into());
163 }
164 let mut ids = std::collections::BTreeSet::new();
165 for row in rows {
166 article(row)?;
167 if !ids.insert((row["language"].as_str(), row["pageId"].as_u64())) {
168 return Err("Articles enregistrés illisibles.".into());
169 }
170 }
171 Ok(())
172}
173#[cfg(test)]
174#[path = "article_store_tests.rs"]
175mod tests;