From d3a11e0b1b15835ccb881a21e604859ec1112270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Bregu=C5=82a?= Date: Sun, 5 Mar 2017 18:00:29 +0100 Subject: [PATCH 01/13] Download only latest version via git --- docs/user-guide/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/installation.md b/docs/user-guide/installation.md index 575bc3d..367e48b 100644 --- a/docs/user-guide/installation.md +++ b/docs/user-guide/installation.md @@ -31,7 +31,7 @@ $ sudo su # switch to Polr directory (replace with other directory path if applicable) $ cd /var/www # clone Polr -$ git clone https://github.com/cydrobolt/polr.git +$ git clone https://github.com/cydrobolt/polr.git --depth=1 # set correct permissions $ chmod -R 755 polr From fc22ed06524fb04521922c3cde6b284e322be134 Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Sat, 11 Mar 2017 10:47:43 -0500 Subject: [PATCH 02/13] Move analytics SQL & logic to StatsHelper, refactor to use left_bound and right_bound --- app/Helpers/StatsHelper.php | 71 +++++++++++++++++++ app/Helpers/UserHelper.php | 2 - .../Api/ApiAnalyticsController.php | 15 ++++ app/Http/Controllers/StatsController.php | 55 +++----------- 4 files changed, 97 insertions(+), 46 deletions(-) create mode 100644 app/Helpers/StatsHelper.php create mode 100644 app/Http/Controllers/Api/ApiAnalyticsController.php diff --git a/app/Helpers/StatsHelper.php b/app/Helpers/StatsHelper.php new file mode 100644 index 0000000..93d5bbc --- /dev/null +++ b/app/Helpers/StatsHelper.php @@ -0,0 +1,71 @@ +link_id = $link_id; + $this->left_bound_parsed = Carbon::parse($left_bound); + $this->right_bound_parsed = Carbon::parse($right_bound); + + if (!$this->left_bound_parsed->lte($this->right_bound_parsed)) { + // If left bound is not less than or equal to right bound + throw new Exception('Invalid bounds.'); + } + } + + public function getBaseRows() { + /** + * Fetches base rows given left date bound, right date bound, and link id + * + * @param integer $link_id + * @param string $left_bound + * @param string $right_bound + * + * @return DB rows + */ + + return DB::table('clicks') + ->where('link_id', $this->link_id) + ->where('created_at', '>=', $this->left_bound_parsed) + ->where('created_at', '<=', $this->right_bound_parsed); + } + + public function getDayStats() { + // Return stats by day from the last 30 days + // date => x + // clicks => y + $stats = $this->getBaseRows() + ->select(DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d') AS x, count(*) AS y")) + ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d')")) + ->orderBy('x', 'asc') + ->get(); + + return $stats; + } + + public function getCountryStats() { + $stats = $this->getBaseRows() + ->select(DB::raw("country AS label, count(*) AS clicks")) + ->groupBy('country') + ->orderBy('clicks', 'desc') + ->get(); + + return $stats; + } + + public function getRefererStats() { + $stats = $this->getBaseRows() + ->select(DB::raw("COALESCE(referer_host, 'Direct') as label, count(*) as clicks")) + ->groupBy('referer_host') + ->orderBy('clicks', 'desc') + ->get(); + + return $stats; + } +} diff --git a/app/Helpers/UserHelper.php b/app/Helpers/UserHelper.php index 70efe1b..9b85fca 100644 --- a/app/Helpers/UserHelper.php +++ b/app/Helpers/UserHelper.php @@ -57,7 +57,6 @@ class UserHelper { public static function resetRecoveryKey($username) { $recovery_key = CryptoHelper::generateRandomHex(50); - $user = self::getUserByUsername($username); if (!$user) { @@ -72,7 +71,6 @@ class UserHelper { public static function userResetKeyCorrect($username, $recovery_key, $inactive=false) { // Given a username and a recovery key, return true if they match. - $user = self::getUserByUsername($username, $inactive); if ($user) { diff --git a/app/Http/Controllers/Api/ApiAnalyticsController.php b/app/Http/Controllers/Api/ApiAnalyticsController.php new file mode 100644 index 0000000..478dddd --- /dev/null +++ b/app/Http/Controllers/Api/ApiAnalyticsController.php @@ -0,0 +1,15 @@ +input('response_type'); + $user = self::getApiUserInfo($request); + + } +} diff --git a/app/Http/Controllers/StatsController.php b/app/Http/Controllers/StatsController.php index 58d5ed6..7657962 100644 --- a/app/Http/Controllers/StatsController.php +++ b/app/Http/Controllers/StatsController.php @@ -6,53 +6,17 @@ use Carbon\Carbon; use App\Models\Link; use App\Models\Clicks; +use App\Helpers\StatsHelper; use Illuminate\Support\Facades\DB; class StatsController extends Controller { const DAYS_TO_FETCH = 30; - private function getBaseRows($link_id) { - // Get past month rows - return DB::table('clicks') - ->where('link_id', $link_id) - ->where('created_at', '>=', Carbon::now()->subDays(self::DAYS_TO_FETCH)); - } - - private function getDayStats($link_id) { - // Return stats by day from the last 30 days - // date => x - // clicks => y - $stats = $this->getBaseRows($link_id) - ->select(DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d') AS x, count(*) AS y")) - ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d')")) - ->orderBy('x', 'asc') - ->get(); - - return $stats; - } - - private function getCountryStats($link_id) { - $stats = $this->getBaseRows($link_id) - ->select(DB::raw("country AS label, count(*) AS clicks")) - ->groupBy('country') - ->orderBy('clicks', 'desc') - ->get(); - - return $stats; - } - - private function getRefererStats($link_id) { - $stats = $this->getBaseRows($link_id) - ->select(DB::raw("COALESCE(referer_host, 'Direct') as label, count(*) as clicks")) - ->groupBy('referer_host') - ->orderBy('clicks', 'desc') - ->get(); - - return $stats; - } - - public function displayStats(Request $request, $short_url) { + // Carbon bounds for StatHelper + $left_bound = Carbon::now()->subDays(self::DAYS_TO_FETCH); + $right_bound = Carbon::now(); + if (!$this->isLoggedIn()) { return redirect(route('login'))->with('error', 'Please login to view link stats.'); } @@ -74,9 +38,12 @@ class StatsController extends Controller { return redirect(route('admin'))->with('error', 'You do not have permission to view stats for this link.'); } - $day_stats = $this->getDayStats($link_id); - $country_stats = $this->getCountryStats($link_id); - $referer_stats = $this->getRefererStats($link_id); + // Fetch base rows for StatHelper + $stats = new StatsHelper($link_id, $left_bound, $right_bound); + + $day_stats = $stats->getDayStats(); + $country_stats = $stats->getCountryStats(); + $referer_stats = $stats->getRefererStats(); return view('link_stats', [ 'link' => $link, From abc9d8f25b45a69aa8d51c94767fd12104bb5816 Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Sun, 12 Mar 2017 18:35:19 -0400 Subject: [PATCH 03/13] Refactor stats API and fix bounds detection --- app/Helpers/StatsHelper.php | 10 ++- app/Helpers/UserHelper.php | 5 +- .../Api/ApiAnalyticsController.php | 61 ++++++++++++++++++- .../Controllers/Api/ApiLinkController.php | 4 +- app/Http/Controllers/UserController.php | 6 -- app/Http/routes.php | 4 ++ resources/views/env.blade.php | 1 + 7 files changed, 76 insertions(+), 15 deletions(-) diff --git a/app/Helpers/StatsHelper.php b/app/Helpers/StatsHelper.php index 93d5bbc..a84e2ba 100644 --- a/app/Helpers/StatsHelper.php +++ b/app/Helpers/StatsHelper.php @@ -15,7 +15,15 @@ class StatsHelper { if (!$this->left_bound_parsed->lte($this->right_bound_parsed)) { // If left bound is not less than or equal to right bound - throw new Exception('Invalid bounds.'); + throw new \Exception('Invalid bounds.'); + } + + $days_diff = $this->left_bound_parsed->diffInDays($this->right_bound_parsed); + $max_days_diff = env('_ANALYTICS_MAX_DAYS_DIFF') ?: 365; + + if ($days_diff > $max_days_diff) { + error_log('too big fam'); + throw new \Exception('Bounds too broad.'); } } diff --git a/app/Helpers/UserHelper.php b/app/Helpers/UserHelper.php index 9b85fca..222fbaa 100644 --- a/app/Helpers/UserHelper.php +++ b/app/Helpers/UserHelper.php @@ -31,9 +31,8 @@ class UserHelper { return ctype_alnum($username); } - public static function validateEmail($email) { - // TODO validate email here - return true; + public static function userIsAdmin($username) { + return (self::getUserByUsername($username)->role == self::$USER_ROLES['admin']); } public static function checkCredentials($username, $password) { diff --git a/app/Http/Controllers/Api/ApiAnalyticsController.php b/app/Http/Controllers/Api/ApiAnalyticsController.php index 478dddd..8e4bc7e 100644 --- a/app/Http/Controllers/Api/ApiAnalyticsController.php +++ b/app/Http/Controllers/Api/ApiAnalyticsController.php @@ -2,14 +2,69 @@ namespace App\Http\Controllers\Api; use Illuminate\Http\Request; -// use App\Factories\LinkFactory; use App\Helpers\LinkHelper; +use App\Helpers\UserHelper; use App\Helpers\StatsHelper; -class ApiLinkController extends ApiController { - public function lookupLinkAnalytics (Request $request) { +class ApiAnalyticsController extends ApiController { + public function lookupLinkStats (Request $request, $stats_type=false) { $response_type = $request->input('response_type'); + + if ($response_type != 'json') { + abort(401, 'Only JSON-encoded data is available for this endpoint.'); + } + $user = self::getApiUserInfo($request); + $validator = \Validator::make($request->all(), [ + 'url_ending' => 'required|alpha_dash', + 'stats_type' => 'alpha_num', + 'left_bound' => 'date', + 'right_bound' => 'date' + ]); + + error_log($validator->errors()); + if ($validator->fails()) { + return abort(400, 'Invalid or missing parameters.'); + } + + $url_ending = $request->input('url_ending'); + $stats_type = $request->input('stats_type'); + $left_bound = $request->input('left_bound'); + $right_bound = $request->input('right_bound'); + $stats_type = $request->input('stats_type'); + + // ensure user can only read own analytics or user is admin + $link = LinkHelper::linkExists($url_ending); + + if ($link === false) { + abort(404, 'Link not found.'); + } + + if (($link->creator != $user->username) && + !(UserHelper::userIsAdmin($username))){ + // If user does not own link and is not an admin + abort(401, 'You do not have access to this link.'); + } + + $stats = new StatsHelper($link->id, $left_bound, $right_bound); + + if ($stats_type == 'day') { + $fetched_stats = $stats->getDayStats(); + } + else if ($stats_type == 'country') { + $fetched_stats = $stats->getCountryStats(); + } + else if ($stats_type == 'referer') { + $fetched_stats = $stats->getRefererStats(); + } + else { + abort(400, 'Invalid analytics type requested.'); + } + + return self::encodeResponse([ + 'url_ending' => $link->short_url, + 'data' => $fetched_stats, + ], 'data_link_' . $stats_type, $response_type, false); } } diff --git a/app/Http/Controllers/Api/ApiLinkController.php b/app/Http/Controllers/Api/ApiLinkController.php index 78ca6ac..0880df2 100644 --- a/app/Http/Controllers/Api/ApiLinkController.php +++ b/app/Http/Controllers/Api/ApiLinkController.php @@ -19,7 +19,7 @@ class ApiLinkController extends ApiController { ]); if ($validator->fails()) { - return abort(400, 'Parameters invalid or missing.'); + return abort(400, 'Invalid or missing parameters.'); } $long_url = $request->input('url'); // * required @@ -48,7 +48,7 @@ class ApiLinkController extends ApiController { ]); if ($validator->fails()) { - return abort(400, 'Parameters invalid or missing.'); + return abort(400, 'Invalid or missing parameters.'); } $url_ending = $request->input('url_ending'); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index a7df828..4899bd8 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -78,12 +78,6 @@ class UserController extends Controller { return redirect(route('signup'))->with('error', 'Sorry, your email or username already exists. Try again.'); } - $email_valid = UserHelper::validateEmail($email); - - if ($email_valid == false) { - return redirect(route('signup'))->with('error', 'Please use a valid email to sign up.'); - } - $acct_activation_needed = env('POLR_ACCT_ACTIVATION'); if ($acct_activation_needed == false) { diff --git a/app/Http/routes.php b/app/Http/routes.php index 3a6222e..bd85dce 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -68,4 +68,8 @@ $app->group(['prefix' => '/api/v2', 'namespace' => 'App\Http\Controllers'], func /* API lookup endpoints */ $app->post('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'Api\ApiLinkController@lookupLink']); $app->get('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'Api\ApiLinkController@lookupLink']); + + /* API data endpoints */ + $app->get('data/link', ['as' => 'api_link_analytics', 'uses' => 'Api\ApiAnalyticsController@lookupLinkStats']); + $app->post('data/link', ['as' => 'api_link_analytics', 'uses' => 'Api\ApiAnalyticsController@lookupLinkStats']); }); diff --git a/resources/views/env.blade.php b/resources/views/env.blade.php index 90d76b8..55da877 100644 --- a/resources/views/env.blade.php +++ b/resources/views/env.blade.php @@ -99,6 +99,7 @@ SESSION_DRIVER=file QUEUE_DRIVER=database _API_KEY_LENGTH=15 +_ANALYTICS_MAX_DAYS_DIFF=365 _PSEUDO_RANDOM_KEY_LENGTH=5 # FILESYSTEM_DRIVER=local From bd250b8ce38f70a7105cd62967aaf55dd89dc937 Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Sun, 12 Mar 2017 19:51:20 -0400 Subject: [PATCH 04/13] Add documentation for analytics API --- .../Api/ApiAnalyticsController.php | 2 +- docs/css/base.css | 6 ++ docs/developer-guide/api.md | 79 ++++++++++++++++++- 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/ApiAnalyticsController.php b/app/Http/Controllers/Api/ApiAnalyticsController.php index 8e4bc7e..2efc4c2 100644 --- a/app/Http/Controllers/Api/ApiAnalyticsController.php +++ b/app/Http/Controllers/Api/ApiAnalyticsController.php @@ -8,7 +8,7 @@ use App\Helpers\StatsHelper; class ApiAnalyticsController extends ApiController { public function lookupLinkStats (Request $request, $stats_type=false) { - $response_type = $request->input('response_type'); + $response_type = $request->input('response_type') ?: 'json'; if ($response_type != 'json') { abort(401, 'Only JSON-encoded data is available for this endpoint.'); diff --git a/docs/css/base.css b/docs/css/base.css index 7a4af72..3183ecd 100644 --- a/docs/css/base.css +++ b/docs/css/base.css @@ -11,3 +11,9 @@ img { width: auto !important; height: auto !important; } + +code:not(.hljs) { + /* Do not wrap pre-formatted code snippets */ + word-wrap: break-word; + white-space: normal; +} diff --git a/docs/developer-guide/api.md b/docs/developer-guide/api.md index ccad048..c7f854b 100644 --- a/docs/developer-guide/api.md +++ b/docs/developer-guide/api.md @@ -29,6 +29,9 @@ The Polr API will reply in `plain_text` or `json`. The response type can be set by providing the `response_type` argument to the request. If not provided, the response type will default to `plain_text`. +Data endpoints will only return JSON-formatted data and will default to `json` if no +`response_type` is provided. + Example `json` responses: ``` { @@ -72,6 +75,7 @@ Arguments: Response: A JSON or plain text representation of the shortened URL. Example: GET `http://example.com/api/v2/action/shorten?key=API_KEY_HERE&url=https://google.com&custom_ending=CUSTOM_ENDING&is_secret=false` + Response: ``` { @@ -95,6 +99,7 @@ Arguments: Remember that the `url` argument must be URL encoded. Example: GET `http://example.com/api/v2/action/lookup?key=API_KEY_HERE&url_ending=2` + Response: ``` { @@ -103,6 +108,77 @@ Response: } ``` +### /api/v2/data/link +Arguments: + + - `url_ending`: the link ending for the URL to look up. (e.g `5ga`) + - `left_bound`: left date bound (e.g `2017-02-28 22:41:43`) + - `right_bound`: right date bound (e.g `2017-03-13 22:41:43`) + - `stats_type`: the type of data to fetch + - `day`: click counts for each day from `left_bound` to `right_bound` + - `country`: click counts per country + - `referer`: click counts per referer + +The dates must be formatted for the `strtotime` PHP function and must be parsable by Carbon. +By default, this API endpoint will only allow users to fetch a maximum of 365 days of data. This setting +can be modified in the `.env` configuration file. + +An API key granted to a regular user can only fetch data for their own links. +Admins can fetch data for any link. + +Response: A JSON representation of the requested analytics data. + +Example: GET `http://example.com/api/v2/data/link?stats_type=day&key=API_KEY_HERE&url_ending=5gk&response_type=json&left_bound=2017-02-28%2022:41:43&right_bound=2017-03-13%2022:21:43` + +Response: +``` +{ + "action":"data_link_day", + "result": { + "url_ending":"5gk", + "data": [ + {"x":"2017-03-10","y":42}, + {"x":"2017-03-11","y":1}, + {"x":"2017-03-12","y":5} + ] + } +} +``` + +Example: GET `http://example.com/api/v2/data/link?stats_type=country&key=API_KEY_HERE&url_ending=5gk&response_type=json&left_bound=2017-02-28%2022:41:43&right_bound=2017-03-13%2022:21:43` + +Response: +``` +{ + "action":"data_link_day", + "result": { + "url_ending":"5gk", + "data": [ + {"label":"FR","clicks":1}, + {"label":"US","clicks":6}, + {"label":"CA","clicks":41} + ] + } +} +``` + +Example: GET `http://example.com/api/v2/data/link?stats_type=country&key=API_KEY_HERE&url_ending=5gk&response_type=json&left_bound=2017-02-28%2022:41:43&right_bound=2017-03-13%2022:21:43` + +Response: +``` +{ + "action":"data_link_day", + "result": { + "url_ending":"5gk", + "data": [ + {"label":"Direct","clicks":6}, + {"label":"reddit.com","clicks":12}, + {"label":"facebook.com","clicks":30} + ] + } +} +``` + ## HTTP Error Codes The API will return an error code if your request was malformed or another error occured while processing your request. @@ -152,4 +228,5 @@ Example `plain_text` error response: ## Testing the API -You may test your integrations on http://demo.polr.me with the credentials "demo-admin"/"demo-admin". Keep in mind the instance is only a demo and may be cleared at any time. +You may test your integrations on http://demo.polr.me with the credentials "demo-admin"/"demo-admin". +The demo instance is reset every day. From 8ebe335a841de22f6eac3635898f579573861bf3 Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Sun, 12 Mar 2017 19:55:25 -0400 Subject: [PATCH 05/13] Fix typo and remove debug statement --- app/Http/Controllers/Api/ApiAnalyticsController.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/ApiAnalyticsController.php b/app/Http/Controllers/Api/ApiAnalyticsController.php index 2efc4c2..692316d 100644 --- a/app/Http/Controllers/Api/ApiAnalyticsController.php +++ b/app/Http/Controllers/Api/ApiAnalyticsController.php @@ -23,7 +23,6 @@ class ApiAnalyticsController extends ApiController { 'right_bound' => 'date' ]); - error_log($validator->errors()); if ($validator->fails()) { return abort(400, 'Invalid or missing parameters.'); } @@ -42,9 +41,9 @@ class ApiAnalyticsController extends ApiController { } if (($link->creator != $user->username) && - !(UserHelper::userIsAdmin($username))){ + !(UserHelper::userIsAdmin($user->username))){ // If user does not own link and is not an admin - abort(401, 'You do not have access to this link.'); + abort(401, 'Unauthorized.'); } $stats = new StatsHelper($link->id, $left_bound, $right_bound); From 8f6d9762e98a418c7f3cfc416199b654c39319b0 Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Sun, 12 Mar 2017 19:58:54 -0400 Subject: [PATCH 06/13] Remove debug statement --- app/Helpers/StatsHelper.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Helpers/StatsHelper.php b/app/Helpers/StatsHelper.php index a84e2ba..d1b4716 100644 --- a/app/Helpers/StatsHelper.php +++ b/app/Helpers/StatsHelper.php @@ -22,7 +22,6 @@ class StatsHelper { $max_days_diff = env('_ANALYTICS_MAX_DAYS_DIFF') ?: 365; if ($days_diff > $max_days_diff) { - error_log('too big fam'); throw new \Exception('Bounds too broad.'); } } From 65794f83d72161bf6330ba04107035692d9ad261 Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Fri, 17 Mar 2017 17:00:13 -0400 Subject: [PATCH 07/13] Use ApiException to handle API errors and use ApiMiddleware to handle API authentication --- app/Exceptions/Api/ApiException.php | 42 +++++++++++++ app/Exceptions/Handler.php | 18 ++++++ .../Api/ApiAnalyticsController.php | 11 ++-- app/Http/Controllers/Api/ApiController.php | 41 ------------ .../Controllers/Api/ApiLinkController.php | 19 +++--- app/Http/Middleware/ApiMiddleware.php | 62 +++++++++++++++++++ app/Http/Middleware/VerifyCsrfToken.php | 2 +- app/Http/routes.php | 15 ++--- bootstrap/app.php | 8 +-- 9 files changed, 152 insertions(+), 66 deletions(-) create mode 100644 app/Exceptions/Api/ApiException.php create mode 100644 app/Http/Middleware/ApiMiddleware.php diff --git a/app/Exceptions/Api/ApiException.php b/app/Exceptions/Api/ApiException.php new file mode 100644 index 0000000..d2f36a6 --- /dev/null +++ b/app/Exceptions/Api/ApiException.php @@ -0,0 +1,42 @@ +response_type = $response_type; + $this->text_code = $text_code; + parent::__construct($message, $status_code, $previous); + } + + private function encodeJsonResponse($status_code, $message, $text_code) { + $response = [ + 'status_code' => $status_code, + 'error_code' => $text_code, + 'error' => $message + ]; + + return json_encode($response); + } + + public function getEncodedErrorMessage() { + if ($this->response_type == 'json') { + return $this->encodeJsonResponse($this->code, $this->message, $this->text_code); + } + else { + return $this->code . ' ' . $this->message; + } + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index c0e1136..2afb883 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -8,6 +8,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Laravel\Lumen\Exceptions\Handler as ExceptionHandler; use Illuminate\Support\Facades\Response; +use App\Exceptions\Api\ApiException; + class Handler extends ExceptionHandler { /** * A list of the exception types that should not be reported. @@ -43,6 +45,7 @@ class Handler extends ExceptionHandler { if (env('APP_DEBUG') != true) { // Render nice error pages if debug is off if ($e instanceof NotFoundHttpException) { + // Handle 404 exceptions if (env('SETTING_REDIRECT_404')) { // Redirect 404s to SETTING_INDEX_REDIRECT return redirect()->to(env('SETTING_INDEX_REDIRECT')); @@ -51,6 +54,7 @@ class Handler extends ExceptionHandler { return view('errors.404'); } if ($e instanceof HttpException) { + // Handle HTTP exceptions thrown by public-facing controllers $status_code = $e->getStatusCode(); $status_message = $e->getMessage(); @@ -63,6 +67,20 @@ class Handler extends ExceptionHandler { return response(view('errors.generic', ['status_code' => $status_code, 'status_message' => $status_message]), $status_code); } } + if ($e instanceof ApiException) { + // Handle HTTP exceptions thrown by API controllers + $status_code = $e->getCode(); + $encoded_status_message = $e->getEncodedErrorMessage(); + if ($e->response_type == 'json') { + return response($encoded_status_message, $status_code) + ->header('Content-Type', 'application/json') + ->header('Access-Control-Allow-Origin', '*'); + } + + return response($encoded_status_message, $status_code) + ->header('Content-Type', 'text/plain') + ->header('Access-Control-Allow-Origin', '*'); + } } return parent::render($request, $e); diff --git a/app/Http/Controllers/Api/ApiAnalyticsController.php b/app/Http/Controllers/Api/ApiAnalyticsController.php index 692316d..abb2d86 100644 --- a/app/Http/Controllers/Api/ApiAnalyticsController.php +++ b/app/Http/Controllers/Api/ApiAnalyticsController.php @@ -5,13 +5,14 @@ use Illuminate\Http\Request; use App\Helpers\LinkHelper; use App\Helpers\UserHelper; use App\Helpers\StatsHelper; +use App\Exceptions\Api\ApiException; class ApiAnalyticsController extends ApiController { public function lookupLinkStats (Request $request, $stats_type=false) { $response_type = $request->input('response_type') ?: 'json'; if ($response_type != 'json') { - abort(401, 'Only JSON-encoded data is available for this endpoint.'); + throw new ApiException('JSON_ONLY', 'Only JSON-encoded data is available for this endpoint.', 401, $response_type); } $user = self::getApiUserInfo($request); @@ -24,7 +25,7 @@ class ApiAnalyticsController extends ApiController { ]); if ($validator->fails()) { - return abort(400, 'Invalid or missing parameters.'); + throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type); } $url_ending = $request->input('url_ending'); @@ -37,13 +38,13 @@ class ApiAnalyticsController extends ApiController { $link = LinkHelper::linkExists($url_ending); if ($link === false) { - abort(404, 'Link not found.'); + throw new ApiException('NOT_FOUND', 'Link not found.', 404, $response_type); } if (($link->creator != $user->username) && !(UserHelper::userIsAdmin($user->username))){ // If user does not own link and is not an admin - abort(401, 'Unauthorized.'); + throw new ApiException('ACCESS_DENIED', 'Unauthorized.', 401, $response_type); } $stats = new StatsHelper($link->id, $left_bound, $right_bound); @@ -58,7 +59,7 @@ class ApiAnalyticsController extends ApiController { $fetched_stats = $stats->getRefererStats(); } else { - abort(400, 'Invalid analytics type requested.'); + throw new ApiException('INVALID_ANALYTICS_TYPE', 'Invalid analytics type requested.', 400, $response_type); } return self::encodeResponse([ diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php index 27d102b..ea3b96c 100644 --- a/app/Http/Controllers/Api/ApiController.php +++ b/app/Http/Controllers/Api/ApiController.php @@ -2,48 +2,8 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; -use Illuminate\Http\Request; - -use App\Models\User; -use App\Helpers\ApiHelper; class ApiController extends Controller { - protected static function getApiUserInfo(Request $request) { - $api_key = $request->input('key'); - - if (!$api_key) { - // no API key provided -- check whether anonymous API is on - - if (env('SETTING_ANON_API')) { - $username = 'ANONIP-' . $request->ip(); - } - else { - abort(401, "Authentication token required."); - } - $user = (object) [ - 'username' => $username - ]; - } - else { - $user = User::where('active', 1) - ->where('api_key', $api_key) - ->where('api_active', 1) - ->first(); - - if (!$user) { - abort(401, "Invalid authentication token."); - } - $username = $user->username; - } - - $api_limit_reached = ApiHelper::checkUserApiQuota($username); - - if ($api_limit_reached) { - abort(403, "Quota exceeded."); - } - return $user; - } - protected static function encodeResponse($result, $action, $response_type='json', $plain_text_response=false) { $response = [ "action" => $action, @@ -64,7 +24,6 @@ class ApiController extends Controller { return response($result) ->header('Content-Type', 'text/plain') ->header('Access-Control-Allow-Origin', '*'); - } } } diff --git a/app/Http/Controllers/Api/ApiLinkController.php b/app/Http/Controllers/Api/ApiLinkController.php index 0880df2..d4c72f2 100644 --- a/app/Http/Controllers/Api/ApiLinkController.php +++ b/app/Http/Controllers/Api/ApiLinkController.php @@ -4,11 +4,13 @@ use Illuminate\Http\Request; use App\Factories\LinkFactory; use App\Helpers\LinkHelper; +use App\Exceptions\Api\ApiException; class ApiLinkController extends ApiController { public function shortenLink(Request $request) { $response_type = $request->input('response_type'); - $user = self::getApiUserInfo($request); + // $user = self::getApiUserInfo($request); + $user = $request->user; // Validate parameters // Encode spaces as %20 to avoid validator conflicts @@ -19,7 +21,7 @@ class ApiLinkController extends ApiController { ]); if ($validator->fails()) { - return abort(400, 'Invalid or missing parameters.'); + throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type); } $long_url = $request->input('url'); // * required @@ -32,15 +34,17 @@ class ApiLinkController extends ApiController { $formatted_link = LinkFactory::createLink($long_url, $is_secret, $custom_ending, $link_ip, $user->username, false, true); } catch (\Exception $e) { - abort(400, $e->getMessage()); + throw new ApiException('CREATE_ERROR', $e->getMessage(), 400, $response_type); } return self::encodeResponse($formatted_link, 'shorten', $response_type); } public function lookupLink(Request $request) { + $user = $request->user; + $response_type = $request->input('response_type'); - $user = self::getApiUserInfo($request); + // $user = self::getApiUserInfo($request); // Validate URL form data $validator = \Validator::make($request->all(), [ @@ -48,7 +52,7 @@ class ApiLinkController extends ApiController { ]); if ($validator->fails()) { - return abort(400, 'Invalid or missing parameters.'); + throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type); } $url_ending = $request->input('url_ending'); @@ -60,7 +64,7 @@ class ApiLinkController extends ApiController { if ($link['secret_key']) { if ($url_key != $link['secret_key']) { - abort(401, "Invalid URL code for secret URL."); + throw new ApiException('ACCESS_DENIED', 'Invalid URL code for secret URL.', 401, $response_type); } } @@ -74,8 +78,7 @@ class ApiLinkController extends ApiController { ], 'lookup', $response_type, $link['long_url']); } else { - abort(404, "Link not found."); + throw new ApiException('NOT_FOUND', 'Link not found.', 404, $response_type); } - } } diff --git a/app/Http/Middleware/ApiMiddleware.php b/app/Http/Middleware/ApiMiddleware.php new file mode 100644 index 0000000..6c4e5eb --- /dev/null +++ b/app/Http/Middleware/ApiMiddleware.php @@ -0,0 +1,62 @@ +input('key'); + $response_type = $request->input('response_type'); + + if (!$api_key) { + // no API key provided; check whether anonymous API is enabled + + if (env('SETTING_ANON_API')) { + $username = 'ANONIP-' . $request->ip(); + } + else { + throw new ApiException('AUTH_ERROR', 'Authentication token required.', 401, $response_type); + } + $user = (object) [ + 'username' => $username + ]; + } + else { + $user = User::where('active', 1) + ->where('api_key', $api_key) + ->where('api_active', 1) + ->first(); + + if (!$user) { + abort(401, "Invalid authentication token."); + } + $username = $user->username; + } + + $api_limit_reached = ApiHelper::checkUserApiQuota($username); + + if ($api_limit_reached) { + throw new ApiException('QUOTA_EXCEEDED', 'Quota exceeded.', 429, $response_type); + } + return $user; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + + public function handle($request, Closure $next) { + $request->user = $this->getApiUserInfo($request); + + return $next($request); + } +} diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index a08af42..d1ee178 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -11,7 +11,7 @@ class VerifyCsrfToken extends BaseVerifier * @var array */ public function handle($request, \Closure $next) { - if ($request->is('api/v*/action/*')) { + if ($request->is('api/v*/action/*') || $request->is('api/v*/data/*')) { // Exclude public API from CSRF protection // but do not exclude private API endpoints return $next($request); diff --git a/app/Http/routes.php b/app/Http/routes.php index bd85dce..7ec879d 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -59,17 +59,18 @@ $app->group(['prefix' => '/api/v2', 'namespace' => 'App\Http\Controllers'], func $app->get('admin/get_admin_users', ['as' => 'api_get_admin_users', 'uses' => 'AdminPaginationController@paginateAdminUsers']); $app->get('admin/get_admin_links', ['as' => 'api_get_admin_links', 'uses' => 'AdminPaginationController@paginateAdminLinks']); $app->get('admin/get_user_links', ['as' => 'api_get_user_links', 'uses' => 'AdminPaginationController@paginateUserLinks']); +}); - +$app->group(['prefix' => '/api/v2', 'namespace' => 'App\Http\Controllers\Api', 'middleware' => 'api'], function ($app) { /* API shorten endpoints */ - $app->post('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'Api\ApiLinkController@shortenLink']); - $app->get('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'Api\ApiLinkController@shortenLink']); + $app->post('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'ApiLinkController@shortenLink']); + $app->get('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'ApiLinkController@shortenLink']); /* API lookup endpoints */ - $app->post('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'Api\ApiLinkController@lookupLink']); - $app->get('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'Api\ApiLinkController@lookupLink']); + $app->post('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'ApiLinkController@lookupLink']); + $app->get('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'ApiLinkController@lookupLink']); /* API data endpoints */ - $app->get('data/link', ['as' => 'api_link_analytics', 'uses' => 'Api\ApiAnalyticsController@lookupLinkStats']); - $app->post('data/link', ['as' => 'api_link_analytics', 'uses' => 'Api\ApiAnalyticsController@lookupLinkStats']); + $app->get('data/link', ['as' => 'api_link_analytics', 'uses' => 'ApiAnalyticsController@lookupLinkStats']); + $app->post('data/link', ['as' => 'api_link_analytics', 'uses' => 'ApiAnalyticsController@lookupLinkStats']); }); diff --git a/bootstrap/app.php b/bootstrap/app.php index ebf9dc2..6558119 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -61,12 +61,12 @@ $app->middleware([ // Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, Illuminate\Session\Middleware\StartSession::class, Illuminate\View\Middleware\ShareErrorsFromSession::class, - App\Http\Middleware\VerifyCsrfToken::class + App\Http\Middleware\VerifyCsrfToken::class, ]); -// $app->routeMiddleware([ - -// ]); +$app->routeMiddleware([ + 'api' => App\Http\Middleware\ApiMiddleware::class, +]); /* |-------------------------------------------------------------------------- From 84bdee52ea0d495501edec866e943d41c0a82f09 Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Fri, 17 Mar 2017 21:08:16 -0400 Subject: [PATCH 08/13] Change route for /about to /about-polr fix #304 --- app/Http/routes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/routes.php b/app/Http/routes.php index 3a6222e..8a3878a 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -17,7 +17,7 @@ if (env('POLR_ALLOW_ACCT_CREATION')) { $app->get('/', ['as' => 'index', 'uses' => 'IndexController@showIndexPage']); $app->get('/logout', ['as' => 'logout', 'uses' => 'UserController@performLogoutUser']); $app->get('/login', ['as' => 'login', 'uses' => 'UserController@displayLoginPage']); -$app->get('/about', ['as' => 'about', 'uses' => 'StaticPageController@displayAbout']); +$app->get('/about-polr', ['as' => 'about', 'uses' => 'StaticPageController@displayAbout']); $app->get('/lost_password', ['as' => 'lost_password', 'uses' => 'UserController@displayLostPasswordPage']); $app->get('/activate/{username}/{recovery_key}', ['as' => 'activate', 'uses' => 'UserController@performActivation']); From 861665f4d778b4d4d675d48ead523f7e769551c9 Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Mon, 20 Mar 2017 16:39:46 -0400 Subject: [PATCH 09/13] Remove unnecessary comments --- app/Exceptions/Api/ApiException.php | 2 -- app/Http/Controllers/Api/ApiLinkController.php | 1 - 2 files changed, 3 deletions(-) diff --git a/app/Exceptions/Api/ApiException.php b/app/Exceptions/Api/ApiException.php index d2f36a6..884d1a1 100644 --- a/app/Exceptions/Api/ApiException.php +++ b/app/Exceptions/Api/ApiException.php @@ -14,8 +14,6 @@ class ApiException extends \Exception { * @return mixed */ public function __construct($text_code='SERVER_ERROR', $message, $status_code = 0, $response_type='plain_text', Exception $previous = null) { - // TODO special Polr error codes for JSON - $this->response_type = $response_type; $this->text_code = $text_code; parent::__construct($message, $status_code, $previous); diff --git a/app/Http/Controllers/Api/ApiLinkController.php b/app/Http/Controllers/Api/ApiLinkController.php index d4c72f2..063ce6e 100644 --- a/app/Http/Controllers/Api/ApiLinkController.php +++ b/app/Http/Controllers/Api/ApiLinkController.php @@ -9,7 +9,6 @@ use App\Exceptions\Api\ApiException; class ApiLinkController extends ApiController { public function shortenLink(Request $request) { $response_type = $request->input('response_type'); - // $user = self::getApiUserInfo($request); $user = $request->user; // Validate parameters From b643906752040b337eff1a91cf0fb7d4ba049e4a Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Mon, 20 Mar 2017 17:01:33 -0400 Subject: [PATCH 10/13] Fix custom ending bug affecting non-truthy endings, update documentation for API error updates --- app/Factories/LinkFactory.php | 4 ++-- .../Controllers/Api/ApiLinkController.php | 2 +- docs/developer-guide/api.md | 6 ++++-- docs/developer-guide/api_errors.md | 19 +++++++++++++++++++ mkdocs.yml | 1 + 5 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 docs/developer-guide/api_errors.md diff --git a/app/Factories/LinkFactory.php b/app/Factories/LinkFactory.php index 0e468e2..21a3bc7 100644 --- a/app/Factories/LinkFactory.php +++ b/app/Factories/LinkFactory.php @@ -55,14 +55,14 @@ class LinkFactory { looks like a shortened URL.'); } - if (!$is_secret && !$custom_ending && (LinkHelper::longLinkExists($long_url) !== false)) { + if (!$is_secret && (!isset($custom_ending)) && (LinkHelper::longLinkExists($long_url) !== false)) { // if link is not specified as secret, is non-custom, and // already exists in Polr, lookup the value and return $existing_link = LinkHelper::longLinkExists($long_url); return self::formatLink($existing_link); } - if ($custom_ending) { + if (isset($custom_ending)) { // has custom ending $ending_conforms = LinkHelper::validateEnding($custom_ending); if (!$ending_conforms) { diff --git a/app/Http/Controllers/Api/ApiLinkController.php b/app/Http/Controllers/Api/ApiLinkController.php index 063ce6e..b41202c 100644 --- a/app/Http/Controllers/Api/ApiLinkController.php +++ b/app/Http/Controllers/Api/ApiLinkController.php @@ -33,7 +33,7 @@ class ApiLinkController extends ApiController { $formatted_link = LinkFactory::createLink($long_url, $is_secret, $custom_ending, $link_ip, $user->username, false, true); } catch (\Exception $e) { - throw new ApiException('CREATE_ERROR', $e->getMessage(), 400, $response_type); + throw new ApiException('CREATION_ERROR', $e->getMessage(), 400, $response_type); } return self::encodeResponse($formatted_link, 'shorten', $response_type); diff --git a/docs/developer-guide/api.md b/docs/developer-guide/api.md index c7f854b..b0edd90 100644 --- a/docs/developer-guide/api.md +++ b/docs/developer-guide/api.md @@ -218,13 +218,15 @@ This status code is returned in the following circumstances: Example `json` error response: ``` { - "error": "custom ending already in use" + "status_code":429, + "error_code":"QUOTA_EXCEEDED", + "error":"Quota exceeded." } ``` Example `plain_text` error response: -`custom ending already in use` +`429 Quota exceeded.` ## Testing the API diff --git a/docs/developer-guide/api_errors.md b/docs/developer-guide/api_errors.md new file mode 100644 index 0000000..62f5585 --- /dev/null +++ b/docs/developer-guide/api_errors.md @@ -0,0 +1,19 @@ +## API Error Text Codes + +`SERVER_ERROR`: A generic, unhandled error has occured. + +`JSON_ONLY`: Only JSON-encoded data is available for this endpoint. + +`MISSING_PARAMETERS`: Invalid or missing parameters. + +`NOT_FOUND`: Object not found. + +`ACCESS_DENIED`: User is not authorized to access the object. + +`INVALID_ANALYTICS_TYPE`: Invalid analytics type requested. + +`CREATION_ERROR`: An error occurred while creating the object. + +`AUTH_ERROR`: An error occured while attempting to authenticate the user to the API. + +`QUOTA_EXCEEDED`: User's API usage has exceeded alloted quota. diff --git a/mkdocs.yml b/mkdocs.yml index 81f9b7a..53198a1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,7 @@ pages: - Developer Guide: - 'Libraries': 'developer-guide/libraries.md' - 'API Documentation': 'developer-guide/api.md' + - 'API Errors': 'developer-guide/api_errors.md' - About: - 'License': 'about/license.md' - 'Contributors': 'about/contributors.md' From e786fc2cc1054bea7eca0b58e2c47dc3bbdd5b54 Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Mon, 20 Mar 2017 17:11:38 -0400 Subject: [PATCH 11/13] Use ->user instead of directly invoking getApiUserInfo --- app/Http/Controllers/Api/ApiAnalyticsController.php | 3 +-- app/Http/Controllers/Api/ApiLinkController.php | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/Http/Controllers/Api/ApiAnalyticsController.php b/app/Http/Controllers/Api/ApiAnalyticsController.php index abb2d86..3908307 100644 --- a/app/Http/Controllers/Api/ApiAnalyticsController.php +++ b/app/Http/Controllers/Api/ApiAnalyticsController.php @@ -9,14 +9,13 @@ use App\Exceptions\Api\ApiException; class ApiAnalyticsController extends ApiController { public function lookupLinkStats (Request $request, $stats_type=false) { + $user = $request->user; $response_type = $request->input('response_type') ?: 'json'; if ($response_type != 'json') { throw new ApiException('JSON_ONLY', 'Only JSON-encoded data is available for this endpoint.', 401, $response_type); } - $user = self::getApiUserInfo($request); - $validator = \Validator::make($request->all(), [ 'url_ending' => 'required|alpha_dash', 'stats_type' => 'alpha_num', diff --git a/app/Http/Controllers/Api/ApiLinkController.php b/app/Http/Controllers/Api/ApiLinkController.php index b41202c..e05c25e 100644 --- a/app/Http/Controllers/Api/ApiLinkController.php +++ b/app/Http/Controllers/Api/ApiLinkController.php @@ -41,9 +41,7 @@ class ApiLinkController extends ApiController { public function lookupLink(Request $request) { $user = $request->user; - $response_type = $request->input('response_type'); - // $user = self::getApiUserInfo($request); // Validate URL form data $validator = \Validator::make($request->all(), [ From fc056a9364b6bcb0116d4407d002f75a768e3152 Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Mon, 20 Mar 2017 17:19:28 -0400 Subject: [PATCH 12/13] Ignore empty , throw ApiException for all AUTH_ERROR instances --- app/Factories/LinkFactory.php | 4 ++-- app/Http/Middleware/ApiMiddleware.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Factories/LinkFactory.php b/app/Factories/LinkFactory.php index 21a3bc7..2320613 100644 --- a/app/Factories/LinkFactory.php +++ b/app/Factories/LinkFactory.php @@ -55,14 +55,14 @@ class LinkFactory { looks like a shortened URL.'); } - if (!$is_secret && (!isset($custom_ending)) && (LinkHelper::longLinkExists($long_url) !== false)) { + if (!$is_secret && (!isset($custom_ending) || $custom_ending === '') && (LinkHelper::longLinkExists($long_url) !== false)) { // if link is not specified as secret, is non-custom, and // already exists in Polr, lookup the value and return $existing_link = LinkHelper::longLinkExists($long_url); return self::formatLink($existing_link); } - if (isset($custom_ending)) { + if (isset($custom_ending) && $custom_ending !== '') { // has custom ending $ending_conforms = LinkHelper::validateEnding($custom_ending); if (!$ending_conforms) { diff --git a/app/Http/Middleware/ApiMiddleware.php b/app/Http/Middleware/ApiMiddleware.php index 6c4e5eb..bbab80c 100644 --- a/app/Http/Middleware/ApiMiddleware.php +++ b/app/Http/Middleware/ApiMiddleware.php @@ -33,7 +33,7 @@ class ApiMiddleware { ->first(); if (!$user) { - abort(401, "Invalid authentication token."); + throw new ApiException('AUTH_ERROR', 'Authentication token required.', 401, $response_type); } $username = $user->username; } From eb8b27a3413c32c696591d51c597b77680b5406c Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Mon, 20 Mar 2017 17:34:09 -0400 Subject: [PATCH 13/13] Catch Exceptions thrown by StatHelper and throw an ApiException --- app/Http/Controllers/Api/ApiAnalyticsController.php | 7 ++++++- docs/developer-guide/api_errors.md | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/ApiAnalyticsController.php b/app/Http/Controllers/Api/ApiAnalyticsController.php index 3908307..c1bc5ad 100644 --- a/app/Http/Controllers/Api/ApiAnalyticsController.php +++ b/app/Http/Controllers/Api/ApiAnalyticsController.php @@ -46,7 +46,12 @@ class ApiAnalyticsController extends ApiController { throw new ApiException('ACCESS_DENIED', 'Unauthorized.', 401, $response_type); } - $stats = new StatsHelper($link->id, $left_bound, $right_bound); + try { + $stats = new StatsHelper($link->id, $left_bound, $right_bound); + } + catch (\Exception $e) { + throw new ApiException('ANALYTICS_ERROR', $e->getMessage(), 400, $response_type); + } if ($stats_type == 'day') { $fetched_stats = $stats->getDayStats(); diff --git a/docs/developer-guide/api_errors.md b/docs/developer-guide/api_errors.md index 62f5585..adbe5e8 100644 --- a/docs/developer-guide/api_errors.md +++ b/docs/developer-guide/api_errors.md @@ -1,4 +1,6 @@ ## API Error Text Codes +To diagnose an unexpected or unhandled error, turn on the `APP_DEBUG` flag by setting +it to `true` in `.env` `SERVER_ERROR`: A generic, unhandled error has occured. @@ -17,3 +19,5 @@ `AUTH_ERROR`: An error occured while attempting to authenticate the user to the API. `QUOTA_EXCEEDED`: User's API usage has exceeded alloted quota. + +`ANALYTICS_ERROR`: Invalid bounds or unexpected error while fetching analytics data.