1
0
mirror of https://github.com/cydrobolt/polr.git synced 2024-09-19 23:21:47 +02:00

Merge branch 'master' into fix-toastr-error

This commit is contained in:
Chaoyi Zha 2017-03-23 16:13:11 -04:00 committed by GitHub
commit 2ee1e70eb1
20 changed files with 425 additions and 121 deletions

View File

@ -0,0 +1,40 @@
<?php
namespace App\Exceptions\Api;
class ApiException extends \Exception {
/**
* Catch an API exception.
*
* @param string $text_code
* @param string $message
* @param integer $status_code
* @param string $response_type
* @param \Exception $previous
*
* @return mixed
*/
public function __construct($text_code='SERVER_ERROR', $message, $status_code = 0, $response_type='plain_text', Exception $previous = null) {
$this->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;
}
}
}

View File

@ -8,6 +8,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler; use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use App\Exceptions\Api\ApiException;
class Handler extends ExceptionHandler { class Handler extends ExceptionHandler {
/** /**
* A list of the exception types that should not be reported. * A list of the exception types that should not be reported.
@ -43,6 +45,7 @@ class Handler extends ExceptionHandler {
if (env('APP_DEBUG') != true) { if (env('APP_DEBUG') != true) {
// Render nice error pages if debug is off // Render nice error pages if debug is off
if ($e instanceof NotFoundHttpException) { if ($e instanceof NotFoundHttpException) {
// Handle 404 exceptions
if (env('SETTING_REDIRECT_404')) { if (env('SETTING_REDIRECT_404')) {
// Redirect 404s to SETTING_INDEX_REDIRECT // Redirect 404s to SETTING_INDEX_REDIRECT
return redirect()->to(env('SETTING_INDEX_REDIRECT')); return redirect()->to(env('SETTING_INDEX_REDIRECT'));
@ -51,6 +54,7 @@ class Handler extends ExceptionHandler {
return view('errors.404'); return view('errors.404');
} }
if ($e instanceof HttpException) { if ($e instanceof HttpException) {
// Handle HTTP exceptions thrown by public-facing controllers
$status_code = $e->getStatusCode(); $status_code = $e->getStatusCode();
$status_message = $e->getMessage(); $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); 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); return parent::render($request, $e);

View File

@ -55,14 +55,14 @@ class LinkFactory {
looks like a shortened URL.'); looks like a shortened URL.');
} }
if (!$is_secret && !$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 // if link is not specified as secret, is non-custom, and
// already exists in Polr, lookup the value and return // already exists in Polr, lookup the value and return
$existing_link = LinkHelper::longLinkExists($long_url); $existing_link = LinkHelper::longLinkExists($long_url);
return self::formatLink($existing_link); return self::formatLink($existing_link);
} }
if ($custom_ending) { if (isset($custom_ending) && $custom_ending !== '') {
// has custom ending // has custom ending
$ending_conforms = LinkHelper::validateEnding($custom_ending); $ending_conforms = LinkHelper::validateEnding($custom_ending);
if (!$ending_conforms) { if (!$ending_conforms) {

View File

@ -0,0 +1,78 @@
<?php
namespace App\Helpers;
use App\Models\Click;
use App\Models\Link;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class StatsHelper {
function __construct($link_id, $left_bound, $right_bound) {
$this->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.');
}
$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) {
throw new \Exception('Bounds too broad.');
}
}
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;
}
}

View File

@ -31,9 +31,8 @@ class UserHelper {
return ctype_alnum($username); return ctype_alnum($username);
} }
public static function validateEmail($email) { public static function userIsAdmin($username) {
// TODO validate email here return (self::getUserByUsername($username)->role == self::$USER_ROLES['admin']);
return true;
} }
public static function checkCredentials($username, $password) { public static function checkCredentials($username, $password) {
@ -57,7 +56,6 @@ class UserHelper {
public static function resetRecoveryKey($username) { public static function resetRecoveryKey($username) {
$recovery_key = CryptoHelper::generateRandomHex(50); $recovery_key = CryptoHelper::generateRandomHex(50);
$user = self::getUserByUsername($username); $user = self::getUserByUsername($username);
if (!$user) { if (!$user) {
@ -72,7 +70,6 @@ class UserHelper {
public static function userResetKeyCorrect($username, $recovery_key, $inactive=false) { public static function userResetKeyCorrect($username, $recovery_key, $inactive=false) {
// Given a username and a recovery key, return true if they match. // Given a username and a recovery key, return true if they match.
$user = self::getUserByUsername($username, $inactive); $user = self::getUserByUsername($username, $inactive);
if ($user) { if ($user) {

View File

@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers\Api;
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) {
$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);
}
$validator = \Validator::make($request->all(), [
'url_ending' => 'required|alpha_dash',
'stats_type' => 'alpha_num',
'left_bound' => 'date',
'right_bound' => 'date'
]);
if ($validator->fails()) {
throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type);
}
$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) {
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
throw new ApiException('ACCESS_DENIED', 'Unauthorized.', 401, $response_type);
}
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();
}
else if ($stats_type == 'country') {
$fetched_stats = $stats->getCountryStats();
}
else if ($stats_type == 'referer') {
$fetched_stats = $stats->getRefererStats();
}
else {
throw new ApiException('INVALID_ANALYTICS_TYPE', 'Invalid analytics type requested.', 400, $response_type);
}
return self::encodeResponse([
'url_ending' => $link->short_url,
'data' => $fetched_stats,
], 'data_link_' . $stats_type, $response_type, false);
}
}

View File

@ -2,48 +2,8 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Helpers\ApiHelper;
class ApiController extends Controller { 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) { protected static function encodeResponse($result, $action, $response_type='json', $plain_text_response=false) {
$response = [ $response = [
"action" => $action, "action" => $action,
@ -64,7 +24,6 @@ class ApiController extends Controller {
return response($result) return response($result)
->header('Content-Type', 'text/plain') ->header('Content-Type', 'text/plain')
->header('Access-Control-Allow-Origin', '*'); ->header('Access-Control-Allow-Origin', '*');
} }
} }
} }

View File

@ -4,11 +4,12 @@ use Illuminate\Http\Request;
use App\Factories\LinkFactory; use App\Factories\LinkFactory;
use App\Helpers\LinkHelper; use App\Helpers\LinkHelper;
use App\Exceptions\Api\ApiException;
class ApiLinkController extends ApiController { class ApiLinkController extends ApiController {
public function shortenLink(Request $request) { public function shortenLink(Request $request) {
$response_type = $request->input('response_type'); $response_type = $request->input('response_type');
$user = self::getApiUserInfo($request); $user = $request->user;
// Validate parameters // Validate parameters
// Encode spaces as %20 to avoid validator conflicts // Encode spaces as %20 to avoid validator conflicts
@ -19,7 +20,7 @@ class ApiLinkController extends ApiController {
]); ]);
if ($validator->fails()) { if ($validator->fails()) {
return abort(400, 'Parameters invalid or missing.'); throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type);
} }
$long_url = $request->input('url'); // * required $long_url = $request->input('url'); // * required
@ -32,15 +33,15 @@ class ApiLinkController extends ApiController {
$formatted_link = LinkFactory::createLink($long_url, $is_secret, $custom_ending, $link_ip, $user->username, false, true); $formatted_link = LinkFactory::createLink($long_url, $is_secret, $custom_ending, $link_ip, $user->username, false, true);
} }
catch (\Exception $e) { catch (\Exception $e) {
abort(400, $e->getMessage()); throw new ApiException('CREATION_ERROR', $e->getMessage(), 400, $response_type);
} }
return self::encodeResponse($formatted_link, 'shorten', $response_type); return self::encodeResponse($formatted_link, 'shorten', $response_type);
} }
public function lookupLink(Request $request) { public function lookupLink(Request $request) {
$user = $request->user;
$response_type = $request->input('response_type'); $response_type = $request->input('response_type');
$user = self::getApiUserInfo($request);
// Validate URL form data // Validate URL form data
$validator = \Validator::make($request->all(), [ $validator = \Validator::make($request->all(), [
@ -48,7 +49,7 @@ class ApiLinkController extends ApiController {
]); ]);
if ($validator->fails()) { if ($validator->fails()) {
return abort(400, 'Parameters invalid or missing.'); throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type);
} }
$url_ending = $request->input('url_ending'); $url_ending = $request->input('url_ending');
@ -60,7 +61,7 @@ class ApiLinkController extends ApiController {
if ($link['secret_key']) { if ($link['secret_key']) {
if ($url_key != $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 +75,7 @@ class ApiLinkController extends ApiController {
], 'lookup', $response_type, $link['long_url']); ], 'lookup', $response_type, $link['long_url']);
} }
else { else {
abort(404, "Link not found."); throw new ApiException('NOT_FOUND', 'Link not found.', 404, $response_type);
} }
} }
} }

View File

@ -6,53 +6,17 @@ use Carbon\Carbon;
use App\Models\Link; use App\Models\Link;
use App\Models\Clicks; use App\Models\Clicks;
use App\Helpers\StatsHelper;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class StatsController extends Controller { class StatsController extends Controller {
const DAYS_TO_FETCH = 30; 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) { 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()) { if (!$this->isLoggedIn()) {
return redirect(route('login'))->with('error', 'Please login to view link stats.'); 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.'); return redirect(route('admin'))->with('error', 'You do not have permission to view stats for this link.');
} }
$day_stats = $this->getDayStats($link_id); // Fetch base rows for StatHelper
$country_stats = $this->getCountryStats($link_id); $stats = new StatsHelper($link_id, $left_bound, $right_bound);
$referer_stats = $this->getRefererStats($link_id);
$day_stats = $stats->getDayStats();
$country_stats = $stats->getCountryStats();
$referer_stats = $stats->getRefererStats();
return view('link_stats', [ return view('link_stats', [
'link' => $link, 'link' => $link,

View File

@ -78,12 +78,6 @@ class UserController extends Controller {
return redirect(route('signup'))->with('error', 'Sorry, your email or username already exists. Try again.'); 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'); $acct_activation_needed = env('POLR_ACCT_ACTIVATION');
if ($acct_activation_needed == false) { if ($acct_activation_needed == false) {

View File

@ -0,0 +1,62 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Models\User;
use App\Helpers\ApiHelper;
use App\Exceptions\Api\ApiException;
class ApiMiddleware {
protected static function getApiUserInfo(Request $request) {
$api_key = $request->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) {
throw new ApiException('AUTH_ERROR', 'Authentication token required.', 401, $response_type);
}
$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);
}
}

View File

@ -11,7 +11,7 @@ class VerifyCsrfToken extends BaseVerifier
* @var array * @var array
*/ */
public function handle($request, \Closure $next) { 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 // Exclude public API from CSRF protection
// but do not exclude private API endpoints // but do not exclude private API endpoints
return $next($request); return $next($request);

View File

@ -17,7 +17,7 @@ if (env('POLR_ALLOW_ACCT_CREATION')) {
$app->get('/', ['as' => 'index', 'uses' => 'IndexController@showIndexPage']); $app->get('/', ['as' => 'index', 'uses' => 'IndexController@showIndexPage']);
$app->get('/logout', ['as' => 'logout', 'uses' => 'UserController@performLogoutUser']); $app->get('/logout', ['as' => 'logout', 'uses' => 'UserController@performLogoutUser']);
$app->get('/login', ['as' => 'login', 'uses' => 'UserController@displayLoginPage']); $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('/lost_password', ['as' => 'lost_password', 'uses' => 'UserController@displayLostPasswordPage']);
$app->get('/activate/{username}/{recovery_key}', ['as' => 'activate', 'uses' => 'UserController@performActivation']); $app->get('/activate/{username}/{recovery_key}', ['as' => 'activate', 'uses' => 'UserController@performActivation']);
@ -59,13 +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_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_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->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 */ /* API shorten endpoints */
$app->post('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' => 'Api\ApiLinkController@shortenLink']); $app->get('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'ApiLinkController@shortenLink']);
/* API lookup endpoints */ /* API lookup endpoints */
$app->post('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' => 'Api\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' => 'ApiAnalyticsController@lookupLinkStats']);
$app->post('data/link', ['as' => 'api_link_analytics', 'uses' => 'ApiAnalyticsController@lookupLinkStats']);
}); });

View File

@ -61,12 +61,12 @@ $app->middleware([
// Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, // Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
Illuminate\Session\Middleware\StartSession::class, Illuminate\Session\Middleware\StartSession::class,
Illuminate\View\Middleware\ShareErrorsFromSession::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,
// ]); ]);
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -11,3 +11,9 @@ img {
width: auto !important; width: auto !important;
height: auto !important; height: auto !important;
} }
code:not(.hljs) {
/* Do not wrap pre-formatted code snippets */
word-wrap: break-word;
white-space: normal;
}

View File

@ -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, set by providing the `response_type` argument to the request. If not provided,
the response type will default to `plain_text`. 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: Example `json` responses:
``` ```
{ {
@ -72,6 +75,7 @@ Arguments:
Response: A JSON or plain text representation of the shortened URL. 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` 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: Response:
``` ```
{ {
@ -95,6 +99,7 @@ Arguments:
Remember that the `url` argument must be URL encoded. 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` Example: GET `http://example.com/api/v2/action/lookup?key=API_KEY_HERE&url_ending=2`
Response: 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 ## HTTP Error Codes
The API will return an error code if your request was malformed or another error occured while processing your request. The API will return an error code if your request was malformed or another error occured while processing your request.
@ -142,14 +218,17 @@ This status code is returned in the following circumstances:
Example `json` error response: 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: Example `plain_text` error response:
`custom ending already in use` `429 Quota exceeded.`
## Testing the API ## 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.

View File

@ -0,0 +1,23 @@
## 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.
`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.
`ANALYTICS_ERROR`: Invalid bounds or unexpected error while fetching analytics data.

View File

@ -31,7 +31,7 @@ $ sudo su
# switch to Polr directory (replace with other directory path if applicable) # switch to Polr directory (replace with other directory path if applicable)
$ cd /var/www $ cd /var/www
# clone Polr # clone Polr
$ git clone https://github.com/cydrobolt/polr.git $ git clone https://github.com/cydrobolt/polr.git --depth=1
# set correct permissions # set correct permissions
$ chmod -R 755 polr $ chmod -R 755 polr

View File

@ -8,6 +8,7 @@ pages:
- Developer Guide: - Developer Guide:
- 'Libraries': 'developer-guide/libraries.md' - 'Libraries': 'developer-guide/libraries.md'
- 'API Documentation': 'developer-guide/api.md' - 'API Documentation': 'developer-guide/api.md'
- 'API Errors': 'developer-guide/api_errors.md'
- About: - About:
- 'License': 'about/license.md' - 'License': 'about/license.md'
- 'Contributors': 'about/contributors.md' - 'Contributors': 'about/contributors.md'

View File

@ -99,6 +99,7 @@ SESSION_DRIVER=file
QUEUE_DRIVER=database QUEUE_DRIVER=database
_API_KEY_LENGTH=15 _API_KEY_LENGTH=15
_ANALYTICS_MAX_DAYS_DIFF=365
_PSEUDO_RANDOM_KEY_LENGTH=5 _PSEUDO_RANDOM_KEY_LENGTH=5
# FILESYSTEM_DRIVER=local # FILESYSTEM_DRIVER=local