+ Παρακαλώ ελέγξτε το 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/config/init_routes.php b/public/app/config/init_routes.php
new file mode 100644
index 0000000..22d6ec0
--- /dev/null
+++ b/public/app/config/init_routes.php
@@ -0,0 +1,12 @@
+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'])
+ ->setName($record['first_name'] .' '. $record['last_name'])
+ ->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']);
+ $roles = json_decode($attributes['Roles_json']);
+ $user
+ ->setRoles($roles)
+ ->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;'. $user->getName(),
+ time()+60*60*8, // 8 hours
+ '/'
+ );
+
+ // check if admin (and redirect differently)
+ $is_admin = (!empty(array_intersect([1,2,3], $roles)));
+
+ // login OK, set Token in session
+ $userManager->createUserToken($user);
+ return [
+ 'success' => true,
+ 'goto' => $is_admin ? '/admin/lessons' : '/user/profile',
+ ];
+
+ } 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'])
+ ->setName($req->POST['name'] .' '. $req->POST['surname'])
+ ->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'
+ ];
+ }
+
+ }
+
+ /** is_connected
+ * checks if the user is connected
+ *
+ * @return true|false
+ */
+ public static function is_connected()
+ {
+ $manager = new Classroom_manager();
+ if ($manager->hasUserToken()) {
+
+ // user is connected;
+ $token = $manager->getUserToken();
+ $user = $token->getUser();
+
+ return $user;
+
+ } else {
+ // user is not connected;
+ return false;
+ }
+ }
+
+
+ /** logout
+ *
+ * performs a secure logout;
+ * regenerates session; deletes cookies;
+ */
+ public static function logout()
+ {
+ $userManager = new Classroom_manager();
+ $userManager->logout();
+
+ // regeneration session ID (prevent session fixation)
+ session_regenerate_id();
+
+ // remove user-conected cookie
+ if (isset($_COOKIE['cluser'])) {
+ unset($_COOKIE['cluser']);
+ setcookie('cluser', '', -1, '/');
+ return true;
+
+ } else {
+ return false;
+ }
+ }
+
+
+
+ /** hasPermition( PERMIT )
+ *
+ * checks if the user owns the specified permition
+ * to access the source
+ *
+ */
+ public static function hasPermition($permit = [0])
+ {
+ if (in_array(0, $permit)) { // permision 0 means public
+ return true; // permision 0 is always granted
+ }
+
+ if ($user = self::is_connected() === false) { // if not connected
+ return false; // then no other permition is granted
+ }
+
+ if ($user instanceof UserInterface) {
+ return ( !empty( array_intersect($permit, $user->getPrivileges()) ) );
+ }
+ }
+
+
+ /** 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 => [2, 3]
+ * permition => ['10', '12', '18']
+ * ],
+ * [
+ * role => [1 , 4]
+ * ],
+ * [
+ * permition => [ 3 ]
+ * ]
+ * ]
+ *
+ * defines (and parses to) a requirements rule of:
+ * [
+ * user should be creator or editor
+ * _AND_ have permition 10 or 12 or 18
+ * ]
+ * OR
+ * [
+ * user should be an administrator or developer
+ * ]
+ * OR
+ * [
+ * user should have permition #3
+ * ]
+ *
+ *
+ */
+ public static function isAuthorized($requirements)
+ {
+ $authorized = false;
+ foreach($requirements as $required) {
+
+ if (isset($required['role'])) { // if a role is required
+ if ( (self::isGranted($required['role'])) // authorize both role
+ && (self::hasPermition($required['permition'] ?? [ 0 ])) ) { // and permition
+ // $authorized = true;
+ return true;
+ }
+
+ } else { // else, if not is not required
+ if (self::hasPermition($required['permition'] ?? [ 0 ])) { // authorize permition
+ // $authorized = true;
+ return true;
+ }
+ }
+ }
+ return $authorized;
+ }
+
+
+ public static function forgot_pass()
+ {
+ }
+
+
+
+ public static function validate_otp()
+ {
+ }
+
+
+ /** allowRoles
+ *
+ * method filters access for certain roles
+ * if user is not grented acces, a forbiden message is sent and app ends._
+ * otherwise the method returns true (app will continue)
+ *
+ * @param $allowed (array) : array of allowed roles
+ * @return true or die();
+ */
+ public static function allowRoles($allowed)
+ {
+ $manager = new Classroom_manager();
+ if ($manager->isGranted($allowed)) { // if valid, return true (continue)
+ return true;
+
+ } else { // no user, no access; die._
+ Render::view('error/general', [
+ 'title' => 'Forbidden',
+ 'message' => 'Access is forbidden'
+ ]);
+ die();
+ return false; // this line will never run
+ }
+ }
+
+
+ /** hasValidRole
+ * like allowRoles() but dowes not stop execution
+ *
+ * @return true|false
+ */
+ public static function hasValidRole($allowed)
+ {
+ $manager = new Classroom_manager();
+ return ($manager->isGranted($allowed));
+ }
+
+
+ public static function in_admin_group()
+ {
+ $manager = new Classroom_manager();
+ return ($manager->isGranted([1, 2, 3]));
+ }
+
+
+
+}
+
+
+// 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/admin/.gitkeep b/public/app/controllers/admin/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/public/app/controllers/admin/Course_admin.php b/public/app/controllers/admin/Course_admin.php
new file mode 100644
index 0000000..fcf38c9
--- /dev/null
+++ b/public/app/controllers/admin/Course_admin.php
@@ -0,0 +1,676 @@
+query(
+ "INSERT INTO course (parent_id, label) VALUES (:parent, :label)",
+ [
+ ':parent' => Registry::get('REQUEST')->POST['parent_id'],
+ ':label' => Registry::get('REQUEST')->POST['label']
+ ]
+ )->lastInsertID();
+
+ $cache = self::update_courses_cache(); // update category caches
+ return $cache['breadcrumbs']; // return
+ }
+
+
+ /** update course
+ * ---
+ * @param void (get data from POST)
+ */
+ public static function update_course()
+ {
+ Registry::use('database')->query(
+ "UPDATE course SET parent_id = :parent, label = :label
+ WHERE id = :id",
+ [
+ ':id' => Registry::get('REQUEST')->POST['id'],
+ ':parent' => Registry::get('REQUEST')->POST['parent_id'],
+ ':label' => Registry::get('REQUEST')->POST['label']
+ ]
+ );
+ $cache = self::update_courses_cache();
+ return $cache['breadcrumbs'];
+ }
+
+
+ /** update courses cache
+ * --- -- -- - - -
+ * Forces re-caching of courses tree
+ * Shall run if anything changes to courses
+ *
+ * @param void
+ * @return (array): ['tree' => ..., 'breadcrumbs' => ... ]
+ */
+ public static function update_courses_cache()
+ {
+ return Cache_service::courses_struct(PROXY_IGNORE_CACHE);
+ }
+
+
+ ## -------------------------------------------------------------------------
+ ##
+ ## LESSON METHODS
+ ##
+ ## -------------------------------------------------------------------------
+
+
+ /** all lessons
+ *
+ */
+ public static function all_lessons()
+ {
+ Render::json(Registry::use('database')->runQuery(
+ "SELECT lesson.*, lesson_privilege.privilege_id FROM lesson
+ LEFT JOIN lesson_privilege ON lesson_privilege.lesson_id = lesson.id",
+ []
+ ));
+ }
+
+
+ /** get_lesson
+ *
+ * @param $id (int) : lesson's id
+ */
+ public static function get_lesson($id)
+ {
+ // get (first) lesson with id = $id; render as json
+ Render::json(Registry::use('database')->query(
+ "SELECT lesson.*,
+ ( SELECT
+ CONCAT('[', GROUP_CONCAT(JSON_OBJECT(
+ 'id', media.id,
+ 'label', media.label,
+ 'type', media.type,
+ 'path', media.path )),
+ ']')
+ FROM media
+ LEFT JOIN lesson_media ON lesson_media.media_id = media.id
+ WHERE lesson_media.lesson_id = :id
+ ) AS medias_json,
+ lesson_privilege.privilege_id
+ FROM lesson
+ LEFT JOIN lesson_privilege ON lesson_privilege.lesson_id = lesson.id
+ WHERE lesson.id = :id",
+ [ ':id' => $id]
+ )->getFirst());
+ }
+
+
+ /** add_lesson
+ * insert a new lesson
+ *
+ * all parametres are passed via $_POST array
+ *
+ * POST @param title
+ * POST @param course_id
+ * POST @param intro
+ * POST @param body
+ * POST @param published
+ */
+ public static function add_lesson()
+ {
+ $post = Registry::get('REQUEST')->POST;
+
+ // insert lesson content into daabase
+ $id = Registry::use('database')->query(
+ "INSERT INTO lesson (title, course_id, intro, `body`, `status`)
+ VALUES (:title, :courseid, :intro, :body, :status)",
+ [
+ ':title' => $post['title'],
+ ':courseid' => $post['course_id'],
+ ':intro' => $post['intro'],
+ ':body' => $post['body'],
+ 'status' => $post['status']
+ ]
+ )->lastInsertID();
+
+ // set lesson privileges to database
+ Registry::use('database')->runQuery(
+ "INSERT INTO lesson_privilege (lesson_id, privilege_id)
+ VALUES (:lesson_id, :privilege_id)",
+ [
+ ':lesson_id' => $id,
+ ':privilege_id' => $post['privilege_id']
+ ]
+ );
+
+ if (isset($post['media'])) {
+ // update media's privileges; NOTE: media table
+ self::update_medias_privileges($post['media'], $post['privilege_id']);
+
+ // set lesson's media files; NOTE: lesson_media table
+ // ERROR: self::create_medias_for_lesson($post['media'], $id, $post['privilege_id']);
+ self::create_medias_for_lesson($post['media'], $id);
+
+ // recache media
+ Cache_service::files_attributes(PROXY_IGNORE_CACHE);
+ }
+
+ return true;
+ }
+
+
+ public static function update_lesson()
+ {
+ $post = Registry::get('REQUEST')->POST;
+
+ // print_r($post); die();
+
+ // update lesson's content
+ Registry::use('database')->query(
+ "UPDATE lesson
+ SET title = :title,
+ course_id = :courseid,
+ intro = :intro, `body` = :body,
+ `status` = :status
+ WHERE id = :id",
+ [
+ ':id' => $post['id'],
+ ':title' => $post['title'],
+ ':courseid' => $post['course_id'],
+ ':intro' => $post['intro'],
+ ':body' => $post['body'],
+ ':status' => $post['status']
+ ]
+ );
+
+ // update lesson's privileges
+ Registry::use('database')->runQuery(
+ "UPDATE lesson_privilege SET privilege_id = :privilege_id
+ WHERE lesson_id = :lesson_id",
+ [
+ ':lesson_id' => $post['id'],
+ ':privilege_id' => $post['privilege_id']
+ ]
+ );
+
+ if (isset($post['media'])) {
+ // update media's privileges
+ self::update_medias_privileges($post['media'], $post['privilege_id']);
+ }
+
+ // remove all old lesson's links to media files
+ Registry::use('database')->runQuery(
+ "DELETE from lesson_media WHERE lesson_id = :lesson",
+ [ ':lesson' => $post['id'] ]
+ );
+
+ if (isset($post['media'])) {
+ // update lesson's media files
+ // ERROR: self::create_medias_for_lesson($post['media'], $post['id'], $post['privilege_id']);
+ self::create_medias_for_lesson($post['media'], $post['id']);
+
+ // recache media
+ Cache_service::files_attributes(PROXY_IGNORE_CACHE);
+ }
+
+ return true;
+ }
+
+
+ ## -------------------------------------------------------------------------
+ ##
+ ## FILE METHODS
+ ##
+ ## -------------------------------------------------------------------------
+
+
+ /** files
+ * echo all files
+ *
+ * @return (array)
+ */
+ public static function files()
+ {
+ return Cache_service::files_attributes();
+ }
+
+
+ /** upload_file
+ * upload the file to the file system
+ *
+ * the method reads the POST and FILES array
+ * to retrieve all needed parametres
+ *
+ * FILES @param file
+ * POST @param folder : lesson's ID or somthing random
+ * POST @param reference : reference type
+ * POST @param
+ */
+ public static function upload_file()
+ {
+ $request = Registry::get('REQUEST');
+
+ $uploaded = self::upload_to_fs(); // upload file to file-system
+
+ if ($uploaded['success']) {
+
+ $media_id = self::define_media([ // define media in database; get id
+ 'title' => $request->POST['title'],
+ 'type' => $uploaded['type'],
+ 'path' => $uploaded['path']
+ ]);
+
+ Render::json([ // render results as json
+ 'success' => true,
+ 'id' => $media_id,
+ 'title' => $request->POST['title'],
+ 'path' => $uploaded['path'],
+ 'type' => $uploaded['type']
+ ]);
+
+ } else {
+ Render::json(['success' => false ]);
+ }
+ }
+
+
+ /** upload to fs
+ * upload file to File-System
+ *
+ * POST @param folder
+ * FILES @param file
+ */
+ private static function upload_to_fs()
+ {
+ $post = Registry::get('REQUEST')->POST;
+ $files = Registry::get('REQUEST')->FILES;
+
+
+ // Checks before uploading the file
+ ////////////////////////////////////////////////////////////////////////
+
+ // ** 1: file is upladed to temporary folder ---------------------------
+ if (! is_uploaded_file($files['file']['tmp_name'])) {
+ return ['success' => 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 ['success' => 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 ['success' => false]; // bye!
+ }
+
+
+ // READY to finaly save/upload the file to CDN /////////////////////////
+
+ $folder = MEDIA_STORAGE_ROOT . $post['folder'];
+ if (!file_exists($folder)) { // create folder if not exists
+ mkdir($folder, 0757, true);
+ }
+
+ // print_r([
+ // 'dir' => $folder,
+ // 'file' => $bare_name,
+ // 'ext' => $file_ext,
+ // 'type' => $file_type
+ // ]); die();
+
+ $relative_filename = $post['folder']
+ .'/'
+ . strtolower(self::clear_file_name($bare_name) .'.'. $file_ext);
+ $store_filename = MEDIA_STORAGE_ROOT . $relative_filename;
+
+ if (move_uploaded_file($files["file"]["tmp_name"], $store_filename)) {
+ return [
+ 'success' => true,
+ 'path' => $relative_filename,
+ 'type' => $mime_type
+ ];
+
+ } else { return ['success' => false]; }
+
+ }
+
+
+ /** clear_file_name
+ * replace greek characters and strip symbols
+ */
+ private static function clear_file_name($str)
+ {
+ $el = mb_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 (array): [title => , path => , type => mime-type]
+ * @return id (int): id of created media record
+ */
+ private static function define_media($data)
+ {
+ $request = Registry::get('REQUEST');
+
+ $media_id = Registry::use('database')->query(
+ "INSERT INTO media (label, `type`, `path`)
+ VALUES (:label, :mimetype, :filepath)",
+ [
+ 'label' => $data['title'],
+ 'mimetype' => $data['type'],
+ 'filepath' => $data['path']
+ ]
+ )->lastInsertID();
+ return $media_id;
+ }
+
+
+ /** create media for lesson
+ *
+ * links lesson to each media-file of the media `id`s array
+ *
+ * @param $media (array): a list of media-file `id`s
+ * @param $lesson_id (int)
+ */
+ private static function create_medias_for_lesson($medias, $lesson_id)
+ {
+ foreach($medias as $key => $medi) {
+ self::link_media_to_lesson($medi, $lesson_id); // link to lesson
+ }
+ return true;
+ }
+
+
+ /** create media for page
+ *
+ * links page to each media-file of the media `id`s array
+ *
+ * @param $media (array): a list of media-file `id`s
+ * @param $page_id (int)
+ */
+ private static function create_medias_for_page($medias, $page_id)
+ {
+ foreach($medias as $key => $medi) {
+ self::link_media_to_page($medi, $page_id); // link to page
+ }
+ 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 $lesson_id (int)
+ */
+ private static function link_media_to_lesson( $media_id, $lesson_id )
+ {
+ Registry::use('database')->runQuery(
+ "INSERT INTO lesson_media (lesson_id, media_id)
+ VALUES (:lesson, :media)",
+ [
+ 'lesson' => $lesson_id,
+ 'media' => $media_id,
+ ]
+ );
+ return true;
+ }
+
+ /** link one media-file to a specific page
+ *
+ * NOTE:
+ * the method does not check if media is linked already
+ * so be sure that the pair of (media_id, page_id) not exist
+ *
+ * @param $media_id (int)
+ * @param $page_id (int)
+ */
+ private static function link_media_to_page( $media_id, $page_id )
+ {
+ Registry::use('database')->runQuery(
+ "INSERT INTO page_media (page_id, media_id)
+ VALUES (:page, :media)",
+ [
+ 'page' => $page_id,
+ 'media' => $media_id,
+ ]
+ );
+ return true;
+ }
+
+
+ /** update medias privileges
+ *
+ * UPDATE media SET privige WHERE IN {list}
+ *
+ * @param $medias (array) : array of media id(s)
+ * @param $privilege (int) : privilege id
+ * @return true (always)
+ */
+ private static function update_medias_privileges($medias, $privilege)
+ {
+ // create WHERE IN (LIST) holders and params for prepare statement
+ $params = [ ':privilege' => $privilege];
+ $holders = [];
+ foreach($medias as $key => $media) {
+ $params[':id'.$key] = $media;
+ $holders[] = ':id'.$key;
+ }
+ $list = '('. implode(',', $holders) .')';
+
+ // print_r(['params' => $params, 'list' => $list]); die();
+
+ Registry::use('database')->runQuery(
+ "UPDATE media SET privilege_id = :privilege
+ WHERE id IN {$list}",
+ $params
+ );
+
+ return true;
+ }
+
+
+
+
+
+
+ ## -------------------------------------------------------------------------
+ ##
+ ## PAGE METHODS
+ ##
+ ## -------------------------------------------------------------------------
+
+
+ /** all pages
+ *
+ */
+ public static function all_pages()
+ {
+ Render::json(Registry::use('database')->runQuery(
+ "SELECT * FROM page", []
+ ));
+ }
+
+
+ /** get_page
+ *
+ * @param $id (int) : page's id
+ */
+ public static function get_page($id)
+ {
+ // get (first) page with id = $id; render as json
+ Render::json(Registry::use('database')->query(
+ "SELECT page.*,
+ ( SELECT
+ CONCAT('[', GROUP_CONCAT(JSON_OBJECT(
+ 'id', media.id,
+ 'label', media.label,
+ 'type', media.type,
+ 'path', media.path )),
+ ']')
+ FROM media
+ LEFT JOIN page_media ON page_media.media_id = media.id
+ WHERE page_media.page_id = :id
+ ) AS medias_json
+ FROM page
+ WHERE page.id = :id",
+ [ ':id' => $id]
+ )->getFirst());
+ }
+
+
+ /** add_page
+ * insert a new page
+ *
+ * all parametres are passed via $_POST array
+ *
+ * POST @param title
+ * POST @param body
+ * POST @param status (int): 0 = draft , 1 = published
+ */
+ public static function add_page()
+ {
+ $post = Registry::get('REQUEST')->POST;
+
+ // insert page content into daabase
+ $id = Registry::use('database')->query(
+ "INSERT INTO page (title, `body`, `status`)
+ VALUES (:title, :body, :status)",
+ [
+ ':title' => $post['title'],
+ ':body' => $post['body'],
+ 'status' => $post['status']
+ ]
+ )->lastInsertID();
+
+
+ if (isset($post['media'])) {
+ // update media's privileges;
+ // NOTE: media table; pages have public files (privilege_id=0)
+ self::update_medias_privileges($post['media'], 0);
+
+ // set page's media files
+ // NOTE: page_media table
+ self::create_medias_for_page($post['media'], $id);
+ }
+
+ self::update_pages_cache(); // update pages cache
+
+ return true;
+ }
+
+
+ /** update_page
+ *
+ * POST @param id (int)
+ * POST @param title
+ * POST @param body
+ * POST @param status (int): 0 = draft , 1 = published
+ */
+ public static function update_page()
+ {
+ $post = Registry::get('REQUEST')->POST;
+
+ // print_r($post); die();
+
+ // update page's content
+ Registry::use('database')->query(
+ "UPDATE page
+ SET title = :title, `body` = :body, `status` = :status
+ WHERE id = :id",
+ [
+ ':id' => $post['id'],
+ ':title' => $post['title'],
+ ':body' => $post['body'],
+ ':status' => $post['status']
+ ]
+ );
+
+ if (isset($post['media'])) {
+ // update media's privileges
+ self::update_medias_privileges($post['media'], 0);
+ }
+
+ // remove all old page's links to media files
+ Registry::use('database')->runQuery(
+ "DELETE from page_media WHERE page_id = :page",
+ [ ':page' => $post['id'] ]
+ );
+
+ if (isset($post['media'])) {
+ // update page's media files
+ self::create_medias_for_page($post['media'], $post['id']);
+ }
+
+ self::update_pages_cache(); // update pages cache
+
+ return true;
+ }
+
+
+ /** update pages cache
+ * --- -- -- - - -
+ * Forces re-caching of pages tree
+ * Shall run if anything changes to courses
+ *
+ * @param void
+ * @return (array): ['tree' => ..., 'breadcrumbs' => ... ]
+ */
+ public static function update_pages_cache()
+ {
+ return Cache_service::pages_list(PROXY_IGNORE_CACHE);
+ }
+
+}
\ No newline at end of file
diff --git a/public/app/controllers/admin/Users_admin.php b/public/app/controllers/admin/Users_admin.php
new file mode 100644
index 0000000..4e335c1
--- /dev/null
+++ b/public/app/controllers/admin/Users_admin.php
@@ -0,0 +1,42 @@
+runQuery(
+ "SELECT * FROM privilege",[]
+ );
+ }
+
+ /** edit privileges
+ * ---
+ */
+ public static function edit_privileges()
+ {
+ // TODO: ...
+ }
+
+
+ /** update course
+ * ---
+ */
+ public static function update_course()
+ {
+ // TODO: ...
+ }
+
+}
\ No newline at end of file
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..4549b50
--- /dev/null
+++ b/public/app/controllers/cli/CliContoller.php
@@ -0,0 +1,55 @@
+GET['type']; // get media-type
+ $real_path = MEDIA_STORAGE_ROOT . $file_path; // construct real path
+
+ if (!file_exists($real_path)) {
+ Render::view('error/404');
+
+ } else {
+ Render::file($real_path, $media_type);
+ }
+
+ } else {
+ Render::view('error/404', [
+ 'error_code' => 403,
+ 'moto' => 'Forbidden',
+ 'message' => ''
+ ]);
+ }
+ }
+
+
+ /** get_file_attributes
+ *
+ * returns attributes of a file
+ * (medias are proxied for speed optimization)
+ *
+ * @param $path (string) : file path
+ * @return $file attributes --or-- false
+ */
+ private static function get_file_attributes($path)
+ {
+ $medias = Cache_service::files_attributes();
+
+ foreach($medias as $key => $medi) {
+
+ if ($medi['path'] == $path) {
+
+ return $medi;
+ }
+ }
+
+ return false;
+ }
+
+
+
+ /** COURSES
+ * -------------------------------------------------------------------------
+ */
+
+ /** course
+ * prepare and render the course view
+ *
+ * @param $id : course id
+ */
+ public static function course($id)
+ {
+ $id = intval($id); // course id
+
+ // collect all data needed for course view
+ // --- -- -- - - -
+ $cache = Cache_service::courses_struct();
+ $tree = $cache['tree'];
+ $breadcrumbs = $cache['breadcrumbs'];
+ $lessons = Course_model::lessons_of_course(intval($id));
+
+ // find parent_ids of current category (to open the menu tree)
+ // ---
+ $course_path = [];
+ foreach($breadcrumbs[$id]['parents'] as $key => $parent) {
+ $course_path[] = intval($parent['id']);
+ }
+ $course_path[] = $id;
+
+ // then Render
+ // --- -- -- - - -
+ Render::view('templates/course', [
+ 'id' => $id,
+ 'title' => $breadcrumbs[ $id ]['rec']['label'],
+ 'categories' => $tree,
+ 'breadcrumbs' => $breadcrumbs,
+ 'lessons' => $lessons,
+ 'course_path' => $course_path,
+ // needed by footer
+ 'entity' => Cache_service::pages_list()
+ ]);
+ }
+
+
+
+ /** LESSONS
+ * ------------------------------------------------------------------------
+ */
+
+
+ /** lesson
+ *
+ * check if user is authorized to view the content;
+ * if so, prepare and render the lesson view
+ *
+ * @param $id : lesson id
+ */
+ public static function lesson($id)
+ {
+ $id = intval($id); // lesson id
+
+ $lesson = Course_model::lesson($id); // get lesson
+ $course_id = $lesson['course_id']; // get course id
+ $pri_id = $lesson['privilege_id']; // privilege needed
+
+
+ if (self::is_admin_or_permited($pri_id)) {
+
+ // collect all data needed for course view
+ // --- -- -- - - -
+ $cache = Cache_service::courses_struct();
+ $tree = $cache['tree'];
+ $breadcrumbs = $cache['breadcrumbs'];
+
+ // find parent_ids of current category (to open the menu tree)
+ // ---
+ $course_path = [];
+ foreach($breadcrumbs[$course_id]['parents'] as $key => $parent) {
+ $course_path[] = intval($parent['id']);
+ }
+ $course_path[] = intval($course_id);
+
+ // NOTE: CRITICAL:
+ // do not pass Class::method directly to the eported variables
+ // $entity = Cache_service::pages_list();
+
+ // var_dump($entity); die();
+
+ // then Render
+ // --- -- -- - - -
+ Render::view('templates/lesson', [
+ // main content
+ 'id' => $id,
+ 'course_id' => $course_id,
+ 'title' => $lesson['title'],
+ 'lesson' => $lesson,
+ // needed for side panel and breadcrunbs
+ 'categories' => $tree,
+ 'breadcrumbs' => $breadcrumbs,
+ 'course_path' => $course_path,
+ // needed by footer
+ 'entity' => Cache_service::pages_list()
+ ]);
+
+ } else { // user is not authorized
+ Render::view('error/general', [
+ 'title' => 'Δεν έχετε πρόσβαση',
+ 'message' => 'Θα πρέπει να αγοράσετε το πακέτο πρόσβασης '
+ . $pri_id
+ .' για να δείτε το περιεχόμενο!
+
+ Αγόρασε τώρα το '
+ ]);
+ }
+
+ }
+
+
+ /** lesson
+ *
+ * check if user is authorized to view the content;
+ * if so, prepare and render the lesson view
+ *
+ * @param $id : lesson id
+ */
+ public static function page($id)
+ {
+ $id = intval($id); // lesson id
+
+ $pages = Cache_service::pages_list();
+ $page = $pages[$id];
+
+ // then Render
+ // --- -- -- - - -
+ Render::view('templates/page', [
+ // main content
+ 'id' => $id,
+ 'page' => $page,
+ // needed by footer
+ 'entity' => $pages
+ ]);
+
+ }
+
+
+
+
+}
\ No newline at end of file
diff --git a/public/app/extends/.gitkeep b/public/app/extends/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/public/app/extends/Cache_service.php b/public/app/extends/Cache_service.php
new file mode 100644
index 0000000..52311f3
--- /dev/null
+++ b/public/app/extends/Cache_service.php
@@ -0,0 +1,82 @@
+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..c27c4bf
--- /dev/null
+++ b/public/app/extends/Classroom_user.php
@@ -0,0 +1,121 @@
+id;
+ }
+
+ /** setID()
+ *
+ * @param int : user id
+ *
+ * @return User
+ */
+ public function setID(int $id): self
+ {
+ $this->id = $id;
+ return $this;
+ }
+
+
+ // name
+ // --- -- -- - - -
+
+ /** getName
+ * @return int
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /** setName()
+ *
+ * @param string : user's full name
+ *
+ * @return User
+ */
+ public function setName(string $name): self
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+
+ // privileges
+ // --- -- -- - - -
+
+ /** 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..52eab53
--- /dev/null
+++ b/public/app/extends/Send_mail.php
@@ -0,0 +1,212 @@
+ user email
+ * name => user full name
+ * subject => subject
+ * body => email message body
+ * ]
+ *
+ * then send the email;
+ *
+ * @param $activator (array): [
+ * email => user email,
+ * name => user full name,
+ * code => activation code
+ * ]
+ *
+ * @return success_starus (boolean)
+ *
+ * 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($activator)
+ {
+ $activation_url = SITE_URL ."/account/activate?ticket=". $activator['code'];
+ $envelope = [
+ 'email' => $activator['email'],
+ 'name' => $activator['name'],
+ 'subject' => 'Εγγραφή στο Classroom',
+ 'body' => "Χαίρετε,
+ το παρόν αυτοποιημένο email σάς έχει σταλεί
+ γιατί έχει γίνει αίτημα εγγραφής σας στο Classroom.
+
+ Όνομα επαφής: ". $activator['name'] ."
+ Email : ". $activator['email'] ."
+
+ Για να ενεργοποιήσετε την πρόσβασή σας στο site
+ θα πρέπει ακολουθήσετε τον παρακάτω σύνδεσμο:
+ ". $activation_url .".
+
+
+ Μετά την ενεργοποίηση θα έχετε πρόσσβαση
+ στο περιεχόμενο του site.
+ Μην απαντήσετε στο email,
+ δεν υπάρχει φυσική επαφή η οποία να λαμβάνει τυχόν replies."
+ ];
+
+ switch (DEFAULT_MAIL_SERVICE) {
+ case 'mailjet':
+ $reply = self::send_mailjet($envelope);
+ break;
+
+ case 'phpMailer':
+ $reply = self::send_phpMailer($envelope);
+ break;
+ }
+
+ return $reply;
+
+ }
+
+
+ /** send phpMailer
+ *
+ * send email using phpMailer class;
+ * optional SMTP server configuration;
+ *
+ * @param $envelope
+ * @return success_starus (boolean)
+ */
+ public static function send_phpMailer($envelope)
+ {
+ $mail = new PHPMailer();
+
+ # // setup smpt
+ $mail->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;
+
+ // 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 = $envelope['subject'];
+ $mail->Body = $envelope['body'];
+
+ return (!$mail->send()) ? false : true;
+
+ }
+
+
+ /** send_mailjet
+ *
+ * send email using CURL mail-relay of Mailjet
+ *
+ * @param $envelope
+ * @return success_starus (boolean)
+ */
+ public static function send_mailjet($envelope)
+ {
+ $body = [
+ 'Messages' => [
+ [
+ 'From' => [
+ 'Email' => REPLY_TO_EMAIL,
+ 'Name' => MAIL_FROM_NAME
+ ],
+ 'To' => [
+ [
+ 'Email' => $envelope['email'],
+ 'Name' => $envelope['name']
+ ]
+ ],
+ 'Subject' => $envelope['subject'],
+ 'HTMLPart' => $envelope['body']
+ ]
+ ]
+ ];
+
+ $ch = curl_init();
+
+ curl_setopt($ch, CURLOPT_URL, "https://api.mailjet.com/v3.1/send");
+ curl_setopt($ch, CURLOPT_POST, 1);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+ 'Content-Type: application/json')
+ );
+ curl_setopt(
+ $ch,
+ CURLOPT_USERPWD,
+ MAILJET_APIKEY. ':'. MAILJET_SECRET
+ );
+ $server_output = curl_exec($ch);
+ curl_close ($ch);
+
+ $response = json_decode($server_output);
+
+ return ($response->Messages[0]->Status == 'success');
+
+ }
+
+
+
+}
+
+
+
+
+
+/** NOTE:
+ * -----------------------------------------------------------------------------
+ * (brainstorming)
+ *
+
+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
+
+ * -----------------------------------------------------------------------------
+ */
diff --git a/public/app/models/.gitkeep b/public/app/models/.gitkeep
new file mode 100644
index 0000000..e69de29
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/Privilege_model.php b/public/app/models/admin/Privilege_model.php
new file mode 100644
index 0000000..c385484
--- /dev/null
+++ b/public/app/models/admin/Privilege_model.php
@@ -0,0 +1,42 @@
+runQuery(
+ "SELECT * FROM privilege",[]
+ );
+ }
+
+ /** edit privileges
+ * ---
+ */
+ public static function edit_privileges()
+ {
+ // TODO: ...
+ }
+
+
+ /** update course
+ * ---
+ */
+ public static function update_course()
+ {
+ // TODO: ...
+ }
+
+}
\ 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..2885dc1
--- /dev/null
+++ b/public/app/models/admin/User_model.php
@@ -0,0 +1,299 @@
+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);
+
+ // set reader role
+ self::set_user_role($new_user_id, 5);
+
+ // 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
+ ];
+ }
+
+
+ /** set_user_privileges
+ *
+ * @param $privileges (array)
+ */
+ public static function set_user_privileges($user, $privileges)
+ {
+ $db = Registry::use('database'); // database connection
+ foreach($privileges as $pri) { // pri = privilege id
+ $db->runQuery(
+ "INSERT INTO user_privilege (user_id, privilege_id) VALUES (:user, :pri)",
+ [ ':user' => $user, ":pri" => $pri ]
+ );
+ }
+ return true;
+ }
+
+
+ /** set_user_role
+ *
+ * user, role are (int) IDs
+ */
+ public static function set_user_role($user, $role)
+ {
+
+ Registry::use('database')->runQuery(
+ "INSERT INTO user_role (user_id, role_id) VALUES (:user, :role)",
+ [ ':user' => $user, ":role" => $role ]
+ );
+
+ return true;
+ }
+
+
+ /** 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/Course_model.php b/public/app/models/cms/Course_model.php
new file mode 100644
index 0000000..6614993
--- /dev/null
+++ b/public/app/models/cms/Course_model.php
@@ -0,0 +1,314 @@
+runQuery(
+ "SELECT * FROM course ORDER BY label",
+ []
+ );
+ }
+
+ /** courses struct
+ *
+ * a super array with almost any info needed about courses
+ *
+ * @return (array) ['tree' => ..., 'breadcrumbs' => ... ]
+ */
+ public static function courses_struct()
+ {
+ $tree = self::category_tree();
+ return [
+ 'tree' => $tree,
+ 'breadcrumbs' => self::breadcrumbs($tree)
+ ];
+ }
+
+
+ /** 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 static function category_tree()
+ {
+ $categories = self::get_categories(); // get all categories
+
+ $tree = self::to_tree($categories); // format to a tree
+
+ $tree_wParents = self::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
+ */
+ private static 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'],
+ 'label' => $rec['label'],
+ 'order' => $rec['order'],
+ 'parent' => $rec['parent_id']
+ ],
+ 'childs' => self::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 static 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'] == [])
+ ? []
+ : self::tree_parents($node['childs'], $push_parents)
+ ];
+ }
+
+ return $tree_with_parents;
+ }
+
+
+ /** all_breadcrumbs
+ * -------------------------------------------------------------------------
+ *
+ * returns an array of all breadcrumbs
+ * where array-key of each record is category[id]
+ *
+ * NOTE:
+ * ---
+ * Course_model::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 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['label'] . $delimiter;
+ }
+ $breadcrumb .= $node['rec']['label']; // 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::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'],
+ 'label' => $kid['rec']['label']
+ ];
+ }
+ return $childs;
+ }
+
+
+
+
+ /** LESSONS
+ * -------------------------------------------------------------------------
+ */
+
+
+ /** lessons of course
+ *
+ * @param $id (int) : course_id
+ */
+ public static function lessons_of_course($id)
+ {
+ // TODO: order results in some way
+
+ return Registry::use('database')->runQuery(
+ "SELECT lesson.*, lesson_privilege.privilege_id FROM lesson
+ LEFT JOIN lesson_privilege ON lesson_privilege.lesson_id = lesson.id
+ WHERE lesson.status = 1 AND lesson.course_id = :id
+ ORDER BY lesson_privilege.privilege_id ASC",
+ [':id' => $id]
+ );
+ }
+
+ /** lesson
+ *
+ * @param $id (int) : lesson id
+ */
+ public static function lesson($id)
+ {
+ // TODO: order results in some way
+
+ return Registry::use('database')->query(
+ "SELECT lesson.*, lesson_privilege.privilege_id FROM lesson
+ LEFT JOIN lesson_privilege ON lesson_privilege.lesson_id = lesson.id
+ WHERE lesson.status = 1 AND lesson.id = :id",
+ [':id' => $id]
+ )->getFirst();
+ }
+
+
+
+ /** files
+ * return all files
+ * @param void
+ * @return array
+ */
+ public static function files()
+ {
+ return Registry::use('database')->runQuery("SELECT * from media", []);
+ }
+
+
+ /** pages
+ *
+ * return all pages as array;
+ * array key of each item shall be the page-id
+ *
+ * @param void
+ * @return array
+ */
+ public static function pages()
+ {
+ $result = [];
+ $pages = Registry::use('database')->runQuery("SELECT * FROM page", []);
+ foreach($pages as $key => $page) {
+ $result[ $page['id'] ] = [
+ 'title' => $page['title'],
+ 'body' => $page['body'],
+ 'status' => $page['status']
+ ];
+ }
+ // print_r($result); die();
+ return $result;
+ }
+
+}
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/.gitkeep b/public/app/routes/.gitkeep
new file mode 100644
index 0000000..e69de29
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 @@
+ 'Διαχείριση Classroom',
+ 'action' => 'panel',
+ ]);
+});
+Route::add('/admin/panel', function () { // admin panel #2
+ Auth::allowRoles([1, 2, 3]);
+ Render::view('admin/skeleton', [
+ 'title' => 'Διαχείριση Classroom',
+ 'action' => 'panel',
+ ]);
+});
+
+
+// admin categories (=courses)
+////////////////////////////////////////////////////////////////////////////////
+
+
+Route::add('/admin/categories', function () { // manage categories page
+ Auth::allowRoles([1, 2, 3]);
+ Render::view('admin/skeleton', [
+ 'title' => 'Διαχείριση Κεφαλαίων',
+ 'action' => 'categories',
+ ]);
+});
+
+Route::add('/admin/api/categories', function () { // ajax: get all courses; with breadcrumbs
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Course_admin::breadcrumbs();
+});
+
+Route::add('/admin/api/categories/add', function () { // ajax: add new category (course)
+ Auth::allowRoles([1, 2, 3]);
+ Course_admin::add_course();
+}, 'post');
+
+Route::add('/admin/api/categories/update', function () { // ajax: edit category (course)
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Course_admin::update_course();
+},
+'post');
+
+
+// admin privileges
+////////////////////////////////////////////////////////////////////////////////
+
+
+Route::add('/admin/privileges', function () { // manage privileges page
+ Auth::allowRoles([1, 2, 3]);
+ Render::view('admin/skeleton', [
+ 'title' => 'Διαχείριση Δικαιωμάτων Πρόσβασης',
+ 'action' => 'privileges',
+ ]);
+});
+
+// ajax: get all privileges (courses; with breadcrumbs)
+// use proxy (cache results); send as json
+Route::add('/admin/api/privileges', function () {
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::json( Cache_service::privileges_hierarchy() );
+});
+
+// add privilege
+
+// update privilege
+
+
+
+// admin lessons
+////////////////////////////////////////////////////////////////////////////////
+
+Route::add('/admin/lessons', function () { // request admin-lessons page
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::view('admin/skeleton', [
+ 'title' => 'Διαχείριση Μαθημάτων',
+ 'action' => 'lessons',
+ ]);
+});
+
+Route::add('/admin/edit_lesson', function () { // new lesson form
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::view('admin/skeleton', [
+ 'title' => 'Νέο Μάθημα',
+ 'action' => 'edit_lesson',
+ ]);
+});
+
+Route::add('/admin/edit_lesson/([0-9]*)', function ($id) { // edit lesson form
+ Auth::allowRoles([1, 2, 3]);
+ Render::view('admin/skeleton', [
+ 'title' => 'Επεξεργασία Μαθήματος',
+ 'action' => 'edit_lesson',
+ 'id' => intval($id)
+ ]);
+});
+
+
+Route::add('/admin/api/lesson/([0-9]*)', function ($id) { // ajax: get lesson
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Course_admin::get_lesson($id);
+});
+
+
+Route::add('/admin/api/lesson/add', function () { // ajax: add lesson
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::json(['success' => Course_admin::add_lesson()]);
+}, 'post');
+
+
+Route::add('/admin/api/lesson/update', function () { // ajax: update lesson
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::json(['success' => Course_admin::update_lesson()]);
+}, 'post');
+
+
+Route::add('/admin/api/lessons', function () { // ajax: get all lessons
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Course_admin::all_lessons();
+});
+
+
+
+// admin files
+////////////////////////////////////////////////////////////////////////////////
+
+Route::add('/admin/files', function () { // admin-files panel
+ Auth::allowRoles([1, 2, 3]);
+ Render::view('admin/skeleton', [
+ 'title' => 'Διαχείριση Μέσων (media)',
+ 'action' => 'files',
+ ]);
+});
+
+
+Route::add('/admin/api/files', function () { // ajax: get lesson
+ Auth::allowRoles([1, 2, 3]);
+ Render::json(Course_admin::files());
+});
+
+// ajax: file-upload
+// ---
+Route::add('/admin/api/file_upload', function () {
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::json(Course_admin::upload_file());
+}, 'post');
+
+
+
+
+// admin page
+////////////////////////////////////////////////////////////////////////////////
+
+Route::add('/admin/pages', function () { // request admin-lessons page
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::view('admin/skeleton', [
+ 'title' => 'Διαχείριση Σελίδων',
+ 'action' => 'pages',
+ ]);
+});
+
+
+Route::add('/admin/edit_page', function () { // new page form
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::view('admin/skeleton', [
+ 'title' => 'Νέα Σελίδα',
+ 'action' => 'edit_page',
+ ]);
+});
+
+Route::add('/admin/edit_page/([0-9]*)', function ($id) { // edit page form
+ Auth::allowRoles([1, 2, 3]);
+ Render::view('admin/skeleton', [
+ 'title' => 'Επεξεργασία Σελίδας',
+ 'action' => 'edit_page',
+ 'id' => intval($id)
+ ]);
+});
+
+
+Route::add('/admin/api/page/([0-9]*)', function ($id) { // ajax: get page
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Course_admin::get_page($id);
+});
+
+
+Route::add('/admin/api/page/add', function () { // ajax: add page
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::json(['success' => Course_admin::add_page()]);
+}, 'post');
+
+
+Route::add('/admin/api/page/update', function () { // ajax: update page
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::json(['success' => Course_admin::update_page()]);
+}, 'post');
+
+
+Route::add('/admin/api/pages', function () { // ajax: get all pages
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Course_admin::all_pages();
+});
+
diff --git a/public/app/routes/frontend.php b/public/app/routes/frontend.php
new file mode 100644
index 0000000..be06b1c
--- /dev/null
+++ b/public/app/routes/frontend.php
@@ -0,0 +1,169 @@
+ Cache_service::courses_struct()['tree'],
+ 'entity' => Cache_service::pages_list()
+ ]);
+ // need redirect?... header('Location: /some/default/url', true, 302);
+});
+
+
+// Error pages
+////////////////////////////////////////////////////////////////////////////////
+
+Route::notFound( function() { // 404 error page
+ header("HTTP/1.0 404 Not Found");
+ Render::view('error/404', ['message' => 'nobody knows it (but you got a secret smile;)']);
+});
+
+
+
+// Common content
+////////////////////////////////////////////////////////////////////////////////
+
+
+Route::add('/course/([0-9]*)', // request course by course-id
+ function($id) { Course::course($id); }
+);
+
+Route::add('/lesson/([0-9]*)', // request lesson by lesson-id
+ function($id) { Course::lesson($id); }
+);
+
+Route::add('/page/([0-9]*)', // request lesson by lesson-id
+ function($id) { Course::page($id); }
+);
+
+Route::add('/serve/file/([0-9a-zA-Z-_\.\/]*)', // serve file by path
+ function ($path) { Course::serve_file($path); } // (also ?type= is sent)
+);
+
+
+
+
+
+// Account
+///////////////////////////////////////////////////////////////////////////////////////////////
+
+// [ok] login
+// [ok] register
+// [ok] activate
+// [..] ask-reset
+// [..] reset
+// [..] profile
+// [..] edit-profile
+// [..] subscriptions
+// [..] payments
+// [..] pay
+// [..] order
+
+
+
+
+/** URL-format Proposals and common Route regex solutions
+ * -----------------------------------------------------------------------------
+ *
+ * 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]); });
+ *
+ * othet routes used in some user-cases
+
+# 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]); }
+# );
+ *
+ *
+ * OTHER 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/routes/user.php b/public/app/routes/user.php
new file mode 100644
index 0000000..7ad0fa6
--- /dev/null
+++ b/public/app/routes/user.php
@@ -0,0 +1,84 @@
+ then sends to POST:/account/check-login
+Route::add('/login', function() { Render::view('user/login'); });
+
+// request registration ... -> then sends to POST:/account/register
+Route::add('/registration', function() { Render::view('user/registration'); });
+
+// request logout
+Route::add('/logout', function() {
+ Auth::logout();
+ header('Location: /'); die();
+});
+
+// request activation
+Route::add('/account/activate', function() { Auth::activate(); } );
+
+// request password reset
+Route::add('/account/reset_password', function() { Auth::reset_password(); } );
+
+
+
+// Replies to common requests (POST method)
+// --- -- -- - - -
+
+// user sends login form
+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'
+);
+
+
+// Account Management
+// --- -- -- - - -
+
+// user profile
+Route::add('/account/profile', function() {
+ $user = Auth::is_connected();
+ if ($user === false) {
+ Render::view('error/general',
+ [
+ 'title' => 'Nope!',
+ 'message' => "