/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

//! An implementation of the [CORS preflight cache](https://fetch.spec.whatwg.org/#cors-preflight-cache)
//! For now this library is XHR-specific.
//! For stuff involving `<img>`, `<iframe>`, `<form>`, etc please check what
//! the request mode should be and compare with the fetch spec
//! This library will eventually become the core of the Fetch crate
//! with CORSRequest being expanded into FetchRequest (etc)

use http::header::HeaderName;
use hyper::Method;
use net_traits::request::{CredentialsMode, Origin, Request};
use servo_url::ServoUrl;
use time::{self, Timespec};

/// Union type for CORS cache entries
///
/// Each entry might pertain to a header or method
#[derive(Clone, Debug)]
pub enum HeaderOrMethod {
    HeaderData(HeaderName),
    MethodData(Method),
}

impl HeaderOrMethod {
    fn match_header(&self, header_name: &HeaderName) -> bool {
        match *self {
            HeaderOrMethod::HeaderData(ref n) => n == header_name,
            _ => false,
        }
    }

    fn match_method(&self, method: &Method) -> bool {
        match *self {
            HeaderOrMethod::MethodData(ref m) => m == method,
            _ => false,
        }
    }
}

/// An entry in the CORS cache
#[derive(Clone, Debug)]
pub struct CorsCacheEntry {
    pub origin: Origin,
    pub url: ServoUrl,
    pub max_age: u32,
    pub credentials: bool,
    pub header_or_method: HeaderOrMethod,
    created: Timespec,
}

impl CorsCacheEntry {
    fn new(
        origin: Origin,
        url: ServoUrl,
        max_age: u32,
        credentials: bool,
        header_or_method: HeaderOrMethod,
    ) -> CorsCacheEntry {
        CorsCacheEntry {
            origin: origin,
            url: url,
            max_age: max_age,
            credentials: credentials,
            header_or_method: header_or_method,
            created: time::now().to_timespec(),
        }
    }
}

fn match_headers(cors_cache: &CorsCacheEntry, cors_req: &Request) -> bool {
    cors_cache.origin == cors_req.origin &&
        cors_cache.url == cors_req.current_url() &&
        (cors_cache.credentials || cors_req.credentials_mode != CredentialsMode::Include)
}

/// A simple, vector-based CORS Cache
#[derive(Clone)]
pub struct CorsCache(Vec<CorsCacheEntry>);

impl CorsCache {
    pub fn new() -> CorsCache {
        CorsCache(vec![])
    }

    fn find_entry_by_header<'a>(
        &'a mut self,
        request: &Request,
        header_name: &HeaderName,
    ) -> Option<&'a mut CorsCacheEntry> {
        self.cleanup();
        self.0
            .iter_mut()
            .find(|e| match_headers(e, request) && e.header_or_method.match_header(header_name))
    }

    fn find_entry_by_method<'a>(
        &'a mut self,
        request: &Request,
        method: Method,
    ) -> Option<&'a mut CorsCacheEntry> {
        // we can take the method from CorSRequest itself
        self.cleanup();
        self.0
            .iter_mut()
            .find(|e| match_headers(e, request) && e.header_or_method.match_method(&method))
    }

    /// [Clear the cache](https://fetch.spec.whatwg.org/#concept-cache-clear)
    pub fn clear(&mut self, request: &Request) {
        let CorsCache(buf) = self.clone();
        let new_buf: Vec<CorsCacheEntry> = buf
            .into_iter()
            .filter(|e| e.origin == request.origin && request.current_url() == e.url)
            .collect();
        *self = CorsCache(new_buf);
    }

    /// Remove old entries
    pub fn cleanup(&mut self) {
        let CorsCache(buf) = self.clone();
        let now = time::now().to_timespec();
        let new_buf: Vec<CorsCacheEntry> = buf
            .into_iter()
            .filter(|e| now.sec < e.created.sec + e.max_age as i64)
            .collect();
        *self = CorsCache(new_buf);
    }

    /// Returns true if an entry with a
    /// [matching header](https://fetch.spec.whatwg.org/#concept-cache-match-header) is found
    pub fn match_header(&mut self, request: &Request, header_name: &HeaderName) -> bool {
        self.find_entry_by_header(&request, header_name).is_some()
    }

    /// Updates max age if an entry for a
    /// [matching header](https://fetch.spec.whatwg.org/#concept-cache-match-header) is found.
    ///
    /// If not, it will insert an equivalent entry
    pub fn match_header_and_update(
        &mut self,
        request: &Request,
        header_name: &HeaderName,
        new_max_age: u32,
    ) -> bool {
        match self
            .find_entry_by_header(&request, header_name)
            .map(|e| e.max_age = new_max_age)
        {
            Some(_) => true,
            None => {
                self.insert(CorsCacheEntry::new(
                    request.origin.clone(),
                    request.current_url(),
                    new_max_age,
                    request.credentials_mode == CredentialsMode::Include,
                    HeaderOrMethod::HeaderData(header_name.clone()),
                ));
                false
            },
        }
    }

    /// Returns true if an entry with a
    /// [matching method](https://fetch.spec.whatwg.org/#concept-cache-match-method) is found
    pub fn match_method(&mut self, request: &Request, method: Method) -> bool {
        self.find_entry_by_method(&request, method).is_some()
    }

    /// Updates max age if an entry for
    /// [a matching method](https://fetch.spec.whatwg.org/#concept-cache-match-method) is found.
    ///
    /// If not, it will insert an equivalent entry
    pub fn match_method_and_update(
        &mut self,
        request: &Request,
        method: Method,
        new_max_age: u32,
    ) -> bool {
        match self
            .find_entry_by_method(&request, method.clone())
            .map(|e| e.max_age = new_max_age)
        {
            Some(_) => true,
            None => {
                self.insert(CorsCacheEntry::new(
                    request.origin.clone(),
                    request.current_url(),
                    new_max_age,
                    request.credentials_mode == CredentialsMode::Include,
                    HeaderOrMethod::MethodData(method),
                ));
                false
            },
        }
    }

    /// Insert an entry
    pub fn insert(&mut self, entry: CorsCacheEntry) {
        self.cleanup();
        self.0.push(entry);
    }
}