<?php

// translate_url.php
// -----------------------------------------------------------------------------
// Translate incoming URLS to their new modern equivalents
//
// Release    > 1.00
// Project    > dmClubURLTranslation-Sep06:S01_C03
// Copyright  > (C) Digital Mail Limited (2006)
// Author     > Richard Knight (RAK) for DML
// -----------------------------------------------------------------------------

// This script has 3 modes of operation:
// (1) It can be passed a URL to translate in the GET parameter 'transurl',
// (2) It can be triggered directly when the webserver issues a '404 Not Found' error
//     (in which case it will try to translate the URL that was requested)
// (3) It can batch process an Apoache web log (of up to 10000 lines)

// -----------------------------------------------------------------------------

// SET CONFIGURABLE OPTIONS

// Set name of default URL match table
$match_table_name = 'legacy_dmclub_3.txt';

// Set name of logfile for unmatched URLs
$unmatched_log = 'unmatched_1.txt';

// Set default forwarding domain for unmatched URLs
$default_forwarding_domain = 'http://cl06.dmclub.net';

// These dealers get the modern URLS, all others get forwarded to the default forwarding domain
$dealers_go_modern = '9000,DigitalMail,9111,dmClub,9222,NoDealer';

// Display PHP errors?
$display_errors = 1;
$debug_level    = 0; // 0=None, 1=Some, 2=Full

// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------

if ($display_errors) {	// Set up error reporting
	ini_set('display_errors', 1);
	error_reporting(E_ALL);
}

// Cookies can override the debug level, unless it is set to 2 (full debugging)
if ($debug_level < 2) {
	if (isset($_COOKIE['trans_engine_mode']) && $_COOKIE['trans_engine_mode'] == 'normal')    $debug_level = 0;
	if (isset($_COOKIE['trans_engine_mode']) && $_COOKIE['trans_engine_mode'] == 'debugging') $debug_level = 1;
}
// Raise the debugging mode from 1 to 2 if its 1 and we are NOT processing a web log
if ($debug_level == 1 && !isset($_POST['logfile'])) $debug_level = 2;


// Initialisation of variables
$debug_msg  = '';
$log_append = '';

$translate = $destination = array();

$modern_dealers = explode(',', strtolower($dealers_go_modern)); // NOT YET USED, MAYBE REMOVE!

// -----------------------------------------------------------------------------

// BUILD ARRAY OF URLS TO TRANSLATE
// Are we translating a single incoming request or processing a logfile?

if (!isset($_POST['logfile'])) { // Incoming request (either set in 'transurl' parameter, or direct)
	// Get URL to translate from the 'transurl' parameter
	// Otherwise use the current URL and referring URL (assumes redirection engine resides on same
	// server, which has beed configured to use 'translate_url.php' when a '404 Not Found' error occurs
	$translate = array();
	if (isset($_GET['transurl'])) {
		$translate[0]['url'] = $_GET['transurl'];
	} else {
		$protocol = isset($_SERVER['HTTPS']) ? 'https' : 'http';
		$translate[0]['url'] = $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
	}
	$translate[0]['ref'] = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
}

// -----------------------------------------------------------------------------
	
else { // Load incoming URIs and referers from log file
	if ($debug_level == 0) $debug_level = 1; 
	if (!move_uploaded_file($_FILES['weblog']['tmp_name'], 'uploads/' . $_FILES['weblog']['name'])) {
		add_debug_msg('<b>COULD NOT UPLOAD WEBLOG</b>');
	} else {
		$weblog = file('uploads/' . $_FILES['weblog']['name']);
		list($desc, $include, $exclude) = get_filter_regex($_POST['process']);
		$stats['count'] = $stats['parsed'] = $stats['blank'] = $stats['badly_formed'] = $stats['matched'] = $stats['matched_ref_empty'] = $stats['ref_empty'] = $stats['parsed_ref_empty'] = 0;
		$not_matched = array();
		foreach($weblog as $line) {
			$stats['count']++;
			if (trim($line) == '') {
				$stats['blank']++;
				continue; // ignore blank lines
			}
			$r = parse_apache_log_line(trim($line));
			if (!$r['error']) {
				if (!$r['ref']) {
					$stats['ref_empty']++;
					if (isset($_POST['exclude'])) continue;
				}
				if (preg_match($include, $r['ref']) && !preg_match($exclude, $r['ref'])) {
					$url = 'http://' . $r['host'] . $r['request'];
					$translate[] = array('url' => $url, 'ref' => $r['ref']);
					add_debug_msg('<b>ANAYSIS OF LOG ENTRY</b><br>' . $line);
					add_debug_msg('<pre>' . print_r($r, 1) . '</pre>');
				} else {
					$not_matched[] = $r['ref'];
				}
			} else {
				$stats['badly_formed']++;
				add_debug_msg('<b>ERROR: </b>' . $r['error'] . '<br>' . $line);
			}
		}
		add_debug_msg('<h3>NOT MATCHED</h3><pre>' . print_r($not_matched, 1) . '</pre>'); 
	}
}

// -----------------------------------------------------------------------------

// LOAD URL TRANSLATION REGEX
$match_table = file('match_tables/' . $match_table_name);

// TRANSLATE URLS
for ($i=0; $i<count($translate); $i++) {
	$trans_res[] = $r = translation_wrapper($translate[$i]['url'], $translate[$i]['ref'], $match_table);
	if (isset($_POST['logfile'])) {
		$stats['parsed']++;
		$stats['parsed_ref_empty']+= (int) !$translate[$i]['ref'];
		$stats['matched']+= (int) $r['match'];
		$stats['matched_ref_empty']+= (int) $r['match'] && !$translate[$i]['ref'];
	}
}

// LOG UNMATCHED URLS
if ($log_append) {
	// Record failure to match in log file
	$fp = @fopen('logs/' . $unmatched_log, 'a');
	if ($fp) {
		fwrite($fp, $log_append);
		fclose($fp);
	} else {
		add_debug_msg('<b>COULD NOT OPEN LOG FILE FOR APPENDING</b> at logs/' . $unmatched_log);
	}
}

// DISPLAY DEBUGGING, OTHERWISE REDIRECT OR TERMINATE THE REQUEST
if ($debug_level) {
	// Include an intermediate debugging page
	if (isset($_POST['sortby_match'])) usort($trans_res, 'compare_match');
	include('debug.php');
} else {
	switch ($trans_res[0]['dest']) {
		case 'TERMINATE':
			header('HTTP/1.1 204 No Response');
			break;
		default:
			header('HTTP/1.1 301 Moved Permanently');
			header('LOCATION: ' . $trans_res[0]['dest']);
			break;
	}
	exit;	
}

// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------

function translation_wrapper($url, $ref, $match_table) {
	// This function calls 'translate_url', and works out the destination URL
	// If 'translate_url' returns false, the destination URL is the in the default forwarding domain
	// eg http://cl06.dmclub.net
	global $modern_dealers, $default_forwarding_domain, $log_append;
	list($translated, $matchline) = translate_url($url, $match_table);
	if ($translated) {
		$dealer = preg_match('/dmx_(?:did|name)=([^&#]+)/', $translated, $r) ? $r[1] : 'UNKNOWN';
		if (in_array(strtolower($dealer), $modern_dealers) || in_array('ALL', $modern_dealers) || $dealer == 'UNKNOWN') {
			$destination = $translated;
		}
	} else {
		$log_append .= str_pad($url, 150) . ' ' . str_pad($ref, 150) . "\n";
		$dealer = 'UNKNOWN';
 	}
	if (!isset($destination)) $destination = $default_forwarding_domain . preg_replace('/(\w+):\/\/([\w.]+)\//', '/', $url);
	return array('url' => $url, 'ref' => $ref, 'dealer' => $dealer, 'dest' => $destination, 'match' => $translated != false, 'line' => $matchline);	
}

// -----------------------------------------------------------------------------

function translate_url($url, $match_table) {
	// Return a translated URL, or false if no match is found
	add_debug_msg('<strong>URL: ' . $url . '</strong>');
	
	$req = dissect_url($url);
	if ($req['error']) return false;
	add_debug_msg('<b>ANAYSIS OF INCOMING URL</b>');
	add_debug_msg('<pre>' . print_r($req, 1) . '</pre>');
	
	if (count($req['params'])) {
		// Reorder parameters alphabetically
		$req['params'] = array_change_key_case($req['params'], CASE_LOWER);
		ksort($req['params']);
		$query = '';
		foreach($req['params'] as $key => $value) $query .= '&' . $key. '=' . $value;
		$req['query'] = '?' . substr($query, 1);
		$req['url'] = $req['protocol'] . '://' . $req['host'] . $req['path'] . $req['query'] . $req['hash'];
		add_debug_msg('<b>URL REORDERED: ' . $req['url'] . '</b>');
	}
	
	$i = 0;
	$dest_url = false;
	foreach($match_table as $line) {
		$i++;
		if (substr($line, 0, 2) == '##' || trim($line) == '') continue; // Ignore comments and empty lines
		list($url_regex, $dest_regex) = explode('>', $line);
		$url_regex = trim($url_regex) . 'i'; // Append 'case insensitive' pattern modifier
		add_debug_msg('Trying: ' . $url_regex);
		if (preg_match($url_regex, $req['url'])) {
			$dest_url = preg_replace($url_regex, trim($dest_regex), $req['url']);
			break;
		}
	}
	add_debug_msg($dest_url ? '<b>MATCH FOUND</b><br>' :  '<b>NO MATCH FOUND</b><br>');
	return array($dest_url, $dest_url ? $i : 999999999);
}

// -----------------------------------------------------------------------------

function dissect_url($url) {
	// Take a URL and return an object that allows every part to be easily accessed
	// namely: the full url, protocol, host, path, query, hash and individual parameters

	$ex = array(); // Contains 'dissected' URL
	$regex = '/(\w+):\/\/([\w.]+)(\/[^\?]*)?(\?[^#]*)?(#.*)*/'; // regular expression to split up URLs - NEW WITH HASH;
	if (preg_match($regex, $url, $result)) {
		$ex['error']    = 0;
		$ex['url']      = $result[0]; // the complete URL
		$ex['protocol'] = $result[1]; // the protocol
		$ex['host']     = $result[2]; // the host
		$ex['path']     = $result[3]; // the path to the page on the host
		$ex['query']    = isset($result[4]) ? $result[4] : ''; // the query string, if any
		$ex['hash']     = isset($result[5]) ? $result[5] : ''; // the hash (anchor) if any
		parse_str(substr($ex['query'], 1), $ex['params']);     // eg. ?a=1&b=2 => ex.params.a (set to 1) and ex.params.b (set to 2)
	} else {
		$ex['error'] = 'BADLY FORMED URL'; // May happen if HTTP Referrer data is stripped from request by a firewall, and this function is passed the referring URL
	}
	return $ex;
}

// -----------------------------------------------------------------------------

function parse_apache_log_line($line) {
	// Parse a session log line using regular expressions
	// extracting and returning all the data in an easily accessible array
	
	$p = array();
	$line = str_replace('+', ' ', $line); // replace + with space
	$split_regex = '/(\S+)\s(\S+)\s(\S+)\s\[(\S+\s+\S+)\]\s"\S+\s(.*)\sHTTP\/\S+"\s\d+\s\S+\s"(.*)"\s"(.*)"/'; // REGEX to split up Apache log entry
	if (preg_match($split_regex, $line, $result)) {
		$p['error']      = 0;
		$p['ip']         = $result[1]; 	// the IP address
		$p['host']       = $result[2]; 	// the Host
		$p['http_auth']  = $result[3];	// The HTTP Authentication login name, if any
		$p['time']       = $result[4]; 	// the time of the request, in Apache log format, without surrounding brackets
		$p['request']    = $result[5]; 	// the complete URL of the request
		$p['ref']        = $result[6]; 	// the complete referring URL
		$p['user_agent'] = $result[7];	// the User Agent string sent by the browser
	} else {
		$p['error'] = 'BADLY FORMED LOG ENTRY';
	}
	return $p;
}

// -----------------------------------------------------------------------------

function get_filter_regex($description) {
	// Set regular expressions used to filter log by referer
	// ( lines matched by $include and not matched by $exclude make it into the results )
	switch($_POST['process']) {
		case 'all':
			$desc    = 'all non-empty valid lines';
			$include = '/.*/';
			$exclude = '/^Z$/';
			break;
		case 'outside':
			$desc    = 'hits from outside (referrer does not contain \'dmclub\')';
			$include = '/.*/';
			$exclude = '/\w+:\/\/[\w.-]*dmclub/i';
			break;
		case 'google':
			$desc    = 'hits from Google';
			$include = '/\w+:\/\/[\w.-]*google/i';
			$exclude = '/^Z$/';
			break;
		case 'yahoo':
			$desc    = 'hits from Yahoo';
			$include = '/\w+:\/\/[\w.-]*yahoo/i';
			$exclude = '/^Z$/';
			break;
		case 'msn':
			$desc    = 'hits from MSN';
			$include = '/\w+:\/\/[\w.-]*msn/i';
			$exclude = '/^Z$/';
			break;
		case 'aol':
			$desc    = 'hits from AOL';
			$include = '/\w+:\/\/[\w.-]*aol/i';
			$exclude = '/^Z$/';
			break;
		case 'ask':
			$desc    = 'hits from Ask';
			$include = '/\w+:\/\/[\w.-]*ask/i';
			$exclude = '/^Z$/';
			break;
		case 'no_referer':
			$desc    = 'lines of logfile with no referrer';
			$include = '/^$/i';
			$exclude = '/^Z$/';
			break;
	}
	return array($desc, $include, $exclude);
}

// -----------------------------------------------------------------------------

function compare_match($a, $b) {
	// Function for sorting results array by regex match (so incoming URLS are grouped by what they matched)
	if ($a['line'] == $b['line']) return 0;
	return ($b['line'] < $a['line']) ? 1 : -1;
}

// -----------------------------------------------------------------------------

function add_debug_msg($msg = '') {
	if ($GLOBALS['debug_level'] < 2) return;
	$GLOBALS['debug_msg'] .= $msg . '<br>';
}

?>
