<?php class OsController { protected $params, $files, $layout = 'admin', $views_folder = LATEPOINT_VIEWS_ABSPATH_SHARED, $return_format = 'html', $extra_css_classes = ['latepoint']; public array $fields_to_update = []; // if an action can only be accessed by a backend user, we need to define capabilities that are required public array $controller_capabilities = ['settings__edit']; // default for controller public array $action_capabilities = []; // per action public array $action_access = [ 'customer' => [], 'public' => [] ]; public $vars; public $route_name; function __construct(){ $this->params = $this->get_params(); $this->files = $this->get_files(); $this->set_layout($this->layout); $this->vars['page_header'] = __('Bookings', 'latepoint'); $this->vars['breadcrumbs'][] = array('label' => __('Dashboard', 'latepoint'), 'link' => OsRouterHelper::build_link(['dashboard', 'index'] )); $this->load_settings(); $this->vars['logged_in_customer'] = OsAuthHelper::get_logged_in_customer(); } public function check_nonce($action, $custom_nonce = ''){ $nonce = !empty($custom_nonce) ? $custom_nonce : $this->params['_wpnonce']; if(!wp_verify_nonce($nonce, $action)){ if($this->get_return_format() == 'json'){ $this->send_json(array('status' => LATEPOINT_STATUS_ERROR, 'message' => __('Invalid Request', 'latepoint'))); }else{ wp_die(); } } } public function can_current_user_access_action(string $action): bool{ if(in_array($action, $this->action_access['public'])){ // public route $can = true; }elseif(in_array($action, $this->action_access['customer']) && OsAuthHelper::get_current_user()->customer){ // customer route & customer is logged in $can = true; }else{ // backend route, check for capabilities $can = OsAuthHelper::get_current_user()->has_capability($this->get_capabilities_required_for_action($action)); } /** * Determines if a currently logged in user can access controller's action * * @since 4.7.0 * @hook latepoint_can_current_user_access_action * * @param {bool} $can Decision true|false * @param {string} $action Name of the action that is being called * @param {LatePoint\Misc\User} $current_user Currently logged in latepoint user * @returns {bool} Decision true|false */ return apply_filters('latepoint_can_current_user_access_action', $can, $action, OsAuthHelper::get_current_user()); } public function get_capabilities_required_for_action($action){ return OsRolesHelper::get_capabilities_required_for_controller_action(get_class($this), $action); } function generate_css_class($view_name){ $class_name_filtered = strtolower(preg_replace('/^Os(\w+)Controller/i', '$1', static::class)); return "latepoint-view-{$class_name_filtered}-{$view_name}"; } protected function load_settings(){ } public function access_not_allowed(){ $this->format_render(__FUNCTION__, [], [], true); exit(); } function format_render($view_name, $extra_vars = array(), $json_return_vars = array(), $from_shared_folder = false){ echo $this->format_render_return($view_name, $extra_vars, $json_return_vars, $from_shared_folder); } // You can pass array to $view_name, ['json_view_name' => ..., 'html_view_name' => ...] function format_render_return($view_name, $extra_vars = array(), $json_return_vars = array(), $from_shared_folder = false){ $html = ''; if($this->get_return_format() == 'json'){ if(is_array($view_name)) $view_name = $view_name['json_view_name']; $response_html = $this->render($this->get_view_uri($view_name, $from_shared_folder), 'none', $extra_vars); $this->send_json(array_merge(array('status' => LATEPOINT_STATUS_SUCCESS, 'message' => $response_html), $json_return_vars)); }else{ if(is_array($view_name)) $view_name = $view_name['html_view_name']; $this->extra_css_classes[] = $this->generate_css_class($view_name); $this->vars['extra_css_classes'] = $this->extra_css_classes; $html = $this->render($this->get_view_uri($view_name, $from_shared_folder), $this->get_layout(), $extra_vars); } return $html; } function set_layout($layout = 'admin'){ if(isset($this->params['layout'])){ $this->layout = $this->params['layout']; }else{ $this->layout = $layout; } } function get_layout(){ return $this->layout; } function set_return_format($format = 'html'){ $this->return_format = $format; } function get_return_format(){ return $this->return_format; } function send_json($data, $status_code = null){ if(!empty($this->fields_to_update)) $data['fields_to_update'] = $this->fields_to_update; wp_send_json($data, $status_code); } function get_view_uri($view_name, $from_shared_folder = false){ if($from_shared_folder){ $view_uri = LATEPOINT_VIEWS_ABSPATH_SHARED.$view_name.'.php'; }else{ $view_uri = $this->views_folder.$view_name.'.php'; } return $view_uri; } private function get_safe_layout_path($layout) { // 1. Remove any path separators and null bytes $layout = str_replace(['/', '\\', "\0"], '', $layout); // 2. Remove any dots to prevent directory traversal $layout = str_replace('.', '', $layout); // 3. Only allow alphanumeric, underscore, and hyphen $layout = preg_replace('/[^a-zA-Z0-9_-]/', '', $layout); // 4. Construct the full path $layout_file = $this->add_extension($layout, '.php'); $full_path = LATEPOINT_VIEWS_LAYOUTS_ABSPATH . $layout_file; // 5. Use realpath to resolve any remaining traversal attempts $real_path = realpath($full_path); $base_path = realpath(LATEPOINT_VIEWS_LAYOUTS_ABSPATH); // 6. Ensure the resolved path is within the layouts directory if ($real_path && $base_path && strpos($real_path, $base_path) === 0) { return $real_path; } return false; } // render view and if needed layout, when layout is rendered - view variable is passed to a layout file function render($view, $layout = 'none', $extra_vars = array()){ $this->vars['route_name'] = $this->route_name; extract($extra_vars); extract($this->vars); ob_start(); if($layout != 'none'){ $layout_path = $this->get_safe_layout_path($layout); // rendering layout, view variable will be passed and used in layout file if($layout_path){ include $layout_path; }else{ __('Invalid layout', 'latepoint'); } }else{ include $this->add_extension($view, '.php'); } $response_html = ob_get_clean(); return $response_html; } /* Adds extension to a file string if its missing */ function add_extension($string = '', $extension = '.php'){ if(substr($string, -strlen($extension))===$extension) return $string; else return $string.$extension; } function get_files(){ return OsParamsHelper::get_files(); } function get_params(){ return OsParamsHelper::get_params(); } }