...
 
Commits (4)
.idea
settings.ini
apikeys.json
......@@ -6,4 +6,9 @@ RewriteRule ^([\w\+\-\$\§\(\)]+)/? index.php?shortcode=$1
<FilesMatch "\.(?:ini)$">
Order allow,deny
Deny from all
</FilesMatch>
\ No newline at end of file
</FilesMatch>
<FilesMatch "apikeys.json">
Order allow,deny
Deny from all
</FilesMatch>
{
"version" : "1.0.0",
"keys" : {
"eRrT3PMjgPJiIyrkkg0XlPbJm2ClV1" : {
"user" : "heinz",
"accesslevel" : 2
}
}
}
\ No newline at end of file
<?php
namespace FSI;
class ErrorCode {
const ERROR = 500;
const MALFORMED_REQUEST = 501;
const MALFORMED_URL = 502;
const AUTHENTICATION_FAILED = 510;
const PERMISSION_ERROR = 511;
const INVALID_SHORTCODE_FORMAT = 520;
const SHORTCODE_ALREADY_EXISTS = 521;
const SHORTCODE_NOT_EXISTS = 522;
const UNKNOWN_ENDPOINT = 555;
}
class API {
private $route;
private $keys;
private $shortener;
private $request;
private $settings;
private $accessLevel;
public function __construct() {
$this->shortener = new Shortener();
$this->settings = SettingsHandler::Instance();
$route = explode('/', trim(strstr($_SERVER["REQUEST_URI"], "api/"), "/ "));
array_shift($route);
$this->route = (string) trim($route[0]);
$this->keys = json_decode(file_get_contents(dirname(__FILE__)."/../apikeys.json"));
$this->request = json_decode(file_get_contents("php://input"));
if (!isset($this->request->apikey) && $this->route != "help")
$this->error("authentication failure.", ErrorCode::AUTHENTICATION_FAILED);
if ($this->route !== "help" && (!isset($this->request) || !isset($this->request->request) || !is_object($this->request->request)))
$this->error("a malformed request has been sent", ErrorCode::MALFORMED_REQUEST);
$this->checkAPIKey();
if (count($route) <= 0)
$this->error("this endpoint does not exist on the API.", ErrorCode::UNKNOWN_ENDPOINT);
$this->route();
}
private function checkAPIKey() {
if ($this->route === "help")
return;
if (!isset($this->keys->keys->{$this->request->apikey}))
$this->error("authentication failure.", ErrorCode::AUTHENTICATION_FAILED);
$this->accessLevel = $this->keys->keys->{$this->request->apikey}->accesslevel;
}
private function route() {
if (in_array("route_".$this->route, get_class_methods($this)))
$this->{"route_".$this->route}();
else
$this->error("the endpoint '".$this->route."' does not exist on the API.", ErrorCode::UNKNOWN_ENDPOINT);
}
private function route_add() {
if (!isset($this->request->request->url))
$this->error("a malformed request has been sent.", ErrorCode::MALFORMED_REQUEST);
$shortcode = isset($this->request->request->shortcode) ? $this->request->request->shortcode : $this->shortener->generateShortcode($this->settings->system->shortcodeLength);
if (strlen($shortcode) < $this->settings->system->shortcodeMinLength)
$this->error("the shortcode must be at least ".$this->settings->system->shortcodeMinLength." characters long.", ErrorCode::INVALID_SHORTCODE_FORMAT);
if (!preg_match("/^(http|https):\/\//", $this->request->request->url))
$this->error("a malformed URL has been sent.", ErrorCode::MALFORMED_URL);
if (preg_match("/^[A-Z]/", $shortcode) && $this->accessLevel > 1)
$this->error("this API key can not create shortcodes starting with an uppercase letter.", ErrorCode::PERMISSION_ERROR);
try {
$user = isset($this->keys->keys->{$this->request->apikey}->user) ? $this->keys->keys->{$this->request->apikey}->user :
$this->request->apikey;
$this->shortener->add($this->request->request->url, $shortcode, $user);
} catch (ShortcodeAlreadyExistsException $exception) {
$this->error("this shortcode has already been taken.", ErrorCode::SHORTCODE_ALREADY_EXISTS);
}
$response = new \stdClass();
$response->shortcode = $shortcode;
$response->url = $this->request->request->url;
$this->success($response);
}
private function route_get() {
if (!isset($this->request->request->url) && !isset($this->request->request->shortcode))
$this->error("a malformed request has been sent. Too few parameters.", ErrorCode::MALFORMED_REQUEST);
if (isset($this->request->request->url) && isset($this->request->request->shortcode))
$this->error("a malformed request has been sent. Too many parameters", ErrorCode::MALFORMED_REQUEST);
$shortcode = isset( $this->request->request->shortcode) ? $this->request->request->shortcode : null;
$url = isset( $this->request->request->url) ? $this->request->request->url : null;
$out = $this->shortener->get($url, $shortcode);
if ($out === null)
$this->error("the requested URL/shortcode does not exist", ErrorCode::SHORTCODE_NOT_EXISTS);
$response = new \stdClass();
if ($url !== null)
$response->shortcode = $out;
elseif($shortcode !== null)
$response->url = $out;
$this->success($response);
}
private function route_help() {
header("Content-Type: application/json");
echo file_get_contents(dirname(__FILE__)."/../help.txt");
}
private function error($msg, $code = ErrorCode::ERROR) {
$obj = new \stdClass();
$obj->status = "ERROR";
$obj->code = $code;
$response = new \stdClass();
$response->msg = $msg;
$obj->response = $response;
$this->respond($obj);
}
function success($response) {
$obj = new \stdClass();
$obj->status = "OK";
$obj->code = 0;
$obj->response = $response;
$this->respond($obj);
}
private function respond($response) {
header("Content-Type: application/json");
echo json_encode($response, JSON_PRETTY_PRINT);
exit;
}
}
\ No newline at end of file
......@@ -6,13 +6,15 @@ class PostHandler {
private $root;
private $db;
private $settings;
private $shortener;
public function __construct(URLShortener $root) {
$this->root = $root;
if (!$root->authenticator->authenticated())
return;
$this->db = DatabaseHandler::Instance();
$this->settings = SettingsHandler::Instance();
$this->db = DatabaseHandler::Instance();
$this->settings = SettingsHandler::Instance();
$this->shortener = new Shortener();
$this->add();
}
......@@ -26,8 +28,7 @@ class PostHandler {
exit;
}
$shortcode = $_POST["shorturl"] == "" ? $this->generateShortcode() : $_POST["shorturl"];
$shortcode = $_POST["shorturl"] == "" ? $this->shortener->generateShortcode($this->settings->system->shortcodeLength) : $_POST["shorturl"];
$minLen = $this->settings->system->shortcodeMinLength;
if (strlen($shortcode) < $minLen) {
new Alert("Der Shortcode muss mindestens <b>$minLen</b> Zeichen lang sein.", AlertLevel::ERROR);
......@@ -40,37 +41,16 @@ class PostHandler {
exit;
}
$query = $this->db->prepare("SELECT shortcode FROM urls WHERE shortcode = ?");
$query->bind_param("s", $shortcode);
$query->execute();
$query->store_result();
if ($query->num_rows > 0) {
try {
$this->shortener->add($_POST["url"], $shortcode, $this->root->authenticator->user());
} catch (ShortcodeAlreadyExistsException $exception) {
new Alert("Der Shortcode <code>$shortcode</code> ist leider bereits vergeben.", AlertLevel::WARNING);
header("Location: ./");
exit;
}
$query = $this->db->prepare("INSERT INTO urls (url, shortcode, rft) VALUES(?, ?, ?)");
$query->bind_param(
"sss",
$_POST["url"],
$shortcode,
$this->root->authenticator->user()
);
$query->execute();
new Alert("Dein Link wurde hinzugefügt.", AlertLevel::OK, "");
header("Location: ./");
}
private function generateShortcode() {
$code = substr(hash("md5", uniqid('', true)), 0, $this->settings->system->shortcodeLength);
$query = $this->db->query("SELECT COUNT(*) as amount FROM urls WHERE shortcode = '$code'");
$result = $query->fetch_object();
if ($result->amount == 0)
return $code;
else
return $this->generateShortcode();
}
}
\ No newline at end of file
<?php
namespace FSI;
class ShortcodeAlreadyExistsException extends \Exception {
}
\ No newline at end of file
<?php
namespace FSI;
class Shortener {
private $db;
public function __construct() {
$this->db = \FSI\DatabaseHandler::Instance();
}
/**
* @param $url
* @param $shortcode
* @param $user
*
* @throws ShortcodeAlreadyExistsException
*/
public function add($url, $shortcode, $user) {
if ($this->shortcodeExists($shortcode)) {
throw new ShortcodeAlreadyExistsException();
}
$query = $this->db->prepare("INSERT INTO urls (url, shortcode, rft) VALUES(?, ?, ?)");
$query->bind_param(
"sss",
$url,
$shortcode,
$user
);
$query->execute();
}
public function get($url = null, $shortcode = null) {
$out = null;
if ($url === null) {
$query = $this->db->prepare("SELECT url FROM urls WHERE shortcode = ? LIMIT 1");
$query->bind_param("s", $shortcode);
$query->execute();
$query->bind_result($out);
$query->fetch();
}
else if ($shortcode == null) {
$query = $this->db->prepare("SELECT shortcode FROM urls WHERE url = ? LIMIT 1");
$query->bind_param("s", $url);
$query->execute();
$query->bind_result($out);
$query->fetch();
}
else {
return $out = null;
}
return $out;
}
private function shortcodeExists($shortcode) {
$query = $this->db->prepare("SELECT shortcode FROM urls WHERE shortcode = ?");
$query->bind_param("s", $shortcode);
$query->execute();
$query->store_result();
return $query->num_rows > 0;
}
public function generateShortcode($length = 6) {
$code = substr(hash("md5", uniqid('', true)), 0, $length);
$query = $this->db->query("SELECT COUNT(*) as amount FROM urls WHERE shortcode = '$code'");
$result = $query->fetch_object();
if ($result->amount == 0)
return $code;
else
return $this->generateShortcode();
}
}
\ No newline at end of file
......@@ -9,8 +9,13 @@ class URLShortener {
public function __construct() {
spl_autoload_register([$this, "autoload"]);
if (isset($_GET["shortcode"]))
if (isset($_GET["shortcode"])) {
if ($_GET["shortcode"] == "api") {
new API();
return;
}
new Redirector();
}
$this->authenticator = new Authenticator($this);
if (isset($_GET["auth"]))
$this->authenticator->sendRequest();
......
@import url("bootstrap.min.css");@import url("ribbon.css");@font-face{font-family:"Linux Libertine";src:url("../fonts/linux_libertine.otf")}body{background:#005196;color:white}h1{font-family:"Linux Libertine",serif}input{font-size:x-large !important;height:auto !important}main{text-align:center}table.table-list{background:white !important;color:#333}table.table-list tr th:first-child{width:50%;text-align:left !important}table.table-list tr td{text-align:left !important}table.table-list tr td input{font-size:medium !important;font-weight:bold;padding:0 8px}.alert{margin-top:12px}.container{width:50%}.input-group-addon{font-size:large}.logo{padding:20px}.logo img{width:40%}.user{text-align:left;position:absolute;left:1%;top:3%;color:#bbb}@media(max-width:991px){.container{width:95%}}
\ No newline at end of file
@import url("bootstrap.min.css");@import url("ribbon.css");@font-face{font-family:"Linux Libertine";src:url("../fonts/linux_libertine.otf")}body{background:#005196;color:white;padding:0 0 50px}h1{font-family:"Linux Libertine",serif}input{font-size:x-large !important;height:auto !important}main{text-align:center}.alert{margin-top:12px}.container{width:50%}.input-group-addon{font-size:large}.link-list{background:white;display:grid;grid-template-columns:1fr 1fr;grid-gap:15px;border-radius:5px;padding:8px;width:100%;box-sizing:border-box;color:#333}.link-list a{max-width:100%;text-align:left;display:block;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.link-list input{font-size:medium !important;font-weight:bold;padding:0 8px}.logo{padding:20px}.logo img{width:40%}.user{text-align:left;position:absolute;left:1%;top:3%;color:#bbb}@media(max-width:991px){.container{width:95%}}
\ No newline at end of file
......@@ -10,6 +10,7 @@
body {
background: #005196;
color: white;
padding: 0 0 50px;
}
h1 {
......@@ -25,23 +26,6 @@ main {
text-align: center;
}
table.table-list {
background: white !important;
color: #333;
}
table.table-list tr th:first-child {
width: 50%;
text-align: left !important;
}
table.table-list tr td {
text-align: left !important;
}
table.table-list tr td input {
font-size: medium !important;
font-weight: bold;
padding: 0 8px;
}
.alert {
margin-top: 12px;
}
......@@ -54,6 +38,32 @@ table.table-list tr td input {
font-size: large;
}
.link-list {
background: white;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 15px;
border-radius: 5px;
padding: 8px;
width: 100%;
box-sizing: border-box;
color: #333;
}
.link-list a {
max-width: 100%;
text-align: left;
display: block;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.link-list input {
font-size: medium !important;
font-weight: bold;
padding: 0 8px;
}
.logo {
padding: 20px;
}
......
# API documentation
Any API request has to be sent via `HTTP PUT` and has to fullfill the following
format:
{
"apikey" : "YOUR_API_KEY"
"request" : {
// [PARAMS] (dependent on endpoint)
}
}
## Endpoints
/new add a new URL <-> shortcode combination
/get get the corresponding URL/shortcode to the request
/help shows this help
### new
Add a new URL <-> shortcode combination. If the parameter `shortcode` is
omitted, a random code is assigned to the URL.
**Example:**
HTTP/1.1 PUT https://fsi.rocks/api/new
{
"apikey" : "YOUR_API_KEY"
"request" : {
"url" : "http(s)://url-to.shorten"
"shortcode" : "your-shortcode" //optional
}
}
### get
Get either the URL or the shortcode of an existing entry, depending on which
information is supplied in the request.
**Example:**
HTTP/1.1 PUT https://fsi.rocks/api/get
{
"apikey" : "YOUR_API_KEY"
"request" : {
"url" : "http(s)://url-for.shortcode" // optional
"shortcode" : "shortcode-for-url" // optional
}
}
submitting both "url" and "shortcode" or neither of them will result in an
error.
## Example
A simple cURL example:
curl -X PUT -H "Content-Type: application/json" -d \
'{"apikey" : "YOUR_API_KEY", "request" : { "shortcode": "test"}}' \
"https://fsi.rocks/api/get"
**Response:**
{
"status": "OK",
"code": 0,
"response": {
"url": "https://fsi.rocks"
}
}
## Error codes
ERROR = 500;
MALFORMED_REQUEST = 501;
MALFORMED_URL = 502;
AUTHENTICATION_FAILED = 510;
PERMISSION_ERROR = 511;
INVALID_SHORTCODE_FORMAT = 520;
SHORTCODE_ALREADY_EXISTS = 521;
SHORTCODE_NOT_EXISTS = 522;
UNKNOWN_ENDPOINT = 555;
<a class="btn btn-lg btn-success" href="./?auth=true">Einloggen</a>
\ No newline at end of file
<a class="btn btn-lg btn-success" href="./?auth=true">Einloggen</a>
<p class="alert alert-info" style="color: #222; margin-top: 50px; font-size: small;">
Bitte lies vor der Nutzung dieses Dienstes die
<a href="https://fsi.hochschule-trier.de/datenschutzerlklaerung">Datenschutzerklärung</a>
der Fachschaft Informatik an der Hochschule Trier, um zu erfahren, welche und wie deine personengezogenen Daten
verarbeitet werden. Bist du mit dem Inhalt dieser Erklärung nicht einverstanden, bitten wir dich von der Nutzung
dieses Dienstes abzusehen.
</p>
\ No newline at end of file
......@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width" />
<meta charset="utf-8" />
<title>URL-shortener – fsi.rocks</title>
<link rel="icon" href="images/favicon.png" sizes="128x128" />
</head>
<body>
<a class="github-fork-ribbon" href="https://gitlab.fsi.hochschule-trier.de/fachschaftsrat/url-shortener" title="open beta">open beta</a>
......
......@@ -17,15 +17,11 @@
</form>
<h2 style="margin-top: 50px;">Deine Links</h2>
<table class="table table-list">
<tr>
<th>URL</th>
<th>Shortlink</th>
</tr>
<section class="link-list">
<b>URL</b>
<b>Shortlink</b>
{% for link in links %}
<tr>
<td><a href="{{link.url}}">{{link.url}}</a></td>
<td><input type="text" value="{{baseurl}}/{{link.shortcode}}" disabled></td>
</tr>
<a href="{{link.url}}" title="{{link.url}}">{{link.url}}</a>
<input type="text" value="{{baseurl}}/{{link.shortcode}}" disabled>
{% endfor %}
</table>
\ No newline at end of file
</section>