Commit bca4c88b authored by Dominik Hebeler's avatar Dominik Hebeler
Browse files

added basic interface to control blacklist/whitelist

parent 95764837
...@@ -6,6 +6,7 @@ use Illuminate\Http\Request; ...@@ -6,6 +6,7 @@ use Illuminate\Http\Request;
use LaravelLocalization; use LaravelLocalization;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Validator;
/** /**
* Before we redirect users to the affiliate shops we will track the clicks ourself. * Before we redirect users to the affiliate shops we will track the clicks ourself.
...@@ -29,7 +30,8 @@ class AdgoalController extends Controller ...@@ -29,7 +30,8 @@ class AdgoalController extends Controller
* After that we will store the necessary information (link and affiliate link) into our database. * After that we will store the necessary information (link and affiliate link) into our database.
* After that we will redirect the user to the affiliate shop * After that we will redirect the user to the affiliate shop
*/ */
public function forward(Request $request){ public function forward(Request $request)
{
// $link = "https://metager.de"; // $link = "https://metager.de";
// $affillink = "https://test.de"; // $affillink = "https://test.de";
// $password = self::generatePassword($affillink, $link); // $password = self::generatePassword($affillink, $link);
...@@ -44,10 +46,10 @@ class AdgoalController extends Controller ...@@ -44,10 +46,10 @@ class AdgoalController extends Controller
'affillink' => ['required', 'url', 'active_url'], 'affillink' => ['required', 'url', 'active_url'],
'link' => ['required', 'url', 'active_url'], 'link' => ['required', 'url', 'active_url'],
# Validation of redirect request so that one cannot generate random redirect URLs pointing to our domains # Validation of redirect request so that one cannot generate random redirect URLs pointing to our domains
'password' => function($attribute, $value, $fail) use($request) { 'password' => function ($attribute, $value, $fail) use ($request) {
// Check if hmac matches // Check if hmac matches
$correctPassword = self::generatePassword($request->input('affillink'), $request->input('link')); $correctPassword = self::generatePassword($request->input('affillink'), $request->input('link'));
if(!hash_equals($correctPassword, $value)){ if (!hash_equals($correctPassword, $value)) {
$fail('The given password is incorrect!'); $fail('The given password is incorrect!');
} }
} }
...@@ -63,10 +65,11 @@ class AdgoalController extends Controller ...@@ -63,10 +65,11 @@ class AdgoalController extends Controller
* at search time. * at search time.
* A Cronjob will pick the data up and store it into Mariadb later (see self::storePartnerCall) * A Cronjob will pick the data up and store it into Mariadb later (see self::storePartnerCall)
*/ */
private function storePartnerCallFast($affillink, $link) { private function storePartnerCallFast($affillink, $link)
{
# Generate Data to store # Generate Data to store
$host = parse_url($link, PHP_URL_HOST); $host = parse_url($link, PHP_URL_HOST);
if(empty($host)){ if (empty($host)) {
return; return;
} }
$storeObject = [ $storeObject = [
...@@ -81,14 +84,17 @@ class AdgoalController extends Controller ...@@ -81,14 +84,17 @@ class AdgoalController extends Controller
$redis->rpush($this::REDIS_STORAGE_KEY, json_encode($storeObject)); $redis->rpush($this::REDIS_STORAGE_KEY, json_encode($storeObject));
} }
public static function storePartnerCalls() { public static function storePartnerCalls()
{
$redis = Redis::connection(config('cache.stores.redis.connection')); $redis = Redis::connection(config('cache.stores.redis.connection'));
DB::transaction(function() use($redis){ DB::transaction(function () use ($redis) {
while(!empty($data = $redis->lpop(self::REDIS_STORAGE_KEY))){ while (!empty($data = $redis->lpop(self::REDIS_STORAGE_KEY))) {
$data = json_decode($data, true); $data = json_decode($data, true);
# Insert data into mariadb table # Insert data into mariadb table
DB::insert('insert into affiliate_clicks (hostname, affillink, link) values (?, ?, ?)', DB::insert(
[$data["host"], $data["affillink"], $data["link"]]); 'insert into affiliate_clicks (hostname, affillink, link) values (?, ?, ?)',
[$data["host"], $data["affillink"], $data["link"]]
);
} }
}); });
} }
...@@ -96,10 +102,11 @@ class AdgoalController extends Controller ...@@ -96,10 +102,11 @@ class AdgoalController extends Controller
/** /**
* Generates a Redirect URL for our partnershops * Generates a Redirect URL for our partnershops
*/ */
public static function generateRedirectUrl($affillink, $link){ public static function generateRedirectUrl($affillink, $link)
{
$password = self::generatePassword($affillink, $link); $password = self::generatePassword($affillink, $link);
return LaravelLocalization::getLocalizedURL( return LaravelLocalization::getLocalizedURL(
LaravelLocalization::getCurrentLocale(), LaravelLocalization::getCurrentLocale(),
route('adgoal-redirect', ["link" => $link, "affillink" => $affillink, "password" => $password]) route('adgoal-redirect', ["link" => $link, "affillink" => $affillink, "password" => $password])
); );
} }
...@@ -107,7 +114,8 @@ class AdgoalController extends Controller ...@@ -107,7 +114,8 @@ class AdgoalController extends Controller
/** /**
* Generates hmac password to validate redirect URLs * Generates hmac password to validate redirect URLs
*/ */
public static function generatePassword($affillink, $link){ public static function generatePassword($affillink, $link)
{
return hash_hmac("sha256", $affillink . $link, config('metager.metager.adgoal.private_key')); return hash_hmac("sha256", $affillink . $link, config('metager.metager.adgoal.private_key'));
} }
...@@ -115,7 +123,8 @@ class AdgoalController extends Controller ...@@ -115,7 +123,8 @@ class AdgoalController extends Controller
/** /**
* Routes for the Admin Interface * Routes for the Admin Interface
*/ */
public function adminIndex(Request $request){ public function adminIndex(Request $request)
{
return view('admin.affiliates.index') return view('admin.affiliates.index')
->with('title', "Affilliates Overview - MetaGer") ->with('title', "Affilliates Overview - MetaGer")
->with('css', [ ->with('css', [
...@@ -129,19 +138,24 @@ class AdgoalController extends Controller ...@@ -129,19 +138,24 @@ class AdgoalController extends Controller
]); ]);
} }
public function blacklistJson(Request $request){ public function blacklistJson(Request $request)
$request->validate([ {
$validator = Validator::make($request->all(), [
"blacklist" => 'boolean' "blacklist" => 'boolean'
]); ]);
if ($validator->fails()) {
return response()->json("Invalid Request Data", 422);
}
$count = 5; # How Many results to return $count = 5; # How Many results to return
$skip = 0; # How many results to skip $skip = 0; # How many results to skip
$blacklist = $request->input('blacklist', true); $blacklist = $request->input('blacklist', true);
$total = DB::select("select count(*) as total_rows from affiliate_blacklist"); $total = DB::select("select count(*) as total_rows from affiliate_blacklist where blacklist = ?", [$blacklist]);
$total = intval($total[0]->{"total_rows"}); $total = intval($total[0]->{"total_rows"});
$blacklistItems = DB::select('select * from affiliate_blacklist where blacklist = ? order by created_at desc limit ? offset ?', [$blacklist, $count, $skip]); $blacklistItems = DB::select('select * from affiliate_blacklist where blacklist = ? order by created_at desc limit ? offset ?', [$blacklist, $count, $skip]);
$result = [ $result = [
"count" => $count, "count" => $count,
"skip" => $skip, "skip" => $skip,
...@@ -152,11 +166,267 @@ class AdgoalController extends Controller ...@@ -152,11 +166,267 @@ class AdgoalController extends Controller
return response()->json($result); return response()->json($result);
} }
public function whitelistJson(Request $request){ public function addblacklistJson(Request $request)
{
$validator = Validator::make($request->all(), [
"hostname" => [
"required",
function ($attribute, $value, $fail) use ($request) {
# Validate that this is indeed a hostname which is resolvable
if (!filter_var(gethostbyname($request->input("hostname")), FILTER_VALIDATE_IP)) {
$fail("The selected entry is not a valid hostname");
}
},
function ($attribute, $value, $fail) use ($request) {
# Validate that entry does not already exist in database
$entry = DB::select("select * from metager.affiliate_blacklist where hostname = ?", [$request->input("hostname")]);
if (sizeof($entry) !== 0) {
$fail("The selected entry does already exist in database");
}
}
],
]);
if ($validator->fails()) {
return response()->json([
"message" => "Invalid Request Data"
], 422);
}
$hostname = $validator->validated()["hostname"];
$rowsInserted = DB::insert("insert into metager.affiliate_blacklist (hostname, blacklist) values (?, 1)", [$hostname]);
if ($rowsInserted === TRUE) {
return response()->json([
"message" => "Entry added."
]);
} else {
return response()->json([
"message" => "Error inserting entry."
], 422);
}
}
public function deleteblacklistJson(Request $request)
{
$validator = Validator::make($request->all(), [
"id" => ["required", "integer", "min:1", function ($attribute, $value, $fail) use ($request) {
$entry = DB::select("select * from metager.affiliate_blacklist where id = ? and blacklist = ?", [$request->input("id"), true]);
if (sizeof($entry) !== 1) {
$fail("The selected entry does not exist in database");
}
}],
]);
if ($validator->fails()) {
return response()->json("Invalid Request Data", 422);
}
$id = intval($validator->validated()["id"]);
$rowsDeleted = DB::delete("delete from metager.affiliate_blacklist where id = ?", [$id]);
if ($rowsDeleted > 0) {
return response()->json([
"message" => "$rowsDeleted entries deleted."
]);
} else {
return response()->json([
"message" => "Error deleting entry."
], 422);
}
}
public function whitelistJson(Request $request)
{
$input = $request->all(); $input = $request->all();
$input["blacklist"] = true; $input["blacklist"] = false;
$request->replace($input); $request->replace($input);
return $this->blacklistJson($request); return $this->blacklistJson($request);
} }
public function addwhitelistJson(Request $request)
{
$validator = Validator::make($request->all(), [
"hostname" => [
"required",
function ($attribute, $value, $fail) use ($request) {
# Validate that this is indeed a hostname which is resolvable
if (!filter_var(gethostbyname($request->input("hostname")), FILTER_VALIDATE_IP)) {
$fail("The selected entry is not a valid hostname");
}
},
function ($attribute, $value, $fail) use ($request) {
# Validate that entry does not already exist in database
$entry = DB::select("select * from metager.affiliate_blacklist where hostname = ?", [$request->input("hostname")]);
if (sizeof($entry) !== 0) {
$fail("The selected entry does already exist in database");
}
}
],
]);
if ($validator->fails()) {
return response()->json([
"message" => "Invalid Request Data"
], 422);
}
$hostname = $validator->validated()["hostname"];
$rowsInserted = DB::insert("insert into metager.affiliate_blacklist (hostname, blacklist) values (?, 0)", [$hostname]);
if ($rowsInserted === TRUE) {
return response()->json([
"message" => "Entry added."
]);
} else {
return response()->json([
"message" => "Error inserting entry."
], 422);
}
}
public function deletewhitelistJson(Request $request)
{
$validator = Validator::make($request->all(), [
"id" => ["required", "integer", "min:1", function ($attribute, $value, $fail) use ($request) {
$entry = DB::select("select * from metager.affiliate_blacklist where id = ? and blacklist = ?", [$request->input("id"), false]);
if (sizeof($entry) !== 1) {
$fail("The selected entry does not exist in database");
}
}],
]);
if ($validator->fails()) {
return response()->json("Invalid Request Data", 422);
}
$id = intval($validator->validated()["id"]);
$rowsDeleted = DB::delete("delete from metager.affiliate_blacklist where id = ?", [$id]);
if ($rowsDeleted > 0) {
return response()->json([
"message" => "$rowsDeleted entries deleted."
]);
} else {
return response()->json([
"message" => "Error deleting entry."
], 422);
}
}
public function hostsJson(Request $request)
{
$validator = Validator::make($request->all(), [
"count" => ["integer", "min:1", "max:50"],
"skip" => ["integer", "min:0"],
]);
if ($validator->fails()) {
return response()->json("Invalid Request Data", 422);
}
$count = intval($request->input("count", 10));
$skip = intval($request->input("skip", 0));
$filter = $request->input("filter", "");
$hostCount = DB::table("metager.affiliate_clicks", "c")
->select(DB::raw("count(distinct c.hostname) as total_hosts"))
->leftJoin("metager.affiliate_blacklist", function ($join) {
$join->on("c.hostname", "=", "metager.affiliate_blacklist.hostname");
})
->where("c.hostname", 'like', "%$filter%")
->whereNull("metager.affiliate_blacklist.hostname")
->get();
$hostCount = $hostCount[0]->{"total_hosts"};
$clickCount = DB::table("metager.affiliate_clicks", "c")
->select(DB::raw("count(*) as click_count"))
->leftJoin("metager.affiliate_blacklist", function ($join) {
$join->on("c.hostname", "=", "metager.affiliate_blacklist.hostname");
})
->where("c.hostname", 'like', "%$filter%")
->whereNull("metager.affiliate_blacklist.hostname")
->get();
$clickCount = $clickCount[0]->{"click_count"};
$hosts = DB::table("metager.affiliate_clicks", "c")
->select("c.hostname", DB::raw('count(c.hostname) as clicks'))
->leftJoin("metager.affiliate_blacklist", function ($join) {
$join->on("c.hostname", "=", "metager.affiliate_blacklist.hostname");
})
->where("c.hostname", 'like', "%$filter%")
->whereNull("metager.affiliate_blacklist.hostname")
->groupBy("c.hostname")
->orderByDesc("clicks")
->limit($count)
->offset($skip)
->get();
$result = [
"count" => $count,
"skip" => $skip,
"total_hosts" => $hostCount,
"total_clicks" => $clickCount,
"hosts" => $hosts
];
return response()->json($result);
}
public function hostClicksJson(Request $request)
{
$validator = Validator::make($request->all(), [
"count" => ["integer", "min:1", "max:50"],
"skip" => ["integer", "min:0"],
"hostname" => [
"required",
function ($attribute, $value, $fail) use ($request) {
# Validate that this is indeed a hostname which is resolvable
if (!filter_var(gethostbyname($request->input("hostname")), FILTER_VALIDATE_IP)) {
$fail("The selected entry is not a valid hostname");
}
},
function ($attribute, $value, $fail) use ($request) {
# Validate that entry does not already exist in database
$entry = DB::table("affiliate_clicks", "c")
->where("hostname", "=", $request->input("hostname"))
->limit(1)
->get();
if (sizeof($entry) !== 1) {
$fail("The selected entry does not exist in database");
}
}
],
]);
if ($validator->fails()) {
return response()->json([
"message" => "Invalid Request Data"
], 422);
}
$count = intval($request->input("count", 10));
$skip = intval($request->input("skip", 0));
$total = DB::table("affiliate_clicks", "c")
->select(DB::raw("count(*) as count"))
->where("hostname", "=", $request->input("hostname"))
->get();
$total = $total[0]->count;
// Query Data
$clicks = DB::table("affiliate_clicks", "c")
->where("hostname", "=", $request->input("hostname"))
->orderByDesc("c.created_at")
->limit($count)
->offset($skip)
->get();
$result = [
"count" => $count,
"skip" => $skip,
"total" => $total,
"results" => $clicks
];
return response()->json($result);
}
} }
window.addEventListener("load", function () { /**
console.log("loaded"); * Global variables
*/
let clicksCount = 10;
let clicksSkip = 0;
window.addEventListener("load", function () {
refreshBlacklist(); refreshBlacklist();
refreshWhitelist(); refreshWhitelist();
refreshHostlist();
document.querySelector("#affilliate-clicks .filter").addEventListener("keyup", refreshHostlist);
document.querySelector("#affilliate-clicks .pagination .forward").addEventListener("click", () => {
clicksSkip += clicksCount;
refreshHostlist();
});
document.querySelector("#affilliate-clicks .pagination .backward").addEventListener("click", () => {
clicksSkip -= clicksCount;
clicksSkip = Math.max(0, clicksSkip);
refreshHostlist();
});
}); });
function refreshBlacklist() { function refreshBlacklist() {
let templateHtml = `
<li>
<div class="number"></div>
<div class="hostname"></div>
<a href="#" class="remove" data-id="">&#x1F5D1;</a>
</li>
`;
let template = document.createElement("template");
template.innerHTML = templateHtml.trim();
template = template.content.firstChild;
fetch('/admin/affiliates/json/blacklist') fetch('/admin/affiliates/json/blacklist')
.then(response => response.json()) .then(response => response.json())
.catch(error => console.log(error)) .catch(error => console.log(error))
.then(blacklist => { .then(blacklist => {
document.querySelector("#blacklist-container > .blacklist > h3 > a").innerHTML = "Blacklist (" + blacklist.total + ")"; document.querySelector("#blacklist-container > .blacklist > h3 > a").innerHTML = "Blacklist (" + blacklist.total + ")";
document.querySelector("#blacklist-container > .blacklist .skeleton").style.display = "none"; document.querySelector("#blacklist-container > .blacklist .skeleton").style.display = "none";
// Todo add returned Items to the list document.querySelector("#blacklist-container > .blacklist > .blacklist-items").innerHTML = "";
blacklist.results.forEach((value, index) => {
let newElement = template.cloneNode(true);
newElement.querySelector(".number").innerHTML = (blacklist.skip + index + 1) + ".";
newElement.querySelector(".hostname").innerHTML = value.hostname;
newElement.querySelector(".remove").setAttribute("data-id", value.id);
newElement.querySelector(".remove").addEventListener("click", removeBlacklistItem);
document.querySelector("#blacklist-container > .blacklist > .blacklist-items").appendChild(newElement);
});
}) })
.catch(error => console.log(error)); .catch(error => console.log(error));
} }
function addBlacklistItem(element) {
let confirmation = confirm("Are you sure to add the selected entry to the blacklist? This will eliminate all income from that partnershop!");
if (!confirmation) {
return;
}
let data = {
"hostname": element.target.dataset.host
}
fetch('/admin/affiliates/json/blacklist', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.catch(error => console.log(error))
.then(response => response.json())
.then(response => {
refreshBlacklist();
refreshHostlist();
});
}
function removeBlacklistItem(element) {
let confirmation = confirm("Are you sure to delete the selected entry from database? This is permanent!");
if (!confirmation) {
return;
}
let data = {
"id": element.target.dataset.id
}
fetch('/admin/affiliates/json/blacklist', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.catch(error => console.log(error))
.then(response => response.json())
.then(response => {
refreshBlacklist();
refreshHostlist();
});
}
function refreshWhitelist() { function refreshWhitelist() {
let templateHtml = `
<li>
<div class="number"></div>
<div class="hostname"></div>
<a href="#" class="remove" data-id="">&#x1F5D1;</a>
</li>
`;
let template = document.createElement("template");
template.innerHTML = templateHtml.trim();
template = template.content.firstChild;
fetch('/admin/affiliates/json/whitelist') fetch('/admin/affiliates/json/whitelist')
.then(response => response.json()) .then(response => response.json())
.catch(error => console.log(error)) .catch(error => console.log(error))
.then(blacklist => { .then(whitelist => {