extern crate core;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
use chrono::{DateTime, Utc};
use parking_lot::Mutex;
use uuid::Uuid;
use product_os_router::{Body, IntoResponse, Request, Response, StatusCode};
use product_os_store_macros::{ ProductOSRelational };
use product_os_store::{ProductOSKeyValueStore, ProductOSRelationalStore, ProductOSRelationalObject, ProductOSRow};
use serde::{ Serialize, Deserialize };
pub use async_trait::async_trait;
mod relational_store;
mod content_handler;
mod content_transform;
use lazy_static::lazy_static;
use regex::Regex;
use serde_json::{Map, Value};
use product_os_capabilities::{Feature, RegistryFeature};
use product_os_command_control::{ProductOSController, Method};
use crate::content_transform::ProductOSContentOptions;
pub use relational_store::setup_content_store;
lazy_static! {
static ref DOUBLE_SLASH_REGEX: Regex = Regex::new(r"//").unwrap();
static ref SLASH_END_REGEX: Regex = Regex::new(r"/$").unwrap();
static ref DOUBLE_DOT_REGEX: Regex = Regex::new(r"[.][.]").unwrap();
static ref ALLOWED_NAME_REGEX: Regex = Regex::new(r"[a-zA-Z0-9_-]+").unwrap();
static ref ELEMENT_FINDER_REGEX: Regex = Regex::new(r"[#][{][a-zA-Z0-9_-]+[}]").unwrap();
}
#[derive(Debug)]
pub enum ProductOSContentError {
StoreError(String),
DoesNotExist(String),
ProcessingError(String),
AuthenticationError(String)
}
#[derive(Debug, Default, ProductOSRelational)]
pub struct ProductOSContentSite {
#[primary_key] pub identifier: Uuid,
pub domain: String,
pub base_path: String
}
#[derive(Debug, Default, ProductOSRelational)]
pub struct ProductOSContentTemplate {
#[primary_key] pub identifier: Uuid,
pub content_type: String, pub layout: Value,
pub default_content_elements: Map<String, Value>, pub format: ProductOSContentFormat
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "markup")]
pub enum ProductOSContentMarkup {
MarkDown
}
impl Display for ProductOSContentMarkup {
fn fmt(&self, f: &mut Formatter) ->std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "override_type")]
pub enum ProductOSContentOverrideType {
Asset,
Template,
Append,
Prepend
}
impl Display for ProductOSContentOverrideType {
fn fmt(&self, f: &mut Formatter) ->std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "state", content = "value")]
pub enum ProductOSContentWorkflowState {
Draft,
Sandbox,
Live,
Archived,
Custom(String)
}
impl Display for ProductOSContentWorkflowState {
fn fmt(&self, f: &mut Formatter) ->std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl Default for ProductOSContentWorkflowState {
fn default() -> Self {
Self::Draft
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum ProductOSContentAssetType {
Content,
Source,
Media(ProductOSContentMediaType),
Applet,
View,
Error(i16)
}
impl Display for ProductOSContentAssetType {
fn fmt(&self, f: &mut Formatter) ->std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum ProductOSContentMediaType {
Image,
Video,
Audio,
Font,
Binary,
Text,
Unknown
}
impl Display for ProductOSContentMediaType {
fn fmt(&self, f: &mut Formatter) ->std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum ProductOSContentFormat {
Binary,
Text,
Block
}
impl Display for ProductOSContentFormat {
fn fmt(&self, f: &mut Formatter) ->std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl Default for ProductOSContentFormat {
fn default() -> Self {
Self::Text
}
}
impl ProductOSContentFormat {
pub fn from_str(string: &str) -> ProductOSContentFormat {
match string {
"Binary" => ProductOSContentFormat::Binary,
"Text" => ProductOSContentFormat::Text,
"Block" => ProductOSContentFormat::Block,
_ => panic!("Unknown content format")
}
}
}
#[derive(Debug, ProductOSRelational)]
pub struct ProductOSContentAsset {
pub identifier: Uuid,
pub site: Uuid,
pub state: ProductOSContentWorkflowState,
pub reference: String,
pub template: Option<Uuid>,
pub data: Option<Vec<u8>>,
pub content_elements: Map<String, Value>,
pub content_element_template_overrides: HashMap<String, ProductOSContentOverrideType>,
pub format: ProductOSContentFormat,
pub path: String,
pub content_kind: Option<ProductOSContentAssetType>,
pub content_type: Option<String>, pub markup: Option<ProductOSContentMarkup>,
pub auth_app_id: Option<String>,
pub scopes: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Default for ProductOSContentAsset {
fn default() -> Self {
Self {
identifier: Default::default(),
site: Default::default(),
state: ProductOSContentWorkflowState::Draft,
reference: "".to_string(),
template: None,
data: None,
format: ProductOSContentFormat::Text,
path: "".to_string(),
content_kind: None,
content_type: None,
markup: None,
auth_app_id: None,
scopes: None,
content_elements: Default::default(),
content_element_template_overrides: Default::default(),
created_at: Utc::now(),
updated_at: Utc::now()
}
}
}
#[derive(Debug, Default, ProductOSRelational)]
pub struct ProductOSContentAnalytics {
pub asset: Uuid,
pub requests: u32,
pub impressions: u32,
pub likes: u32
}
pub struct ProductOSContentPlatform {
controller: Option<Arc<Mutex<ProductOSController>>>,
content_store: Arc<ProductOSRelationalStore>,
cache_store: Option<Arc<ProductOSKeyValueStore>>,
tag_generator: Arc<Mutex<product_os_security::RandomGenerator>>,
auth_base_path: String
}
impl ProductOSContentPlatform {
pub fn new(relational_store: Arc<ProductOSRelationalStore>, key_value_store: Option<Arc<ProductOSKeyValueStore>>, controller: Option<Arc<Mutex<ProductOSController>>>, auth_base: String) -> Self {
Self {
content_store: relational_store,
cache_store: key_value_store,
controller,
tag_generator: Arc::new(Mutex::new(product_os_security::RandomGenerator::new())),
auth_base_path: auth_base
}
}
pub async fn generate_content(&self, request: Request<Body>) -> Result<Response<Body>, ProductOSContentError> {
let uri = request.uri();
let domain = match uri.host() {
None => {
match request.headers().get("host") {
None => return Err(ProductOSContentError::ProcessingError(format!("Unknown domain for uri: {}", uri))),
Some(h) => {
let host_parts = h.to_str().unwrap().split(":").collect::<Vec<&str>>();
let host = host_parts.get(0).unwrap().to_owned();
host.to_string()
}
}
}
Some(dom) => dom.to_string(),
};
let original_path = request.uri().path();
let path = ProductOSContentPlatform::get_path(original_path);
let path_parts: Vec<&str> = path.split("/").collect();
let mut regex_search_path = String::from("");
for path_part in path_parts.clone() {
if path_part != "" {
let mut search_part = String::from("/(");
search_part.push_str(path_part);
search_part.push_str("|[:][a-zA-Z0-9_-]+)");
regex_search_path.push_str(search_part.as_str());
}
}
regex_search_path.push_str("/?");
let authentication_need = match self.content_store.get_one::<ProductOSContentAsset>(ProductOSContentAsset::relational_query_advanced(
product_os_store::Fields::AllInTables(vec!("ProductOSContentAssets".to_string())),
Some(product_os_store::Join::InnerJoin(product_os_store::Table::Table("ProductOSContentSites".to_string()), "ProductOSContentAssets.site".to_string(), "ProductOSContentSites.identifier".to_string())),
Some(product_os_store::Expression::And(Box::new(product_os_store::Expression::SimilarTo("path".to_string(), regex_search_path)),
Box::new(product_os_store::Expression::EqualTo("ProductOSContentSites.domain".to_string(), domain.to_string())))),
None, None, None)).await {
Ok(res) => {
match res {
Some(asset) => {
match asset.auth_app_id.to_owned() {
None => Ok((asset, None)),
Some(_) => {
match asset.scopes.to_owned() {
None => Ok((asset, None)),
Some(scopes) => {
match request.headers().get("authorization") {
None => Err(ProductOSContentError::AuthenticationError("Authorization Header missing".to_string())),
Some(token) => Ok((asset, Some((token, scopes))))
}
}
}
}
}
}
None => Err(ProductOSContentError::DoesNotExist("Page does not exist".to_string()))
}
}
Err(_) => Err(ProductOSContentError::StoreError("Error finding asset".to_string()))
};
let authentication_check = match authentication_need {
Ok((asset, authentication_values)) => {
match authentication_values {
None => Ok(asset),
Some((token, scopes)) => {
match &self.controller {
Some(controller_unlocked) => {
let token_string = token.to_str().unwrap();
let data = serde_json::from_str(format!(r##"
{{
"token": "{token_string}",
"scope": "{scopes}"
}}
"##).as_str()).unwrap();
let mut auth_path = self.auth_base_path.to_owned();
auth_path.push_str("/user/authenticate/verify");
match controller_unlocked.try_lock_for(core::time::Duration::new(10, 0)) {
None => Err(ProductOSContentError::ProcessingError("Failed to check authentication".to_string())),
Some(controller) => {
let asker = controller.find_feature_and_prepare_ask("Authentication".to_string(), auth_path, Some(data), HashMap::new(), HashMap::new(), Method::POST);
match asker {
Ok(ask) => {
match ask.ask().await {
Ok(_) => Ok(asset),
Err(_) => Err(ProductOSContentError::AuthenticationError("Authentication failure error".to_string()))
}
}
Err(_) => Err(ProductOSContentError::AuthenticationError("Failed to complete authentication".to_string()))
}
}
}
},
None => Err(ProductOSContentError::AuthenticationError("Failed to check authentication".to_string()))
}
}
}
}
Err(e) => return Err(e)
};
match authentication_check {
Ok(asset) => {
match asset.template {
None => {
let content_type = match asset.content_type.to_owned() {
None => "application/octet-stream".to_string(),
Some(ct) => ct,
};
match asset.data {
None => {
let etag = self.tag_generator.clone().lock().get_random_string(32);
match Response::builder()
.status(StatusCode::OK)
.header("content-type", content_type)
.header("cache-control", "must-revalidate,max-age=3600")
.header("etag", etag)
.body(Body::empty()) {
Ok(res) => Ok(res),
Err(_) => Err(ProductOSContentError::ProcessingError("Error constructing page".to_string()))
}
}
Some(data) => {
let etag = self.tag_generator.clone().lock().get_random_string(32);
match Response::builder()
.status(StatusCode::OK)
.header("content-type", content_type)
.header("cache-control", "must-revalidate,max-age=3600")
.header("etag", etag)
.body(Body::from(data)) {
Ok(res) => Ok(res),
Err(_) => Err(ProductOSContentError::ProcessingError("Error constructing content".to_string()))
}
}
}
}
Some(_) => {
let etag = self.tag_generator.clone().lock().get_random_string(32);
let mut content_identifier = String::new();
content_identifier.push_str("content_cache:");
content_identifier.push_str(path.as_str());
let mut content_type_identifier = String::new();
content_type_identifier.push_str("content_type_cache:");
content_identifier.push_str(path.as_str());
let (content, content_type) = match &self.cache_store {
None => {
match self.construct_content_with_template(asset, path_parts).await {
Ok((content, content_type)) => {
(content, content_type)
}
Err(e) => return Err(e)
}
}
Some(cache) => {
match cache.group_get(content_identifier.to_owned()) {
Ok(content) => {
match cache.group_get(content_type_identifier.to_owned()) {
Ok(content_type) => {
(content, content_type)
}
Err(_) => {
(content, String::from("text/html"))
}
}
}
Err(_) => {
match self.construct_content_with_template(asset, path_parts).await {
Ok((content, content_type)) => {
match cache.group_set(content_identifier.to_owned(), content.to_owned()) {
Ok(_) => {}
Err(_) => {}
}
match cache.group_set(content_type_identifier.to_owned(), content_type.to_owned()) {
Ok(_) => {}
Err(_) => {}
}
(content, content_type)
}
Err(e) => return Err(e)
}
}
}
}
};
match Response::builder()
.status(StatusCode::OK)
.header("content-type", content_type)
.header("cache-control", "must-revalidate,max-age=3600")
.header("etag", etag)
.body(Body::from(content)) {
Ok(res) => Ok(res),
Err(_) => Err(ProductOSContentError::ProcessingError("Error constructing page".to_string()))
}
}
}
}
Err(e) => Err(e)
}
}
fn get_path(original_path: &str) -> String {
let mut path = original_path.to_string();
if path.as_str() != "/" {
path = DOUBLE_SLASH_REGEX.replace_all(path.as_str(), "").to_string();
path = SLASH_END_REGEX.replace_all(path.as_str(), "").to_string();
}
if !path.starts_with("/") {
let temp_path = path.clone();
path = String::from("/");
path.push_str(temp_path.as_str());
}
path = DOUBLE_DOT_REGEX.replace_all(path.as_str(), "").to_string();
path
}
async fn construct_content_with_template(&self, asset: ProductOSContentAsset, path_parts: Vec<&str>) -> Result<(String, String), ProductOSContentError> {
match asset.template {
None => return Err(ProductOSContentError::StoreError("No template specified".to_string())),
Some(template) => {
let content_data = match self.content_store.get_one::<ProductOSContentTemplate>(ProductOSContentTemplate::relational_query_basic(
product_os_store::Fields::All,
Some(product_os_store::Expression::EqualTo("identifier".to_string(), template.to_string())))).await {
Ok(res) => {
match res {
Some(template) => Ok((asset, template)),
None => Err(ProductOSContentError::DoesNotExist("Page does not exist".to_string()))
}
}
Err(_) => Err(ProductOSContentError::StoreError("Error finding asset".to_string()))
};
match content_data {
Ok((asset, template)) => {
let mut path_variables = Map::new();
let mut template_path_parts = asset.path.split("/");
let path_part_counter = 0;
for path_value in path_parts {
match template_path_parts.nth(path_part_counter) {
None => {}
Some(part) => {
match part.strip_prefix(":") {
None => (),
Some(variable) => {
path_variables.insert(variable.to_string(), Value::String(path_value.to_string()));
}
}
}
}
}
let mut content = match template.format {
ProductOSContentFormat::Binary => return Err(ProductOSContentError::ProcessingError("Binary not allowed for template".to_string())),
ProductOSContentFormat::Text => {
match template.layout {
Value::String(sl) => sl,
_ => return Err(ProductOSContentError::ProcessingError("Invalid template format".to_string()))
}
}
ProductOSContentFormat::Block => {
match template.layout {
_ => return Err(ProductOSContentError::ProcessingError("Invalid template format".to_string()))
}
}
};
for (element_type, content_element) in template.default_content_elements {
let mut regex_finder_string = r"[#][{]".to_string();
regex_finder_string.push_str(element_type.to_string().as_str());
regex_finder_string.push_str("[}]");
let mut finder_string = r"#{".to_string();
finder_string.push_str(element_type.to_string().as_str());
finder_string.push_str("}");
let regex_finder = Regex::new(regex_finder_string.as_str()).unwrap();
let content_element = match template.format {
ProductOSContentFormat::Binary => return Err(ProductOSContentError::ProcessingError("Binary not allowed for content element".to_string())),
ProductOSContentFormat::Text => {
match content_element {
Value::String(ce) => ce,
_ => return Err(ProductOSContentError::ProcessingError("Invalid content element format".to_string()))
}
}
ProductOSContentFormat::Block => {
match content_element {
_ => return Err(ProductOSContentError::ProcessingError("Invalid content element format".to_string()))
}
}
};
let element = match asset.content_element_template_overrides.get(&element_type) {
None => content_element.to_string(), Some(override_type) => {
match override_type {
ProductOSContentOverrideType::Asset => finder_string, ProductOSContentOverrideType::Template => content_element.to_string(), ProductOSContentOverrideType::Append => {
let mut append_string = String::from(content_element);
append_string.push_str("\n");
append_string.push_str(finder_string.as_str());
append_string
}
ProductOSContentOverrideType::Prepend => {
let mut append_string = finder_string;
append_string.push_str("\n");
append_string.push_str(content_element.as_str());
append_string
}
}
}
};
content = regex_finder.replace_all(content.as_str(), element).to_string();
}
let mut rounds = 0;
let mut data_map = Map::new();
data_map.insert("content".to_string(), Value::Object(asset.content_elements));
data_map.insert("contentFormat".to_string(), Value::String(asset.format.to_string()));
let data = serde_json::Value::Object(data_map);
let mut options_map = Map::new();
options_map.insert("contentReplacements".to_string(), Value::Bool(true));
let options = serde_json::Value::Object(options_map);
while ELEMENT_FINDER_REGEX.is_match(content.as_str()) && rounds < 5 {
content = match content_transform::transform_content(content, Some(data.clone()), None, Some(ProductOSContentOptions::new(options.clone()))) {
Ok(c) => c,
Err(_) => return Err(ProductOSContentError::ProcessingError("Invalid content parsing".to_string()))
};
rounds = rounds + 1;
}
content = match content_transform::transform_content(content, Some(data.clone()), Some(path_variables), None) {
Ok(c) => c,
Err(_) => return Err(ProductOSContentError::ProcessingError("Invalid content parsing".to_string()))
};
let content_type = match asset.content_type.to_owned() {
None => template.content_type,
Some(ct) => ct,
};
Ok((content, content_type))
}
Err(e) => Err(e)
}
}
}
}
pub async fn error_response(&self, domain: String, status_code: StatusCode) -> Response<Body> {
let numeric_error_code = status_code.as_u16();
let result = match self.content_store.get_one::<ProductOSContentAsset>(ProductOSContentAsset::relational_query_advanced(
product_os_store::Fields::AllInTables(vec!("ProductOSContentAssets".to_string())),
Some(product_os_store::Join::InnerJoin(product_os_store::Table::Table("ProductOSContentSites".to_string()), "ProductOSContentAssets.site".to_string(), "ProductOSContentSites.identifier".to_string())),
Some(product_os_store::Expression::And3(Box::new(product_os_store::Expression::EqualTo("ProductOSContentAssets.content_kind->>'type'".to_string(), "Error".to_string())),
Box::new(product_os_store::Expression::EqualTo("(ProductOSContentAssets.content_kind->>'value')::numeric".to_string(), numeric_error_code.to_string())),
Box::new(product_os_store::Expression::EqualTo("ProductOSContentSites.domain".to_string(), domain.to_string())))),
None, None, None)).await {
Ok(res) => Ok(res),
Err(_) => Err(ProductOSContentError::ProcessingError("Error constructing page".to_string()))
};
match result {
Ok(res) => {
match res {
Some(asset) => {
match self.construct_content_with_template(asset, vec!()).await {
Ok((content, _)) => {
match Response::builder()
.status(status_code)
.body(Body::from(content)) {
Ok(res) => res,
Err(_) => Response::builder()
.status(status_code)
.body(Body::empty())
.unwrap()
}
}
Err(_) => Response::builder()
.status(status_code)
.body(Body::empty())
.unwrap()
}
}
None => Response::builder()
.status(status_code)
.body(Body::empty())
.unwrap()
}
}
Err(_) => Response::builder()
.status(status_code)
.body(Body::empty())
.unwrap()
}
}
}
#[async_trait]
impl Feature for ProductOSContentPlatform {
fn identifier(&self) -> String {
"Content".to_string()
}
async fn register(&self, feature: Arc<dyn Feature>, base_path: String, router: &mut product_os_router::ProductOSRouter) -> RegistryFeature {
let path = match base_path.as_str() {
"" => None,
_ => Some(base_path.clone())
};
content_handler::content_handler(path, router, feature.clone());
let path_string = match base_path.as_str() {
"" => "/*fallback_path".to_string(),
_ => base_path.clone()
};
RegistryFeature {
identifier: "Content".to_string(),
paths: vec!(path_string),
feature: Some(feature),
feature_mut: None
}
}
async fn register_mut(&self, feature: Arc<Mutex<dyn Feature>>, base_path: String, router: &mut product_os_router::ProductOSRouter) -> RegistryFeature {
panic!("Mutable content server not allowed to be registered")
}
async fn request(&self, request: Request<Body>, _: String) -> Response {
let path = request.uri().path();
match
if path.ends_with("/user/create") { self.generate_content(request).await }
else { Err(ProductOSContentError::ProcessingError("Endpoint does not exist".to_string())) }
{
Ok(response) => response.into_response(),
Err(_) => Response::builder()
.status(StatusCode::NOT_IMPLEMENTED)
.body(Body::from("{}"))
.unwrap().into_response()
}
}
async fn request_mut(&mut self, request: Request<Body>, version: String) -> Response {
self.request(request, version).await
}
}