devshort

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

commit c57058c77a97967600393ed572316836c07ae330
parent 1aee26df4fb7e98ef8790bfa746013554a9f4efc
Author: Florian <flokX@users.noreply.github.com>
Date:   Sun, 29 Dec 2019 11:56:04 +0100

Merge pull request #12 from flokX/admin-panel-redesign

Admin panel redesign
Diffstat:
MLICENSE | 2+-
Aadmin-auth.php | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aadmin.php | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dadmin/.htaccess | 20--------------------
Dadmin/config.json | 20--------------------
Dadmin/index.php | 117-------------------------------------------------------------------------------
Dadmin/main.js | 115-------------------------------------------------------------------------------
Massets/main.css | 8++------
Aassets/main.js | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adata/.htaccess | 10++++++++++
Adata/config.json | 17+++++++++++++++++
Radmin/stats.json -> data/stats.json | 0
Mindex.php | 22+++++++++++-----------
Dinstaller.php | 90-------------------------------------------------------------------------------
Mredirect.php | 20++++++++++----------
15 files changed, 451 insertions(+), 390 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 flokX +Copyright (c) 2019-2020 flokX Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/admin-auth.php b/admin-auth.php @@ -0,0 +1,109 @@ +<?php + +// All relevant changes can be made in the data file. Please read the docs: https://github.com/flokX/devShort/wiki + +session_start(); +$incorrect_password = false; + +$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); + +// If no password is in the config.json file, redirect to wiki page +if (!$config_content["admin_password"]) { + header("Location: https://github.com/flokX/devShort/wiki/Installation#installation"); + exit; +} + +// First run: Hash password if it's in the config.json as clear text +$admin_password = $config_content["admin_password"]; +if (password_get_info($admin_password)["algo"] === 0) { + $hash = password_hash($admin_password, PASSWORD_DEFAULT); +} else { + $hash = $admin_password; +} +$config_content["admin_password"] = $hash; +file_put_contents($config_path, json_encode($config_content, JSON_PRETTY_PRINT)); + +// Logout user in session if mode is logout +if (isset($_GET["logout"])) { + unset($_SESSION["user_authenticated"]); + header("Location: index.php"); + exit; +} + +// Login user in session if mode is login and post data is available +if (isset($_GET["login"]) && isset($_POST["input_password"])) { + if (password_verify($_POST["input_password"], $config_content["admin_password"])) { + $_SESSION["user_authenticated"] = true; + header("Location: admin.php"); + exit; + } else { + $incorrect_password = true; + } +} + +// Generate custom buttons for the footer +$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>Login | <?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"> + <nav class="mt-3" aria-label="breadcrumb"> + <ol class="breadcrumb shadow-sm"> + <li class="breadcrumb-item"><a href="<?php echo $config_content["settings"]["home_link"]; ?>">Home</a></li> + <li class="breadcrumb-item"><?php echo $config_content["settings"]["name"]; ?></li> + <li class="breadcrumb-item active" aria-current="page">Login</li> + </ol> + </nav> + <h1 class="mt-5">Login</h1> + <p class="lead">Please sign in to access the admin panel. If you need help, visit <a href="https://github.com/flokX/devShort/wiki">the devShort wiki</a>.</p> + <form action="admin-auth.php?login" method="POST"> + <div class="alert alert-danger" role="alert" <?php if (!$incorrect_password) { echo "style=\"display: none;\""; } ?>> + The given password was incorrect, please try again! + </div> + <div class="form-group"> + <label for="inputPassword">Password</label> + <input class="form-control" id="inputPassword" name="input_password" type="password" autofocus required> + </div> + <button class="btn btn-primary" type="submit">Login</button> + </form> + </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> + +</body> + +</html> diff --git a/admin.php b/admin.php @@ -0,0 +1,159 @@ +<?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); + +// Check if authentication is valid +session_start(); +if (!isset($_SESSION["user_authenticated"])) { + header("Location: admin-auth.php?login"); + exit; +} + +// Deliver stats.json content for the program (make AJAX calls and charts reloading possible) +if (isset($_GET["get_stats"])) { + header("Content-Type: application/json"); + readfile($stats_path); + exit; +} + +// 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)); + header("Content-Type: application/json"); + echo "{\"status\": \"successful\"}"; + exit; +} + +// Generate custom buttons for the footer +$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"> + <h5 class="card-title">Tools</h5> + <a class="card-link" id="refresh-1" href="#refresh">Refresh charts</a> + <a class="card-link" href="admin-auth.php?logout">Logout</a> + </div> + </div> + <div class="card mb-3"> + <div class="card-body"> + <h5 class="card-title">Add shortlink</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 mb-3"> + <div class="card-body"> + <p class="mb-0" id="version-1">powered by <a href="https://github.com/flokX/devShort">devShort</a></p> + </div> + </div> + <div class="card d-md-none mb-3"> + <div class="card-body text-center"> + <a class="card-link" id="refresh-2" href="#refresh">Refresh charts</a> + <a class="card-link" href="admin-auth.php?logout">Logout</a> + </div> + </div> + </div> + <div class="col-md-8 col-lg-9"> + <div class="d-flex justify-content-center"> + <div class="spinner-border text-primary my-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" id="version-2">powered by <a href="https://github.com/flokX/devShort">devShort</a></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,117 +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="card mb-3"> - <div class="card-body"> - <h5 class="card-title">Add shortlink <small><a class="card-link" id="refresh" href="#refresh">Refresh charts</a></small></h5> - <form class="form-inline" id="add-form"> - <label class="sr-only" for="name">Name</label> - <input class="form-control mb-2 mr-sm-2" id="name" type="text" placeholder="Link1" required> - <label class="sr-only" for="url">URL (destination)</label> - <input class="form-control mb-2 mr-sm-2" id="url" type="url" placeholder="https://example.com" required> - <button class="btn btn-primary mb-2" 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="d-flex justify-content-center"> - <div class="spinner-border text-primary" id="spinner" role="status"> - <span class="sr-only">Loading...</span> - </div> - </div> - <div id="charts"></div> - <p class="text-center mt-4 mb-5">powered by <a href="https://github.com/flokX/devShort">devShort</a> v2.3.0 (Latest: <a href="https://github.com/flokX/devShort/releases"><img src="https://img.shields.io/github/release/flokX/devShort.svg" alt="Latest release"></a>, <a href="https://github.com/flokX/devShort/wiki/Installation#update-or-reinstallation">How to update</a>)</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,115 +0,0 @@ -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'); - -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(); - }); -} - -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'; -}); - -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'; - } - } -}); - -document.getElementById('refresh').addEventListener('click', function (event) { - 'use strict'; - event.preventDefault(); - getCharts(); -}); - -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.css b/assets/main.css @@ -1,9 +1,4 @@ -/* Custom page CSS (by Bootstrap) --------------------------------------------------- */ -/* Not required for template or sticky footer method. */ - .container { width: auto; - max-width: 800px; padding: 0 15px; -} +} +\ No newline at end of file diff --git a/assets/main.js b/assets/main.js @@ -0,0 +1,132 @@ +/* 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'); +const version = "v3.0.0"; + +/* 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('admin.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'; + } + } +}); + +/* Refresh charts */ +function refreshCharts(event) { + 'use strict'; + event.preventDefault(); + getCharts(); +} +document.getElementById('refresh-1').addEventListener('click', refreshCharts); +document.getElementById('refresh-2').addEventListener('click', refreshCharts); + +/* Check for updates */ +fetch('https://devshort.flokX.dev/api.php?mode=version&current=' + version).then(function (response) { + return response.json(); +}).then(function (json) { + let inner = ' ' + version; + if (json['latest'] !== version) { + inner += ' <span class="text-warning">(<a class="text-warning" href="https://github.com/flokX/devShort/releases/latest">update available</a>!)</span>'; + } + document.getElementById('version-1').insertAdjacentHTML('beforeend', inner); + document.getElementById('version-2').insertAdjacentHTML('beforeend', inner); +}); + +/* Get charts and date (remove old when necessary) */ +function getCharts() { + 'use strict'; + spinner.style.display = 'block'; + while (chartsDiv.firstChild) { + chartsDiv.firstChild.remove(); + } + fetch('admin.php?get_stats').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('admin.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,17 @@ +{ + "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/stats.json b/data/stats.json diff --git a/index.php b/index.php @@ -2,16 +2,16 @@ // All relevant changes can be made in the data file. Please read the docs: https://github.com/flokX/devShort/wiki -$base_path = implode(DIRECTORY_SEPARATOR, array(__DIR__, "admin")); -$config_content = json_decode(file_get_contents($base_path . DIRECTORY_SEPARATOR . "config.json"), true); +$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); -// Counts the access -$filename = $base_path . DIRECTORY_SEPARATOR . "stats.json"; -$stats = json_decode(file_get_contents($filename), true); -$stats["Index"][mktime(0, 0, 0)] += 1; -file_put_contents($filename, json_encode($stats, JSON_PRETTY_PRINT)); +// Count the access +$stats_content["Index"][mktime(0, 0, 0)] += 1; +file_put_contents($stats_path, json_encode($stats_content)); -// Generator for page customization +// Generate custom buttons for the footer $links_string = ""; if ($config_content["settings"]["custom_links"]) { foreach ($config_content["settings"]["custom_links"] as $name => $url) { @@ -41,9 +41,9 @@ if ($config_content["settings"]["custom_links"]) { <main class="flex-shrink-0"> <div class="container"> <nav class="mt-3" aria-label="breadcrumb"> - <ol class="breadcrumb"> + <ol class="breadcrumb shadow-sm"> <li class="breadcrumb-item"><a href="<?php echo $config_content["settings"]["home_link"]; ?>">Home</a></li> - <li class="breadcrumb-item" aria-current="page"><?php echo $config_content["settings"]["name"]; ?></li> + <li class="breadcrumb-item active" aria-current="page"><?php echo $config_content["settings"]["name"]; ?></li> </ol> </nav> <h1 class="mt-5"><?php echo $config_content["settings"]["name"]; ?></h1> @@ -53,7 +53,7 @@ if ($config_content["settings"]["custom_links"]) { <li class="list-inline-item">-</li> <li class="list-inline-item"><a href="<?php echo $config_content["settings"]["home_link"]; ?>">Home page</a></li> <li class="list-inline-item">-</li> - <li class="list-inline-item"><a href="admin">Admin panel</a></li> + <li class="list-inline-item"><a href="admin.php">Admin panel</a></li> </ul> </div> </main> diff --git a/installer.php b/installer.php @@ -1,90 +0,0 @@ -<?php - -// All relevant changes can be made in the data file. Please read the docs: https://github.com/flokX/devShort/wiki - -$success = false; -$config_path = implode(DIRECTORY_SEPARATOR, array(__DIR__, "admin", "config.json")); -$config_content = json_decode(file_get_contents($config_path), true); - -if ($config_content["installer"]["password"]) { - - // Create the .htpasswd for the secure directory. If already a hashed password is in the data.json file, copy it. - $htpasswd_path = implode(DIRECTORY_SEPARATOR, array(__DIR__, "admin", ".htpasswd")); - $admin_password = $config_content["installer"]["password"]; - if (password_get_info($admin_password)["algo"] === 0) { - $hash = password_hash($admin_password, PASSWORD_DEFAULT); - } else { - $hash = $admin_password; - } - file_put_contents($htpasswd_path, $config_content["installer"]["username"] . ":" . $hash); - - // Create the .htaccess for the secure directory. - $secure_htaccess = "# Authentication -AuthType Basic -AuthName \"devShort admin area\" -AuthUserFile $htpasswd_path -require valid-user"; - file_put_contents(implode(DIRECTORY_SEPARATOR, array(__DIR__, "admin", ".htaccess")), $secure_htaccess); - - // Change password entry to the hash and remove installer file. - $config_content["installer"]["password"] = $hash; - file_put_contents($config_path, json_encode($config_content, JSON_PRETTY_PRINT)); - unlink(__DIR__ . DIRECTORY_SEPARATOR . "installer.php"); - $success = true; - -} - -?> - -<!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="The devShort team"> - <link href="assets/icon.png" rel="icon"> - <title>Installer | devShort</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"> - <nav class="mt-3" aria-label="breadcrumb"> - <ol class="breadcrumb"> - <li class="breadcrumb-item">devShort</li> - <li class="breadcrumb-item active" aria-current="page">Installer</li> - </ol> - </nav> - <?php - - if ($success) { - echo "<h1 class=\"mt-5\">Successful installed!</h1> -<p class=\"lead\">Now you can start to shorten links. For more information visit the <a href=\"https://github.com/flokX/devShort/wiki\">devShort wiki</a>.</p> -<a href=\"admin\" class=\"btn btn-primary btn-block\" role=\"button\">Go to the admin panel</a>"; - } else { - echo "<h1 class=\"mt-5\">Error while installing.</h1> -<p class=\"lead\">Please configure the <i>config.json</i> as shown in the <a href=\"https://github.com/flokX/devShort/wiki/Installation#installation\">devShort wiki</a> and try again.</p> -<p>We assume that you have not yet set an admin password.</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") ?> <a href="https://github.com/flokX/devShort">devShort</a></span> - <span class="text-muted"><a href="https://github.com/flokX/devShort/wiki" class="badge badge-secondary">devShort wiki</a></span> - </div> - </div> - </footer> - -</body> - -</html> diff --git a/redirect.php b/redirect.php @@ -10,17 +10,17 @@ if (in_array($short, $return_404)) { exit; } -// Counts the access to the given $name +$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); + +// Count the access to the given $name function count_access($base_path, $name) { - $filename = $base_path . DIRECTORY_SEPARATOR . "stats.json"; - $stats = json_decode(file_get_contents($filename), true); - $stats[$name][mktime(0, 0, 0)] += 1; - file_put_contents($filename, json_encode($stats, JSON_PRETTY_PRINT)); + $stats_content[$name][mktime(0, 0, 0)] += 1; + file_put_contents($stats_path, json_encode($stats_content)); } -$base_path = implode(DIRECTORY_SEPARATOR, array(__DIR__, "admin")); -$config_content = json_decode(file_get_contents($base_path . DIRECTORY_SEPARATOR . "config.json"), true); - if (array_key_exists($short, $config_content["shortlinks"])) { header("Location: " . $config_content["shortlinks"][$short], $http_response_code=303); count_access($base_path, $short); @@ -32,7 +32,7 @@ if (array_key_exists($short, $config_content["shortlinks"])) { header("HTTP/1.1 404 Not Found"); count_access($base_path, "404-request"); - // Generator for page customization + // Generate custom buttons for the footer $links_string = ""; if ($config_content["settings"]["custom_links"]) { foreach ($config_content["settings"]["custom_links"] as $name => $url) { @@ -63,7 +63,7 @@ if (array_key_exists($short, $config_content["shortlinks"])) { <main class="flex-shrink-0"> <div class="container"> <nav class="mt-3" aria-label="breadcrumb"> - <ol class="breadcrumb"> + <ol class="breadcrumb shadow-sm"> <li class="breadcrumb-item"><a href="<?php echo $config_content["settings"]["home_link"]; ?>">Home</a></li> <li class="breadcrumb-item"><?php echo $config_content["settings"]["name"]; ?></li> <li class="breadcrumb-item active" aria-current="page">404</li>