log = mfLoghandler::singleton(); $this->requestLog = new mfLog_File(); $this->requestLog->init(BASEDIR."/var/log/api-request.log"); $this->logRequest(); $this->loadRequest($params); $this->logRequest2(); register_shutdown_function(["mfBaseApicontroller", "return_errors"]); // allow origins from config file if(defined("API_CORS_ALLOWED_HOSTNAMES") && is_array(API_CORS_ALLOWED_HOSTNAMES)) { foreach(API_CORS_ALLOWED_HOSTNAMES as $origin) { $this->addAllowedOrigin($origin); } } // CORS preflight // allow all origins if($this->http_method == "OPTIONS") { // dont execute route, OPTIONS only requires CORS headers header("Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS"); header("Access-Control-Allow-Headers: X-Api-Key, accept, Content-Type"); if(preg_match('#^(https?)://([^/:]+)(:\d+)?/?$#i', $this->headers['origin'], $m)) { $origin_proto = $m[1]; $origin_hostname = $m[2]; $origin_port = $m[3]; $allowed_origin = $origin_proto."://".$origin_hostname; if($origin_port) { $allowed_origin .= "$origin_port"; } header("Access-Control-Allow-Origin: $allowed_origin"); $this->return(mfResponse::Ok()); } } // run Controllers init() function if(method_exists($this,"init")) { $this->init(); } if($this->requireAuth) { $this->authenticateUser(); if(method_exists($this,"authenticated")) { $afterAuthResult = $this->authenticated(); // event defined in extending class if(mfResponse::isResponse($afterAuthResult)) { $this->return($afterAuthResult); } } } // Apicontroller should add allowed hostnames with $this->addAllowedOrigin() $this->createCorsHeaders(); // route to action $this->route = $params['apicall'].((array_key_exists("apiparams", $params)) ? $params['apiparams'] : ""); $responseData = $this->runRoute($this->route); if(!$responseData) { $this->return(mfResponse::InternalServerError()); } // return respnse $this->return($responseData); } private function logRequest() { $this->requestLog->debug("=================================================================="); $this->requestLog->debug("new API request: ".$_SERVER['REQUEST_METHOD']." ".$_SERVER['REQUEST_URI']. " from ".$_SERVER['REMOTE_ADDR']); $this->requestLogstr = ""; foreach($_GET as $key => $value) { $this->requestLogstr .= "; $key='$value'"; } $this->requestLog->debug("GET: ".print_r($_GET, true)); $this->requestLogstr = ""; foreach($_POST as $key => $value) { $this->requestLogstr .= "; $key='$value'"; } $this->requestLog->debug("POST: ".print_r($_POST, true)); } // things to log after loadRequest() private function logRequest2() { $this->requestLog->debug("POST Raw: ".$this->raw_post_body); $this->requestLog->debug("POST JSON: ".$this->request_json); $this->requestLog->debug("Headers: ".print_r($this->headers, true)); } private function authenticateUser() { $key = false; //var_dump($this->headers);exit; if(array_key_exists("x-api-key", $this->headers) && $this->headers['x-api-key']) { $key = $this->headers['x-api-key']; // change to X-Auth-Token } if(array_key_exists("apikey", $this->get)) { $key = $this->get['apikey']; // token } $me = new User; $me->loadByApikey($key); if(!$me->id) { header("Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS"); header("Access-Control-Allow-Headers: X-Api-Key, accept, Content-Type"); if(preg_match('#^(https?)://([^/:]+)(:\d+)?/?$#i', $this->headers['origin'], $m)) { $origin_proto = $m[1]; $origin_hostname = $m[2]; header("Access-Control-Allow-Origin: ".$origin_proto."://".$origin_hostname); } $this->return(mfResponse::Unauthorized(['message' => "API key missing or invalid"])); } $_SESSION[MFAPPNAME.'_username'] = $me->username; $this->log->info("Authenticated '".$me->username."' with api key"); $this->me = $me; } private function loadRequest($params) { foreach(apache_request_headers() as $header => $value) { $this->headers[strtolower($header)] = $value; } // GET parameters $get = $params; unset($get['mod']); unset($get['action']); unset($get['apiv']); unset($get['apicall']); unset($get['apiparams']); unset($get['http_method']); $this->get = $get; $this->mod = $params['mod']; $this->action = $params['action']; // check for api version $apiversion = API_VERSION; if($params['apiv'] && $params['apiv'] != $apiversion) { $apiversion = $params['apiv']; } $this->apiversion = $apiversion; $this->http_method = strtoupper($_SERVER['REQUEST_METHOD']); if($this->http_method == "BREW") { // easter egg :) $this->return(mfResponse::ImATeaPot()); } // POST Request $post = []; if($this->http_method == "POST") { $post = $this->getPostRequest(); if($post === false) { $post = []; //$this->return(mfResponse::BadRequest(["message" => "Invalid request body; expected Form-Urlencoded or JSON format"])); } $this->post = $post; } return true; } private function getPostRequest() { $body = $this->getRequestBody(); if(!is_string($body) && is_array($body)) { // request is parsed already ($_POST) return $body; } // otherwise request likely is json $json_request = json_decode($body); if(json_last_error() === JSON_ERROR_NONE) { //var_dump((array)$json_request);exit; $this->request_json = $body; return (array)$json_request; } return false; } private function getRequestBody() { $request_charset = "utf-8"; if(array_key_exists("CONTENT_TYPE", $_SERVER) && preg_match('#application/json#i', $_SERVER["CONTENT_TYPE"])) { // request body is JSON $request_body = file_get_contents('php://input'); $m = []; if(preg_match('#charset\s*=\s*["\']?([^ "\']+)["\']?\s*;?#i', $_SERVER["CONTENT_TYPE"], $m)) { $request_charset = strtolower($m[1]); } if($request_charset != "utf-8") { $request_body = mb_convert_encoding($request_body, "utf-8", $request_charset); } //var_dump(mb_detect_encoding($request_body), $charset); return $request_body; } // Request body is urlencoded or multipart-formdata if(array_key_exists("CONTENT_TYPE", $_SERVER) && preg_match('#charset\s*=\s*["\']?([^ "\']+)["\']?\s*;?#i', $_SERVER["CONTENT_TYPE"], $m)) { $request_charset = strtolower($m[1]); } $post = []; if($request_charset == "utf-8") { $post = $_POST; } else { foreach($_POST as $key => $value) { $post[mb_convert_encoding($key, "utf-8", $request_charset)] = mb_convert_encoding($value, "utf-8", $request_charset); } } return $post; } protected function return($response) { //var_dump($response);exit; $code = 500; $status = "Internal Server Error"; $data = []; if($response['code']) { $code = $response['code']; } if($response['status']) { $status = $response['status']; } if(is_array($response['data'])) { $data = $response['data']; } $proto = "HTTP/1.0"; if($_SERVER["SERVER_PROTOCOL"]) { $proto = $_SERVER["SERVER_PROTOCOL"]; } $this->requestLog->debug("$proto $code $status"); $this->requestLog->debug("status $status, result: "); foreach($data as $key => $res) { if(is_array($res)) { $this->requestLog->debug($key.": (".count($res).")"); } elseif(is_object($res)) { $this->requestLog->debug($key.": object"); } else { $this->requestLog->debug($key.": ".$res); } } header("$proto $code $status"); header("Content-type: application/json"); //http_response_code($code); echo json_encode(["status" => $status, "result" => $data]); exit; } public static function staticReturn($response) { //var_dump($response);exit; $code = 500; $status = "Internal Server Error"; $data = []; if($response['code']) { $code = $response['code']; } if($response['status']) { $status = $response['status']; } if(is_array($response['data'])) { $data = $response['data']; } $proto = "HTTP/1.0"; if($_SERVER["SERVER_PROTOCOL"]) { $proto = $_SERVER["SERVER_PROTOCOL"]; } $log = new mfLog_File(); $log->init(BASEDIR."/var/log/api-request.log"); $log->debug("$proto $code $status"); $log->debug("status $status, result: "); foreach($data as $key => $res) { if(is_array($res)) { $log->debug($key.": (".count($res).")"); } else { $log->debug($key.": ".$res); } } header("$proto $code $status"); header("Content-type: application/json"); //http_response_code($code); echo json_encode(["status" => $status, "result" => $data]); exit; } private function checkAuth() { } private function createCorsHeaders() { header("Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS"); header("Access-Control-Allow-Headers: X-Api-Key, accept, Content-Type"); if(!is_array($this->allowed_origins) || !count($this->allowed_origins)) { return true; } if(!array_key_exists("origin", $this->headers)) { if(!$this->allowMissingOrigin) { $this->return(mfResponse::Forbidden()); } return true; } $request_origin = ["proto" => false, "hostname" => "", "port" => false]; $m = []; if(preg_match('#^(https?)://([^/:]+)(:\d+)?/?$#i', $this->headers['origin'], $m)) { $request_origin['proto'] = $m[1]; $request_origin['hostname'] = $m[2]; if(array_key_exists(3, $m) && $m[3]) { $request_origin['port'] = $m[3]; } } else { $this->return(mfResponse::Forbidden(["message" => "Malformed Origin header"])); } if($request_origin['hostname'] == "localhost") { // always allow requests from localhost $this->log->debug("Allowing localhost Origin"); $allowed_origin = $request_origin['proto']."://".$request_origin['hostname']; if($request_origin['port']) { $allowed_origin .= $request_origin['port']; } header("Access-Control-Allow-Origin: $allowed_origin"); return true; } foreach($this->allowed_origins as $origin) { //echo $origin." -> ".$_SERVER["HTTP_HOST"]; $proto = false; $hostname = $origin; $m = []; if(preg_match('#^(https?)://([^/]+)/?$#i', $origin, $m)) { $proto = $m[1]; $hostname = $m[2]; } if(substr($hostname, 0, 2) == "*.") { $hostname = str_replace("*.", ".*\\.", $hostname); } //var_dump($hostname);exit; //if($hostname == $request_origin['hostname']) { if(preg_match('/^'.$hostname.'$/', $request_origin['hostname'])) { if($proto) { if($proto == $request_origin['proto']) { $allowed_origin = $proto."://".$request_origin['hostname']; if($request_origin['port']) { $allowed_origin .= $request_origin['port']; } header("Access-Control-Allow-Origin: $allowed_origin"); return true; } } else { $allowed_origin = $request_origin['proto']."://".$request_origin['hostname']; if($request_origin['port']) { $allowed_origin .= $request_origin['port']; } header("Access-Control-Allow-Origin: $allowed_origin"); return true; } } } if(!$this->allowMissingOrigin) { $this->return(mfResponse::Forbidden()); exit; } return true; } protected function runRoute($params) { if(!is_array($this->routes) || !count($this->routes)) { return false; } //var_dump($params);exit; $params = trim($params, "/"); $m = []; if(preg_match('/\.(csv|json|html|txt)$/', $params, $m)) { if($m[1]) { $format = strtolower($m[1]); $params = preg_replace("/\.$format$/", "", $params); $this->format = $format; } } //var_dump($params);exit; $req_parts = explode("/", $params); $req_count = count($req_parts); foreach($this->routes as $route) { if($route['method'] != $this->http_method) { continue; } $route_string = trim($route['route'], "/"); $route_parts = explode("/", $route_string); $route_count = count($route_parts); if($req_count != $route_count) { continue; } // same number of parts $vars = []; foreach($route_parts as $i => $rp) { if(substr($rp,0,1) == ":") { // part is variable $var_name = substr($rp, 1); $vars[$var_name] = $req_parts[$i]; continue; } else { if($rp != $req_parts[$i]) { continue 2; // break out of this loop and continue outer foreach (try next route) } } } // found valid route return $this->call($route['action'], $vars); } // no route found $this->return(mfResponse::BadRequest(["message" => "Invalid endpoint"])); exit; } /** * Shutdown handler to return PHP errors as API response. * Errors are still logged in error_log */ public static function return_errors() { $error = error_get_last(); //var_dump($error);exit; if($error && $error['type'] & (E_ERROR|E_CORE_ERROR|E_COMPILE_ERROR|E_USER_ERROR|E_RECOVERABLE_ERROR)) { mfBaseApicontroller::staticReturn(mfResponse::InternalServerError(["message" => "An internal error occured, please try again"])); } /*header("Content-type: application/json"); http_response_code($code); echo json_encode(["status" => $status, "result" => $data]); exit;*/ } private function call($function, $params = []) { $args = $params; if(count($params) === 1) { $args = reset($params); } if(is_array($function)) { return call_user_func($function, $args); } return $this->__call($function,$args); } protected function addRoute($route, $action, $method) { $this->routes[] = [ "route" => $route, "action" => $action, "method" => $method ]; return true; } protected function addAllowedOrigin($origin) { $this->allowed_origins[] = trim($origin); $this->allowed_origins = array_unique($this->allowed_origins); return true; } public static function loadApiClass($name) { //var_dump($name);exit; if(!$name) { return false; } $folder = APPDIR."Api/".((defined("CURRENT_API_VERSION")) ? CURRENT_API_VERSION : API_VERSION); $m = []; if(preg_match('/(.+)Apicontroller/',$name, $m)) { $classname = $m[1]."Apicontroller"; $filename = "$classname.php"; if(file_exists("$folder/$filename")) { require_once "$folder/$filename"; } } } protected function user() { if(!MFUSELOGIN) { trigger_error("mvcfronk: Tried to access mfBaseController::user(), though MFUSELOGIN is set to false.", E_USER_WARNING); return false; } if(!$this->mfUser) { $this->mfUser=mfUser::singleton(); } return $this->mfUser; } protected function db() { $args=func_get_args(); // if no arguments, just return a DB instance if(!$args) { // don't allow managed FronkDB instance, but new custom instance is allowed if(!FRONKDB) { return false; } if(!is_object($this->mfDBI)) { $this->mfDBI=FronkDB::singleton(); } } else { // else return a new instance $dbhost=$args[0]; $dbuser=$args[1]; $dbpass=$args[2]; $dbname=$args[3]; $this->mfDBI = $this->getNewDBInstance($dbhost,$dbuser,$dbpass,$dbname); } return $this->mfDBI; } public function __call($name,$params) { if(method_exists($this,$name)) { return call_user_func(array($this, $name), $params); } else { // if function doesn't exist, maybe it's an Action $funcname=lcfirst($name); if(!preg_match('/Action$/',$name)) { $funcname.="Action"; } if(method_exists($this,$funcname)) { return call_user_func(array($this, $funcname), $params); } else { throw new Exception(get_class($this).": $name not found",404); } } } public function __get($name) { if($name == "db") { return $this->db(); } return null; } protected function logout() { mfLoginController::logout(); $this->redirect(DEFAULT_ROUTE); } /* * private internal functions */ private function getNewDBInstance($dbhost=false,$dbuser=false,$dbpass=false,$dbname=false) { if(!$dbhost) $dbhost=FRONKDB_DBHOST; if(!$dbuser) $dbhost=FRONKDB_DBUSER; if(!$dbpass) $dbhost=FRONKDB_DBPASS; if(!$dbname) $dbname=FRONKDB_DBNAME; return new FronkDB($dbhost,$dbuser,$dbpass,$dbname); } public static function redirect($mod=false,$action=false,$params=false,$anker=false) { //var_dump($mod); //var_dump($action); $log = mfLoghandler::singleton(); if(MFUSEFANCYURLS && defined('MFFANCYBASEURL')) { // use fancy urls $url=MFFANCYBASEURL; if($mod) { $url.="/$mod"; if($action) { $url.="/$action"; } } } else { // no fancy urls if(!$mod) { $url="?"; } elseif($mod) { $url="?action=$mod"; if($action) { $url.="_$action"; } } } /* if(is_array($params) && count($params)) { foreach($params as $k => $v) { $url.="&$k=$v"; } }*/ if(is_array($params) && count($params)) { $url .= (MFUSEFANCYURLS) ? "/?" : "&"; foreach($params as $k => $v) { $v = urlencode($v); if($k) { $k = urlencode($k); $url .= "$k=$v&"; } else { $url .= "$v&"; } } $url = preg_replace('/&$/', '', $url); } if($anker) { $url.="#$anker"; } $log->debug("Redirecting to $url"); header("Location: $url"); exit; } public static function getUrl($mod, $action=null, $param=null) { if(!$mod) { return ""; } if(MFUSEFANCYURLS) { // use fancy urls $url=MFFANCYBASEURL; if($mod) { $url.="/$mod"; if($action) { $url.="/$action"; } } $url = preg_replace('#//#','/',$url); } else { // no fancy urls $url="?action=$mod"; if($action) { $url.="_$action"; } } if(is_array($param) && count($param)) { $url .= (MFUSEFANCYURLS) ? "/" : "&"; $param_qs = http_build_query($param); $url .= "$param_qs"; } return $url; } public static function returnJson($data) { if(is_array($data)) { header("Content-Type: application/json"); echo json_encode($data); exit; } else { throw new Exception("Data not an array"); } } // Helper functions public static function dateToTimestamp($date) { $t = array(0,0,0); // extract day, month, year if (!preg_match('/^(\d{1,2})\.(\d{1,2})\.(\d{2,4})/',$date,$d)) { return false; } // extract time if available if (preg_match('/(\d\d):(\d\d):(\d\d)$/',$date,$t)) { if (!$t[3]) { $t[3] = 0; } } // make and return timestamp $ts = mktime($t[1],$t[2],$t[3],$d[2],$d[1],$d[3]); return $ts; } public static function dateToDB($date,$type='l') { // get timestamp $ts = self::dateToTimestamp($date); // only proceed if timestamp conversion was successful if(!$ts) { return false; } // return date and time if long type requested if($type = 'l') { $dbdate = date('Y-m-d H:i:s',$ts); } else { $dbdate = date('Y-m-d',$ts); } return $dbdate; } }