From 68fc9e55e538e03f94509731ab41a0c8cf96710f Mon Sep 17 00:00:00 2001 From: George Halkiadakis Date: Mon, 17 Apr 2023 12:12:22 +0300 Subject: setup a real server environment (apache DocumentRoot=/var/www/public) --- public/app/controllers/Auth.php | 330 ++++++++++++++++++++++++++++ public/app/controllers/Product.php | 52 +++++ public/app/controllers/Resolve.php | 91 ++++++++ public/app/controllers/admin/.gitkeep | 0 public/app/controllers/api/.gitkeep | 0 public/app/controllers/api/Common_api.php | 281 +++++++++++++++++++++++ public/app/controllers/api/Doc_api.php | 34 +++ public/app/controllers/cli/CliContoller.php | 85 +++++++ 8 files changed, 873 insertions(+) create mode 100644 public/app/controllers/Auth.php create mode 100644 public/app/controllers/Product.php create mode 100644 public/app/controllers/Resolve.php create mode 100644 public/app/controllers/admin/.gitkeep create mode 100644 public/app/controllers/api/.gitkeep create mode 100644 public/app/controllers/api/Common_api.php create mode 100644 public/app/controllers/api/Doc_api.php create mode 100644 public/app/controllers/cli/CliContoller.php (limited to 'public/app/controllers') diff --git a/public/app/controllers/Auth.php b/public/app/controllers/Auth.php new file mode 100644 index 0000000..f215388 --- /dev/null +++ b/public/app/controllers/Auth.php @@ -0,0 +1,330 @@ +POST['email']); + + // if no user exists, return false + if ($record === false) return false; + + // user is valid; check user password + // create a user object + $user = (new Classroom_user()) + ->setID($record['id']) + ->setUserName($record['email']) + ->setPassword($record['password']) + ->setEnabled($record['active']); + + // let user manager to validate user credentials + $userManager = new Classroom_manager(); + + if ($userManager->isPasswordValid($user, $req->POST['password'])) { + + // get user's security attributes + $attributes = User_Model::getUser($record['id']); + $user + ->setRoles(json_decode($attributes['Roles_json'])) + ->setPrivileges( + array_merge( + json_decode($attributes['RootPrivileges_json']), + self::merge_lists_array( + json_decode($attributes['SubPrivileges_json']) + ) + ) + ); + + // regeneration session ID (prevent session fixation) + session_regenerate_id(); + // set cookie for connected user + setcookie( + 'cluser', + 'connected', + time()+60*60*8, // 8 hours + '/' + ); + + + // login OK, set Token in session + $userManager->createUserToken($user); + return true; + + } else { + return false; + } + } + + + /** + * merges an array of lists to one list + */ + private static function merge_lists_array( $list ) + { + $current = []; + foreach($list as $sublist) { + $current = array_merge($current, $sublist); + } + return $current; + } + + + /** activate + * resolves a call like: /account/activate?ticket=ca42d68cfba5fbbafeacc010b8e3a551 + */ + public static function activate() + { + $req = Registry::get('REQUEST'); + + // get the record of the target user + $check = User_model::activate($req->GET['ticket']); + + if ($check == true) { + Render::view('/error/general', [ + 'title' => ACCOUNT_ACTIVATED_TITLE, + 'message' => ACCOUNT_ACTIVATED_MESSAGE + ]); + + } else { + Render::view('/error/general', [ + 'title' => NOT_VALID_ACTIVATION_TITLE, + 'message' => NOT_VALID_ACTIVATION_MESSAGE + ]); + } + + } + + /** register + * + * Method for new user registration + * + */ + public static function register() + { + $userManager = new Classroom_manager(); + $req = Registry::get('REQUEST'); + + // create a salted password hash + $password = $userManager->cryptPassword($req->POST['password']); + + // echo $password; print_r($req->POST); die(); // OK! + + $user = (new Classroom_user()) + ->setUserName($req->POST['email']) + ->setPassword($password) + ->setRoles([ READER ]) // Role: authorized reader + ->setPrivileges([]); // none privilege until acount confirmation + + // create user record + $activation_code = User_model::registerUser($req->POST, $password); + + // TODO: + // handle error on user registration + // ... + // + // if ($activatopn_code[] == -1) { + // return [ + // 'success' => false, + // 'message' => REGISTRATION_USER_EXISTS + // ]; + // } + + $send_mail = Send_mail::send_activation_code([ + 'email' => $req->POST['email'], + 'name' => $req->POST['name'] .' '. $req->POST['surname'], + 'code' => $activation_code['activation'] + ]); + + // Send replies + if ($send_mail) { + return [ + 'success' => true, + 'message' => REGISTRATION_SUCCESS + ]; + + } else { + return [ + 'success' => false, + 'message' => 'error on sending email' + ]; + } + + } + + + public static function is_connected() + { + $manager = new Classroom_manager(); + if ($manager->hasUserToken()) { + + echo 'user is connected'; + $token = $manager->getUserToken(); + $user = $token->getUser(); + + return $user; + + } else { + echo 'user is not connected'; + return false; + } + } + + + + + + + public static function logout() + { + $userManager = new UserManager(); + $userManager->logout(); + + // remove user-conected cookie + if (isset($_COOKIE['cluser'])) { + unset($_COOKIE['cluser']); + setcookie('cluser', null, -1, '/'); + return true; + } else { + return false; + } + } + + + + /** isGranted( ROLE ) + * + * checks if the user is granted (some of) the specified role(s) + * to access the source + * + * NOTE: + * if no roles are specified then user is granted + * (because every user is granted the 'no-role') + * + * @param $roles (array): array of roles to check (if any is granted) + * + */ + public static function isGranted($roles = []) + { + // no role required ? user is granted access + if ($roles == []) return true; + + // else, UserManager knows if user isGranted + $userManager = new UserManager(); + if ($userManager->isGranted($roles)) { + return true; + + } else { + return false; + } + } + + + /** hasPermition( PERMIT ) + * + * checks if the user owns the specified permition + * to access the source + * + */ + public static function hasPermition($permit = []) + { + if ($permit == []) return true; + } + + + /** isAuthenticated() + * + * chechs if the user's roles and permitions + * satisfy the specified requirements + * to access the source + * + * @param $requirements (array of rules-array) + * + * example: + * [ + * [ + * role => ['editor','designer'] + * permition => ['10', '12', '18'] + * ], + * [ + * role => ['admin' , 'developερ'] + * ], + * [ + * permition => [ 3 ] + * ] + * ] + * + * defines (and parses to) a requirements rule of: + * [ + * user should be editor or designer + * and have permition 10 or 12 or 18 + * ] + * OR + * [ + * user should be an administratoe or developer + * ] + * OR + * [ + * user should have permition #3 + * ] + * + * + */ + public static function isAuthorized($requirements) + { + $authorized = false; + foreach($requirements as $required) { + if ( (self::isGranted($required['role'] ?? [])) + && (self::hasPermition($required['permition'] ?? [])) ) { + $authorized = true; + } + } + return $authorized; + } + + + + + public static function forgot_pass() + { + } + + + + public static function validate_otp() + { + } + + + +} + + +// NOTE: +// check: https://netcorecloud.com/tutorials/send-an-email-via-gmail-smtp-server-using-php/ \ No newline at end of file diff --git a/public/app/controllers/Product.php b/public/app/controllers/Product.php new file mode 100644 index 0000000..0193ed9 --- /dev/null +++ b/public/app/controllers/Product.php @@ -0,0 +1,52 @@ + 'Of all the things I\'ve lost, I miss my mind the most.']); + return ; + } + + // render page + load_template('products', [ 'data' => [ + 'title' => $product['Title'], + 'seo' => [], + 'tree' => proxy( + [\app\models\market\ProductCategories_model::class, 'tree'], + [], CACHE_ROOT_TTL + ), + 'hierarchyPath' => explode('.', substr($product['Hierarchy'], 1, -1)), + 'sections' => [ + [ + 'view' => 'content/product_list/Product_List_Filters', + 'key' => 'filters', + 'data' => [] + ], + [ + 'view' => 'content/product/product_details', + 'key' => 'product', + 'data' => $product + ] + ] + ] + ]); + + } + + +} diff --git a/public/app/controllers/Resolve.php b/public/app/controllers/Resolve.php new file mode 100644 index 0000000..7b9222c --- /dev/null +++ b/public/app/controllers/Resolve.php @@ -0,0 +1,91 @@ + [ + 'title' => $page['Title'], + 'seo' => [], + 'sections' => [ + [ + 'view' => 'content/static', + 'key' => 'page', + 'data' => $page + ] + ] + ] + ]); + return ; + } + + + if (!PRODUCTION) Benchmark::add_spot('proxy-category'); + + // proxying check if products category with this url + $category = proxy( + [\app\models\market\ProductCategories_model::class, 'categoryFromUrl'], + [ $fullUrl ], CACHE_CATEGORY_TTL + ); + // if category exist, construct category + if ($category !== false) { + // render page + load_template('products', [ 'data' => [ + 'title' => $category['Title'], + 'seo' => [], + 'tree' => proxy( + [\app\models\market\ProductCategories_model::class, 'tree'], + [], CACHE_ROOT_TTL + ), + 'hierarchyPath' => explode('.', substr($category['Hierarchy'], 1, -1)), + 'sections' => [ + [ + 'view' => 'content/product_list/Product_List_Filters', + 'key' => 'filters', + 'data' => $category + ], + [ + 'view' => 'content/product_list/product_list', + 'key' => 'products', + 'data' => $category['products'] + ] + ] + ] + ]); + return ; + } + + + // not page nor category?: reply 404 + header("HTTP/1.0 404 Not Found"); + render_view('error/404', ['message' => 'Say something I\'m givin\' up on you.']); + + } + +} \ No newline at end of file diff --git a/public/app/controllers/admin/.gitkeep b/public/app/controllers/admin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/app/controllers/api/.gitkeep b/public/app/controllers/api/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/app/controllers/api/Common_api.php b/public/app/controllers/api/Common_api.php new file mode 100644 index 0000000..a8bd75f --- /dev/null +++ b/public/app/controllers/api/Common_api.php @@ -0,0 +1,281 @@ + data-source] pairs + * where 'label' is a friendly name of the datasource + * (and 'datasource' the actual table/data-source) + */ + public static function APITables() + { + /** allowed tables for api call + * @return: (array) an of api-call arrays + * each api-call array has the folloing form: + * [ label => [ + * 'table' => (string) actual table in database, + * (optional) 'filter' => (array) of allowed filtering fields, + * (optional) 'sort' => (array) of allowed sorting fields, + * ] + * ] + */ + return [ + 'page' => [ + 'table' => 'pages', + 'filter' => ['Title'] + ], + 'product' => [ + 'table' => 'products', + 'filter' => [ 'ID', 'Title' ], + 'sort' => ['ID'] + ], + 'category' => [ + 'table' => 'product_categories', + 'filter' => ['Title', 'Hierarchy', 'Level'] + ] + ]; + } + + + /** select anything on any (allowed table) + * + supports filtering and sorting + * check self::parse_Where_OrderBy() for options; + * sets a limti of 1000 records + */ + public static function table($table) + { + $sources = self::APITables(); + if (array_key_exists($table, $sources)) { + + $query = Registry::get('REQUEST')->QUERY; + + // Get parametres? => parse filters + if ($query !== false) { + + $parsed = self::parse_Where_OrderBy( + $query, + ($sources[$table]['filter'] ?? []), + ($sources[$table][ 'sort' ] ?? []) + ); + $where = $parsed['filter'] ? (' WHERE ' . $parsed['filter']) : ''; + $orderBy = $parsed['sort'] ? (' ORDER BY '. $parsed['sort']) : '' ; + $bindArguments = $parsed['bind']; + + } else { + $where = ''; + $orderBy = ''; + $bindArguments = []; + } + + $db = Registry::use('database'); + $result = $db->runQuery('SELECT * + FROM '. $sources[$table]['table'] + . $where + . $orderBy + .' LIMIT 1000', + $bindArguments + ); + + reply_json([ + 'success' => true, + 'client' => Registry::get('REQUEST')->SIGNATURE, + 'result' => $result + ]); + + } else { + reply_json(['success' => false]); + } + die(); + } + + + /** get record of {table} by ID + * (if table has no ID then return false) + */ + public static function record($table, $id) + { + $sources = self::APITables(); + if (array_key_exists($table, $sources)) { + + $db = Registry::use('database'); + $result = $db->runQuery("SELECT * + FROM ". $sources[$table]['table'] ." + WHERE ID = :id", + [':id' => $id] + ); + reply_json([ + 'success' => true, + 'data' => $result[0] + ]); + + } else { + reply_json(['status' => false]); + + } + die(); + } + + + /** category_by_url + * product categorie by full-friendly-URL + * @param $url (string): full friendly url + */ + public static function category_by_url($url) + { + $category = proxy( + [\app\models\market\ProductCategories_model::class, 'categoryFromUrl'], + [ $url ], CACHE_CATEGORY_TTL + ); + if ($category !== false) { + reply_json([ + 'success' => true, + 'result' => $category + ]); + + } else { + reply_json(['status' => false]); + + } + die(); + } + + /** parse Where & OrderBy + * ------------------------------------------------------------------------- + * parses SAFELY the query string to Where {CONDITIONS} and ORDER BY clauses + * according to the specified rules. + * + * The rules: + * ** filters: ?fieldname=[operator]:value &... + * where operators:[ like | startlike | endlike | eq | gt | gteq | lt |t leq ] + * + * ** Order by: ?_sort=fieldname[:[asc|desc]][,field[,]] + * + * for example: ?id=gt:4&active=1&_sort:reputation:desc,category + * parses to WHERE id > 4 AND active = 4 ORDER BY reputation desc, catetory asc + * + * ------------------------------------------------------------------------- + * arguments: + * @param $query (string): string to be parsed + * @param $filters (array): the allowed fields to apply filters + * @param $sortings (array): the allowed fields to sort the result + * @return array('filter'=>(string) , 'sort'=>(string) , 'bind'=>(array)) + */ + private static function parse_Where_OrderBy($query, $filters=[], $sortings=[]) + { + // break query to [key => value] pairs + parse_str($query, $queryArray); + + $filterClause = []; // array to hold filter/WHERE clauses + $sortClause = []; // array to hold sort/ORDER-BY caluses + $bindings = []; // array to hold variable bindigs + + // parse filers + // --------------------------------------------------------------------- + foreach($queryArray as $filter => $value) { + + if (in_array($filter, $filters)) { + + $parts = explode(':', $value ); // that is => [operator], value + if (count($parts) == 0) { + // forget it + + } else if (count($parts) == 1) { + $filterClause[] = "{$filter} = :{$filter}"; + $bindings[$filter] = $parts[0]; + + } else { + + switch ($parts[0]) { + case 'like': + $filterClause[] = "{$filter} LIKE :{$filter}"; + $bindings[':'.$filter] = '%'.$parts[1].'%'; + break; + + case 'startlike': + $filterClause[] = "{$filter} LIKE :{$filter}"; + $bindings[':'.$filter] = $parts[1].'%'; + break; + + case 'endlike': + $filterClause[] = "{$filter} LIKE :{$filter}"; + $bindings[':'.$filter] = '%'.$parts[1]; + break; + + case 'gt': + $filterClause[] = "{$filter} > :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + break; + + case 'gteq': + $filterClause[] = "{$filter} >= :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + break; + + case 'lt': + $filterClause[] = "{$filter} < :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + break; + + case 'lteq': + $filterClause[] = "{$filter} <= :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + break; + + case 'eq': + default: + $filterClause[] = "{$filter} = :{$filter}"; + $bindings[':'.$filter] = $parts[1]; + } + } + } + } + $whereSQL = ($filterClause == []) + ? false + : implode(' AND ', $filterClause); + + + // parse sort options + // --------------------------------------------------------------------- + if (isset($queryArray['_sort'])) { + $sortTerms = explode(',', $queryArray['_sort']); + + foreach($sortTerms as $term) { + + $parts = explode(':', $term); + if ($parts != [] && in_array($parts[0], $sortings) ) { + $sortClause[] = (count($parts)==1) + ? $parts[0] + : $parts[0] .' '. (($parts[1] == 'desc') ? 'desc' : 'asc'); + } + } + + } + $sortSQL = ($sortClause == []) + ? false + : implode(', ', $sortClause); + + return ([ + 'filter' => $whereSQL, + 'sort' => $sortSQL, + 'bind' => $bindings + ]); + + } + +} \ No newline at end of file diff --git a/public/app/controllers/api/Doc_api.php b/public/app/controllers/api/Doc_api.php new file mode 100644 index 0000000..e893552 --- /dev/null +++ b/public/app/controllers/api/Doc_api.php @@ -0,0 +1,34 @@ + true, 'result' => $tree ]); + die(); + } + + + public static function dbDoc() + { + $j = new Jorge(); + reply_json([ + 'success' => true, + 'result' => $j->databaseDocumentation() + ]); + die(); + + } +} \ No newline at end of file diff --git a/public/app/controllers/cli/CliContoller.php b/public/app/controllers/cli/CliContoller.php new file mode 100644 index 0000000..310e2ae --- /dev/null +++ b/public/app/controllers/cli/CliContoller.php @@ -0,0 +1,85 @@ +runQuery( + "SELECT * FROM product_categories", + [] + ); + } + + + + /** (re-) cacheCategoryByUrl( url ) + * regenerate cache of a category_product + * by category's friendly url + * @param $url (string): the category's friendly url + */ + public static function cacheCategoryByUrl(string $url) + { + // Only Cli interface is allowed + if (php_sapi_name() != 'cli') return false; + + # PROXY call: + # MARKET \ ProductCategories_model::tree(): + # + Refresh Cache + # -------------------------------------- + $reply = proxy( + [\app\models\market\ProductCategories_model::class, 'categoryFromUrl'], + [ $url ], \CACHE_CATEGORY_TTL, + \PROXY_IGNORE_CACHE + ); + echo ($reply == false) ? textColor("Failed\n", \FAIL) : textColor("Done!\n", \SUCCESS); + } +} \ No newline at end of file -- cgit v1.2.3