1use std::sync::Arc;
6
7use crate::common_metric_data::CommonMetricDataInternal;
8use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
9use crate::metrics::Metric;
10use crate::metrics::MetricType;
11use crate::storage::StorageManager;
12use crate::util::truncate_string_at_boundary_with_error;
13use crate::Glean;
14use crate::{CommonMetricData, TestGetValue};
15
16const MAX_URL_LENGTH: usize = 8192;
18
19#[derive(Clone, Debug)]
24pub struct UrlMetric {
25 meta: Arc<CommonMetricDataInternal>,
26}
27
28impl MetricType for UrlMetric {
29 fn meta(&self) -> &CommonMetricDataInternal {
30 &self.meta
31 }
32}
33
34impl UrlMetric {
39 pub fn new(meta: CommonMetricData) -> Self {
41 Self {
42 meta: Arc::new(meta.into()),
43 }
44 }
45
46 fn is_valid_url_scheme(&self, value: String) -> bool {
47 let mut splits = value.split(':');
48 if let Some(scheme) = splits.next() {
49 if scheme.is_empty() {
50 return false;
51 }
52 let mut chars = scheme.chars();
53 return chars.next().unwrap().is_ascii_alphabetic()
56 && chars.all(|c| c.is_ascii_alphanumeric() || ['+', '-', '.'].contains(&c));
57 }
58
59 false
61 }
62
63 pub fn set<S: Into<String>>(&self, value: S) {
73 let value = value.into();
74 let metric = self.clone();
75 crate::launch_with_glean(move |glean| metric.set_sync(glean, value))
76 }
77
78 #[doc(hidden)]
80 pub fn set_sync<S: Into<String>>(&self, glean: &Glean, value: S) {
81 if !self.should_record(glean) {
82 return;
83 }
84
85 let s = truncate_string_at_boundary_with_error(glean, &self.meta, value, MAX_URL_LENGTH);
86
87 if s.starts_with("data:") {
88 record_error(
89 glean,
90 &self.meta,
91 ErrorType::InvalidValue,
92 "URL metric does not support data URLs.",
93 None,
94 );
95 return;
96 }
97
98 if !self.is_valid_url_scheme(s.clone()) {
99 let msg = format!("\"{}\" does not start with a valid URL scheme.", s);
100 record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
101 return;
102 }
103
104 let value = Metric::Url(s);
105 glean.storage().record(glean, &self.meta, &value)
106 }
107
108 #[doc(hidden)]
109 pub(crate) fn get_value<'a, S: Into<Option<&'a str>>>(
110 &self,
111 glean: &Glean,
112 ping_name: S,
113 ) -> Option<String> {
114 let queried_ping_name = ping_name
115 .into()
116 .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
117
118 match StorageManager.snapshot_metric_for_test(
119 glean.storage(),
120 queried_ping_name,
121 &self.meta.identifier(glean),
122 self.meta.inner.lifetime,
123 ) {
124 Some(Metric::Url(s)) => Some(s),
125 _ => None,
126 }
127 }
128
129 pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
141 crate::block_on_dispatcher();
142
143 crate::core::with_glean(|glean| {
144 test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
145 })
146 }
147}
148
149impl TestGetValue<String> for UrlMetric {
150 fn test_get_value(&self, ping_name: Option<String>) -> Option<String> {
165 crate::block_on_dispatcher();
166 crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
167 }
168}
169
170#[cfg(test)]
171mod test {
172 use super::*;
173 use crate::tests::new_glean;
174 use crate::Lifetime;
175
176 #[test]
177 fn payload_is_correct() {
178 let (glean, _t) = new_glean(None);
179
180 let metric = UrlMetric::new(CommonMetricData {
181 name: "url_metric".into(),
182 category: "test".into(),
183 send_in_pings: vec!["store1".into()],
184 lifetime: Lifetime::Application,
185 disabled: false,
186 dynamic_label: None,
187 });
188
189 let sample_url = "glean://test".to_string();
190 metric.set_sync(&glean, sample_url.clone());
191 assert_eq!(sample_url, metric.get_value(&glean, "store1").unwrap());
192 }
193
194 #[test]
195 fn does_not_record_url_exceeding_maximum_length() {
196 let (glean, _t) = new_glean(None);
197
198 let metric = UrlMetric::new(CommonMetricData {
199 name: "url_metric".into(),
200 category: "test".into(),
201 send_in_pings: vec!["store1".into()],
202 lifetime: Lifetime::Application,
203 disabled: false,
204 dynamic_label: None,
205 });
206
207 let long_path_base = "abcdefgh";
213
214 let test_url = format!("glean://{}", long_path_base.repeat(2000));
216 metric.set_sync(&glean, test_url);
217
218 let expected = format!("glean://{}", long_path_base.repeat(1023));
223
224 assert_eq!(metric.get_value(&glean, "store1").unwrap(), expected);
225 assert_eq!(
226 1,
227 test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow)
228 .unwrap()
229 );
230 }
231
232 #[test]
233 fn does_not_record_data_urls() {
234 let (glean, _t) = new_glean(None);
235
236 let metric = UrlMetric::new(CommonMetricData {
237 name: "url_metric".into(),
238 category: "test".into(),
239 send_in_pings: vec!["store1".into()],
240 lifetime: Lifetime::Application,
241 disabled: false,
242 dynamic_label: None,
243 });
244
245 let test_url = "data:application/json";
246 metric.set_sync(&glean, test_url);
247
248 assert!(metric.get_value(&glean, "store1").is_none());
249
250 assert_eq!(
251 1,
252 test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).unwrap()
253 );
254 }
255
256 #[test]
257 fn url_validation_works_and_records_errors() {
258 let (glean, _t) = new_glean(None);
259
260 let metric = UrlMetric::new(CommonMetricData {
261 name: "url_metric".into(),
262 category: "test".into(),
263 send_in_pings: vec!["store1".into()],
264 lifetime: Lifetime::Application,
265 disabled: false,
266 dynamic_label: None,
267 });
268
269 let incorrects = vec![
270 "",
271 "1glean://test",
274 "-glean://test",
275 "шеллы://test",
277 "g!lean://test",
278 "g=lean://test",
279 "glean//test",
281 ];
282
283 let corrects = vec![
284 "g:",
286 "glean://",
288 "glean:",
290 "glean:test",
291 "glean:test.com",
292 "g-lean://test",
294 "g+lean://test",
295 "g.lean://test",
296 "glean://test?hello=world",
298 "https://infra.spec.whatwg.org/#ascii-alpha",
300 "https://infra.spec.whatwg.org/#ascii-alpha?test=for-glean",
301 ];
302
303 for incorrect in incorrects.clone().into_iter() {
304 metric.set_sync(&glean, incorrect);
305 assert!(metric.get_value(&glean, "store1").is_none());
306 }
307
308 assert_eq!(
309 incorrects.len(),
310 test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).unwrap()
311 as usize
312 );
313
314 for correct in corrects.into_iter() {
315 metric.set_sync(&glean, correct);
316 assert_eq!(metric.get_value(&glean, "store1").unwrap(), correct);
317 }
318 }
319}