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;
}