From 1fb607ba631cf9a6c9eb7ed922bf723d2dfe03f3 Mon Sep 17 00:00:00 2001 From: Chaoyi Zha Date: Fri, 20 Apr 2018 21:35:45 -0400 Subject: [PATCH] Bulk link creation API (#443) * Add bulk shortening API endpoint & create controller methods * Update documentation * Remove space %20 mapping for bulk API --- app/Factories/LinkFactory.php | 5 +- .../Controllers/Api/ApiLinkController.php | 87 ++++++++++++++++--- app/Http/routes.php | 1 + docs/developer-guide/api.md | 68 +++++++++++++-- docs/developer-guide/api_errors.md | 2 + docs/user-guide/installation.md | 6 +- 6 files changed, 141 insertions(+), 28 deletions(-) diff --git a/app/Factories/LinkFactory.php b/app/Factories/LinkFactory.php index 14e5463..7ff2bda 100644 --- a/app/Factories/LinkFactory.php +++ b/app/Factories/LinkFactory.php @@ -66,13 +66,13 @@ class LinkFactory { // has custom ending $ending_conforms = LinkHelper::validateEnding($custom_ending); if (!$ending_conforms) { - throw new \Exception('Sorry, but custom endings + throw new \Exception('Custom endings can only contain alphanumeric characters, hyphens, and underscores.'); } $ending_in_use = LinkHelper::linkExists($custom_ending); if ($ending_in_use) { - throw new \Exception('Sorry, but this URL ending is already in use.'); + throw new \Exception('This URL ending is already in use.'); } $link_ending = $custom_ending; @@ -97,7 +97,6 @@ class LinkFactory { $link->is_api = $is_api; if ($creator) { - // if user is logged in, save user as creator $link->creator = $creator; } diff --git a/app/Http/Controllers/Api/ApiLinkController.php b/app/Http/Controllers/Api/ApiLinkController.php index e05c25e..b1f377c 100644 --- a/app/Http/Controllers/Api/ApiLinkController.php +++ b/app/Http/Controllers/Api/ApiLinkController.php @@ -7,12 +7,22 @@ use App\Helpers\LinkHelper; use App\Exceptions\Api\ApiException; class ApiLinkController extends ApiController { + protected function getShortenedLink($long_url, $is_secret, $custom_ending, $link_ip, $username, $response_type) { + try { + $formatted_link = LinkFactory::createLink( + $long_url, $is_secret, $custom_ending, $link_ip, $username, false, true); + } + catch (\Exception $e) { + throw new ApiException('CREATION_ERROR', $e->getMessage(), 400, $response_type); + } + + return $formatted_link; + } + public function shortenLink(Request $request) { $response_type = $request->input('response_type'); $user = $request->user; - // Validate parameters - // Encode spaces as %20 to avoid validator conflicts $validator = \Validator::make(array_merge([ 'url' => str_replace(' ', '%20', $request->input('url')) ], $request->except('url')), [ @@ -23,22 +33,71 @@ class ApiLinkController extends ApiController { throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type); } - $long_url = $request->input('url'); // * required - $is_secret = ($request->input('is_secret') == 'true' ? true : false); - - $link_ip = $request->ip(); - $custom_ending = $request->input('custom_ending'); - - try { - $formatted_link = LinkFactory::createLink($long_url, $is_secret, $custom_ending, $link_ip, $user->username, false, true); - } - catch (\Exception $e) { - throw new ApiException('CREATION_ERROR', $e->getMessage(), 400, $response_type); - } + $formatted_link = $this->getShortenedLink( + $request->input('url'), + ($request->input('is_secret') == 'true' ? true : false), + $request->input('custom_ending'), + $request->ip(), + $user->username, + $response_type + ); return self::encodeResponse($formatted_link, 'shorten', $response_type); } + public function shortenLinksBulk(Request $request) { + $response_type = $request->input('response_type', 'json'); + $request_data = $request->input('data'); + + $user = $request->user; + $link_ip = $request->ip(); + $username = $user->username; + + if ($response_type != 'json') { + throw new ApiException('JSON_ONLY', 'Only JSON-encoded responses are available for this endpoint.', 401, $response_type); + } + + $links_array_raw_json = json_decode($request_data, true); + + if ($links_array_raw_json === null) { + throw new ApiException('INVALID_PARAMETERS', 'Invalid JSON.', 400, $response_type); + } + + $links_array = $links_array_raw_json['links']; + + foreach ($links_array as $link) { + $validator = \Validator::make($link, [ + 'url' => 'required|url' + ]); + + if ($validator->fails()) { + throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type); + } + } + + $formatted_links = []; + + foreach ($links_array as $link) { + $formatted_link = $this->getShortenedLink( + $link['url'], + (array_get($link, 'is_secret') == 'true' ? true : false), + array_get($link, 'custom_ending'), + $link_ip, + $username, + $response_type + ); + + $formatted_links[] = [ + 'long_url' => $link['url'], + 'short_url' => $formatted_link + ]; + } + + return self::encodeResponse([ + 'shortened_links' => $formatted_links + ], 'shorten_bulk', 'json'); + } + public function lookupLink(Request $request) { $user = $request->user; $response_type = $request->input('response_type'); diff --git a/app/Http/routes.php b/app/Http/routes.php index 7982af5..e936cb1 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -66,6 +66,7 @@ $app->group(['prefix' => '/api/v2', 'namespace' => 'App\Http\Controllers\Api', ' /* API shorten endpoints */ $app->post('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'ApiLinkController@shortenLink']); $app->get('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'ApiLinkController@shortenLink']); + $app->post('action/shorten_bulk', ['as' => 'api_shorten_url_bulk', 'uses' => 'ApiLinkController@shortenLinksBulk']); /* API lookup endpoints */ $app->post('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'ApiLinkController@lookupLink']); diff --git a/docs/developer-guide/api.md b/docs/developer-guide/api.md index b0edd90..bcd1927 100644 --- a/docs/developer-guide/api.md +++ b/docs/developer-guide/api.md @@ -2,8 +2,8 @@ ----------------------------- ## API keys -To authenticate a user to Polr, you will need to provide an API key along with -each request to the Polr API, as a GET or POST parameter. (e.g `?key=API_KEY_HERE`) +To authenticate a user to Polr, you *must* provide an API key along with +each request to Polr API endpoints, as a GET or POST parameter. (e.g `?key=API_KEY_HERE`) ## Assigning an API key To assign an API key, log on from an administrator account, head over to the "Admin" @@ -26,11 +26,9 @@ See [API endpoints](#api-endpoints) for more information on the actions. ## Response Type 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. +set by providing the `response_type` argument to the request. If this argument is not provided, +the response type will default to either `plain_text` or `json` depending +on the endpoint. Example `json` responses: ``` @@ -84,7 +82,61 @@ Response: } ``` -Remember that the `url` argument must be URL encoded. +The `url` argument must be URL encoded. + +### /api/v2/action/shorten_bulk + +_`POST` only_ + +Arguments: + + - `data`: a string containing a JSON-encoded object with an array of links + +Example `data` argument: +```json +{ + "links": [ + { + "url": "https://polrproject.org/" + }, + { + "url": "https://youtube.com", + "is_secret": true + }, + { + "url": "https://github.com/cydrobolt/polr", + "custom_ending": "polrgithub" + } + ] +} +``` + +Response: A JSON-encoded object with a list of shortened links. + +Example response: +``` +{ + "action": "shorten_bulk", + "result": { + "shortened_links": [ + { + "long_url": "https://polrproject.org/", + "short_url": "http://demo.polr.me/81" + }, + { + "long_url": "https://youtube.com", + "short_url": "http://demo.polr.me/84/b496" + }, + { + "long_url": "https://github.com/cydrobolt/polr", + "short_url": "http://demo.polr.me/polrgithub" + } + ] + } +} +``` + + ### /api/v2/action/lookup The `lookup` action takes a single argument: `url_ending`. This is the URL to diff --git a/docs/developer-guide/api_errors.md b/docs/developer-guide/api_errors.md index adbe5e8..7346b66 100644 --- a/docs/developer-guide/api_errors.md +++ b/docs/developer-guide/api_errors.md @@ -8,6 +8,8 @@ it to `true` in `.env` `MISSING_PARAMETERS`: Invalid or missing parameters. +`INVALID_PARAMETERS`: Invalid parameters. + `NOT_FOUND`: Object not found. `ACCESS_DENIED`: User is not authorized to access the object. diff --git a/docs/user-guide/installation.md b/docs/user-guide/installation.md index f97a930..96feef9 100644 --- a/docs/user-guide/installation.md +++ b/docs/user-guide/installation.md @@ -23,7 +23,7 @@ you may be interested in looking at a [legacy 1.x release](https://github.com/cy - JSON PHP Extension - PHP curl extension -## Downloading the source code +## Download the source code If you would like to download a stable version of Polr, you may check out [the releases page](https://github.com/cydrobolt/polr/releases). @@ -49,7 +49,7 @@ $ chown -R apache polr $ chcon -R -t httpd_sys_rw_content_t polr/storage polr/.env ``` -## Installing using `composer` +## Install `composer` dependencies ```bash # download composer package @@ -177,7 +177,7 @@ server { To run Polr on another HTTP server or on shared hosting, you will need to set the home directory to `/PATH_TO_POLR/public`, not the root Polr folder. -## Creating the database +## Create the database ### MySQL