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/config/app_constants.php | 166 ++++ 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 ++ public/app/extends/Classroom_manager.php | 106 +++ public/app/extends/Classroom_user.php | 88 ++ public/app/extends/Send_mail.php | 190 +++++ public/app/models/Jorge.php | 172 ++++ public/app/models/_info.md | 150 ++++ public/app/models/admin/History_model.php | 34 + public/app/models/admin/User_model.php | 263 ++++++ public/app/models/cms/Media_model.php | 36 + public/app/models/cms/Page_model.php | 18 + public/app/models/market/Market_repository.php | 235 ++++++ public/app/models/market/Payment_template.txt | 98 +++ .../app/models/market/ProductCategories_model.php | 199 +++++ public/app/models/market/Product_model.php | 49 ++ public/app/models/todo.md | 132 +++ public/app/routes/api.php | 81 ++ public/app/routes/backend.php | 9 + public/app/routes/frontend.php | 207 +++++ public/app/views/admin/skeleton.php | 37 + public/app/views/components/breadcrumbs.php | 30 + public/app/views/components/header_includes.php | 23 + public/app/views/components/top-bar.php | 17 + public/app/views/components/user-management.php | 42 + public/app/views/error/404.php | 44 + public/app/views/error/general.php | 55 ++ public/app/views/group.php | 13 + public/app/views/item.php | 4 + public/app/views/simple/Model.php | 887 +++++++++++++++++++++ public/app/views/simple/Render.php | 182 +++++ public/app/views/simple/category.php | 156 ++++ public/app/views/simple/index.php | 123 +++ public/app/views/simple/lorem-dates.php | 18 + public/app/views/simple/lorem-intros.php | 721 +++++++++++++++++ public/app/views/simple/lorem-posts.php | 93 +++ public/app/views/simple/post.php | 236 ++++++ public/app/views/user/login.php | 91 +++ public/app/views/user/new-password.php | 65 ++ public/app/views/user/registration.php | 120 +++ public/app/views/welcome.php | 28 + 47 files changed, 6091 insertions(+) create mode 100644 public/app/config/app_constants.php 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 create mode 100644 public/app/extends/Classroom_manager.php create mode 100644 public/app/extends/Classroom_user.php create mode 100644 public/app/extends/Send_mail.php create mode 100644 public/app/models/Jorge.php create mode 100644 public/app/models/_info.md create mode 100644 public/app/models/admin/History_model.php create mode 100644 public/app/models/admin/User_model.php create mode 100644 public/app/models/cms/Media_model.php create mode 100644 public/app/models/cms/Page_model.php create mode 100644 public/app/models/market/Market_repository.php create mode 100644 public/app/models/market/Payment_template.txt create mode 100644 public/app/models/market/ProductCategories_model.php create mode 100644 public/app/models/market/Product_model.php create mode 100644 public/app/models/todo.md create mode 100644 public/app/routes/api.php create mode 100644 public/app/routes/backend.php create mode 100644 public/app/routes/frontend.php create mode 100644 public/app/views/admin/skeleton.php create mode 100644 public/app/views/components/breadcrumbs.php create mode 100644 public/app/views/components/header_includes.php create mode 100644 public/app/views/components/top-bar.php create mode 100644 public/app/views/components/user-management.php create mode 100644 public/app/views/error/404.php create mode 100644 public/app/views/error/general.php create mode 100644 public/app/views/group.php create mode 100644 public/app/views/item.php create mode 100644 public/app/views/simple/Model.php create mode 100644 public/app/views/simple/Render.php create mode 100644 public/app/views/simple/category.php create mode 100644 public/app/views/simple/index.php create mode 100644 public/app/views/simple/lorem-dates.php create mode 100644 public/app/views/simple/lorem-intros.php create mode 100644 public/app/views/simple/lorem-posts.php create mode 100644 public/app/views/simple/post.php create mode 100644 public/app/views/user/login.php create mode 100644 public/app/views/user/new-password.php create mode 100644 public/app/views/user/registration.php create mode 100644 public/app/views/welcome.php (limited to 'public/app') diff --git a/public/app/config/app_constants.php b/public/app/config/app_constants.php new file mode 100644 index 0000000..0d04dc2 --- /dev/null +++ b/public/app/config/app_constants.php @@ -0,0 +1,166 @@ + 'Login', + 2 => 'Account', + 3 => 'Payment' +]); + + + +// ERRORS AND ERROR MESSAGES /////////////////////////////////////////////////// +// ----------------------------------------------------------------------------- + +define('EMPTY_REQUIRED_FIELDS', 'Δεν έχουν συμπληρωθεί όλα τα υποχρεωτικά πεδία'); + + + +// ASSETS: PATHS USED HEAVILY ////////////////////////////////////////////////// +// ----------------------------------------------------------------------------- +// these paths are used in order to easily find and autoload useful elemets + + +// JAVASCRIPT LIBRARY PATHS +// ----------------------------------------------------------------------------- +define('JS_DIR', '/content/'); // path to contruct link +define('JS_LIBRARIES', array( // path after to the actual file + 'jquery' => 'lib/jquery/dist/jquery.min.js', + + 'require/files' => 'lib/require/require.files.js', + 'require/slim' => 'lib/require/require.slim.js', + + 'plugins' => 'lib/plugins/plugins.min.js', + 'plugins/setup' => 'lib/plugins/plugins.setup.js' + ) +); + + +// CSS ASSET PATHS +// ----------------------------------------------------------------------------- +define('CSS_DIR', '/content/css/'); // path to contruct link +define('CSS_FILES', array( // path after to the actual file + 'main' => 'main.min.css', + 'filter' => 'filter.css', + 'overides' => 'overides.css', + ) +); + + +// FONTS +// ----------------------------------------------------------------------------- +define('FONTS_DIR', '/content/fonts/'); +define('FONT_FILES', array( + 'iconfont' => 'iconfont.woff2', + 'CFAstyStdBold' => 'CFAstyStd-Bold.woff2', + 'CFAstyStdBook' => 'CFAstyStd-Book.woff2', + 'CFAstyStdMedium' => 'CFAstyStd-Medium.woff2' + ) +); + + + + +// Mesagges //////////////////////////////////////////////////////////////////// +// ----------------------------------------------------------------------------- + +// 404 +// --- +define('PAGE_404_TITLE', '404: Not Found'); + +// registration +// --- +define('REGISTRATION_SUCCESS', "

Επιτυχής Εγγραφή

+

+ Παρακαλώ ελέγξτε το email σας και ακολουθήστε το σύνδεσμο ενεργοποίησης + που σας 'εχει αποσταλεί, ώστε να έχετε πρόσβαση σε επιπλέον περιεχόμενο του site +

" +); +define ('REGISTRATION_USER_EXISTS', "

Αποτυχία Εγγραφής

+

+ Υπάρχει ήδη χρήστης με αυτό το email. Αν δεν θυμάστε το password + μπορείτε να κάνετε επαναφορά του ακολουθώντας + αυτό το σύνδεσμο. +

" +); + + +// activation +// --- +define('ACCOUNT_ACTIVATED_TITLE', 'Ο λογαριασμός σας έχει ενεργοποιηθεί'); +define('ACCOUNT_ACTIVATED_MESSAGE', 'Τώρα μπορείτε να κάνετε + Είσοδο στο σύστημα + για να δείτε περισσότερο περιεχόμενο'); +define('NOT_VALID_ACTIVATION_TITLE', 'Σφάλμα!'); +define('NOT_VALID_ACTIVATION_MESSAGE', 'Ο κωδικός ενεργοποίησης δεν είναι σωστός. + Μήπως τον έχετε ενεργοποιήσει στο παρελθόν;'); \ No newline at end of file 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 diff --git a/public/app/extends/Classroom_manager.php b/public/app/extends/Classroom_manager.php new file mode 100644 index 0000000..4d38ba3 --- /dev/null +++ b/public/app/extends/Classroom_manager.php @@ -0,0 +1,106 @@ +POST + * + */ + public function register_user() + { + $req = Registry::get('REQUEST'); + $register = User_model::registerUser($req->POST); + + if ($register['success']) { + + //create OTP; + $otp = $this->create_OPT($register['id']); + + // send for account confirmation + + + } else { + print_r($register); + } + + } + + + public function login_user() + { + + } + + + /** forgot password + * + * send an otp + * (then verify) + * + */ + public function forgot_password() + { + + } + + + /** create OTP + * + * create a random OTP + * keep it into User's database Table + * + * @param $id (int): user id + * + */ + public function create_OTP($id) + { + $otp = rand(100000,999999); + User_model::set_OTP($id, $otp); + + return true; + } + + + /** Verify Account + * + * check if posted OTP matched the one sent to the user + * + * @param $identity (array): pair of [index_key => identity_value] + * example: [ 'email' => 'geo@roptron.gr' ] + * + */ + public function verify_otp($identity, $otp) + { + + } + + +} \ No newline at end of file diff --git a/public/app/extends/Classroom_user.php b/public/app/extends/Classroom_user.php new file mode 100644 index 0000000..1c26e73 --- /dev/null +++ b/public/app/extends/Classroom_user.php @@ -0,0 +1,88 @@ +id; + } + + /** setID() + * + * @param int : user id + * + * @return User + */ + public function setID(int $id): self + { + $this->id = $id; + return $this; + } + + + + /** getPrivileges + * + * @return privileges (array) + */ + public function getPrivileges(): array + { + return $this->privileges; + } + + + /** setPrivileges() + * + * @param array $privileges + * + * @return User + */ + public function setPrivileges(array $privileges): self + { + $this->privileges = $privileges; + return $this; + } + + +} \ No newline at end of file diff --git a/public/app/extends/Send_mail.php b/public/app/extends/Send_mail.php new file mode 100644 index 0000000..fa0d8d3 --- /dev/null +++ b/public/app/extends/Send_mail.php @@ -0,0 +1,190 @@ +SMTPDebug = 3; + $mail->IsSMTP(); + $mail->Host = MAIL_HOST; + $mail->SMPTAuth = false; + $mail->SMTPSecure = MAIL_ENCRYPTION; + // $mail->Protocol = 'mail'; + + $mail->Mailer = MAIL_MAILER; + $mail->Port = MAIL_PORT; + $mail->Username = MAIL_USERNAME; + $mail->Password = MAIL_PASSWORD; + + return $mail; + } + + + /** + * + */ + private static function init_slack() + { + + } + + + /** + * + * @param $envelope (array): mail from/to and activation code + * + * TODO: + * check for email templating: + * + https://stackoverflow.com/questions/2391171/how-to-get-output-from-local-script-in-php + * + https://stackoverflow.com/questions/171318/how-do-i-capture-php-output-into-a-variable + */ + public static function send_activation_code($envelope) + { + $mail = self::init_mail(); + + // setup format + $mail->CharSet = 'utf-8'; + $mail->IsHTML(true); + + // setup THE mail + // --- -- -- - - - + // It's important not to use the submitter's address as the from address as it's forgery, + // which will cause your messages to fail SPF checks. + // Use an address in your own domain as the from address, put the submitter's address in a reply-to + $mail->setFrom(NO_REPLY_EMAIL, MAIL_FROM_NAME); + $mail->addAddress($envelope['email'], $envelope['name']); + $mail->addReplyTo(REPLY_TO_EMAIL, MAIL_FROM_NAME); + $mail->Subject = 'Εγγραφή στο Classroom'; + $mail->Body = "Χαίρετε,
+ το παρόν αυτοποιημένο email σάς έχει σταλεί γιατί έχει γίνει αίτημα εγγραφής σας στο Classroom.
+
+ Όνομα επαφής: ". $envelope['name'] ."
+ Email : ". $envelope['email'] ."
+
+ Για να ενεργοποιήσετε την πρόσβασή σας στο site θα πρέπει να παρακαλώ πατήστε στον + παρακάτω σύνδεσμο url + . +
+
+ Μετά την ενεργοποίηση θα έχετε πρόσσβαση στο περιεχόμενο του site.
+
+ Μην απαντήσετε στο email γιατί δεν υπάρχει φυσική επαφή η οποία να λαμβάνει τυχόν replies.
"; + if (!$mail->send()) { + return false; + } else { + return true; + } + + } + + + + +} + + + + + +/** + * + * + +Optimization per concept alternative technologies +--- -- -- - - - + +## Session +File Session +Database Session +Redis Session +Memcached Session + +--- + +## Cache +File Cache +Database Cache +Redis Cache +Memcashed Cache + +--- + +## Log +File Log +Database Log +Log event to Slack +Log event to Email + + * + * + */ + +/** custom email api + * + * host : https://api.roptron.gr + * apikey : md5(hash)-substring(0,5, md5(service)) 8668d1c4a8f5aba236a8f821d148340f-0c83f + * message : serialize([ + * from[email] => from name + * replyTo[email] => reply-to-Name + * to => [ + * email => email-name + * secondemail => another email name + * ] + * type => html + * charset => utf8 + * subject => mail subject + * mailBody => html string + * ]) + * + */ + +/** Several custom APIs + * + * Mailer + * + * sendpulse + * rU8WEmcHsRcEQaH + * rU8WEmcHsRcEQaH + * + * mailjet + * piipiis@g + * EN6Xg8!99ZTKaYp7 + * + * + * sendGrid + * piipiis + * EN6Xg899ZTKU8WEmcHsR54321 + * + * + * time4vps... + * https://community.time4vps.com/discussion/75/are-any-ports-blocked-by-time4vps + * + * test activation code: + * ca42d68cfba5fbbafeacc010b8e3a551 + * + * + * + * MAIL API + * --- -- -- - - - + * client = ip/mp5(IP) or domain/md5(ref_domain) + * token = md5(client + timestamp + rand(0,10000)) + * + * api : auth_content : client : token + * --- : --- : --- : --- + * mail : ip|domain : md5(*) : md5(...) -> send an email + * image : ip|domain : md5(*) : md5(...) -> proccess an image (resize|crop|convert) + * slack : ip|domain : md5(*) : md5(...) -> send a slack message to channel + * database : ip|domain : md5(*) : md5(...) -> + * + */ + \ No newline at end of file diff --git a/public/app/models/Jorge.php b/public/app/models/Jorge.php new file mode 100644 index 0000000..5d79007 --- /dev/null +++ b/public/app/models/Jorge.php @@ -0,0 +1,172 @@ + [], + 'relate' => [], + 'fields' => [] + ]; + + public function __construct() + { + $db = Registry::use('database'); + + // get all tables + $tableList = $db->runQuery("SHOW TABLES", []); + + // get all CREATE TABLE statements + foreach($tableList as $tableArray) { + $table = array_shift($tableArray); + $create = $db->runQuery("SHOW CREATE TABLE {$table}", []); + $this->tables['create'][$table] = $create[0]['Create Table']; + } + + // define relations + $this->tables['relate'] = [ + + 'lesson' => [ + 'course' => 'course.id = lesson.course_id', + 'lesson_media' => 'lesson_media.lesson_id = lesson.id', + 'lesson_privilege' => 'lesson_privilege.lesson_id = lesson.id' + ], + + 'page' => [ + 'page_media' => 'page_media.page_id = page.id' + ], + + 'user' => [ + 'user_privilege' => 'user_privilege.user_id = user.id' + ], + + 'privilege' => [ + 'user_privilege' => 'user_privilege.privilege_id = privilege.id' + ], + + ]; + + $this->extractTableFields(); + + // return true; + } + + + /** Create... + * one or more database tables + * --- + * @param $datasource (string|void) : table name or none + * if none then all tables will be created + * @return (boolean) + * + * NOTE: + * for safety reasons this method is not executing SQL; + * it just echoes the SQL that needs to be executed in + * order to crate all database tables. + */ + public function create($datasource = '') + { + // if no datasource passed then datasource is all tables + if ($datasource == '') { + $datasource = array_keys(self::$tables['create']); + + } else { + // if datasource exist, make it an array + if (array_key_exists($datasource, self::$tables['create'])) { + $datasource = [ $datasource ]; + + } else { + // table does not exist + return fasle; + } + } + + $sql = ""; + foreach( $datasource as $table ) { + $sql .= "-- CREATE ". $table .";\n". self::$tables['create'][$table] . "\n\n"; + } + + return ['sql' => $sql]; + } + + + /** Calculate fields of every table + * --- + * Parse every CREATE SQL statement and extract list of fields; + * Algorithm implements linear-parsing (faster than a recursive one) + */ + private function extractTableFields() + { + // words used by SQL that can not be field names + $nonFields = ['PRIMARY', 'KEY', '(', ')', 'CONSTRAINT', 'UNIQUE']; // reduced list (used on CREATE) + + foreach( $this->tables['create'] as $table => $sqlCreate ) { + + $fields = []; // variable to hold the extracted fields + + // get text between first open-parentesis and last close-parentesis + // this is waht lies between 'CREATE TABLE table(' and ') [ENGINE whatever]' + // (including the patenteses) + preg_match_all( + "/\((((?>[^()]+)|(?R))*)\)/", + str_replace("\n", '', $sqlCreate), + $match + ); + // remove 1st+last parentesis + $mainDefinitions = substr($match[0][0], 1, -1); + + // replace comma (,) on sub-parenthesis + // ex: 'decimal(18, 2)' turns to 'decimal(18: 2)' + // or: 'PRIMARY KEY (id1, id2)' turns to 'PRIMARY KEY (id1: id2)' + $sentences = preg_replace( + '/\\(([0-9a-zA-Z_`]*)[ ,]([0-9a-zA-Z_` ]*)\\)/', + '($1:$2)', + $mainDefinitions, + -1 + ); + + // devide the banth of sentences to field definitions + $defines = explode(',', $sentences); + + // now each denine holds a full field definition + // like: `ID` int(11) NOT NULL AUTO_INCREMENT + + foreach($defines as $def) { + // check the first term of each sentence + $terms = explode(' ', trim($def,)); + $term = array_shift($terms); + + if (!in_array($term, $nonFields)) { + $fields[ str_replace('`', '', $term) ] = implode(' ', $terms); + } + } + + $this->tables['fields'][$table] = $fields; + } + + return; + } + + public function databaseDocumentation() + { + return $this->tables['fields']; + } + +} \ No newline at end of file diff --git a/public/app/models/_info.md b/public/app/models/_info.md new file mode 100644 index 0000000..5504523 --- /dev/null +++ b/public/app/models/_info.md @@ -0,0 +1,150 @@ + +# info about the structrure of the models + +According to the former impementation (atcom) the project will need to handle 200+ tables. +The former project includes 209 tables with 5 or more rows (and 325 tables totaly) + + + +## Database schema + +Main tables and recomended constraints: + +``` +SET NAMES utf8; +SET time_zone = '+00:00'; +SET foreign_key_checks = 0; +SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'; + +SET NAMES utf8mb4; + +DROP TABLE IF EXISTS `course`; +CREATE TABLE `course` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `parent_id` int(11) NOT NULL COMMENT 'if 0 then this is a root course', + `label` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `lesson`; +CREATE TABLE `lesson` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `course_id` int(11) NOT NULL, + `title` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `body` text COLLATE utf8mb4_unicode_ci NOT NULL, + `published` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0 = unpublished, 1 = published', + PRIMARY KEY (`id`), + KEY `course_id` (`course_id`), + CONSTRAINT `lesson_ibfk_1` FOREIGN KEY (`course_id`) REFERENCES `course` (`id`), + CONSTRAINT `lesson_ibfk_2` FOREIGN KEY (`course_id`) REFERENCES `course` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `lesson_media`; +CREATE TABLE `lesson_media` ( + `lesson_id` int(11) NOT NULL, + `media_id` int(11) NOT NULL, + PRIMARY KEY (`lesson_id`,`media_id`), + KEY `media_id` (`media_id`), + CONSTRAINT `lesson_media_ibfk_1` FOREIGN KEY (`lesson_id`) REFERENCES `lesson` (`id`), + CONSTRAINT `lesson_media_ibfk_2` FOREIGN KEY (`lesson_id`) REFERENCES `lesson` (`id`), + CONSTRAINT `lesson_media_ibfk_3` FOREIGN KEY (`lesson_id`) REFERENCES `lesson` (`id`), + CONSTRAINT `lesson_media_ibfk_4` FOREIGN KEY (`media_id`) REFERENCES `media` (`id`), + CONSTRAINT `lesson_media_ibfk_5` FOREIGN KEY (`lesson_id`) REFERENCES `lesson` (`id`) ON DELETE NO ACTION, + CONSTRAINT `lesson_media_ibfk_6` FOREIGN KEY (`media_id`) REFERENCES `media` (`id`) ON DELETE NO ACTION, + CONSTRAINT `lesson_media_ibfk_7` FOREIGN KEY (`lesson_id`) REFERENCES `lesson` (`id`) ON DELETE NO ACTION, + CONSTRAINT `lesson_media_ibfk_8` FOREIGN KEY (`media_id`) REFERENCES `media` (`id`) ON DELETE NO ACTION +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `lesson_privilege`; +CREATE TABLE `lesson_privilege` ( + `lesson_id` int(11) NOT NULL, + `privilege_id` int(11) NOT NULL COMMENT 'minimum privilege required to access the lesson', + KEY `lesson_id` (`lesson_id`), + KEY `privilege_id` (`privilege_id`), + CONSTRAINT `lesson_privilege_ibfk_1` FOREIGN KEY (`lesson_id`) REFERENCES `lesson` (`id`), + CONSTRAINT `lesson_privilege_ibfk_2` FOREIGN KEY (`privilege_id`) REFERENCES `privilege` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `media`; +CREATE TABLE `media` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `label` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `path` varchar(320) COLLATE utf8mb4_unicode_ci NOT NULL, + `referable` tinyint(4) NOT NULL DEFAULT '1' COMMENT '0 = hidden, 1 = referable', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `page`; +CREATE TABLE `page` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `title` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + `body` text COLLATE utf8mb4_unicode_ci NOT NULL, + `published` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0 = unpublished; 1 = published', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `page_media`; +CREATE TABLE `page_media` ( + `page_id` int(11) NOT NULL, + `media_id` int(11) NOT NULL, + KEY `page_id` (`page_id`), + KEY `media_id` (`media_id`), + CONSTRAINT `page_media_ibfk_1` FOREIGN KEY (`page_id`) REFERENCES `page` (`id`) ON DELETE NO ACTION, + CONSTRAINT `page_media_ibfk_2` FOREIGN KEY (`media_id`) REFERENCES `media` (`id`) ON DELETE NO ACTION +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `privilege`; +CREATE TABLE `privilege` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `alias` varchar(8) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'keep it simple; use latin', + `label` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `alias` (`alias`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `privilege_inherit`; +CREATE TABLE `privilege_inherit` ( + `higher_id` int(11) NOT NULL COMMENT 'higher priviledges inherit (include) lower ones', + `lower_id` int(11) NOT NULL, + PRIMARY KEY (`higher_id`,`lower_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `first_name` int(11) NOT NULL, + `last_name` int(11) NOT NULL, + `email` int(11) NOT NULL, + `expiration` int(11) NOT NULL COMMENT 'account expiration date; 0 = never', + `password` int(11) NOT NULL, + `salt` int(11) NOT NULL, + `otp` int(11) NOT NULL COMMENT 'one time password for reset password', + `otp_expiration` int(11) NOT NULL, + `active` int(11) NOT NULL COMMENT 'account flag; 1=active, 0=inactive', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `user_privilege`; +CREATE TABLE `user_privilege` ( + `user_id` int(11) NOT NULL, + `privilege_id` int(11) NOT NULL, + PRIMARY KEY (`user_id`,`privilege_id`), + KEY `privilege_id` (`privilege_id`), + CONSTRAINT `user_privilege_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`), + CONSTRAINT `user_privilege_ibfk_2` FOREIGN KEY (`privilege_id`) REFERENCES `privilege` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +``` + + diff --git a/public/app/models/admin/History_model.php b/public/app/models/admin/History_model.php new file mode 100644 index 0000000..04ce630 --- /dev/null +++ b/public/app/models/admin/History_model.php @@ -0,0 +1,34 @@ +query( + "INSERT INTO history + (user_id, `type`, `message`, `note`, `ip`) + VALUES + (:uid, :type, :msg, :note, :ip)", + [ + ':uid' => $user_id, + ':type' => $type, + ':msg' => $message, + ':note' => $note, + ':ip' => Registry::get('REQUEST')->IP + ] + )->lastInsertID(); + + } + + + +} \ No newline at end of file diff --git a/public/app/models/admin/User_model.php b/public/app/models/admin/User_model.php new file mode 100644 index 0000000..62d99db --- /dev/null +++ b/public/app/models/admin/User_model.php @@ -0,0 +1,263 @@ +query( + "SELECT * FROM user WHERE email = :email AND active = 1", + [ ':email' => $email ] + )->getFirst(); + + // if no user, return false + if ($user === false) return false; + + return $user; + } + + + /** get user (by index key) + * + * user detailed array + * includes all user properties + granted roles + privileges + * + * @param $id (int) : user id + * @param $value (string) + */ + public static function getUser($id) + { + $user = Registry::use('database')->query( + "SELECT user.*, + ( -- construct array (json) of roles granted to user + SELECT CONCAT( + '[', + GROUP_CONCAT(role.id), + ']' + ) + FROM `role` + WHERE role.id IN ( + SELECT user_role.role_id + FROM user_role + WHERE user_role.user_id = :id + ) + ) AS Roles_json, + ( -- construct array of (root-)privileges granted to user + SELECT CONCAT( + '[', + GROUP_CONCAT(privilege.id), + ']' + ) + FROM privilege + WHERE privilege.id IN ( + SELECT user_privilege.privilege_id + FROM user_privilege + WHERE user_privilege.user_id = :id + ) + ) AS RootPrivileges_json, + ( -- construct array of (root-)privileges granted to user + SELECT CONCAT( + '[', + GROUP_CONCAT(privilege.includes), + ']' + ) + FROM privilege + WHERE privilege.id IN ( + SELECT user_privilege.privilege_id + FROM user_privilege + WHERE user_privilege.user_id = :id + ) + ) AS SubPrivileges_json + FROM user + WHERE id = :id", + [ ':id' => $id ] + )->getFirst(); + + + // if no user, return false + if ($user === false) return false; + + + // TODO: + // * merge root+sub privilede lists + // * convert json strings to php arrays + + + // TODO: + // cache user super array + + return $user; + } + + + /** create user + * + * creates user record; + * assigns privileged (usualy defaults); + * creates activation_code + * + * @param $data (array): Request->POST array + * @param $password (string): secure hashed password + * + * @return $activation_code + * + */ + public static function registerUser($data, $password, $privileges = DEFAULT_PRIVILEGES) + { + $required_fields = [ + 'name', + 'surname', + 'email', + 'password' + ]; + + // check required fields + $isOK = true; + foreach($required_fields as $fi) { + if (empty($data[$fi])) $isOK = false; + } + // if empty required fields exists ... return false + if (!$isOK) { + return [ "success" => false, 'error' => EMPTY_REQUIRED_FIELDS ]; + } + + + // TODO: + // check if email exists + // ... + + // create an activation code + $activation_code = md5($data['email'].time().rand(0, 10000)); + + // if isOK go on and... + // create user record + $new_user_id = Registry::use('database')->query( + "INSERT INTO user + (`first_name`, `last_name`, `email`, `password`, `active`, `activation`) + VALUES + (:nam, :surname, :email, :pass, :act, :actcode)", + [ + ':nam' => $data['name'], + ':surname' => $data['surname'], + ':email' => $data['email'], + ':pass' => $password, + ':act' => 0, // needs email confirmation to be activated ... + ':actcode' => $activation_code // ... with the activation code + ] + )->lastInsertID(); + + // set default privileges + // self::set_user_privileges($new_user_id, $privileges); + + // update history + History::trackUserAccess($new_user_id, TRACK_ACCOUNT, 'Create User Account'); + + // return success and user id + return [ + "success" => true, + 'id' => $new_user_id , + 'activation' => $activation_code + ]; + } + + + /** activate + * + * check if activation code is valid; + * if valid, set account active; + * + * @param $ticket (hex/MD5): activation code; + * + */ + public static function activate($ticket) + { + $user = Registry::use('database')->query( + "SELECT * FROM user WHERE activation = :ticket", + [ 'ticket' => $ticket ] + )->getFirst(); + + // if no user with this activation code, return false + if ($user === false) return false; + + // remove activation code from user record + Registry::use('database')->runQuery( + "UPDATE user + SET active = 1, `activation` = NULL + WHERE activation = :ticket", + [ 'ticket' => $ticket ] + ); + + // update history + History::trackUserAccess($user['id'], TRACK_ACCOUNT, 'User Account Activated'); + + return true; + + } + + +} + +/* example query getUser (super-array) +--- -- -- - - - + +SELECT user.*, +( -- array (json) of roles granted to user + SELECT CONCAT('[', GROUP_CONCAT(role.id), ']') + FROM `role` + WHERE role.id IN ( + SELECT user_role.role_id + FROM user_role + WHERE user_role.user_id = 1 + ) +) AS Roles_json, +( + SELECT CONCAT( + '[', + GROUP_CONCAT(privilege.id), + ']' + ) + FROM privilege + WHERE privilege.id IN ( + SELECT user_privilege.privilege_id + FROM user_privilege + WHERE user_privilege.user_id = 1 + ) +) AS RootPrivileges_json, +( + SELECT CONCAT( -- array of array of sub-privileges + '[', + GROUP_CONCAT( -- array (json) of subprivileges + ( + SELECT CONCAT( + '[', + GROUP_CONCAT(included_id), + ']' + ) + FROM privilege_includes + WHERE privilege_id = privilege.id + ) + ), + ']' + ) + FROM privilege + WHERE privilege.id IN ( + SELECT user_privilege.privilege_id + FROM user_privilege + WHERE user_id = 1 + ) +) AS SubPrivileges_json +FROM user +WHERE id = 1 +--- */ \ No newline at end of file diff --git a/public/app/models/cms/Media_model.php b/public/app/models/cms/Media_model.php new file mode 100644 index 0000000..6a4a681 --- /dev/null +++ b/public/app/models/cms/Media_model.php @@ -0,0 +1,36 @@ +, rights =>, path => ] + + // validate ticket + // ValidateAccess::for($ticket) + + // serve + // header("Content-type: type"); + // passthru('cat $media_path'); + + + return Registry::use('database')->query( + "SELECT * FROM pages + WHERE FullFriendlyUrl = :url AND IsActive = 1", + [ ':url' => $url ] + )->getFirst(); + } + +} + + + +// check: +// https://stackoverflow.com/questions/1353850/serve-image-with-php-script-vs-direct-loading-an-image \ No newline at end of file diff --git a/public/app/models/cms/Page_model.php b/public/app/models/cms/Page_model.php new file mode 100644 index 0000000..fda5fb1 --- /dev/null +++ b/public/app/models/cms/Page_model.php @@ -0,0 +1,18 @@ +query( + "SELECT * FROM pages + WHERE FullFriendlyUrl = :url AND IsActive = 1", + [ ':url' => $url ] + )->getFirst(); + } + +} \ No newline at end of file diff --git a/public/app/models/market/Market_repository.php b/public/app/models/market/Market_repository.php new file mode 100644 index 0000000..1338426 --- /dev/null +++ b/public/app/models/market/Market_repository.php @@ -0,0 +1,235 @@ + "SELECT * FROM cars WHERE age = :Age" + * ... won't make your code sexier nor easier to read; it will just increase + * your label's entropy and make trickier to pick your next sensable label; + * + * repository classes include two methods (inherited by the abstract, so + * you do not need to implement something more than just construct + * the $repository array) + * ** pull: for pulling a query by a friendly name + * ** echo: optional method used by some devolopment tools + * + * TODO: development tool using echo is still in progress + */ +class Market_repository extends Repository +{ + //// use Repository; // repository trait includes + //// // a method to pull an entity of the gathering + //// // and method to (TODO:) ... + + /** repository is an array of Prepare SWL queries; + * each query is attached to the array via a friendly label; + * + * NOTE: (PROPOSAL) + * --- + * about the label: + * 1. keep names short descriptive but not too long + * 2. main entity/entities shall be UPPERCASED + * 3. determinands/adjectives shall be Cappitalized + * 4. words are connected with underscore (_) + * 5. if SQL inludes bind-holderd, label shall be suffixed with '_by:' + * followed by holder names connected with undercores (case-sensitively) + * + * about the data-bind holders (if present): + * 1. all holders are prefixed with ':' + * 2. holder name shall begin with a letter and contain one word. + * + * example: + * 'Active_EMPLOYEES_by:Age_CityID' => "SELECT Em.*, c.* + * FROM employee Em LEFT JOIN city c ON c.id = Em.city_id + * WHERE c.id = :CityID AND Em.age = :Age" + * + * It is recommended to leave a descriptive comment befor each declaration + * and you may also include sql-comments inside the query. + */ + public static $repository = [ + + // PRODUCTS by: storeID, ProductUrl + // picks all product properties + // + default image + price for certain store + 'Active_PRODUCT_by:storeID_ProductUrl' => + "SELECT p.*, + pc.ID AS ProductCategory, + pc.Hierarchy, + pc.FullFriendlyUrl AS `Path`, + prices.ProductPrice_OriginalPrice AS Price, + prices.ProductPrice_DiscountedPrice AS DiscountPrice, + prices.UnitMeasurePrice_OriginalPrice AS PerUnitPrice, + prices.UnitMeasurePrice_DiscountedPrice AS PerUnitDiscountPrice, + b.Title BrandName + FROM products p + LEFT JOIN products_to_product_categories p2pc ON p.ID = p2pc.SimpleProductID + LEFT JOIN product_categories pc ON p2pc.ProductCategoryID = pc.ID + LEFT JOIN prices ON p.SKU = prices.SKU + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + LEFT JOIN brands b ON b.ID = p.BrandID + WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1 + AND p2i.Order = 1 + AND p.IsActive = 1 + AND p.Published = 1 + AND prices.PriceListID = :storeID + AND p.FriendlyUrl = :ProductUrl" + , + + // PRODUCTS by: storeID, ProductID + // picks all product properties + // + default image + price for certain store + 'Active_PRODUCT_by:storeID_ProductID' => + "SELECT p.*, + pc.ID AS ProductCategory, + pc.Hierarchy, + pc.FullFriendlyUrl AS `Path`, + prices.ProductPrice_OriginalPrice AS Price, + prices.ProductPrice_DiscountedPrice AS DiscountPrice, + prices.UnitMeasurePrice_OriginalPrice AS PerUnitPrice, + prices.UnitMeasurePrice_DiscountedPrice AS PerUnitDiscountPrice, + b.Title BrandName + FROM products p + LEFT JOIN products_to_product_categories p2pc ON p.ID = p2pc.SimpleProductID + LEFT JOIN product_categories pc ON p2pc.ProductCategoryID = pc.ID + LEFT JOIN prices ON p.SKU = prices.SKU + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + LEFT JOIN brands b ON b.ID = p.BrandID + WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1 + AND p2i.Order = 1 + AND p.IsActive = 1 + AND p.Published = 1 + AND prices.PriceListID = :storeID + AND p.ID = :ProductID" + , + + // CATEGORY-PRODUCTS by: storeID, likeHierarchy + // traverses all products from category and subcategories + // picke price and default image + 'CATEGORY-PRODUCTS_by:storeID_likeHierarchy' => + "SELECT p.*, + assets.Url AS ImageUrl, + pc.FullFriendlyUrl AS `Path`, + prices.ProductPrice_OriginalPrice AS Price, + prices.ProductPrice_DiscountedPrice AS DiscountPrice, + prices.UnitMeasurePrice_OriginalPrice AS PerUnitPrice, + prices.UnitMeasurePrice_DiscountedPrice AS PerUnitDiscountPrice + FROM products p + LEFT JOIN prices ON p.SKU = prices.SKU + LEFT JOIN products_to_product_categories p2pc ON p2pc.SimpleProductID = p.ID + LEFT JOIN product_categories pc ON pc.ID = p2pc.ProductCategoryID + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + WHERE p2pc.ProductCategoryID IN ( + SELECT `ID` FROM product_categories + WHERE Hierarchy LIKE :likeHierarchy + ) + AND p2i.Order = 1 + AND p.IsActive = 1 + AND p.Published = 1 + AND prices.PriceListID = :storeID" + , + + // Count Products + // of all last-lever Product categories + // by StoreID + // --- + // used on creating product_categories tree + // ProductCategories_model::tree() + 'Count_Last_Level_PRODUCTS_by:storeID' => + "SELECT pc.ID, COUNT(p.ID) as `Counter` + FROM product_categories pc + LEFT JOIN products_to_product_categories p2pc ON pc.ID = p2pc.ProductCategoryID + LEFT JOIN products p ON p2pc.SimpleProductID = p.ID + LEFT JOIN prices ON p.SKU = prices.SKU + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1 + AND p2i.Order = 1 + AND pc.Level = 2 + AND p.IsActive = 1 + AND p.Published = 1 + AND prices.PriceListID = :storeID + GROUP BY pc.ID" + , + + // PRODUCT by: ProductID + // Join images array (json string) + // Join prices for all stores (json string) + 'Active_PRODUCT_complete_by:ProductID' => + "SELECT p.*, pc.ID AS ProductCategory, + pc.Hierarchy, pc.FullFriendlyUrl AS `Path`, + ( -- get all store prices as Json + SELECT JSON_ARRAYAGG(JSON_OBJECT( + 'Store', prices.PriceListID, + 'Price', prices.ProductPrice_OriginalPrice, + 'DiscountPrice', prices.ProductPrice_DiscountedPrice, + 'PerUnitPrice', prices.UnitMeasurePrice_OriginalPrice, + 'PerUnitDiscountPrice', prices.UnitMeasurePrice_DiscountedPrice + )) + FROM prices + WHERE prices.SKU = p.SKU + ) as pricesJson, + ( -- get all images as Json + SELECT JSON_ARRAYAGG(JSON_OBJECT( + 'Order', p2i.Order, + 'Url', assets.Url, + 'Title', assets.Title, + 'Description', assets.Description + )) + FROM products_to_images p2i + LEFT JOIN assets ON assets.ID = p2i.ImageID + WHERE p2i.SimpleProductID = p.ID + ) AS imagesJson, + b.Title BrandName + FROM products p + LEFT JOIN products_to_product_categories p2pc ON p.ID = p2pc.SimpleProductID + LEFT JOIN product_categories pc ON p2pc.ProductCategoryID = pc.ID + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets ON assets.ID = p2i.ImageID + LEFT JOIN brands b ON b.ID = p.BrandID + WHERE pc.IsActive = 1 AND pc.IsCurrentlyActive = 1 + AND p2i.Order = 1 + AND p.IsActive = 1 + AND p.Published = 1 + AND p.ID = :ProductID" + , + + // Product images by: ProductID + // (sorted by 'order' field) + 'PRODUCT_IMAGES_by:productID' => + "SELECT a.Url, a.Title, a.Description + FROM products p + LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + LEFT JOIN assets a ON a.ID = p2i.ImageID + WHERE p.IsActive = 1 + AND p.Published = 1 + AND p.ID = :productID + ORDER BY p2i.Order" + + ]; + + /* no need to impement anything else; + * you can overide if needed the default methods: + * + public static function pull($entity){ } + * + public static function echo($content =false){ } + */ + +} diff --git a/public/app/models/market/Payment_template.txt b/public/app/models/market/Payment_template.txt new file mode 100644 index 0000000..4a66a1a --- /dev/null +++ b/public/app/models/market/Payment_template.txt @@ -0,0 +1,98 @@ +class WC_Piraeusbank_Gateway extends WC_Payment_Gateway { + + public function __construct() + + function pb_get_pages($title = false, $indent = true) + + function pb_get_installments($title = false, $indent = true) + + function generate_piraeusbank_form($order_id) + + function process_payment($order_id) + + function receipt_page($order) + + function check_piraeusbank_response() + + function piraeusbank_message() + + function woocommerce_add_piraeusbank_gateway($methods) + + function piraeusbank_plugin_action_links($links, $file) + +} + + + +class WC_NBG_Gateway extends WC_Payment_Gateway { + + public function __construct() + + public function admin_options() + + function init_form_fields() + + function nbg_get_pages($title = false, $indent = true) + + function nbg_get_installments($title = false, $indent = true) + + function generate_nbg_form($order_id) + + function process_payment($order_id) + + function receipt_page($order) { + + function check_nbg_response() + + function nbg_message() + + function woocommerce_add_nbg_gateway($methods) + + function nbg_plugin_action_links($links, $file) + +} + + + +global $wc; + + $this->id = 'nbg_gateway'; + $this->icon = apply_filters('nbg_icon', plugins_url('assets/nbg.png', __FILE__)); + $this->has_fields = false; + $this->notify_url = WC()->api_request_url('WC_NBG_Gateway'); + $this->method_description = __('National Bank Greece Payment Gateway allows you to accept payment through various channels such as Maestro, Mastercard and Visa cards On your Woocommerce Powered Site.', 'woocommerce-nbg-payment-gateway'); + $this->redirect_page_id = $this->get_option('redirect_page_id'); + $this->method_title = 'National Bank of Greece Gateway'; + + // Load the form fields. + $this->init_form_fields(); + + //dhmioyrgia vashs + + global $wpdb; + + if ($wpdb->get_var("SHOW TABLES LIKE '" . $wpdb->prefix . "nbg_transactions'") === $wpdb->prefix . 'nbg_transactions') { + // The database table exist + } else { + // Table does not exist + $query = 'CREATE TABLE IF NOT EXISTS ' . $wpdb->prefix . 'nbg_transactions (id int(11) unsigned NOT NULL AUTO_INCREMENT,merchantreference varchar(30) not null, reference varchar(100) not null, orderid varchar(100) not null , timestamp datetime default null, PRIMARY KEY (id))'; + $wpdb->query($query); + } + + + // Load the settings. + $this->init_settings(); + + + // Define user set variables + $this->title = $this->get_option('title'); + $this->description = $this->get_option('description'); + $this->nbg_Username = $this->get_option('nbg_Username'); + $this->nbg_Password = $this->get_option('nbg_Password'); + + $this->nbg_description= $this->get_option('nbg_description'); + $this->mode = $this->get_option('mode'); + $this->nbg_installments= $this->get_option('nbg_installments'); + //Actions + add_action('woocommerce_receipt_nbg_gateway', array($this, 'receipt_page')); + add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this diff --git a/public/app/models/market/ProductCategories_model.php b/public/app/models/market/ProductCategories_model.php new file mode 100644 index 0000000..d8c274e --- /dev/null +++ b/public/app/models/market/ProductCategories_model.php @@ -0,0 +1,199 @@ +query( "SELECT * FROM product_categories + WHERE FullFriendlyUrl = :url AND IsActive = 1", + [ ':url' => $url ] + )->getFirst(); + + if ($category === false) { return false; } // no category? -> false + + // GET PRODUCTS of category + $category['products'] = self::categoryProductsByHierarchy($category['Hierarchy']); + return $category; + } + + + /** Get (product_) category from ID + * --- (self explanatory) + * @param $id (int): Catgory ID (usualy from products) + * @return $category (array); also includes all category products + */ + public static function categoryFromID($id) + { + return Registry::use('database')->query( "SELECT * FROM product_categories + WHERE `ID` = :url AND IsActive = 1", + [':id' => $id] + )->getFirst(); + + } + + + /** categoryProductsByHierarchy + * --- + * This method returns the products belonging to a certain + * category (and all it's the children/sub-categories); + * It uses category.Hierarchy in a WHERE LIKE condition to + * make it fast. + * + * The method is category.Level agnostic + * + * @param $category (array) + * @return $products (array) + */ + public static function categoryProductsByHierarchy($hierarchy, $store = 904) + { + // GET PRODUCTS of category + // NOTE: + // category.Hierarchy is used + + return Registry::use('database')->runQuery( + Market_repository::pull('CATEGORY-PRODUCTS_by:storeID_likeHierarchy'), + [ + ':likeHierarchy' => $hierarchy.'%', + ':storeID' => 904 + ] + ); + } + + + # depricated: + # now Proxy has the responsibility to cache whatever + # --- + # /** GET tree of product_categories + # * --- + # * get from cache or cache it after creation + # */ + # public static function tree($store = 904) + # { + # $cacheKey = get_called_class() . $store .'/tree'; + # + # // IF category CACHED + # $cache = Registry::use('cache'); + # $tree = $cache->get($cacheKey); + # if ($tree !== false) { + # return $tree; + # } + # + # // (not cached) Create and Cache it + # $tree = self::createCategoriesTree(); + # $cache->set($cacheKey, $tree, CACHE_ROOT_TTL); + # + # return $tree; + # } + + + /** CREATE product_categories tree + * NOTE: includes active categories only; + * + * Algorith uses 'Hierarchy' for category path-positioning, + * implementing a linear conctruction of the requested tree; + * (faster and less source-consuming than a recursive algo) + */ + public static function tree($store = 904) + { + $tree = []; // variable to hold results + + $db = Registry::use('database'); + + // get raw categories data --------------------------------------------- + + $raw = $db->runQuery( "SELECT `ID`, Title, `Level`, Hierarchy, + ParentID, `Order`, FullFriendlyUrl + FROM product_categories pc + WHERE IsActive = 1 AND IsCurrentlyActive = 1 + ORDER BY pc.Level asc, pc.Order asc, pc.Hierarchy asc", + [] + ); + + // count products per 3rd level category ------------------------------- + + $countProducts = $db->runQuery( + Market_repository::pull('Count_Last_Level_PRODUCTS_by:storeID'), + [ ':storeID' => $store ] + ); + // map categoryID -> Counter + $mapCounter = []; + foreach($countProducts as $rec) $mapCounter[$rec['ID']] = $rec['Counter']; + + // parse raw data to hierarchical tree (2 pass) ------------------------ + + // 1st pass: parse raw data, construct and fill the $tree array ........ + foreach($raw as $rec) { + + // create category path from Hierarchy (format: .10.10100. ) + // so remove 1st and last dots (.) from Hierarchy string + // then explode via dot-character (.) + $categoryPath = explode('.', substr($rec['Hierarchy'], 1, -1)); + + switch ($rec['Level']) { + case 0: + $tree[$categoryPath[0]] = [ + 'info' => $rec, + 'childs' => [] + ]; break; + + case 1: + $tree[$categoryPath[0]]['childs'][$categoryPath[1]] = [ + 'info' => $rec, + 'childs' => [] + ]; break; + + case 2: + $tree[$categoryPath[0]]['childs'][$categoryPath[1]]['childs'][$categoryPath[2]] = [ + 'info' => $rec, + 'count' => isset($mapCounter[$categoryPath[2]]) ? $mapCounter[$categoryPath[2]] : 0 + ]; break; + + default: + // nothing... + } + + } + + // 2nd pass: remove categories with no products ........................ + foreach($tree as $id0 => $rec0) { + $count0 = 0; + + foreach($rec0['childs'] as $id1 => $rec1) { + $count1 = 0; + + foreach($rec1['childs'] as $id2 => $rec2) { + if ($rec2['count']>0) $count1++; + else unset($tree[$id0]['childs'][$id1]['childs'][$id2]); + } + + if ($count1 > 0) $count0++; + else unset($tree[$id0]['childs'][$id1]); + } + + if (!$count0) unset($tree[$id0]); + } + + return $tree; + } + + +} \ No newline at end of file diff --git a/public/app/models/market/Product_model.php b/public/app/models/market/Product_model.php new file mode 100644 index 0000000..aa554a1 --- /dev/null +++ b/public/app/models/market/Product_model.php @@ -0,0 +1,49 @@ +query( + Market::pull('Active_PRODUCT_by:storeID_ProductUrl'), + [ + ':storeID' => $store, + ':ProductUrl' => $url + ] + )->getFirst(); + + // if no product, return false + if ($product === false) return false; + + // inject product's images + $product['Images'] = self::getProductImages($product['ID']); + + return $product; + } + + + public static function getProductImages($id) + { + ## return Registry::use('database')->runQuery( + ## "SELECT a.Url, a.Title, a.Description + ## FROM products p + ## LEFT JOIN products_to_images p2i ON p2i.SimpleProductID = p.ID + ## LEFT JOIN assets a ON a.ID = p2i.ImageID + ## WHERE p.IsActive = 1 AND p.Published = 1 + ## AND p.ID = :id + ## ORDER BY p2i.Order", + ## [ ':id' => $id ] + ## ); + + return Registry::use('database')->runQuery( + Market::pull('PRODUCT_IMAGES_by:productID'), + ['productID' => $id ] + ); + } + +} \ No newline at end of file diff --git a/public/app/models/todo.md b/public/app/models/todo.md new file mode 100644 index 0000000..defd21e --- /dev/null +++ b/public/app/models/todo.md @@ -0,0 +1,132 @@ + + +Privilege +--- + +Privilege::list_of_privileges( privilege_id ) + +Privilege::privileges_tree() + + + + + +User +--- + +User::get_privileges + + +User::has_privilege( privilege_id ) + + +User::reset_password() + -> create otp + -> send email + + +User::set_privilege( privilege_id ) + + +User::track_action() + -> user_id + -> action : Contoller::method(args) + -> timestamp + + + +Lesson: +--- + +Lesson::create_lesson( POST ) + +Lesson::get_lesson( lesson_id ) + + + +Course +--- + +Course::create_course( POST ) + +Course::courses_tree() + + + +Upgrades (??) +--- + + +A1 + +A2 : includes A1 + +A3 : includes A1, A2 + +A4 : includes A1, A2, A3 + + +A1 100 +A2 200 +A3 350 +A4 500 + +B1 100 +B2 200 +B3 400 +B4 800 + + +:1 -> 0% +:2 -> 15% +:3 -> 20% + + + + +A1 -> A2 +A1 -> A3 +A1 -> A4 + +A2 -> A3 +A2 -> A4 + +A3 -> A4 + +A1 < A2 < A3 < A4 + + + +B1 -> B2 +B1 -> B3 +B1 -> B4 + +B2 -> B3 +B2 -> B4 + +B3 -> B4 + + +A1+B1 + +A2+B2 + +A3+B3 + + +A1+B1 -> A2+B2 +A1+B1 -> A3+B3 +A1+B1 -> A4+B4 + +A2+B2 -> A3+B3 +A2+B2 -> A4+B4 + +A3+B3 -> A4+B4 + + + +UPGRADE PRICE = upper_priv_price - own_priv_price + +buy 1 item : -0% +buy 2 items : -10% +buy 3 items : -15% diff --git a/public/app/routes/api.php b/public/app/routes/api.php new file mode 100644 index 0000000..61e485b --- /dev/null +++ b/public/app/routes/api.php @@ -0,0 +1,81 @@ + 'nobody knows it (but you got a secret smile;)']); +}); // 404 + + + +// Account +/////////////////////////////////////////////////////////////////////////////////////////////// + +// [ok] login +// [ok] register +// [ok] activate +// [..] ask-reset +// [..] reset +// [..] profile +// [..] edit-profile +// [..] subscriptions +// [..] payments +// [..] pay +// [..] order + + +Route::add('/login', function() { Render::view('user/login'); }); +Route::add('/registration', function() { Render::view('user/registration'); }); + + +Route::add('/account/check-login', function() { + $response = Auth::login(); + Render::json(['status' => $response]); + }, + 'post' +); + +// user sends a registration form; +Route::add('/account/register', function() { + $response = Auth::register(); + Render::json($response); + }, + 'post' +); + +// user requests activation +Route::add('/account/activate', function() { Auth::activate(); } ); + +// user profile +Route::add('/account/profile', function() { + $user = Auth::is_connected(); + if ($user === false) { + Render::view('error/general', + [ + 'title' => 'Nope!', + 'message' => "

No profile

User is not connected" + ] + ); + + } else { + print_r($user); + } + } +); + +// test connection +// NOTE: this is only for testing purposes +if (!PRODUCTION) { + Route::add('/check/connection', function() { Auth::is_connected(); }); +} + +Route::add('/account/reset_password', function() { Auth::reset_password(); } ); + + + + + +// Resolve urls +// --- -- -- - - - +Route::add('/course/([0-9a-zA-Z-_]*)', + function($label) { Course::show([$label]); } +); + +Route::add('/lesson/([0-9a-f]*)/([0-9a-zA-Z-_]*)', + function($ticket, $lesson) { Lesson::show([$lesson, $ticket]); } +); + +Route::add('/media/([0-9a-f]*)/([0-9a-zA-Z-_]*)/([0-9]*)/', + function($ticket, $type, $id) { Media::serve([$type, $id, $ticket]); } +); + + +// 4-th level paths belong to products +// --- +Route::add('/about/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', + function($a, $b, $c) { Page::show([$a, $b, $c]); } +); + + +/** URL-format Proposal: + * ----------------------------------------------------------------------------- + * + * Page (almost-static content): + * /about/{url} +# Route::add('/about/([0-9a-zA-Z-_\/]*)', function($url) { Page::fromUrl($url); }); + * + * Product: + * /{level-1}/{level-2}/{level-3}/{friendly-url}-{id} +# Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)-([0-9]*)', +# function($category, $subcategory, $group, $label, $id) { +# Product::fromID([$$category, $subcategory, $group, $label], $id); +# } +# ); + * + * Category + * /{category}/[ {subcategory}[ /{group}]] +Route::add('/([0-9a-zA-Z-_]*)', function($a) { Category::url([$a]); }; +Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b) { Category::url([$a, $b]); }); +Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b, $c) { Category::url([$a, $b, $c]); }); + * ----------------------------------------------------------------------------- + */ + + +/* EXAMPLES (use it for brainsrtorming) + * ----------------------------------------------------------------------------- + * + // Typical Use + // --- + Route::add('/art', function() { Art::blah(); }); + Route::add('/art/db', function() { Art::fromDatabase(); }); + Route::add('/art/([0-9]*)', function($id) { Art::getInfo($id); }); + + + // Get-Post route example + // --- + Route::add('/contact-form', function() { + echo '
'; + }, 'get'); + + Route::add('/contact-form', function() { + echo 'Hey! The form has been sent:
'; print_r($_POST); + }, 'post'); + + + // Accept number as parameter + // --- + Route::add('/foo/([0-9]*)/bar', function($var1) { + // echo $var1.' is a number!'; + echo "{$var1} is a number!"; + }); + + + // About pages + // --- + Route::add('/about/([0-9a-zA-Z-_]*)', function($page) { + InfoPage::render('about/'.page); + }); + + + // handle a request like: /foo/123/bar/3254-lefki-zaxari-marata-1kg + // where first-numeric-part of last-uri-section (3254) is the product number + // --- + Route::add('/foo/([0-9]*)/bar/([0-9]*)-([0-9a-zA-Z-_]*)', function($num, $id, $name) { + echo $num .' is some nomber, id = '. $id .' and label = '. $name; + }); + + + // handle a request like: /zaxares-glykantika/lefki-zaxari-year-2022-45678 + // where last-numeric-part of last-uri-section (45678) is the product number + // --- + Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)-([0-9]*)', + function($category, $subcategory, $group, $label, $id) { + Shop::product([$$category, $subcategory, $group, $label], $id); + }); + + + // last chance to resolve (some friendly url) + // --- + Route::add('/([0-9a-zA-Z-_]*)', function($a) { Resolve::uri([$a]); }); + Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b) { Resolve::uri([$a, $b]); }); + Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)', function($a, $b, $c) { Resolve::uri([$a, $b, $c]); }); + + + // Product page + // handle a request like: /pantopoleio/zaxares/lefki-zaxari/lefki-zaxari-year-2022-45678 + // where last-numeric-part of last-uri-section (45678) is the product number + // --- + Route::add('/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)/([0-9a-zA-Z-_]*)-([0-9]*)', + function($category, $subcategory, $group, $label, $id) { + Shop::product([$$category, $subcategory, $group, $label], $id); + }); +* ------------------------------------------------------------------------------ +*/ diff --git a/public/app/views/admin/skeleton.php b/public/app/views/admin/skeleton.php new file mode 100644 index 0000000..a9c2dcf --- /dev/null +++ b/public/app/views/admin/skeleton.php @@ -0,0 +1,37 @@ + + + + + + ΣΚΛΑΒΕΝΙΤΗΣ - <?=$page['title']?> + + + + + + + + + + + + + +
+ + + +
+ +
+ +
+ +
+ +
+ + + diff --git a/public/app/views/components/breadcrumbs.php b/public/app/views/components/breadcrumbs.php new file mode 100644 index 0000000..471cfb6 --- /dev/null +++ b/public/app/views/components/breadcrumbs.php @@ -0,0 +1,30 @@ + + +
\ No newline at end of file diff --git a/public/app/views/components/header_includes.php b/public/app/views/components/header_includes.php new file mode 100644 index 0000000..4ee6904 --- /dev/null +++ b/public/app/views/components/header_includes.php @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/views/components/top-bar.php b/public/app/views/components/top-bar.php new file mode 100644 index 0000000..ed96e35 --- /dev/null +++ b/public/app/views/components/top-bar.php @@ -0,0 +1,17 @@ +
+ + 0, + 'title' => '', + 'parents' => [] + ]); + ?> + + + +
\ No newline at end of file diff --git a/public/app/views/components/user-management.php b/public/app/views/components/user-management.php new file mode 100644 index 0000000..02a439e --- /dev/null +++ b/public/app/views/components/user-management.php @@ -0,0 +1,42 @@ +
+ + + + + +
+ + + diff --git a/public/app/views/error/404.php b/public/app/views/error/404.php new file mode 100644 index 0000000..2b67e8d --- /dev/null +++ b/public/app/views/error/404.php @@ -0,0 +1,44 @@ + + + + + 404: Not Found + + + + +
+
+

4O4

+ Not found
+ +
+
+ + diff --git a/public/app/views/error/general.php b/public/app/views/error/general.php new file mode 100644 index 0000000..257e68d --- /dev/null +++ b/public/app/views/error/general.php @@ -0,0 +1,55 @@ + + + + + + <?= isset($title) + ? (SITE_TITLE .": ". $title) + : PAGE_404_TITLE + ?> + + + + + +
+
+

+ Oops!
+ — +
+ +
+
+ + diff --git a/public/app/views/group.php b/public/app/views/group.php new file mode 100644 index 0000000..5d5dea0 --- /dev/null +++ b/public/app/views/group.php @@ -0,0 +1,13 @@ +
+
+ + + +
+ $item]) ?> +
+ + + +
+
diff --git a/public/app/views/item.php b/public/app/views/item.php new file mode 100644 index 0000000..2354c81 --- /dev/null +++ b/public/app/views/item.php @@ -0,0 +1,4 @@ +

+

+ +

\ No newline at end of file diff --git a/public/app/views/simple/Model.php b/public/app/views/simple/Model.php new file mode 100644 index 0000000..6ea1e7d --- /dev/null +++ b/public/app/views/simple/Model.php @@ -0,0 +1,887 @@ +pdo_pythia= $db->connect(); + + /// // database for wiki + /// $this->pdo = new PDO('mysql:unix_socket=/cloudsql/pythia-251711:europe-west4:pythia-db-eu;dbname=' . DB_DATABASE . ';charset=utf8', DB_USERNAME, DB_PASSWORD, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC)); + /// $this->user_id = $_SESSION['user_session']; + + $this->pdo = $GLOBALS['DB_con']; + + } + + + /** Close Instance + * --------------------------------------------------- (probably not needed) + */ + public function close() + { + // Default + // $this->pdo=Database::disconnect(); + } + + + + ## ------------------------------------------------------------------------- + ## + ## CATEGORY METHODS + ## + ## ------------------------------------------------------------------------- + + + /** get all categories + * + * raw table data (simplest SELECT) + * + */ + public function get_categories() + { + $sql = "SELECT * FROM wiki_category ORDER BY title"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + + /** constuct a category_tree + * + * returns a tree representation of the categories + * + * NOTE: + * category_tree() is an expensive method; + * it calls 2 other methods implementing recursive algorithms + * thus it uses many sources to run (particularly RAM). + * Caching the result is strogly recommended. + * + */ + public function category_tree() + { + $categories = $this->get_categories(); // get all categories + + $tree = $this->to_tree($categories); // format to a tree + + $tree_wParents = $this->tree_parents($tree); // add parents section for each tree-node + + return $tree_wParents; + } + + + /** to_tree + * + * constructs a tree from raw-table data; + * this is a private method and uses a recursive algorithm + * + * @param $dataset (array): flar array of records with id/parent-id pairs + * @return $root (array): id of root category + * + * (**) each node has 2 parts: + * .... .. rec : all record attributes/data as passed into $dataset + * .... .. childs : array of (children) nodes + */ + public function to_tree($dataset, $root = 0) + { + $return = []; + + // loop data ; search for direct children of root + foreach($dataset as $key => $rec) { + + $child = $rec['id']; + $parent = $rec['parent_id']; + + if ($parent == $root) { // a direct child is found + + unset($dataset[$key]); // remove item (no need to traverse again) + + // Append the child into result array ; parse its children + $return[] = [ + 'rec' => [ + 'id' => $rec['id'], + 'title' => $rec['title'], + 'parent' => $rec['parent_id'] + ], + 'childs' => $this->to_tree($dataset, $child) // recursively + ]; + } + } + return empty($return) ? [] : $return; + } + + + /** tree_parents + * + * adds a section to each tree node with all parents of each node + * + * @param $tree (array) : nodes array (each node has `rec` and `childs` sections ) + * @param $parents (array); DO NOT SET IT (takes values automaticaly) + * @return array of nodes with an extra node[parents] section + * + */ + private function tree_parents($tree, $parents = []) + { + $tree_with_parents = []; + + foreach($tree as $key => $node) { + // parents to be pushed for node's children + $push_parents = $parents; // parents so far + $push_parents[] = $node['rec']; // this record will be a new parent + + $tree_with_parents[$key] = [ + 'rec' => $node['rec'], + 'parents' => $parents, + 'childs' => ($node['childs'] == []) + ? [] + : $this->tree_parents($node['childs'], $push_parents) + ]; + } + + return $tree_with_parents; + } + + + /** add_category() + * + * add new category + * + * @param $recuest (array): [ title => , parent_id => ] + * @return true; + * + * TODO: catch database error -> return succes => false + */ + public function add_category($request) + { + $sql = "INSERT INTO wiki_category (title, parent_id) VALUES (:title, :parent_id)"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + 'title' => $request['title'], + 'parent_id' => $request['parent_id'] + ]); + $inserted_id = $this->pdo->lastInsertId(); + + return [ "success" => true, 'id' => $inserted_id ]; + } + + + /** update_category() + * + * update an existing category + * + * @param $recuest (array): [ id => , title => , parent_id => ] + * @return true; + * + * TODO: catch database error -> return false + */ + public function update_category($request) + { + $sql = "UPDATE wiki_category SET title = :title, parent_id = :parent_id WHERE id = :id"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + 'title' => $request['title'], + 'parent_id' => $request['parent_id'], + 'id' => $request['id'] + ]); ; + + return true; + } + + + + ## ------------------------------------------------------------------------- + ## + ## POST (ARTICLE) METHODS + ## + ## ------------------------------------------------------------------------- + + + /** get_post + * + * get a certain post (given the post's `id`) + * + * @param $id (int) + * @param $only_publised (bool): should the post be published? (default=true) + */ + public function get_post($id, $only_published = true) { + + $where_clause = ($only_published) ? ' AND post.status = 1' : ''; + + // depricated: $sql = "SELECT * FROM post WHERE id = :id {$where_clause}"; + // Select post + // + inject tags json + // TODO: + inject media + // --- + $sql = "SELECT wiki_post.*, + ( SELECT CONCAT('[', GROUP_CONCAT(JSON_OBJECT('id', wiki_tag.id, 'name', wiki_tag.name)), ']') + FROM wiki_tag + WHERE wiki_tag.id IN ( SELECT tag_id FROM wiki_post_tags WHERE post_id = :id ) + ) AS tags_json, + ( SELECT + CONCAT('[', GROUP_CONCAT(JSON_OBJECT( + 'id', wiki_media.id, + 'title', wiki_media.title, + 'type', wiki_media.type, + 'path', wiki_media.path, + 'reference', wiki_post_media.reference )), + ']') + FROM wiki_media + LEFT JOIN wiki_post_media ON wiki_post_media.media_id = wiki_media.id + WHERE wiki_post_media.post_id = :id + ) AS medias_json + FROM wiki_post + WHERE wiki_post.deleted IS NULL AND wiki_post.id = :id"; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['id' => $id]); + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return ($result == []) + ? false // no result? false + : $result[0]; // else return 1st record + } + + + /** add post + * + * insert post to database + * + * @param $data (array): an array with all post-properties + * @return: an array with `status` (bool), `id` (int) given to post + */ + public function add_post($data) + { + $post = [ + 'title' => $data['title'], + 'category_id' => $data['category_id'], + 'body' => $data['body'], + 'intro' => $data['intro'], + 'status' => $data['status'], + 'userid' => $this->user_id + ]; + + $sql = "INSERT INTO wiki_post (`title`, category_id, `body`, intro, `status`, user_id) + VALUES (:title, :category_id, :body, :intro, :status, :userid)"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute($post); + + $inserted_id = $this->pdo->lastInsertId(); + + // create + link tags to the post + // --- -- -- - - - + if (isset($data['tags'])) { // existing tags... + $this->set_post_tags($inserted_id, $data['tags']); + } + if (isset($data['new_tags'])) { // new tags... + $this->create_tags_for_post($data['new_tags'], $inserted_id); + } + + // set medias for post + // --- -- -- - - - + if (isset($data['media'])) { // new tags... + $this->create_medias_for_post($data['media'], $inserted_id); + } + + return [ "success" => true, 'id' => $inserted_id ]; + } + + + /** update post + * + * update post record to database + * + * @param $data (array): an array with all post-properties + */ + public function update_post($data) + { + $post = [ + 'id' => $data['id'], + 'title' => $data['title'], + 'category_id' => $data['category_id'], + 'body' => $data['body'], + 'intro' => $data['intro'], + 'status' => $data['status'] + ]; + $sql = "UPDATE wiki_post + SET `title` = :title, category_id = :category_id, + `body` = :body, intro = :intro, + `status` = :status + WHERE id = :id"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute($post); + + // create + link tags to the post + // --- -- -- - - - + if (isset($data['tags'])) { // old tags... + $this->set_post_tags($post['id'], $data['tags']); + } + if (isset($data['new_tags'])) { // new tags... + $this->create_tags_for_post($data['new_tags'], $post['id']); + } + + // set medias for post + // --- -- -- - - - + if (isset($data['media'])) { + $this->remove_media_from_post($post['id']); // remove old + $this->create_medias_for_post($data['media'], $post['id']); // re-create new + } + + return true; + } + + + /** latest_published_posts + * + * latest PUBLISHED posts of a category (by category_id) + * + * TODO: handle sorting + * + * @param $category_id (int) ; if 0 then no category is specified + * @param $limit (int) ; 0 = no limit + * @param $fieldset (string): list of fields; default all = '*' + */ + public function latest_published_posts($category_id, $limit = 10, $fieldset = '*') + { + + $where_clause = ($category_id) + ? " AND category_id = :categoryid " + : " "; + + $limit_clause = ($limit) + ? " LIMIT ". $limit + : ""; + + $sql = "SELECT {$fieldset} FROM wiki_post + WHERE `status` = 1 AND deleted IS NULL ". $where_clause + ." ORDER BY creation_date DESC ". $limit_clause; + + $stmt = $this->pdo->prepare($sql); + + if (!$category_id) { + $stmt->execute(); + + } else { + $stmt->execute(['categoryid' => $category_id]); + } + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return ($result === null) + ? false // no result? return false + : $result; // else return results + } + + + /** delete post + * + * NOTE: + * post is *marked* as deleted; + * a cron-job will make the actual deletions in a later time + * + * @param $id (int): post id + */ + public function delete_post($id) + { + if (intval($id) == 0) { // no valid id? + return ["success" => false]; // return false + } + + $sql = "UPDATE wiki_post SET deleted = 1 WHERE id = :id"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['id' => $id]); + + return ["success" => true]; + } + + + /** all posts + * published + unpublished ; any category ; no limit + * + * @param $filedset (string): list of fields; default all = '*' + */ + public function all_posts($fieldset = '*') + { + + $sql = "SELECT {$fieldset} FROM wiki_post WHERE deleted IS NULL"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(); + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return ($result === null) + ? false // no result? return false + : $result; // else return results + } + + + + ## ------------------------------------------------------------------------- + ## + ## MEDIA METHODS + ## + ## ------------------------------------------------------------------------- + + /** create media for post + * + * links post to each media-file of the media `id`s array + * @param $media (array): a list of media-file `id`s + * @param $post_id (int) + */ + private function create_medias_for_post( $medias, $post_id ) + { + foreach($medias as $medi_string) { + $medi = explode(';', $medi_string); // [0]: media_id, [1]: reference + $this->link_media_to_post($medi[0], $post_id, $medi[1]); // link to post + } + return true; + } + + /** remove_media_from_post + * + * remove all media records/attachments from post + * + * @param $post_id (int) + */ + private function remove_media_from_post( $post_id ) + { + $sql = "DELETE FROM wiki_post_media WHERE post_id = :postid"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['postid' => $post_id]); + return true; + } + + /** link one media-file to a specific post + * + * NOTE: + * the method does not check if media is linked already + * so be sure that the pair of (media_id,post_id) not exist + * + * @param $media_id (int) + * @param $post_id (int) + * @param $reference (int|bit) + */ + private function link_media_to_post( $media_id, $post_id, $reference ) + { + $sql = "INSERT INTO wiki_post_media (post_id, media_id, reference) VALUES (:postid, :mediaid, :reference)"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + 'postid' => $post_id, + 'mediaid' => $media_id, + 'reference' => $reference + ]); + return true; + } + + + + + + ## ------------------------------------------------------------------------- + ## + ## FILE METHODS + ## + ## ------------------------------------------------------------------------- + + + /** upload_file_to_folder + * + * upload the file to CloudStorage + * in a virtual folder + * + * @param $folder + */ + public function upload_file_to_folder($folder) + { + // Checks before uploading the file + //////////////////////////////////////////////////////////////////////// + + // ** 1: file is upladed to temporary folder --------------------------- + if (! is_uploaded_file($_FILES['file']['tmp_name'])) { + return false; // bye! + } + + // ** 2: File belongs to the allowed MIME types ------------------------ + $allowed_file_types = array( + 'application/pdf', + 'image/png', 'image/jpeg', + 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ); + // Recomended MIME type checking via mime_content_type(): + $mime_type = mime_content_type($_FILES['file']['tmp_name']); + if (! in_array($mime_type, $allowed_file_types)) { // File type NOT allowed ... + return false; // bye! + } + + $file_name = $_FILES['file']['name']; + $file_type = $_FILES['file']['type']; // do not take it for granted + $file_size = $_FILES['file']['size']; + $file_tmp = $_FILES['file']['tmp_name']; + + $bare_name = pathinfo($file_name, PATHINFO_FILENAME); + $file_ext = pathinfo($file_name, PATHINFO_EXTENSION); + + + // ** 3: filename or size checks may be added -------------------------- + if ($file_name == "") { + return false; // bye! + } + + // READY to finaly save/upload the file to CDN ///////////////////////// + + $store_filename = $folder .'/'. strtolower($this->clear_file_name($bare_name) .'.'. $file_ext); + + $file_destination = $this->wiki_files_root .'/'. $store_filename; + + // depricated; it is autoloaded include_once CLOUDSTORAGE; + $cloudstorage = new CloudStorage(); + + if ($cloudstorage->upload_object($file_destination, $file_tmp)) { + + return [ + 'path' => $file_destination, + 'type' => $mime_type + ]; + + } else { return false; } + } + + /** uc_split + * + * this is an mb_str_split polyfill because + * pythia uses a php version < 7.4 + * + * CHECK: https://www.php.net/manual/en/function.mb-str-split.php + */ + private function uc_split($string, $length = 1, $encoding = 'UTF-8' ) + { + if(!empty($string)){ + $split = array(); + $mb_strlen = mb_strlen($string,$encoding); + for($pi = 0; $pi < $mb_strlen; $pi += $length){ + $substr = mb_substr($string, $pi,$length,$encoding); + if( !empty($substr)) { + $split[] = $substr; + } + } + } + return $split; + } + + /** clear_file_name + * replace greek characters and strip symbols + */ + private function clear_file_name($str) + { + $el = $this->uc_split("ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩαβγδεζηθικλμνξοπρστυφχψωάέήίόύώϊϋς "); + $en = str_split( "ABGDEZHUIKLMNJOPRSTYFXCVabgdezhuiklmnjoprstyfxcvaehioyviys-"); + $strip = str_split("!@#$%^&*()+~`[]{};'/<>?=\""); + + return str_replace($strip, '', str_replace($el, $en, $str)); + } + + + /** define_media + * + * create a record in media table + * + * @param $data + * @return id (int): id of created media record + */ + public function define_media($data) + { + $sql = "INSERT INTO wiki_media (`title`, `type`, `path`) + VALUES (:title, :mimetype, :filepath)"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + 'title' => $data['title'], + 'mimetype' => $data['type'], + 'filepath' => $data['path'] + ]); + $inserted_id = $this->pdo->lastInsertId(); + + return $inserted_id; + } + + + ## ------------------------------------------------------------------------- + ## + ## TAG METHODS + ## + ## ------------------------------------------------------------------------- + + /** all tags + * + * @return: (array) all tags + */ + public function all_tags() + { + $sql = "SELECT * FROM wiki_tag"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(); + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return ($result === null) + ? false // no result? return false + : $result; // else return results + } + + + /** create_tag + * + * @param $tag (string): the tag label + * @return : id (int) given to tag + */ + public function create_tag($tag) + { + $sql = "INSERT INTO wiki_tag (`name`) VALUES (:label)"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ 'label' => $tag ]); + + $tag_id = $this->pdo->lastInsertId(); + + return $tag_id; + } + + /** destroy_tag + * + * @param $tag_id (int) + */ + public function destroy_tag($id) + { + // remove all post-links to this tag + $clean_posts = "DELETE FROM wiki_post_tags WHERE tag_id = :tagid"; + $clean = $this->pdo->prepare($clean_posts); + $clean->execute([ 'tagid' => $id ]); + + // remove tag from tags table + $sql = "DELETE FROM wiki_tag WHERE id = :tagid"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ 'tagid' => $id ]); + + return true; + } + + /** posts_of_tag + * + * return posts linked to the specified tag (by tag-id) + * + * @param tag_id + * @param limit + * @param fieldset (string): list of post field-names separated with a comma (,) + * @param only_published (bool) + */ + public function posts_of_tag($tag_id, $limit = 0, $fieldset = '*', $only_published = true) + { + // format post field-names for SELECT SQL + // post_fields will be somethning like... `post.*` + // or... `post.id, post.title, post.into` + // --- -- -- - - - + $fields = explode(',', str_replace(' ', '', $fieldset)); + $fields_arr = []; + foreach($fields as $f) { $fields_arr[] = 'wiki_post.'. $f; } + $post_fields = implode(', ', $fields_arr); + + $limit_clause = ($limit) + ? " LIMIT ". $limit + : ""; + + $filter = ($only_published) + ? " AND post.status = 1 " + : ""; + + $sql = "SELECT {$post_fields} FROM wiki_post + LEFT JOIN wiki_post_tags ON wiki_post_tags.post_id = wiki_post.id + LEFT JOIN wiki_tag ON wiki_tag.id = wiki_post_tags.tag_id + WHERE wiki_post.deleted IS NULL AND wiki_tag.id = :tagid {$filter} + {$limit_clause}"; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['tagid' => $tag_id]); + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return $result; + } + + + /** tag_name + * return tag name (name = label / title / text) + * by tag_id + * + * @param $id (int): given tag-id + * @return (string) + */ + public function tag_name($id) { + $sql = "SELECT `name` FROM wiki_tag WHERE id = :id LIMIT 1"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['id' => $id]); + + $result = $stmt->fetch(); + + return $result['name']; + + } + + /** set_tags_to_post() + * + * REMOVE OLD tags from post; + * then SET NEW tags + * + * NOTE: + * make sure that any old tags are removed from post; + * new tags should not be linked in a later cycle + * + */ + private function set_post_tags($post_id, $tags) + { + $this->remove_tags_from_post($post_id); // remobve any old tags + + foreach($tags as $tag) { // set each tag to post + $this->link_tag_to_post($tag, $post_id); + } + } + + /** remove_tags_from_post + * + * @param $post_id (int) + */ + private function remove_tags_from_post( $post_id ) + { + $sql = "DELETE FROM wiki_post_tags WHERE post_id = :postid"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(['postid' => $post_id]); + return true; + } + + /** set one tag to a specific post + * + * NOTE: + * the method does not check if tag is linked already + * + */ + private function link_tag_to_post( $tag_id, $post_id ) + { + $sql = "INSERT INTO wiki_post_tags (post_id, tag_id) VALUES (:postid, :tagid)"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + 'postid' => $post_id, + 'tagid' => $tag_id + ]); + return true; + } + + /** create tags for post + * + * creates an array of tags; links post to each one; + * @param $tags (array): a list of tags (labels/texts) + * @param $post_id (int) + */ + private function create_tags_for_post( $tags, $post_id ) + { + foreach($tags as $tag) { + $tag_id = $this->create_tag($tag); // create tag + $this->link_tag_to_post($tag_id, $post_id); // link to post + } + return true; + } + + + public function tags_stats() + { + $sql = "SELECT tag.id, tag.name, COUNT(post_tags.post_id) as totals + FROM wiki_tag + LEFT JOIN wiki_post_tags ON wiki_post_tags.tag_id = wiki_tag.id + GROUP BY wiki_tag.id"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute(); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + + ## ------------------------------------------------------------------------- + ## + ## USER METHODS + ## + ## ------------------------------------------------------------------------- + + /** user_info + * + * no user is passed; user info are exrtacted from Session + * + */ + private function user_info() + { + $user = []; + $keys = [ + 'user_session' => 'user', + 'employee_id' => 'employee', + 'store_id' => 'store', + 'site_id' => 'site', + 'unit_id' => 'unit', + 'employee_rID' => 'rid' + ]; + + foreach($keys as $key => $attribute) { + if (isset($_SESSION[$key]) && ($_SESSION[$key] != '')) { + $user[$attribute] = $_SESSION[$key]; + } + } + return $wiki_user; + } + + + /** update post stats + * + */ + public function update_post_stats($post_id) + { + + $user_id = $_SESSION['user_session']; // get user from session + + $sql = "INSERT INTO wiki_stats + (post_id, user_id, views) VALUES (:post, :user, 1) + ON DUPLICATE KEY UPDATE views = views + 1"; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute([ + 'user' => $user_id, + 'post' => $post_id + ]); + + return true; + + } + +} diff --git a/public/app/views/simple/Render.php b/public/app/views/simple/Render.php new file mode 100644 index 0000000..79a93a0 --- /dev/null +++ b/public/app/views/simple/Render.php @@ -0,0 +1,182 @@ +"; + } + + } + + + /** all_breadcrumbs + * ------------------------------------------------------------------------- + * + * returns an array of all breadcrumbs + * where array-key of each record is category[id] + * + * NOTE: + * --- + * Render::all_breadcrumbs returns an indexed super-array; + * each array item includes a banch of information: [ + * breadcrumb, + * rec: [ id , title ], + * parents: [ [id, title] , ... ] + * childs: [ [id, title] , ... ], + * level + * ] + * + * Use Cases: + * --- + * as a super-array, the output can be used in many cases + * for example... + * into form elements + * .. while selecting category for a post + * .. or editing a category + * or directry referring to category's parents/childs + * + * Arguments: + * --- + * @param $tree (array) : category tree (with childs and parents parts) + * @param $detimiter (string, optional) : string to split breadcrumb's path-nodes + * @param $exception (int, optional) : id of category to exclude (subcategories shall be excluded too) + * @param $l (int, not-pass) : depth level of the node; DO NOT SET (takes values automaticaly) + * @return array of breadcrumbs + * ------------------------------------------------------------------------- + */ + static public function all_breadcrumbs($tree, $delimiter = " / ", $exception = 0, $l = 0) + { + $all = []; // results array + + foreach($tree as $node) { // loop through all nodes + + if (intval($node['rec']['id']) != $exception) { // if node is not exception + + // construct breadcrumb html of node + // --- -- -- - - - + $breadcrumb = ""; + foreach($node['parents'] as $par) { // first: join path titles + $breadcrumb .= $par['title'] . $delimiter; + } + $breadcrumb .= $node['rec']['title']; // last: append title + + // make a new super record + // --- -- -- - - - + $all[$node['rec']['id']] = [ // set record is as key + 'breadcrumb' => $breadcrumb, // add breadcrump to results + 'rec' => $node['rec'], // + node info + 'parents' => $node['parents'], // + parents array + 'childs' => self::first_level_childs($node), // + direct childs + 'level' => $l // + level + ]; + + // recursively traverse children nodes + // --- -- -- - - - + if (isset($node['childs']) && $node['childs'] != []) { + $child_breadcrumbs = self::all_breadcrumbs( + $node['childs'], + $delimiter, + $exception, + $l+1 + ); + + $all = $all + $child_breadcrumbs; // concatenate arrays (keep array-keys) + } + } + + } + return $all; + + } + + /** first_level_childs + * --- -- -- - - - + * used by all_breadcrumbs() + */ + static private function first_level_childs($node) + { + $childs = []; + if ($node['childs'] == []) { + return []; + } + foreach($node['childs'] as $key => $kid) { + $childs[] = [ + 'id' => $kid['rec']['id'], + 'title' => $kid['rec']['title'] + ]; + } + return $childs; + } + + + /** date_box + * creates a pretty date box; outputs greek months + * + * @param $date (string) in 'Y-m-d H:i:s' format + * @return html (string) of the date-box + */ + static public function date_box($date) + { + $months_Gr = ['', + 'Ιαν', 'Φεβ', 'Μαρ', 'Απρ', + 'Μάι', 'Ιουν', 'Ιουλ', 'Αυγ', + 'Σεπ', 'Οκτ', 'Νοε', 'Δεκ' + ]; + $date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date); + $day = $date->format("j"); + $month = $months_Gr[intval($date->format("m"))]; + $year = $date->format("Y"); + + // print_r([$date, $day, $month, $year]); die(); + + return "
+

{$day}

+

{$month}

+

{$year}

+
"; + } + + /** date_friendly + * outputs an unformated greek-language date string + * + * @param $date (string) in 'Y-m-d H:i:s' format + */ + static public function date_friendly($date, $display_time = false) + { + $months_Gr = ['', + 'Ιαν', 'Φεβ', 'Μαρ', 'Απρ', 'Μάι', 'Ιουν', + 'Ιουλ', 'Αυγ', 'Σεπ', 'Οκτ', 'Νοε', 'Δεκ' + ]; + $date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date); + $day = $date->format("j"); + $month = $months_Gr[intval($date->format("m"))]; + $year = $date->format("Y"); + $time = ($display_time) ? $date->format("H:i") : ''; + + return "{$day} {$month} {$year} {$time}"; + } + +} diff --git a/public/app/views/simple/category.php b/public/app/views/simple/category.php new file mode 100644 index 0000000..15d8b5e --- /dev/null +++ b/public/app/views/simple/category.php @@ -0,0 +1,156 @@ +category_tree(); +$breadcrumbs = Render::all_breadcrumbs($categories); + +// get ALL posts of category +$posts = $wiki->latest_published_posts($category_id, 0); + + +// special case: +// ----------------------------------------------------------------------------- +// ONE post + NO subcaegories ..then redirect to the only post +if (($breadcrumbs[ $category_id ]['childs'] == []) && (count($posts) == 1)) { + header('Location: /wiki/post?id='. $posts[0]['id'], true, 302); + die(); +} + + +// find parent_ids of current category (to open the menu tree) +$category_path = []; +foreach($breadcrumbs[$category_id]['parents'] as $parent) { + $category_path[] = intval($parent['id']); +} +$category_path[] = intval($category_id); + + +// format $posts array +// --- +$posts_formated = []; +foreach($posts as $post) { + $date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $post['creation_date']); + $posts_formated[] = [ + 'id' => $post['id'], + 'title' => $post['title'], + 'intro' => $post['intro'] ?? false, + 'date' => Render::date_box($post['creation_date']) + ]; +} + + +// then render the page ... +// ----------------------------------------------------------------------------- +?> + + + + Wiki + + false ]); ?> + + + + + +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ + +

+ +

+ + + + + + +

Υποκατηγορίες

+
+ + $child) : ?> + + + + + + + +
+ + + + + + + + +

Άρθρα / Δημοσιεύσεις

+ + $posts_formated, + 'fieldset' => ['intro', 'date'] + ]); + //////////////////////////////////////////////////// + ?> + + + +
+ +
+
+
+ + + + diff --git a/public/app/views/simple/index.php b/public/app/views/simple/index.php new file mode 100644 index 0000000..9860db0 --- /dev/null +++ b/public/app/views/simple/index.php @@ -0,0 +1,123 @@ +category_tree(); +$breadcrumbs = Render::all_breadcrumbs($categories); +$posts = $wiki->latest_published_posts(0, 7); // 0 = no category ; 7 = posts limit + +// format posts array; inject breadcrumbs +// --- +$posts_formated = []; +foreach($posts as $post) { + $posts_formated[] = [ + 'id' => $post['id'], + 'title' => $post['title'], + 'category' => $breadcrumbs[ $post['category_id'] ]['breadcrumb'], + 'intro' => $post['intro'] ?? false, + 'date' => Render::date_box($post['creation_date']) + ]; +} + +// optional fields to be printed into post-list +// --- +$fieldset = [ + 'date', + 'intro', + 'category' +]; + +// then render the page ... +// ----------------------------------------------------------------------------- +?> + + + + Wiki + + false ]); ?> + + + + + +
+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ + +
+ +

+ Welcome to Wiki! +

+ + +

Ίσως κάποιο εισαγωγικό κείμενο, στατικό ή δυναμικό. + Pellentesque non mauris ut orci volutpat sollicitudin ut vel ligula. Nullam et leo eu ante ornare ultrices a a leo. Proin non accumsan ex, sit amet finibus purus. +

+ Vestibulum posuere maximus nisl eget consequat. Etiam venenatis lectus sed libero vestibulum viverra. Sed nec nibh ac nulla venenatis tincidunt eu ut diam. Sed tincidunt, arcu sit amet pharetra ornare, libero nunc placerat nunc, at dictum dui diam sit amet metus. Sed venenatis vehicula nisl at hendrerit. +

+ + +

Τελευταίες Δημοσιεύσεις

+ + $posts_formated, + 'fieldset' => $fieldset + ]); + //////////////////////////////////////////////////////// + ?> + +
+ + +
+ +
+ +
+ + + + diff --git a/public/app/views/simple/lorem-dates.php b/public/app/views/simple/lorem-dates.php new file mode 100644 index 0000000..7a7c000 --- /dev/null +++ b/public/app/views/simple/lorem-dates.php @@ -0,0 +1,18 @@ +setTimestamp(time() - rand(0,30000)*1000 + rand(1,1000) ); + + // return time() - rand(0,30000)*1000 + rand(1,1000); + return $dt->format('Y-m-d H:i:s'); +} + + +include 'pythia.php'; + +$posts = $wiki->latest_posts(0, 900); + +foreach($posts as $post) { + $wiki->set_date($post['id'], some_date()); +} +echo "done!"; \ No newline at end of file diff --git a/public/app/views/simple/lorem-intros.php b/public/app/views/simple/lorem-intros.php new file mode 100644 index 0000000..a3629e1 --- /dev/null +++ b/public/app/views/simple/lorem-intros.php @@ -0,0 +1,721 @@ +latest_published_posts(0, 900, 'id, title, intro'); +$tags = $wiki->all_tags(); + +shuffle($posts); +shuffle($titles); + + +for ($i = 0 ; $i < 500 ; $i++) { + // $wiki->set_intro($posts[$i]['id'], some_lorem_intro()); + // $wiki->set_status($posts[$i]['id'], 0); + // $wiki->set_title($posts[$i]['id'], $titles[$i]); + shuffle($tags); + $c = ($i % 3) + 1; + for ($j = 0; $j < $c; $j ++) { $wiki->link_tag_to_post($tags[$j]['id'], $posts[$i]['id']); } +} +echo "done!"; + + + diff --git a/public/app/views/simple/lorem-posts.php b/public/app/views/simple/lorem-posts.php new file mode 100644 index 0000000..5f7fccb --- /dev/null +++ b/public/app/views/simple/lorem-posts.php @@ -0,0 +1,93 @@ +". $lorem[$i] ."

"; + } + + return $html; +} + +function some_lorem_title() { + // make an array of + $lorem = explode(".", // sentences only + str_replace(". ", ".", + implode(" ", LOREM) // made from all Lorem-ipsums + ) + ); + shuffle($lorem); + return $lorem[0]; +} + + +include 'pythia.php'; + + +$categories = $wiki->get_categories(); + +$non_root = []; +foreach($categories as $node) { + if (intval($node['parent_id']) > 0) { $non_root[] = $node['id']; } + // if (intval($node['parent_id']) > 5) { $non_root[] = $node['id']; } +} +// print_r($non_root); die(); + +echo "
";
+for( $i = 0 ; $i < 250 ; $i++ ) {
+
+    $temp = $non_root;
+    shuffle($temp);
+
+    $post = [
+        'title' => some_lorem_title(),
+        'category' => intval($temp[0]),
+        'body' => some_lorem_html()
+    ];
+    print_r($post);
+
+    $wiki->add_post($post);
+}
+echo "
"; \ No newline at end of file diff --git a/public/app/views/simple/post.php b/public/app/views/simple/post.php new file mode 100644 index 0000000..9e37cd9 --- /dev/null +++ b/public/app/views/simple/post.php @@ -0,0 +1,236 @@ +get_post($post_id); +$categories = $wiki->category_tree(); +$breadcrumbs = Render::all_breadcrumbs($categories); + +if ($post == false) { // when post not found + $post = [ // create a virtual record with error messages + 'id' => 0, + 'title' => 'Το άρθρο που αναζητήσατε δεν υπάρχει', + 'category_id' => 0, + 'intro' => 'Για να βρειτε το άρθρο που ψάχνετε χρησιμοποιήστε την αναζήτηση + του wiki ή πλοηγηθείτε στο δέντρο των κατηγοριών.', + 'body' => "" + ]; + +} else { // post found; prepare article's data to render + + $category_id = $post['category_id']; + + // find parent_ids of current category (to open the menu tree) + $category_path = []; + foreach($breadcrumbs[$category_id]['parents'] as $parent) { + $category_path[] = intval($parent['id']); + } + $category_path[] = intval($category_id); + +} + + +// format special parts of post +$parsedown = new Parsedown(); +$body_html = $parsedown->text($post['body']); // body: markdown to html + +// decode json output (tags + medias) +if ($post['tags_json'] != null) { $tags = json_decode($post['tags_json']); } +if ($post['medias_json'] != null) { $medias = json_decode($post['medias_json']); } + + +// then render the page ... +// ----------------------------------------------------------------------------- +?> + + + + Wiki + + false ]); ?> + + + + +
+ +
+ + +
+ +
+ +
+ +
+ + + +
+ + +
+ + +

+ + + + + + + + +

+ + + + + + +

+ + + +
+ +
+ + + +
+

Συνημμένα:

+
    + + reference == 1) : ?> +
  1. + + title?> + +
  2. + + +
+
+ + + + +
+ Επικέτες: + + name?> + +
+ + +
+ +
+ +
+
+ + + + + + + + + + + + + + diff --git a/public/app/views/user/login.php b/public/app/views/user/login.php new file mode 100644 index 0000000..ba29158 --- /dev/null +++ b/public/app/views/user/login.php @@ -0,0 +1,91 @@ + + + + + <?=SITE_TITLE?> - Είσοδος Χρήστη + + + + + + +
+
+

Είσοδος Χρήστη

+
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + + + + diff --git a/public/app/views/user/new-password.php b/public/app/views/user/new-password.php new file mode 100644 index 0000000..06e7fac --- /dev/null +++ b/public/app/views/user/new-password.php @@ -0,0 +1,65 @@ + + + + + <?=SITE_TITLE?> - Επαναφορά κωδικού πρόσβασης + + + + + + + + + + + + +
+
+

Επαναφορά κωδικού πρόσβασης

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + diff --git a/public/app/views/user/registration.php b/public/app/views/user/registration.php new file mode 100644 index 0000000..e3527f0 --- /dev/null +++ b/public/app/views/user/registration.php @@ -0,0 +1,120 @@ + + + + + <?=SITE_TITLE?> - Εγγραφή + + + + + + +
+
+
+

Classroom: Εγγραφή

+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+

Αν έχετε ήδη λογαρισμό συνδεθείτε.

+

Σε κάθε περίπτωση μπορείτε να επιστρέψετε στην αρχική σελίδα.

+
+
+
+ + + + + diff --git a/public/app/views/welcome.php b/public/app/views/welcome.php new file mode 100644 index 0000000..de00645 --- /dev/null +++ b/public/app/views/welcome.php @@ -0,0 +1,28 @@ + + + + + <?=SITE_TITLE?> - Welcome! + + + + + + + + + + + + + + + \ No newline at end of file -- cgit v1.2.3