routes = []; $this->cookies= new Cookies(); } /** * return $cookies * @return Cookies */ public function getCookies(): Cookies { return $this->cookies; } /** * return api routes * @return array */ public function registerApiRoutes():array { return [ 'POST test' => 'api', 'GET test/cookies/(.*)' => 'apiTestCookie', 'GET test/(.*)' => 'apiWithText', ]; } /** * method for api * @return JsonResponse */ public function api(): JsonResponse { return new JsonResponse(200,['test'=>'OK']); } /** * method for api * @param string $text * @return JsonResponse */ public function apiWithText(string $text): JsonResponse { return new JsonResponse(200,['text'=>$text]); } /** * method to test cookie via api * @param string $text * @return JsonResponse */ public function apiTestCookie(string $text): JsonResponse { if (empty($text)){ $text= ''; } $this->cookies->addCookie( 'Test-Cookie', $text, time()+3, !empty($_SERVER['SCRIPT_NAME']) ? dirname($_SERVER['SCRIPT_NAME']) : '/', !empty($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '' ); return new JsonResponse(200,['text'=>$text]); } /** * Triggered after Pico has loaded all available plugins * * This event is triggered nevertheless the plugin is enabled or not. * It is NOT guaranteed that plugin dependencies are fulfilled! * * * @param object[] $plugins loaded plugin instances */ public function onPluginsLoaded(array $plugins) { $this->routes = []; foreach($plugins as $plugin){ if ($plugin instanceof ApiAware){ $routes = $plugin->registerApiRoutes(); if (is_array($routes)){ foreach($routes as $route => $methodName){ if (is_string($methodName) && method_exists($plugin,$methodName)){ $this->routes[$route] = [$plugin,$methodName,false]; } } } if ($plugin instanceof LateApiAware){ $routes = $plugin->registerLateApiRoutes(); if (is_array($routes)){ foreach($routes as $route => $methodName){ if (is_string($methodName) && method_exists($plugin,$methodName)){ $this->routes[$route] = [$plugin,$methodName,true]; } } } } } } } /** * trig api * Triggered before Pico loads its theme * use this event because could be reach in 15 ms instead of 60 ms after rendering * * @see Pico::loadTheme() * @see DummyPlugin::onThemeLoaded() * * @param string &$theme name of current theme */ public function onThemeLoading(&$theme) { $output = null; if ($this->resolveApi($output)){ if (!($output instanceof JsonResponse)){ throw new Exception("Return of resolveApi should be JsonResponse at this point", 1); } else { throw new SpecialOutputException($output); } } } /** * send cookies of not already sent. * * Triggered after Pico has rendered the page * * @param string &$output contents which will be sent to the user * @return void */ public function onPageRendered(&$output) { if ($this->resolveApi($output,true)){ $output = $output->send(); } if (JsonResponse::canSendHeaders()){ $this->getCookies()->sendCookiesOnce(); } } /** * resolve api * @param null|string|JsonResponse $output * @param bool $isLate * @return bool $outputChanged */ protected function resolveApi(&$output, bool $isLate = false): bool { $outputChanged = false; if (isset($_GET['api'])){ $route = $this->getPico()->getUrlParameter( 'api', FILTER_UNSAFE_RAW, [ 'default' => '' ], [ FILTER_FLAG_STRIP_LOW, FILTER_FLAG_STRIP_HIGH ] ); $route = trim($route); if (empty($route)){ $output = new JsonResponse(404,['code'=>404,'reason'=>'Empty api route'],[],$this->cookies); } elseif (!preg_match('/^[A-Za-z0-9_\-.\/]+$/',$route)) { $output = new JsonResponse(404,['code'=>404,'reason'=>"Route '$route' use forbidden characters !"],[],$this->cookies); } else { ob_start(); $response = null; try { $data = $this->searchCorrespondingRoute($route); if ($data['isLate'] !== $isLate){ if ($isLate){ return new Exception('Calling an api route but catch onPageRedered whereas should be caught onThemeLoading !'); } else { ob_end_clean(); return false; } } $response = call_user_func_array([$data['plugin'],$data['methodName']],$data['params']); if (!($response instanceof JsonResponse)){ $response = null; throw new Exception("Return of '{$data['methodName']}' should be instanceof of 'JsonResponse'"); } else { $response->setCookies($this->cookies); } } catch (BadMethodException $th) { $code = 405; $content = ['reason'=>$th->getMessage()]; } catch (NotFoundRouteException $th) { $code = 404; $content = ['reason'=>"Route '$route' not found !"]; } catch (Throwable $th) { $code = 500; $content = ['reason'=>$th->__toString()]; } $rawOutput = ob_get_contents(); ob_end_clean(); if (empty($response)){ if (!empty($rawOutput)){ $content['rawOutput'] = $rawOutput; } $content = array_merge(['code'=>$code],$content); $response = (new JsonResponse($code,$content,[],$this->cookies)); } elseif (!empty($rawOutput)) { $response->mergeInContent(compact(['rawOutput'])); } $output = $response; } $outputChanged = true; } return $outputChanged; } /** * search corresponding route * @param string $route * @return array ['plugin'=>$plugin,'methodName'=>string,'params'=>array] * @throws BadMethodException * @throws NotFoundRouteException */ protected function searchCorrespondingRoute(string $route): array { if (empty($_SERVER['REQUEST_METHOD'])){ throw new BadMethodException('Method not defined'); } $method = $_SERVER['REQUEST_METHOD']; if (!in_array($method,['GET','POST'],true)){ throw new BadMethodException('Not allowed method'); } $splittedRoute = explode('/',$route); $nb = count($splittedRoute); $badMethod = false; for ($i=0; $i < 2**$nb; $i++) { $params = []; $splittedRouteFiltered = []; foreach ($splittedRoute as $idx => $value) { $currentPower = $nb - $idx - 1 ; if ((2**$currentPower & $i) > 0){ $splittedRouteFiltered[] = '(.*)'; $params[] = $value; } else { $splittedRouteFiltered[] = $value; } } $searchingRoute = implode('/',$splittedRouteFiltered); $data = []; if (array_key_exists("$method $searchingRoute",$this->routes)){ $data = $this->routes["$method $searchingRoute"]; } elseif (array_key_exists("$searchingRoute",$this->routes)){ $data = $this->routes["$searchingRoute"]; } if (!empty($data)){ return [ 'plugin' => $data[0], 'methodName' => $data[1], 'params' => $params, 'isLate' => $data[2] ]; } elseif (!$badMethod && array_key_exists((($method == 'GET') ? 'POST' : 'GET' )." $searchingRoute",$this->routes)){ $badMethod = true; } } if ($badMethod){ throw new BadMethodException('Not allowed method'); } throw new NotFoundRouteException(''); } }