Commit 4e0c602d authored by Dominik Hebeler's avatar Dominik Hebeler
Browse files

Gathering affiliate clicks

parent bee09293
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use \App\Http\Controllers\AdgoalController;
use Illuminate\Support\Facades\DB;
class StorePartnerCalls extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'affilliates:store';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Stores cached clicks on affiliate links into DB.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
AdgoalController::storePartnerCalls();
# Remove old entries
# The duration in hours for entries to last is defined as constant in AdgoalController
DB::delete("delete from affiliate_clicks where created_at < DATE_SUB(NOW(), INTERVAL ? HOUR);", [\App\Http\Controllers\AdgoalController::STORAGE_DURATION_HOURS]);
return 0;
}
}
...@@ -30,6 +30,8 @@ class Kernel extends ConsoleKernel ...@@ -30,6 +30,8 @@ class Kernel extends ConsoleKernel
$schedule->command('requests:useragents')->everyFiveMinutes(); $schedule->command('requests:useragents')->everyFiveMinutes();
$schedule->command('logs:gather')->everyMinute(); $schedule->command('logs:gather')->everyMinute();
$schedule->command('spam:load')->everyMinute(); $schedule->command('spam:load')->everyMinute();
$schedule->command('affilliates:store')->everyMinute()
->onOneServer();
$schedule->call(function () { $schedule->call(function () {
DB::table('monthlyrequests')->truncate(); DB::table('monthlyrequests')->truncate();
DB::disconnect('mysql'); DB::disconnect('mysql');
......
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use LaravelLocalization;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
/**
* Before we redirect users to the affiliate shops we will track the clicks ourself.
* Reason is that many Affiliate shops redirect to invalid URLs (404, 500, ... Errors) which leads to bad user experience.
* We will store the clicked Affiliate Link together with the final URL the user should land on.
* No userdata or other metadata will be stored together with that information.
*
* That way we can do manual validation of affiliate links and exclude bad partnershop links to be shown in the future. Since this will be
* a lot of links we count the clicks so we can validate the most used ones first.
*/
class AdgoalController extends Controller
{
# Data will be stored for 24 hours
const STORAGE_DURATION_HOURS = 24;
const REDIS_STORAGE_KEY = "affiliate_click";
/**
* This function is called when a user clicks on a affiliate link. It will first validate that the URL
* was generated by us to prevent random redirect links to be manually created for our domains.
* 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
*/
public function forward(Request $request){
// $link = "https://metager.de";
// $affillink = "https://test.de";
// $password = self::generatePassword($affillink, $link);
// dd(route('adgoal-redirect', ["link" => $link, "affillink" => $affillink, "password" => $password]));
/**
* Get Parameters (Result informations)
* 1. affillink (With Affiliate Redirect)
* 2. link
* 5. Password (hmac with adgoal private key and the two parameters)
*/
$request->validate([
'affillink' => ['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
'password' => function($attribute, $value, $fail) use($request) {
// Check if hmac matches
$correctPassword = self::generatePassword($request->input('affillink'), $request->input('link'));
if(!hash_equals($correctPassword, $value)){
$fail('The given password is incorrect!');
}
}
]);
$this->storePartnerCallFast($request->input('affillink'), $request->input('link'));
return redirect($request->input('affillink'));
}
/**
* Stores Click information into Redis Cache for fast execution since this is synchronous call
* at search time.
* A Cronjob will pick the data up and store it into Mariadb later (see self::storePartnerCall)
*/
private function storePartnerCallFast($affillink, $link) {
# Generate Data to store
$host = parse_url($link, PHP_URL_HOST);
if(empty($host)){
return;
}
$storeObject = [
"host" => $host,
"affillink" => $affillink,
"link" => $link,
];
# Store Data in Redis
$redis = Redis::connection(config('cache.stores.redis.connection'));
$redis->rpush($this::REDIS_STORAGE_KEY, json_encode($storeObject));
}
public static function storePartnerCalls() {
$redis = Redis::connection(config('cache.stores.redis.connection'));
DB::transaction(function() use($redis){
while(!empty($data = $redis->lpop(self::REDIS_STORAGE_KEY))){
$data = json_decode($data, true);
# Insert data into mariadb table
DB::insert('insert into affiliate_clicks (hostname, affillink, link) values (?, ?, ?)',
[$data["host"], $data["affillink"], $data["link"]]);
}
});
}
/**
* Generates a Redirect URL for our partnershops
*/
public static function generateRedirectUrl($affillink, $link){
$password = self::generatePassword($affillink, $link);
return LaravelLocalization::getLocalizedURL(
LaravelLocalization::getCurrentLocale(),
route('adgoal-redirect', ["link" => $link, "affillink" => $affillink, "password" => $password])
);
}
/**
* Generates hmac password to validate redirect URLs
*/
public static function generatePassword($affillink, $link){
return hash_hmac("sha256", $affillink . $link, config('metager.metager.adgoal.private_key'));
}
}
...@@ -192,7 +192,10 @@ class Adgoal ...@@ -192,7 +192,10 @@ class Adgoal
} }
# Den Link hinzufügen: # Den Link hinzufügen:
$result->link = $partnershop["click_url"]; # Redirect the user over our tracker
# see \App\Http\Controllers\AdgoalController
# for more information
$result->link = \App\Http\Controllers\AdgoalController::generateRedirectUrl($partnershop["click_url"], $targetUrl);
$result->partnershop = true; $result->partnershop = true;
$result->changed = true; $result->changed = true;
} }
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AffiliateClicks extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('affiliate_clicks', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('hostname');
$table->string('affillink');
$table->string('link');
$table->timestamp('created_at')->nullable(false)->useCurrent();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('affiliate_clicks');
}
}
...@@ -351,6 +351,10 @@ Route::get('metrics', function (Request $request) { ...@@ -351,6 +351,10 @@ Route::get('metrics', function (Request $request) {
->header('Content-Type', RenderTextFormat::MIME_TYPE); ->header('Content-Type', RenderTextFormat::MIME_TYPE);
}); });
Route::group(['prefix' => 'partner'], function() {
Route::get('r', 'AdgoalController@forward')->name('adgoal-redirect');
});
Route::group(['prefix' => 'health-check'], function () { Route::group(['prefix' => 'health-check'], function () {
Route::get('liveness', 'HealthcheckController@liveness'); Route::get('liveness', 'HealthcheckController@liveness');
Route::get('liveness-scheduler', 'HealthcheckController@livenessScheduler'); Route::get('liveness-scheduler', 'HealthcheckController@livenessScheduler');
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment