1
0
mirror of https://github.com/cydrobolt/polr.git synced 2024-09-19 15:11:40 +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 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);

View File

@ -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) || $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) && $custom_ending !== '') {
// has custom ending
$ending_conforms = LinkHelper::validateEnding($custom_ending);
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);
}
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) {
@ -57,7 +56,6 @@ class UserHelper {
public static function resetRecoveryKey($username) {
$recovery_key = CryptoHelper::generateRandomHex(50);
$user = self::getUserByUsername($username);
if (!$user) {
@ -72,7 +70,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) {

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;
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', '*');
}
}
}

View File

@ -4,11 +4,12 @@ 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 = $request->user;
// Validate parameters
// Encode spaces as %20 to avoid validator conflicts
@ -19,7 +20,7 @@ class ApiLinkController extends ApiController {
]);
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
@ -32,15 +33,15 @@ 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('CREATION_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);
// Validate URL form data
$validator = \Validator::make($request->all(), [
@ -48,7 +49,7 @@ class ApiLinkController extends ApiController {
]);
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');
@ -60,7 +61,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 +75,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);
}
}
}

View File

@ -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,

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.');
}
$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) {

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
*/
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);

View File

@ -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']);
@ -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_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' => '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\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,
]);
/*
|--------------------------------------------------------------------------

View File

@ -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;
}

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,
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.
@ -142,14 +218,17 @@ 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
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)
$ 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

View File

@ -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'

View File

@ -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