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:
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">© <?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">© <?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">© <?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¤t=' + 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">© <?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>