How to create Token-based Download [Part 2]

Reading Time: 8 minutes

In “How to create Token-based Download” we want to build a system that allows downloading certain files with certain access tokens. With Part1 we have created the Frontend of our Token-based Download. In Part 2 we want to add functionality to it. That means we are creating the Backend, the part “everything” relies on. So here we go

Prerequisites

In the following, we will create a Database quick and easy. An explanation will follow in a different tutorial. Then we will deal with checking for a valid token, look for the filepath and send the data to the user. So let’s get right into it

Creating a database

To create a Token-based Download we need a database. This is the part where all of our data is stored. We will be using two tables. One that holds the tokens connected to a video ID. And one table that holds the video ID and the assigned filepath. Why we chose this we will be explaining in another tutorial. But it has something to do with how many entries refer to another entry. For now, it is easier to “copy this code” and import it to your implementation:

-- phpMyAdmin SQL Dump
-- version 4.8.3
-- https://www.phpmyadmin.net/
--
-- Host: 127.0.0.1
-- Erstellungszeit: 21. Okt 2019 um 23:10
-- Server-Version: 10.1.35-MariaDB
-- PHP-Version: 7.2.9

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;

--
-- Datenbank: `privatevideodonwload`
--

-- --------------------------------------------------------

--
-- Tabellenstruktur für Tabelle `pvc_video`
--

CREATE TABLE `pvc_video` (
  `id` int(11) NOT NULL,
  `path` varchar(250) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

--
-- Indizes der exportierten Tabellen
--

--
-- Indizes für die Tabelle `pvc_video`
--
ALTER TABLE `pvc_video`
  ADD PRIMARY KEY (`id`);

--
-- AUTO_INCREMENT für exportierte Tabellen
--

--
-- AUTO_INCREMENT für Tabelle `pvc_video`
--
ALTER TABLE `pvc_video`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
COMMIT;

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
-- phpMyAdmin SQL Dump
-- version 4.8.3
-- https://www.phpmyadmin.net/
--
-- Host: 127.0.0.1
-- Erstellungszeit: 21. Okt 2019 um 23:10
-- Server-Version: 10.1.35-MariaDB
-- PHP-Version: 7.2.9

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;

--
-- Datenbank: `privatevideodonwload`
--

-- --------------------------------------------------------

--
-- Tabellenstruktur für Tabelle `pvd_code`
--

CREATE TABLE `pvd_code` (
  `Code` varchar(50) NOT NULL,
  `Assigned_Video` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

--
-- Indizes der exportierten Tabellen
--

--
-- Indizes für die Tabelle `pvd_code`
--
ALTER TABLE `pvd_code`
  ADD PRIMARY KEY (`Code`),
  ADD KEY `Video_Fremd` (`Assigned_Video`);

--
-- Constraints der exportierten Tabellen
--

--
-- Constraints der Tabelle `pvd_code`
--
ALTER TABLE `pvd_code`
  ADD CONSTRAINT `Video_Fremd` FOREIGN KEY (`Assigned_Video`) REFERENCES `pvc_video` (`id`);
COMMIT;

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

Check if the token is valid

In the last part, we have created the checkCode.php file. Now we can write some lines of code to check whether the token is valid. What we want to achieve is that we call the .php file with the token as a parameter. The file then accesses the backend.php file and returns a JSON-Object with the key “success” and the value “true” or “false”. We achieve this by adding the following code

<?php
//Include the backend.php to be able to access. CHANGE TO YOUR OWN!
include 'L:\Programme\Xampp\download-files\backend.php';
//Return JSON
header('Content-type: application/json');

//Call the method in the backend.php file to check if token was valid
checkToken();

//Return successfull as true otherwise false
if ($isValid) {
    //Encode as JSON
    echo(json_encode(array("success" => true), JSON_PARTIAL_OUTPUT_ON_ERROR));
} else {
    //Encode as JSON
    echo(json_encode(array("success" => false), JSON_PARTIAL_OUTPUT_ON_ERROR));
}

//Close the connection with the database
$con -> close();

Send Data to download file

For our Backend, we need one more file. This file is called sendData.php. Its job is to “send” us the correct data for the given token. In our case, it adds the functionality to be able to download a file. We want to open our “download” folder an create the file. The next part is adding functionality to this file. In our case, we need to change the “header”. That “header” tells our browser which type of content and belongs to the HTTP-Specifications. Every time you access a webpage it is transferred and for being able to download a file we need to edit this. We do this by adding the following lines to the file:

//DOWNLOAD FILE
header("Cache-Control: no-cache, must-revalidate");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Content-Description: File Transfer");
header("Content-Type: application/force-download");
//Basename equals the last part of file
header("Content-disposition: attachment; filename=" . basename($path));
header("Content-type: " . mime_content_type($path));
//Removes unwanted output
ob_clean();
flush();
//Read file and download
readfile($path);

This allows us to tell the browser that a file should be downloaded (open the download popup). It sets the correct “Content-Type” and outputs the data of the file by with the last line “readfile($path)”. But the big question now should be – which file and what path? And here comes our “hidden” folder into play.

Retrieve data from a token

Open the “backend.php” file and here we want to start adding the following thing:

  • Create a database connection
  • Escape the Token given through the parameter
  • Create a checkToken() function for checking if the token is valid
  • Add a getPath() function to retrieve the file path assigned to the token
  • Create a removeToken() function to remove the (one time use only) token (optional)

So let’s start in chronological order

Create a database connection

<?php
//Credentials for login
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "privatevideodonwload";

//Save the connction for further use
$con = mysqli_connect($servername, $username, $password, $dbname);
if (!$con) {
    die("No connection to database possible");
}

For your purpose, you have to change the credentials. This creates a variable called “$con” that saves the MySQL-Connection for further use.

Escape the Token given through the parameter

//The Token as GET-Parameter
$code = "";
if (isset($_GET['code'])) {
    $code = $_GET['code'];
}

//Excape the code for mysql statement
$escaped = mysqli_real_escape_string($con, $code);
$codeEscaped = addcslashes($escaped, '%_');

//Path to the file we want to be able to download
$path = "";
//Boolean if token is valid
$isValid;

In this part, we retrieve the Parameter and escape it. This stops attackers from using SQL-Injections. We will use the escaped Code for SQL-Statements

Create a checkToken function

//Function to check the database for the token. If one exists set isValid true
function checkToken()
{
    global $codeEscaped, $con, $isValid;
    //Statement for checking if there is exacly one entry with the code
    $statement = "SELECT Assigned_Video FROM pvd_code WHERE Code = '" . $codeEscaped . "'";
    $resultToken = mysqli_query($con, $statement);
    if (mysqli_num_rows($resultToken) == 1) {
        $isValid = true;
    } else {
        $isValid = false;
    }
}

In this part, we want access to the global variables. Afterward, we select the Assigned_Video from the table pvd_code if our token is present. If there is one result we set isValid as true. That means our token is usable as it is in the pool.

Create a getPath function

function getPath()
{
    global $path, $con, $codeEscaped;
    //Execute Statement for getting the path by joining on the criteria code
    $statement = "SELECT pvc_video.path FROM pvc_video LEFT JOIN pvd_code ON pvc_video.id = pvd_code.Assigned_Video WHERE pvd_code.code = '" . $codeEscaped . "'";
    $result = mysqli_query($con, $statement);
    //If there was only one entry carry on
    if (mysqli_num_rows($result) == 1) {
        while ($row = mysqli_fetch_array($result, MYSQLI_ASSOC)) {
            $path = $row['path'];
        }
    }
}

As before we need access to the global variables. The Rest of the code is basically one MySQL-Statement. Let’s break it down into little parts. The result we want in the end is “pvc_video.path”. We get it from the table “pvc_video”. The JOIN part basically says that we “add” the “pvd_code.Assigned_Video” to the “pvc_video” table where pvc_video.id equals “pvd_code.Assigned_Video”. With the last part, we only select the “Assigned_Video” where “pvd_code.code” equals the token. So basically our token and our path are attached to this statement.

Create a removeToken function (optional)

function removeToken()
{
    global $codeEscaped, $con;
    //Remove the used token --> onetime use only
    $removeStatement = "DELETE FROM pvd_code WHERE code = '" . $codeEscaped . "'";
    mysqli_query($con, $removeStatement);
}

This is a pretty easy part. We remove the entry where “code” equals our token. Not much to explain but still important if you want a “one time use only” system.

Putting everything together

So now every part is functional in its own way. But we need to add it together. That means we need to finish editing the “sendData.php”. First, we need to call the “getPath()”-function and then check if the “path” variable is empty:

<?php
include 'L:\Programme\Xampp\download-files\backend.php';

//Call the getPath function from backend.php
getPath();

if ($path != "") {
    //DOWNLOAD FILE
[...]

(Optional) When we start reading the file to download it we also want to remove the token. So we call “removeToken()”:

[...] 
readfile($path);

    //Reset path
    $path = "";
    
    removeToken();

And in the end, we want to show Error messages and close the database connection when we are done:

[...]    
//Reset path
    $path = "";
    
    removeToken();
    //ERROR MESSAGES
} else if (mysqli_num_rows($resultToken) == 0) {
    echo ("Your code invalid. Please use a different one");
} else {
    echo ("Error! We were unable to process you request");
}

//Close Database connection here because this is the last step in the chain of downloading a file
$con->close();
Finished result
How to create Token-based Download [Part 2]
The result of how it should look when you have finished this tutorial

Conclusion

Finished! If you followed all the steps you have a token-based Download system. For rounding it up here we have all of the code that has been added today:

checkCode.php
<?php
//Include the backend.php to be able to access. CHANGE TO YOUR OWN!
include 'L:\Programme\Xampp\download-files\backend.php';
//Return JSON
header('Content-type: application/json');

//Call the method in the backend.php file to check if token was valid
checkToken();

//Return successfull as true otherwise false
if ($isValid) {
    //Encode as JSON
    echo(json_encode(array("success" => true), JSON_PARTIAL_OUTPUT_ON_ERROR));
} else {
    //Encode as JSON
    echo(json_encode(array("success" => false), JSON_PARTIAL_OUTPUT_ON_ERROR));
}

//Close the connection with the database
$con -> close();
sendData.php
<?php
include 'L:\Programme\Xampp\download-files\backend.php';

getPath();

if ($path != "") {
    //DOWNLOAD FILE
    header("Cache-Control: no-cache, must-revalidate");
    header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
    header("Content-Description: File Transfer");
    header("Content-Type: application/force-download");
    //Basename equals the last part of file
    header("Content-disposition: attachment; filename=" . basename($path));
    header("Content-type: " . mime_content_type($path));
    //Removes unwanted output
    ob_clean();
    flush();
    //Read file and download
    readfile($path);

    //Reset path
    $path = "";
    
    removeToken();
    //ERROR MESSAGES
} else if (mysqli_num_rows($resultToken) == 0) {
    echo ("Your code invalid. Please use a different one");
} else {
    echo ("Error! We were unable to process you request");
}

//Close Database connection here because this is the last step in the chain of downloading a file
$con->close();
backend.php
<?php
//Credentials for login
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "privatevideodonwload";

//Save the connction for further use
$con = mysqli_connect($servername, $username, $password, $dbname);
if (!$con) {
    die("No connection to database possible");
}

//The Token as GET-Parameter
$code = "";
if (isset($_GET['code'])) {
    $code = $_GET['code'];
}

//Excape the code for mysql statement
$escaped = mysqli_real_escape_string($con, $code);
$codeEscaped = addcslashes($escaped, '%_');

//Path to the file we want to be able to download
$path = "";
//Boolean if token is valid
$isValid;

//Function to check the database for the token. If one exists set isValid true
function checkToken()
{
    global $codeEscaped, $con, $isValid;
    //Statement for checking if there is exacly one entry with the code
    $statement = "SELECT Assigned_Video FROM pvd_code WHERE Code = '" . $codeEscaped . "'";
    $resultToken = mysqli_query($con, $statement);
    if (mysqli_num_rows($resultToken) == 1) {
        $isValid = true;
    } else {
        $isValid = false;
    }
}

function getPath()
{
    global $path, $con, $codeEscaped;
    //Execute Statement for getting the path by joining on the criteria code
    $statement = "SELECT pvc_video.path FROM pvc_video LEFT JOIN pvd_code ON pvc_video.id = pvd_code.Assigned_Video WHERE pvd_code.code = '" . $codeEscaped . "'";
    $result = mysqli_query($con, $statement);
    //If there was only one entry carry on
    if (mysqli_num_rows($result) == 1) {
        while ($row = mysqli_fetch_array($result, MYSQLI_ASSOC)) {
            $path = $row['path'];
        }
    }
}

function removeToken()
{
    global $codeEscaped, $con;
    //Remove the used token --> onetime use only
    $removeStatement = "DELETE FROM pvd_code WHERE code = '" . $codeEscaped . "'";
    mysqli_query($con, $removeStatement);
}

Thanks for tuning in and reading till the end. If you enjoyed it write us a comment. Otherwise, if you have any questions about today’s tutorial, just ask us in the comments, on Twitter or Instagram or send us a mail.

Liam

Leave a Reply

Your email address will not be published. Required fields are marked *