diff options
Diffstat (limited to 'public/app/controllers')
| -rw-r--r-- | public/app/controllers/.gitkeep | 0 | ||||
| -rw-r--r-- | public/app/controllers/Admin.php | 58 | ||||
| -rw-r--r-- | public/app/controllers/Auth.php | 382 | ||||
| -rw-r--r-- | public/app/controllers/admin/.gitkeep | 0 | ||||
| -rw-r--r-- | public/app/controllers/admin/Course_admin.php | 676 | ||||
| -rw-r--r-- | public/app/controllers/admin/Users_admin.php | 42 | ||||
| -rw-r--r-- | public/app/controllers/api/.gitkeep | 0 | ||||
| -rw-r--r-- | public/app/controllers/api/Common_api.php | 281 | ||||
| -rw-r--r-- | public/app/controllers/api/Doc_api.php | 34 | ||||
| -rw-r--r-- | public/app/controllers/cli/CliContoller.php | 55 | ||||
| -rw-r--r-- | public/app/controllers/cms/Course.php | 252 |
11 files changed, 1780 insertions, 0 deletions
diff --git a/public/app/controllers/.gitkeep b/public/app/controllers/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/public/app/controllers/.gitkeep diff --git a/public/app/controllers/Admin.php b/public/app/controllers/Admin.php new file mode 100644 index 0000000..6f6858c --- /dev/null +++ b/public/app/controllers/Admin.php @@ -0,0 +1,58 @@ +<?php +namespace app\controllers; + +use Registry; +use Render; + +// user classes and models +use app\extends\Classroom_user; +use app\extends\Classroom_manager; +use app\models\admin\User_model; + +use app\extends\Send_mail; +use app\extends\Mail_jet; + + +/** class Auth + * + * handles user's Authentication and Authorizarion + * + */ +class Admin { + + /** admin cateogories + * + */ + public static function categories() + { + Render::view('admin/categories'); + } + + + /** admin lessons + * + */ + public static function lessons() + { + + } + + + /** admin pages + * + */ + public static function pages() + { + + } + + /** admin users + * + */ + public static function users() + { + + } + + +}
\ No newline at end of file diff --git a/public/app/controllers/Auth.php b/public/app/controllers/Auth.php new file mode 100644 index 0000000..4a6b9ed --- /dev/null +++ b/public/app/controllers/Auth.php @@ -0,0 +1,382 @@ +<?php +namespace app\controllers; + +use Registry; +use Render; + +// user classes and models +use app\extends\Classroom_user; +use app\extends\Classroom_manager; +use app\models\admin\User_model; + +use app\extends\Send_mail; +use app\extends\Mail_jet; + + +/** class Auth + * + * handles user's Authentication and Authorizarion + * + */ +class Auth { + + /** login + * + * checks visitor's credentials; + * if valid, authenticates user + * + */ + public static function login() + { + $req = Registry::get('REQUEST'); + + // get the record of the target user + $record = User_model::checkUser($req->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 --- /dev/null +++ b/public/app/controllers/admin/.gitkeep 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 @@ +<?php + +namespace app\controllers\admin; + +use Registry; +use Render; +use app\models\cms\Course_model; +use app\extends\Cache_service; + +class Course_admin { + + + ## ------------------------------------------------------------------------- + ## + ## COURSE (category) METHODS + ## + ## ------------------------------------------------------------------------- + + + /** breadcrumbs + * --- + * responds to ajax GET: /admin/api/categories + */ + public static function breadcrumbs() + { + Render::json( Cache_service::courses_struct()['breadcrumbs'] ); + } + + /** add course + * --- + * @param void (get data from POST) + */ + public static function add_course() + { + // insert new category; id = new category id + $id = Registry::use('database')->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 @@ +<?php + +namespace app\controllers\admin; + +use Registry; +use Render; + + +class Users_admin { + + /** privileges + * --- + * get all privileges + * + * @param void + * @return privileges (array) + */ + public static function privileges() + { + return Registry::use('database')->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 --- /dev/null +++ b/public/app/controllers/api/.gitkeep 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 @@ +<?php + +namespace app\controllers\api; + +use \Registry; +use app\models\market\Product_model; +use app\models\market\ProductCategories_model; + +/** Common_api + * --- + * Controller for handling common API calls + * about main tables of the project; + * + * includes aglo for passing special filters and sorting + * (check parse_Where_OrderBy() method) + */ +class Common_api +{ + + /** APITables + * Method returns allowed tables to accept API calls + * The return is an array of [label => 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 @@ +<?php + +namespace app\controllers\api; + +use app\models\market\ProductCategories_model; +use app\models\Jorge; + +class Doc_api +{ + + /** */ + public static function tree($store = 904) + { + $tree = proxy( + [ProductCategories_model::class,'tree'], + [], CACHE_ROOT_TTL, + PROXY_IGNORE_CACHE + ); + reply_json([ 'success' => 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 @@ +<?php + +namespace app\controllers\cli; + +/** CliContoller + * + * A typical cli constoller; this one is used to + * re-generate cashes of commonly used entities + * with expensive queries like (ex:) categories; + * + * Proxy calls make use of PROXY_IGNORE_CACHE + * + * CliController calls Model methods who create + * cashed data with the exact same arguments as + * when called from the web interface; this way + * the Model::method(arguments) triplete creates + * the same keys as via the web interface. + * + * NOTE: Only Cli interface is allowed¨ + * `if (php_sapi_name() != 'cli') return false;` + * + * The Cli-interfaced script can run manualy or + * (most usual scenario) scheduled via a cron job + * in order to refresh cashes befor expiration. + * + * --------------------------------------------- + */ +class CliContoller +{ + + + /** (re-) cacheCategoriesTree + * regenerate cache for product_categories tree + * @param (void) + */ + public static function cacheCategoriesTree() + { + // Only Cli interface is allowed + if (php_sapi_name() != 'cli') return false; + + # PROXY call: + # CMS \ Course_model::courses_struct(): + # + Refresh Cache + # -------------------------------------- + echo textColor("Creating category_products tree Cache ... ", \NORMAL); + $reply = proxy( + [\app\models\cms\Course_model::class, 'courses_struct'], + [], \CACHE_ROOT_TTL, + \PROXY_IGNORE_CACHE + ); + echo ($reply == false) ? textColor("Failed\n", \FAIL) : textColor("Done!\n", \SUCCESS); + } + + +}
\ No newline at end of file diff --git a/public/app/controllers/cms/Course.php b/public/app/controllers/cms/Course.php new file mode 100644 index 0000000..47eac5f --- /dev/null +++ b/public/app/controllers/cms/Course.php @@ -0,0 +1,252 @@ +<?php + +namespace app\controllers\cms; + +use Registry; +use Render; +use app\controllers\Auth; +use app\models\cms\Course_model; +use app\extends\Cache_service; + +class Course { + + + /** PERMITION + * ------------------------------------------------------------------------- + */ + + + /** is admin or permited + * + * shortcut method for checking access authorization + * + * returns true if user belogns to the admin group + * OR has the specified permition/privilege + * + * @param $privilege_id (int) + * + * NOTE: + * unlike the other authorization methods ... + * $privilege_id is NOT an array but a single privilege id + * + * @return true|false + */ + private static function is_admin_or_permited( $privilege_id ) + { + return ( + Auth::in_admin_group() + || Auth::hasPermition([ $privilege_id ]) + ); + } + + + + /** FILES + * ------------------------------------------------------------------------- + */ + + /** serve file by file_path + * (request is valid only for admin users) + * + * @param $file_path (string): relative file path + * GET @param type (string) : media-type of file + */ + public static function serve_file($file_path) + { + $file = self::get_file_attributes($file_path); + + // if user is authorized + if (self::is_admin_or_permited($file['privilege_id'])) { + + $media_type = Registry::get('REQUEST')->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 + .' για να δείτε το περιεχόμενο! + <br><br> + Αγόρασε τώρα το <button class="btn">πακέτο πρόσβασης '. $pri_id .'</button>' + ]); + } + + } + + + /** 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 |
