devshort

private self-hosted shortlink service
git clone https://git.clttr.info/devshort.git
Log (Feed) | Files | Refs (Tags) | README | LICENSE

commit 0eec4e2a26a7ba5dabd2a839b52578f56fc8729a
parent 8062c4333b64fb133be9743a4aea9064617d19ce
Author: Florian <flokX@users.noreply.github.com>
Date:   Thu, 26 Dec 2019 10:54:53 +0100

Relocate files

New directoy structure:
* admin panel in root
* user data in "data/"
* changed paths in admn.php

Diffstat:
Aadmin.php | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dadmin/.htaccess | 20--------------------
Dadmin/config.json | 20--------------------
Dadmin/index.php | 137-------------------------------------------------------------------------------
Dadmin/main.js | 120-------------------------------------------------------------------------------
Aassets/main.js | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adata/.htaccess | 10++++++++++
Adata/config.json | 20++++++++++++++++++++
Radmin/stats.json -> data/stats.json | 0
9 files changed, 287 insertions(+), 297 deletions(-)

diff --git a/admin.php b/admin.php @@ -0,0 +1,137 @@ +<?php + +// All relevant changes can be made in the data file. Please read the docs: https://github.com/flokX/devShort/wiki + +$config_path = implode(DIRECTORY_SEPARATOR, array(__DIR__, "data", "config.json")); +$config_content = json_decode(file_get_contents($config_path), true); +$stats_path = implode(DIRECTORY_SEPARATOR, array(__DIR__, "data", "stats.json")); +$stats_content = json_decode(file_get_contents($stats_path), true); + +// Filter the names that the admin interface doesn't break +function filter_name($nameRaw) { + $name = filter_var($nameRaw, FILTER_SANITIZE_STRING); + $name = str_replace(" ", "-", $name); + $name = preg_replace("/[^A-Za-z0-9-_]/", "", $name); + return $name; +} + +// API functions to delete and add the shortlinks via the admin panel +if (isset($_GET["delete"]) || isset($_GET["add"])) { + $data = json_decode(file_get_contents("php://input"), true); + if (isset($_GET["delete"])) { + unset($config_content["shortlinks"][$data["name"]]); + unset($stats_content[$data["name"]]); + } else if (isset($_GET["add"])) { + $filtered = array("name" => filter_name($data["name"]), + "url" => filter_var($data["url"], FILTER_SANITIZE_URL)); + if (!filter_var($filtered["url"], FILTER_VALIDATE_URL)) { + echo "{\"status\": \"unvalid-url\"}"; + exit; + } + $config_content["shortlinks"][$filtered["name"]] = $filtered["url"]; + $stats_content[$filtered["name"]] = array(); + } + file_put_contents($config_path, json_encode($config_content, JSON_PRETTY_PRINT)); + file_put_contents($stats_path, json_encode($stats_content, JSON_PRETTY_PRINT)); + header("Content-Type: application/json"); + echo "{\"status\": \"successful\"}"; + exit; +} + +// Generator for page customization +$links_string = ""; +if ($config_content["settings"]["custom_links"]) { + foreach ($config_content["settings"]["custom_links"] as $name => $url) { + $links_string = $links_string . "<a href=\"$url\" class=\"badge badge-secondary\">$name</a> "; + } + $links_string = substr($links_string, 0, -1); +} + +?> + +<!doctype html> +<html class="h-100" lang="en"> + +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="robots" content="noindex, nofollow"> + <meta name="author" content="<?php echo $config_content["settings"]["author"]; ?> and the devShort team"> + <link href="<?php echo $config_content["settings"]["favicon"]; ?>" rel="icon"> + <title>Admin panel | <?php echo $config_content["settings"]["name"]; ?></title> + <link href="assets/vendor/bootstrap/bootstrap.min.css" rel="stylesheet"> + <link href="assets/main.css" rel="stylesheet"> +</head> + +<body class="d-flex flex-column h-100"> + + <main class="flex-shrink-0"> + <div class="container"> + <h1 class="mt-5 text-center"><?php echo $config_content["settings"]["name"]; ?></h1> + <h4 class="mb-4 text-center">admin panel</h4> + <div class="row"> + <div class="col-md-4 col-lg-3"> + <div class="card d-none d-md-block mb-3"> + <div class="card-body"> + <a class="card-link" id="refresh" href="#refresh">Refresh charts</a> + </div> + </div> + <div class="card mb-3"> + <div class="card-body"> + <h5 class="card-title">Add shortlink <small class="d-md-none"><a class="card-link" id="refresh" href="#refresh">Refresh charts</a></small></h5> + <form id="add-form"> + <div class="form-group"> + <label for="name">Name</label> + <input class="form-control mb-2 mb-sm-0 mr-sm-2" id="name" type="text" placeholder="Link1" required> + </div> + <div class="form-group"> + <label for="url">URL (destination)</label> + <input class="form-control mb-2 mb-sm-0 mr-sm-2" id="url" type="url" placeholder="https://example.com" required> + </div> + <button class="btn btn-primary" type="submit">Add</button> + <div id="status"></div> + </form> + </div> + </div> + <div class="card mb-3"> + <div class="card-body"> + <h5 class="card-title">Search</h5> + <form> + <input class="form-control" id="search-bar" type="text"> + </form> + </div> + </div> + <div class="card d-none d-md-block"> + <div class="card-body"> + <p class="mb-0">powered by <a href="https://github.com/flokX/devShort">devShort</a> v2.4.0</p> + </div> + </div> + </div> + <div class="col-md-8 col-lg-9"> + <div class="d-flex justify-content-center"> + <div class="spinner-border text-primary mt-4" id="spinner" role="status"> + <span class="sr-only">Loading...</span> + </div> + </div> + <div id="charts"></div> + </div> + </div> + <p class="text-center d-md-none mt-1 mb-5">powered by <a href="https://github.com/flokX/devShort">devShort</a> v2.4.0</p> + </div> + </main> + + <footer class="footer mt-auto py-3 bg-light"> + <div class="container"> + <div class="d-flex justify-content-between align-items-center"> + <span class="text-muted">&copy; <?php echo date("Y") . " " . $config_content["settings"]["author"]; ?> and <a href="https://github.com/flokX/devShort">devShort</a></span> + <?php if ($links_string) { echo "<span class=\"text-muted\">$links_string</span>"; } ?> + </div> + </div> + </footer> + + <script src="assets/vendor/frappe-charts/frappe-charts.min.iife.js"></script> + <script src="assets/main.js"></script> + +</body> + +</html> diff --git a/admin/.htaccess b/admin/.htaccess @@ -1,20 +0,0 @@ -# Lock the whole directory until the installer runned - -# Generated by Nextcloud -# Nextcloud is licensed under the AGPL v3 license - -# line below if for Apache 2.4 -<ifModule mod_authz_core.c> -Require all denied -</ifModule> - -# line below if for Apache 2.2 -<ifModule !mod_authz_core.c> -deny from all -Satisfy All -</ifModule> - -# section for Apache 2.2 and 2.4 -<ifModule mod_autoindex.c> -IndexIgnore * -</ifModule> diff --git a/admin/config.json b/admin/config.json @@ -1,20 +0,0 @@ -{ - "installer": { - "username": "admin", - "password": "" - }, - "settings": { - "name": "devShort shortlink service", - "author": "The admin", - "home_link": "https://github.com/flokX/devShort", - "favicon": "assets/icon.png", - "custom_links": { - "devShort wiki": "https://github.com/flokX/devShort/wiki", - "devShort author": "https://github.com/flokX" - } - }, - "shortlinks": { - "repo": "https://github.com/flokX/devShort", - "wiki": "https://github.com/flokX/devShort/wiki" - } -} diff --git a/admin/index.php b/admin/index.php @@ -1,137 +0,0 @@ -<?php - -// All relevant changes can be made in the data file. Please read the docs: https://github.com/flokX/devShort/wiki - -$config_path = __DIR__ . DIRECTORY_SEPARATOR . "config.json"; -$config_content = json_decode(file_get_contents($config_path), true); -$stats_path = __DIR__ . DIRECTORY_SEPARATOR . "stats.json"; -$stats_content = json_decode(file_get_contents($stats_path), true); - -// Filter the names that the admin interface doesn't break -function filter_name($nameRaw) { - $name = filter_var($nameRaw, FILTER_SANITIZE_STRING); - $name = str_replace(" ", "-", $name); - $name = preg_replace("/[^A-Za-z0-9-_]/", "", $name); - return $name; -} - -// API functions to delete and add the shortlinks via the admin panel -if (isset($_GET["delete"]) || isset($_GET["add"])) { - $data = json_decode(file_get_contents("php://input"), true); - if (isset($_GET["delete"])) { - unset($config_content["shortlinks"][$data["name"]]); - unset($stats_content[$data["name"]]); - } else if (isset($_GET["add"])) { - $filtered = array("name" => filter_name($data["name"]), - "url" => filter_var($data["url"], FILTER_SANITIZE_URL)); - if (!filter_var($filtered["url"], FILTER_VALIDATE_URL)) { - echo "{\"status\": \"unvalid-url\"}"; - exit; - } - $config_content["shortlinks"][$filtered["name"]] = $filtered["url"]; - $stats_content[$filtered["name"]] = array(); - } - file_put_contents($config_path, json_encode($config_content, JSON_PRETTY_PRINT)); - file_put_contents($stats_path, json_encode($stats_content, JSON_PRETTY_PRINT)); - header("Content-Type: application/json"); - echo "{\"status\": \"successful\"}"; - exit; -} - -// Generator for page customization -$links_string = ""; -if ($config_content["settings"]["custom_links"]) { - foreach ($config_content["settings"]["custom_links"] as $name => $url) { - $links_string = $links_string . "<a href=\"$url\" class=\"badge badge-secondary\">$name</a> "; - } - $links_string = substr($links_string, 0, -1); -} - -?> - -<!doctype html> -<html class="h-100" lang="en"> - -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> - <meta name="robots" content="noindex, nofollow"> - <meta name="author" content="<?php echo $config_content["settings"]["author"]; ?> and the devShort team"> - <link href="../<?php echo $config_content["settings"]["favicon"]; ?>" rel="icon"> - <title>Admin panel | <?php echo $config_content["settings"]["name"]; ?></title> - <link href="../assets/vendor/bootstrap/bootstrap.min.css" rel="stylesheet"> - <link href="../assets/main.css" rel="stylesheet"> -</head> - -<body class="d-flex flex-column h-100"> - - <main class="flex-shrink-0"> - <div class="container"> - <h1 class="mt-5 text-center"><?php echo $config_content["settings"]["name"]; ?></h1> - <h4 class="mb-4 text-center">admin panel</h4> - <div class="row"> - <div class="col-md-4 col-lg-3"> - <div class="card d-none d-md-block mb-3"> - <div class="card-body"> - <a class="card-link" id="refresh" href="#refresh">Refresh charts</a> - </div> - </div> - <div class="card mb-3"> - <div class="card-body"> - <h5 class="card-title">Add shortlink <small class="d-md-none"><a class="card-link" id="refresh" href="#refresh">Refresh charts</a></small></h5> - <form id="add-form"> - <div class="form-group"> - <label for="name">Name</label> - <input class="form-control mb-2 mb-sm-0 mr-sm-2" id="name" type="text" placeholder="Link1" required> - </div> - <div class="form-group"> - <label for="url">URL (destination)</label> - <input class="form-control mb-2 mb-sm-0 mr-sm-2" id="url" type="url" placeholder="https://example.com" required> - </div> - <button class="btn btn-primary" type="submit">Add</button> - <div id="status"></div> - </form> - </div> - </div> - <div class="card mb-3"> - <div class="card-body"> - <h5 class="card-title">Search</h5> - <form> - <input class="form-control" id="search-bar" type="text"> - </form> - </div> - </div> - <div class="card d-none d-md-block"> - <div class="card-body"> - <p class="mb-0">powered by <a href="https://github.com/flokX/devShort">devShort</a> v2.4.0</p> - </div> - </div> - </div> - <div class="col-md-8 col-lg-9"> - <div class="d-flex justify-content-center"> - <div class="spinner-border text-primary mt-4" id="spinner" role="status"> - <span class="sr-only">Loading...</span> - </div> - </div> - <div id="charts"></div> - </div> - </div> - <p class="text-center d-md-none mt-1 mb-5">powered by <a href="https://github.com/flokX/devShort">devShort</a> v2.4.0</p> - </div> - </main> - - <footer class="footer mt-auto py-3 bg-light"> - <div class="container"> - <div class="d-flex justify-content-between align-items-center"> - <span class="text-muted">&copy; <?php echo date("Y") . " " . $config_content["settings"]["author"]; ?> and <a href="https://github.com/flokX/devShort">devShort</a></span> - <?php if ($links_string) { echo "<span class=\"text-muted\">$links_string</span>"; } ?> - </div> - </div> - </footer> - - <script src="../assets/vendor/frappe-charts/frappe-charts.min.iife.js"></script> - <script src="main.js"></script> - -</body> - -</html> diff --git a/admin/main.js b/admin/main.js @@ -1,120 +0,0 @@ -/* Register variables */ -const currentDate = new Date(); -const startDate = new Date(new Date().setFullYear(currentDate.getFullYear() - 1)); -const spinner = document.getElementById('spinner'); -const chartsDiv = document.getElementById('charts'); -const statusDiv = document.getElementById('status'); - -/* Helper function to post to page api */ -function post(url, data) { - 'use strict'; - return fetch(url, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }).then(function (response) { - return response.json(); - }); -} - -/* Add a new shortlink */ -document.getElementById('add-form').addEventListener('submit', function (event) { - 'use strict'; - event.preventDefault(); - spinner.style.display = ''; - post('index.php?add', { - name: document.getElementById('name').value, - url: document.getElementById('url').value - }).then(function (data) { - if (data.status === 'successful') { - document.getElementById('name').value = ''; - document.getElementById('url').value = 'https://'; - if (statusDiv.firstChild) { - statusDiv.firstChild.remove(); - } - getCharts(); - } else if (data.status === 'unvalid-url') { - statusDiv.insertAdjacentHTML('afterbegin', '<div class="alert alert-danger" role="alert">Unvalid URL. Please provide a valid URL.</div>'); - } else { - statusDiv.insertAdjacentHTML('afterbegin', '<div class="alert alert-danger" role="alert">Error. Please try again.</div>'); - } - }); - spinner.style.display = 'none'; -}); - -/* Provide search functionality */ -var searchBox = document.getElementById('search-bar'); -searchBox.addEventListener('input', function (event) { - 'use strict'; - for (let node of chartsDiv.childNodes) { - let linkName = node.firstElementChild.innerHTML.toLowerCase(); - if (linkName.includes(searchBox.value.toLowerCase())) { - node.style.display = 'block'; - } else { - node.style.display = 'none'; - } - } -}); - -/* Reload charts */ -document.getElementById('refresh').addEventListener('click', function (event) { - 'use strict'; - event.preventDefault(); - getCharts(); -}); - -/* Get charts and date (remove old when necessary) */ -function getCharts() { - 'use strict'; - spinner.style.display = 'block'; - while (chartsDiv.firstChild) { - chartsDiv.firstChild.remove(); - } - fetch('stats.json', { - cache: 'no-cache', - credentials: 'same-origin' - }).then(function (response) { - return response.json(); - }).then(function (json) { - for (let [name, data] of Object.entries(json)) { - chartsDiv.insertAdjacentHTML('beforeend', `<div id="card-${name}" class="card text-center mb-3"> - <div class="card-header">${name}</div> - <div class="card-body p-2"> - <div id="heatmap-${name}" class="overflow-auto"></div> - </div> - <div class="card-footer text-muted"> - <a id="export-${name}" href="#export" class="card-link">Export chart</a><a id="delete-${name}" href="#delete" class="card-link">Delete shortlink and dataset</a> - </div> -</div>`); - let heatmap = new frappe.Chart('div#heatmap-' + name, { - type: 'heatmap', - title: 'Access statistics for ' + name, - data: { - dataPoints: data, - start: startDate, - end: currentDate - }, - countLabel: 'Access(es)', - discreteDomains: 0 - }); - document.getElementById('export-' + name).addEventListener('click', function (event) { - event.preventDefault(); - heatmap.export(); - }); - document.getElementById('delete-' + name).addEventListener('click', function (event) { - event.preventDefault(); - post('index.php?delete', { - name: name - }).then(function () { - document.getElementById('card-' + name).remove(); - }); - }); - } - spinner.style.display = 'none'; - }); -} - -getCharts(); diff --git a/assets/main.js b/assets/main.js @@ -0,0 +1,120 @@ +/* Register variables */ +const currentDate = new Date(); +const startDate = new Date(new Date().setFullYear(currentDate.getFullYear() - 1)); +const spinner = document.getElementById('spinner'); +const chartsDiv = document.getElementById('charts'); +const statusDiv = document.getElementById('status'); + +/* Helper function to post to page api */ +function post(url, data) { + 'use strict'; + return fetch(url, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }).then(function (response) { + return response.json(); + }); +} + +/* Add a new shortlink */ +document.getElementById('add-form').addEventListener('submit', function (event) { + 'use strict'; + event.preventDefault(); + spinner.style.display = ''; + post('index.php?add', { + name: document.getElementById('name').value, + url: document.getElementById('url').value + }).then(function (data) { + if (data.status === 'successful') { + document.getElementById('name').value = ''; + document.getElementById('url').value = 'https://'; + if (statusDiv.firstChild) { + statusDiv.firstChild.remove(); + } + getCharts(); + } else if (data.status === 'unvalid-url') { + statusDiv.insertAdjacentHTML('afterbegin', '<div class="alert alert-danger" role="alert">Unvalid URL. Please provide a valid URL.</div>'); + } else { + statusDiv.insertAdjacentHTML('afterbegin', '<div class="alert alert-danger" role="alert">Error. Please try again.</div>'); + } + }); + spinner.style.display = 'none'; +}); + +/* Provide search functionality */ +var searchBox = document.getElementById('search-bar'); +searchBox.addEventListener('input', function (event) { + 'use strict'; + for (let node of chartsDiv.childNodes) { + let linkName = node.firstElementChild.innerHTML.toLowerCase(); + if (linkName.includes(searchBox.value.toLowerCase())) { + node.style.display = 'block'; + } else { + node.style.display = 'none'; + } + } +}); + +/* Reload charts */ +document.getElementById('refresh').addEventListener('click', function (event) { + 'use strict'; + event.preventDefault(); + getCharts(); +}); + +/* Get charts and date (remove old when necessary) */ +function getCharts() { + 'use strict'; + spinner.style.display = 'block'; + while (chartsDiv.firstChild) { + chartsDiv.firstChild.remove(); + } + fetch('../data/stats.json', { + cache: 'no-cache', + credentials: 'same-origin' + }).then(function (response) { + return response.json(); + }).then(function (json) { + for (let [name, data] of Object.entries(json)) { + chartsDiv.insertAdjacentHTML('beforeend', `<div id="card-${name}" class="card text-center mb-3"> + <div class="card-header">${name}</div> + <div class="card-body p-2"> + <div id="heatmap-${name}" class="overflow-auto"></div> + </div> + <div class="card-footer text-muted"> + <a id="export-${name}" href="#export" class="card-link">Export chart</a><a id="delete-${name}" href="#delete" class="card-link">Delete shortlink and dataset</a> + </div> +</div>`); + let heatmap = new frappe.Chart('div#heatmap-' + name, { + type: 'heatmap', + title: 'Access statistics for ' + name, + data: { + dataPoints: data, + start: startDate, + end: currentDate + }, + countLabel: 'Access(es)', + discreteDomains: 0 + }); + document.getElementById('export-' + name).addEventListener('click', function (event) { + event.preventDefault(); + heatmap.export(); + }); + document.getElementById('delete-' + name).addEventListener('click', function (event) { + event.preventDefault(); + post('index.php?delete', { + name: name + }).then(function () { + document.getElementById('card-' + name).remove(); + }); + }); + } + spinner.style.display = 'none'; + }); +} + +getCharts(); diff --git a/data/.htaccess b/data/.htaccess @@ -0,0 +1,10 @@ +# Lock the whole directory because the data should not be freely accessible +# (aplication configuration data) + +<ifModule mod_authz_core.c> + Require all denied +</ifModule> + +<ifModule mod_autoindex.c> + IndexIgnore * +</ifModule> diff --git a/data/config.json b/data/config.json @@ -0,0 +1,20 @@ +{ + "installer": { + "username": "admin", + "password": "123" + }, + "settings": { + "name": "devShort shortlink service", + "author": "The admin", + "home_link": "https://github.com/flokX/devShort", + "favicon": "assets/icon.png", + "custom_links": { + "devShort wiki": "https://github.com/flokX/devShort/wiki", + "devShort author": "https://github.com/flokX" + } + }, + "shortlinks": { + "repo": "https://github.com/flokX/devShort", + "wiki": "https://github.com/flokX/devShort/wiki" + } +} diff --git a/admin/stats.json b/data/stats.json