Skip to content
← Back to Functions
Code

Redirect Map Validator

Checks redirect rules for loops, chains, invalid targets, and non-SEF URLs.

Function signature

ogValidateRedirectMap(input = array(), options = array())

Categories

  • Security

Parameters

inputStructured workflow input array documented by this helper.optionsOptional documented policy controls for the helper.

Return value

Public-safe status string returned by the function for controller branching or logging.

  • success
  • message
  • data

Compatibility

Existing function name, slug, path, and call order preserved; advertised metadata corrected to the actual source behavior.

Minimum PHP version: 7.4

Security notes

Use caller-owned allowlists and context-specific escaping; validate file paths, routes, email tokens, cart totals, discount rules, and tax-region rules before production use.

Code

<?php

/*
 * Copyright (c) 2026 Jeffery L. Paris <jparis@phpog.com>.
 * Free for personal and internal use. Paid project use requires visible credit
 * to Jeffery L. Paris. Corporate use requires a paid license fee unless a
 * separate written license states otherwise.
 */

/**
 * Checks redirect rules for loops, chains, invalid targets, and non-SEF URLs.
 *
 * Primary use case: Migration and .htaccess audits.
 * Typical inputs: redirect rows, site policy.
 * Typical output: redirect audit report.
 *
 * Implementation note: Detect loops before deploy.
 *
 * @param array $input Structured input values for this helper contract.
 * @param array $options Optional policy and formatting controls.
 * @return array Structured result data with success, message, and data keys.
 */
function ogValidateRedirectMap($input = array(), $options = array()) {
	$result = array(
		'success' => false,
		'message' => '',
		'data' => array()
	);

	if (!is_array($input)) {
		$result['message'] = 'Input must be an array.';
		return $result;
	}

	if (!is_array($options)) {
		$options = array();
	}

	$redirects = array();
	if (!empty($input['redirects']) && is_array($input['redirects'])) {
		$redirects = $input['redirects'];
	} else {
		$redirects = $input;
	}

	$allowed_host = 'phpog.com';
	if (!empty($options['allowed_host'])) {
		$allowed_host = preg_replace('/^www\./i', '', trim((string)$options['allowed_host']));
	}

	$targets = array();
	$issues = array();
	$checked = 0;

	foreach ($redirects as $index => $redirect) {
		$checked++;
		if (!is_array($redirect)) {
			$issues[] = array('index' => $index, 'issue' => 'Redirect row must be an array.');
			continue;
		}

		$from = '';
		$to = '';
		$status = 301;

		if (!empty($redirect['from'])) {
			$from = trim((string)$redirect['from']);
		}
		if (!empty($redirect['to'])) {
			$to = trim((string)$redirect['to']);
		}
		if (!empty($redirect['status'])) {
			$status = (int)$redirect['status'];
		}

		if (empty($from) || empty($to)) {
			$issues[] = array('index' => $index, 'issue' => 'Redirect source and target are required.');
			continue;
		}

		if ($status != 301 && $status != 302 && $status != 307 && $status != 308) {
			$issues[] = array('index' => $index, 'issue' => 'Redirect status is not approved.');
		}

		if (strpos($from, '..') !== false || strpos($to, '..') !== false) {
			$issues[] = array('index' => $index, 'issue' => 'Traversal-like path segment detected.');
		}

		if (preg_match('/[\r\n]/', $from) || preg_match('/[\r\n]/', $to)) {
			$issues[] = array('index' => $index, 'issue' => 'Header injection characters detected.');
		}

		if (preg_match('/^http:\/\//i', $to)) {
			$issues[] = array('index' => $index, 'issue' => 'Insecure http target detected.');
		}

		if (preg_match('/^https?:\/\/www\./i', $to)) {
			$issues[] = array('index' => $index, 'issue' => 'www target detected.');
		}

		if (preg_match('/^https?:\/\//i', $to)) {
			$host = parse_url($to, PHP_URL_HOST);
			if (preg_replace('/^www\./i', '', (string)$host) != $allowed_host) {
				$issues[] = array('index' => $index, 'issue' => 'External redirect target detected.');
			}
		} elseif (substr($to, 0, 2) == '//') {
			$host = parse_url('https:' . $to, PHP_URL_HOST);
			if (preg_replace('/^www\./i', '', (string)$host) != $allowed_host) {
				$issues[] = array('index' => $index, 'issue' => 'External protocol-relative target detected.');
			}
		} elseif (substr($to, 0, 1) != '/') {
			$issues[] = array('index' => $index, 'issue' => 'Target must be local or approved host.');
		}

		$targets[$from] = $to;
	}

	foreach ($targets as $from => $to) {
		$seen = array($from => true);
		$current = $to;
		$depth = 0;
		while (!empty($targets[$current])) {
			$depth++;
			if (!empty($seen[$current])) {
				$issues[] = array('from' => $from, 'issue' => 'Redirect loop detected.');
				break;
			}
			if ($depth > 5) {
				$issues[] = array('from' => $from, 'issue' => 'Redirect chain is too long.');
				break;
			}
			$seen[$current] = true;
			$current = $targets[$current];
		}
	}

	$result['success'] = empty($issues);
	if (empty($issues)) {
		$result['message'] = 'Redirect map passed validation.';
	} else {
		$result['message'] = 'Redirect map has issues.';
	}
	$result['data'] = array(
		'checked' => $checked,
		'issues' => $issues
	);

	return $result;
}