This page shows a step-by-step procedure to help you adapt a PHP script and .htaccess file so that multiple paths (like /admin, /abc, and wp-login.php) are protected by IP address. We assume:
.htaccess..htaccess file where you want to apply the new rules.Decide what you need to protect:
wp-login.php, admin, abc)./admin/, /abc/).This choice determines which Apache directives you’ll use in .htaccess.
Use <FilesMatch> in your .htaccess, for example:
<FilesMatch "^(wp-login\.php|admin|abc)$">
Order Deny,Allow
Deny from all
Allow from 1.2.3.4
Allow from ...
</FilesMatch>
^(wp-login\.php|admin|abc)$ means:
wp-login.phpadminabcDeny from all blocks everyone by default.Allow from 1.2.3.4 permits the IP 1.2.3.4. You can add more Allow from ... lines.If you need to protect all files in /admin/ or /abc/, you generally cannot do that with <FilesMatch> alone. Instead, you might need:
<Directory "/var/www/html/admin">
Require ip 1.2.3.4
</Directory><If> directive in .htaccess (if your hosting allows):
<If "%{REQUEST_URI} =~ m#^/(admin|abc)/# || %{REQUEST_URI} == '/wp-login.php'">
Order Deny,Allow
Deny from all
Allow from 1.2.3.4
Allow from ...
</If>For simplicity in this tutorial, we’ll show <FilesMatch> for multiple filenames.
.htaccess FileLocate your .htaccess (often in public_html/ or www/) and add or modify a block like:
<FilesMatch "^(wp-login\.php|admin|abc)$">
Order Deny,Allow
Deny from all
Allow from 1.2.3.4
</FilesMatch>
If you already have <Files wp-login.php> ... </Files> in your file, remove or replace that with <FilesMatch>. Make sure AllowOverride is enabled so that .htaccess rules take effect.
Your script might currently have two functions named something like:
parseWpLoginBlock($content)updateWpLoginBlock($content, $newIPs)Change these to handle <FilesMatch> instead of <Files>. For example:
function parseFilesMatchBlock($content) {
$lines = explode("\n", $content);
$startIndex = -1;
$endIndex = -1;
$ips = [];
// Look for <FilesMatch "^(wp-login\.php|admin|abc)$"> and </FilesMatch>
for ($i = 0; $i < count($lines); $i++) {
$trimmed = trim($lines[$i]);
if (stripos($trimmed, '<FilesMatch "') !== false
&& stripos($trimmed, '^(wp-login') !== false) {
$startIndex = $i;
}
if (stripos($trimmed, '</FilesMatch>') !== false && $startIndex !== -1) {
$endIndex = $i;
break;
}
}
// If found, parse lines for "Allow from"
if ($startIndex !== -1 && $endIndex !== -1) {
for ($j = $startIndex; $j <= $endIndex; $j++) {
if (stripos(trim($lines[$j]), 'Allow from') === 0) {
$parts = preg_split('/\s+/', trim($lines[$j]));
if (isset($parts[2])) {
$ips[] = $parts[2];
}
}
}
}
return [
'startIndex' => $startIndex,
'endIndex' => $endIndex,
'ips' => $ips
];
}
function updateFilesMatchBlock($content, $newIPs) {
$lines = explode("\n", $content);
$blockInfo = parseFilesMatchBlock($content);
$startIndex = $blockInfo['startIndex'];
$endIndex = $blockInfo['endIndex'];
if ($startIndex === -1 || $endIndex === -1) {
return $content; // No matching block found, do nothing
}
$rebuiltBlock = [];
for ($i = $startIndex; $i <= $endIndex; $i++) {
$trimmed = trim($lines[$i]);
// Skip old "Allow from" lines
if (stripos($trimmed, 'Allow from') === 0) {
continue;
}
$rebuiltBlock[] = $lines[$i];
// Right after "Deny from all", re-inject new "Allow from" lines
if (stripos($trimmed, 'Deny from all') === 0) {
foreach ($newIPs as $ip) {
$rebuiltBlock[] = "Allow from " . $ip;
}
}
}
// Replace that block segment in the array
$blockLength = ($endIndex - $startIndex) + 1;
array_splice($lines, $startIndex, $blockLength, $rebuiltBlock);
return implode("\n", $lines);
}
Anywhere in your script that calls the old functions (parseWpLoginBlock, updateWpLoginBlock) should be updated to call parseFilesMatchBlock and updateFilesMatchBlock instead.
.htaccess to confirm the lines under <FilesMatch "^(wp-login\.php|admin|abc)$"> are updated with the new IPs.Require all denied and Require ip <IP> instead of Order Deny,Allow.AllowOverride All is enabled in your Apache config for that directory.<FilesMatch> pattern exactly matches the filenames you want to protect. For example, admin is not the same as admin.php.Below is a complete revised script that demonstrates how to adapt your original code to manage IPs for <FilesMatch "^(wp-login\.php|admin|abc)$">. Update the $htaccessPath and $STATIC_PASSWORD_HASH to match your environment.
<?php
session_start();
/**************************************************************
* CONFIGURATION
**************************************************************/
// Store the hashed password here (Replace this with your own!)
$STATIC_PASSWORD_HASH = '$2y$10$EXAMPLE_ONLY_ReplaceWithRealHash';
// Path to the .htaccess file in your public_html folder
$htaccessPath = '/home/username/public_html/.htaccess'; // <-- Adjust this path
/**************************************************************
* SIMPLE PASSWORD PROTECTION CHECK
**************************************************************/
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
if (isset($_POST['password'])) {
// Use password_verify() instead of direct comparison
if (password_verify($_POST['password'], $STATIC_PASSWORD_HASH)) {
$_SESSION['authenticated'] = true;
header("Location: " . $_SERVER['PHP_SELF']);
exit;
} else {
$loginError = "Invalid password.";
}
}
// Display a simple login form (Bootstrap 5)
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Login Required</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-md-6 col-lg-4">
<div class="card">
<div class="card-body">
<h1 class="h5 text-center mb-4">Protected Access</h1>
<?php if (!empty($loginError)): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($loginError); ?></div>
<?php endif; ?>
<form method="post">
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" name="password" id="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<?php
exit;
}
/**************************************************************
* HELPER FUNCTIONS TO READ & UPDATE THE <FilesMatch> BLOCK
**************************************************************/
/**
* Extract IP lines from the <FilesMatch "^(wp-login\.php|admin|abc)$"> block.
*
* @param string $content The entire .htaccess content
* @return array {
* 'startIndex' => int,
* 'endIndex' => int,
* 'ips' => string[]
* }
*/
function parseFilesMatchBlock($content) {
$lines = explode("\n", $content);
$startIndex = -1;
$endIndex = -1;
$ips = [];
// Identify <FilesMatch> start and </FilesMatch> end
for ($i = 0; $i < count($lines); $i++) {
$trimmed = trim($lines[$i]);
// Look for <FilesMatch "^(wp-login\.php|admin|abc)$">
if (stripos($trimmed, '<FilesMatch "') !== false
&& stripos($trimmed, '^(wp-login') !== false) {
$startIndex = $i;
}
// Look for </FilesMatch>
if (stripos($trimmed, '</FilesMatch>') !== false && $startIndex !== -1) {
$endIndex = $i;
break;
}
}
// Parse lines between startIndex and endIndex for "Allow from"
if ($startIndex !== -1 && $endIndex !== -1) {
for ($j = $startIndex; $j <= $endIndex; $j++) {
if (stripos(trim($lines[$j]), 'Allow from') === 0) {
$parts = preg_split('/\s+/', trim($lines[$j]));
if (isset($parts[2])) {
$ips[] = $parts[2];
}
}
}
}
return [
'startIndex' => $startIndex,
'endIndex' => $endIndex,
'ips' => $ips
];
}
/**
* Replace the IPs in the <FilesMatch> block with new IPs.
*
* @param string $content The entire .htaccess content
* @param array $newIPs Array of new IP strings
* @return string Updated .htaccess content
*/
function updateFilesMatchBlock($content, $newIPs) {
$lines = explode("\n", $content);
$blockInfo = parseFilesMatchBlock($content);
$startIndex = $blockInfo['startIndex'];
$endIndex = $blockInfo['endIndex'];
// If we cannot find the block, do nothing
if ($startIndex === -1 || $endIndex === -1) {
return $content;
}
// Rebuild the block lines
$rebuiltBlock = [];
for ($i = $startIndex; $i <= $endIndex; $i++) {
$trimmed = trim($lines[$i]);
// Skip existing "Allow from" lines (we'll re-inject new ones)
if (stripos($trimmed, 'Allow from') === 0) {
continue;
}
$rebuiltBlock[] = $lines[$i];
// Right after "Deny from all", insert new "Allow from" lines
if (stripos($trimmed, 'Deny from all') === 0) {
foreach ($newIPs as $ip) {
$rebuiltBlock[] = "Allow from " . $ip;
}
}
}
// Replace old lines in $lines with the rebuilt block
$blockLength = ($endIndex - $startIndex) + 1;
array_splice($lines, $startIndex, $blockLength, $rebuiltBlock);
return implode("\n", $lines);
}
/**************************************************************
* PROCESS FORM SUBMISSION (SAVE NEW IP ADDRESSES)
**************************************************************/
if (isset($_POST['save_ips']) && isset($_POST['ips'])) {
// Clean the posted IPs, remove empty lines, etc.
$newIPs = array_map('trim', $_POST['ips']);
$newIPs = array_filter($newIPs, fn($ip) => $ip !== ''); // Remove empty
// Read .htaccess content
if (!file_exists($htaccessPath)) {
die("Error: .htaccess not found at $htaccessPath");
}
$htaccessContent = file_get_contents($htaccessPath);
// Update block
$updatedContent = updateFilesMatchBlock($htaccessContent, $newIPs);
// Save changes
file_put_contents($htaccessPath, $updatedContent);
// Redirect or confirm success
header("Location: " . $_SERVER['PHP_SELF'] . "?updated=1");
exit;
}
/**************************************************************
* READ CURRENT IPs AND DISPLAY HTML FORM
**************************************************************/
$htaccessContent = file_get_contents($htaccessPath);
$blockInfo = parseFilesMatchBlock($htaccessContent);
$currentIPs = $blockInfo['ips'];
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Manage Protected Paths IPs</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-4">
<h1 class="mb-4">Manage Allowed IPs for <code>FilesMatch</code> <small>(wp-login.php, admin, abc)</small></h1>
<?php if (isset($_GET['updated'])): ?>
<div class="alert alert-success">IP addresses have been updated successfully!</div>
<?php endif; ?>
<form method="post">
<input type="hidden" name="save_ips" value="1" />
<table class="table table-bordered bg-white">
<thead>
<tr>
<th>#</th>
<th>IP Address</th>
</tr>
</thead>
<tbody>
<?php
if (!empty($currentIPs)) {
foreach ($currentIPs as $index => $ip) {
?>
<tr>
<td><?php echo ($index + 1); ?></td>
<td>
<input type="text" name="ips[]" value="<?php echo htmlspecialchars($ip); ?>" class="form-control">
</td>
</tr>
<?php
}
} else {
// If no IPs exist, show one empty row
?>
<tr>
<td>1</td>
<td>
<input type="text" name="ips[]" value="" class="form-control" placeholder="e.g. 1.2.3.4">
</td>
</tr>
<?php
}
?>
</tbody>
</table>
<div class="mb-3">
<button type="button" class="btn btn-secondary" onclick="addRow()">+ Add another IP</button>
</div>
<button type="submit" class="btn btn-primary">Save IPs</button>
</form>
</div>
<script>
function addRow() {
const tableBody = document.querySelector('table tbody');
const rowCount = tableBody.rows.length;
const newRow = tableBody.insertRow();
let cellIndex = newRow.insertCell(0);
cellIndex.innerHTML = rowCount + 1;
let cellIP = newRow.insertCell(1);
cellIP.innerHTML = '<input type="text" name="ips[]" value="" class="form-control" placeholder="e.g. 1.2.3.4">';
}
</script>
</body>
</html>
By following these steps, you will:
<FilesMatch> block (or <If>/<Directory>, depending on your real needs) in .htaccess.<FilesMatch "^(wp-login\.php|admin|abc)$">.Copy/paste the above into your Grav site, and you’re set!