- Web-based image processing application using Clipping Magic API - Batch processing with PNG and TIFF output formats - Microsoft Azure AD authentication integration - Automatic file cleanup after 24 hours - Real-time processing status and bulk download features 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
675 lines
No EOL
22 KiB
PHP
Executable file
675 lines
No EOL
22 KiB
PHP
Executable file
<?php
|
|
// Load configuration
|
|
$config = require 'config.php';
|
|
|
|
function generateUniqueId($length = 4) {
|
|
$characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
$id = '';
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$id .= $characters[rand(0, strlen($characters) - 1)];
|
|
}
|
|
return $id;
|
|
}
|
|
|
|
function logMessage($type, $message, $data = null) {
|
|
global $config;
|
|
$logFile = $config['directories']['logs'] . date('Y-m-d') . '.log';
|
|
$timestamp = date('Y-m-d H:i:s');
|
|
$logData = [
|
|
'timestamp' => $timestamp,
|
|
'type' => $type,
|
|
'message' => $message,
|
|
'data' => $data
|
|
];
|
|
|
|
file_put_contents($logFile, json_encode($logData) . "\n", FILE_APPEND);
|
|
}
|
|
|
|
// Create necessary directories
|
|
foreach ($config['directories'] as $dir) {
|
|
if (!file_exists($dir)) {
|
|
mkdir($dir, 0777, true);
|
|
}
|
|
}
|
|
|
|
function cleanupOldFiles($directories) {
|
|
// 24 hours in seconds
|
|
$timeThreshold = 24 * 60 * 60;
|
|
$now = time();
|
|
|
|
foreach ($directories as $dir) {
|
|
if (!is_dir($dir)) {
|
|
continue;
|
|
}
|
|
|
|
$files = glob($dir . '*');
|
|
foreach ($files as $file) {
|
|
// Skip if it's not a file
|
|
if (!is_file($file)) {
|
|
continue;
|
|
}
|
|
|
|
// Check file age
|
|
$fileAge = $now - filemtime($file);
|
|
if ($fileAge > $timeThreshold) {
|
|
unlink($file);
|
|
logMessage('info', 'Deleted old file', ['file' => $file, 'age' => $fileAge]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
$response = [];
|
|
$fileMapping = [];
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (isset($_FILES['files'])) {
|
|
// Cleanup old files before processing new ones
|
|
cleanupOldFiles([
|
|
$config['directories']['upload'],
|
|
$config['directories']['processed']
|
|
]);
|
|
|
|
$files = reArrayFiles($_FILES['files']);
|
|
|
|
foreach ($files as $file) {
|
|
$originalFileName = basename($file['name']);
|
|
$uniqueId = generateUniqueId();
|
|
$fileInfo = pathinfo($originalFileName);
|
|
$newFileName = $uniqueId . '_' . $fileInfo['filename'] . '.' . $fileInfo['extension'];
|
|
$targetFilePath = $config['directories']['upload'] . $newFileName;
|
|
|
|
// Store original filename for later use
|
|
$fileMapping[$newFileName] = $originalFileName;
|
|
|
|
// Validate file
|
|
if (!in_array($file['type'], $config['processing']['allowed_types'])) {
|
|
$response[$originalFileName] = [
|
|
'success' => false,
|
|
'error' => $config['errors']['invalid_type']
|
|
];
|
|
continue;
|
|
}
|
|
|
|
if ($file['size'] > $config['processing']['max_file_size']) {
|
|
$response[$originalFileName] = [
|
|
'success' => false,
|
|
'error' => $config['errors']['file_too_large']
|
|
];
|
|
continue;
|
|
}
|
|
|
|
if (move_uploaded_file($file['tmp_name'], $targetFilePath)) {
|
|
try {
|
|
// Process with Clipping Magic API
|
|
$results = processWithClippingMagic($targetFilePath, $newFileName, $config);
|
|
if (!empty($results['png']) && !empty($results['tiff'])) {
|
|
$response[$originalFileName] = [
|
|
'success' => true,
|
|
'results' => [
|
|
'png' => $results['png'],
|
|
'tiff' => $results['tiff']
|
|
],
|
|
'uniqueId' => $uniqueId,
|
|
'originalName' => $originalFileName,
|
|
'debug' => [
|
|
'newFileName' => $newFileName,
|
|
'processedFiles' => $results
|
|
]
|
|
];
|
|
logMessage('debug', 'File processed successfully', $response[$originalFileName]);
|
|
} else {
|
|
throw new Exception('Failed to generate both PNG and TIFF outputs');
|
|
}
|
|
} catch (Exception $e) {
|
|
$response[$originalFileName] = [
|
|
'success' => false,
|
|
'error' => sprintf($config['errors']['processing'], $e->getMessage())
|
|
];
|
|
}
|
|
} else {
|
|
$response[$originalFileName] = [
|
|
'success' => false,
|
|
'error' => $config['errors']['upload']
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
header('Content-Type: application/json');
|
|
echo json_encode(['response' => $response]);
|
|
exit;
|
|
}
|
|
|
|
function processWithClippingMagic($filePath, $fileName, $config) {
|
|
// Get base filename without extension but keep the unique ID prefix
|
|
$baseFileName = pathinfo($fileName, PATHINFO_FILENAME);
|
|
|
|
// Process for PNG result
|
|
$pngResult = sendToClippingMagic($filePath, $fileName, $config['processing']['formats']['png'], $config);
|
|
|
|
// Process for TIFF with clipping path
|
|
$tiffResult = sendToClippingMagic($filePath, $fileName, $config['processing']['formats']['tiff'], $config);
|
|
|
|
return [
|
|
'png' => $baseFileName . '_result.png',
|
|
'tiff' => $baseFileName . '_clipping_path_tiff.tiff'
|
|
];
|
|
}
|
|
function sendToClippingMagic($filePath, $fileName, $formatConfig, $config) {
|
|
logMessage('info', 'Starting API request', [
|
|
'file' => $fileName,
|
|
'format' => $formatConfig['format']
|
|
]);
|
|
|
|
$ch = curl_init($config['api']['base_url'] . $config['api']['endpoints']['images']);
|
|
|
|
$postFields = [
|
|
'image' => new CURLFile($filePath),
|
|
'format' => $formatConfig['format'],
|
|
'test' => 'false'
|
|
];
|
|
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => 1,
|
|
CURLOPT_POSTFIELDS => $postFields,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Authorization: Basic ' . $config['api']['key']
|
|
]
|
|
]);
|
|
|
|
$result = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
logMessage('debug', 'API response received', [
|
|
'httpCode' => $httpCode,
|
|
'responseSize' => strlen($result),
|
|
'firstBytes' => bin2hex(substr($result, 0, 8))
|
|
]);
|
|
|
|
if (curl_errno($ch)) {
|
|
throw new Exception(curl_error($ch));
|
|
}
|
|
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
// Check if the response looks like it contains image data
|
|
$firstBytes = substr($result, 0, 4);
|
|
$isPNG = strpos($firstBytes, "\x89\x50\x4E\x47") === 0;
|
|
$isTIFF = strpos($firstBytes, "\x49\x49\x2A\x00") === 0 || strpos($firstBytes, "\x4D\x4D\x00\x2A") === 0;
|
|
|
|
if (!$isPNG && !$isTIFF) {
|
|
$jsonResponse = json_decode($result, true);
|
|
if ($jsonResponse && isset($jsonResponse['error'])) {
|
|
throw new Exception($jsonResponse['error']);
|
|
} else if ($httpCode !== 200) {
|
|
throw new Exception(sprintf($config['errors']['api_error'], $httpCode));
|
|
}
|
|
}
|
|
|
|
// Generate output filename using the unique ID prefix
|
|
$outputFileName = pathinfo($fileName, PATHINFO_FILENAME) .
|
|
'_' . $formatConfig['format'] .
|
|
$formatConfig['extension'];
|
|
$outputPath = $config['directories']['processed'] . $outputFileName;
|
|
|
|
// Save the result
|
|
file_put_contents($outputPath, $result);
|
|
|
|
return $outputFileName;
|
|
}
|
|
|
|
function reArrayFiles($files) {
|
|
$file_array = array();
|
|
$file_count = count($files['name']);
|
|
$file_keys = array_keys($files);
|
|
|
|
for ($i = 0; $i < $file_count; $i++) {
|
|
foreach ($file_keys as $key) {
|
|
$file_array[$i][$key] = $files[$key][$i];
|
|
}
|
|
}
|
|
return $file_array;
|
|
}
|
|
?>
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Clipping Magic Processor</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
body {
|
|
font-family: 'Montserrat', sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
.app-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
text-align: center; /* Center child elements horizontally */
|
|
}
|
|
|
|
|
|
.upload-container {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
}
|
|
.logo {
|
|
width: 300px;
|
|
height: auto;
|
|
margin: 0 auto 20px; /* Ensures the logo is centered horizontally with margin for spacing */
|
|
}
|
|
.upload-button {
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
.upload-button:hover {
|
|
background-color: #45a049;
|
|
}
|
|
|
|
.image-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.image-box {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.image-preview {
|
|
width: 100%;
|
|
height: 200px;
|
|
object-fit: contain;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.status {
|
|
margin: 10px 0;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
text-align: center;
|
|
}
|
|
|
|
.processing { background: #fff3cd; color: #856404; }
|
|
.complete { background: #d4edda; color: #155724; }
|
|
.error { background: #f8d7da; color: #721c24; }
|
|
|
|
.download-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.download-button {
|
|
flex: 1;
|
|
padding: 8px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: #007bff;
|
|
color: white;
|
|
cursor: pointer;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
.download-button:hover {
|
|
background: #0056b3;
|
|
}
|
|
|
|
.bulk-download {
|
|
text-align: center;
|
|
margin: 20px 0;
|
|
padding: 20px;
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.preview-wrapper {
|
|
width: 100%;
|
|
height: 200px;
|
|
position: relative;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.image-preview {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
</style>
|
|
|
|
<!-- auth 1 of 4 -->
|
|
<script src="https://alcdn.msauth.net/browser/2.15.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
|
<style>
|
|
#protected-content {
|
|
display: none;
|
|
}
|
|
</style>
|
|
<!-- end auth block -->
|
|
|
|
</head>
|
|
<body>
|
|
<div class="app-container">
|
|
|
|
<!-- auth 2 of 4 -->
|
|
<div style="text-align: left;">
|
|
<button id="logout-button" onclick="signOut()" style="display:none;">Log Out</button>
|
|
<button id="login-button" onclick="signIn()" style="display:none;">Log In</button>
|
|
</div>
|
|
<!-- end auth block -->
|
|
|
|
<img src="clipping_magic.svg" alt="Clipping Magic" class="logo">
|
|
<div class="upload-container" id="protected-content">
|
|
<input type="file" id="fileUpload" multiple accept="image/*" style="display: none;">
|
|
<button class="upload-button" onclick="document.getElementById('fileUpload').click()">
|
|
Upload Images
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bulk-download" id="bulkDownloadContainer" style="display: none;">
|
|
<button class="download-button" onclick="downloadAll('png')">Download All PNGs</button>
|
|
<button class="download-button" onclick="downloadAll('tiff')">Download All TIFFs</button>
|
|
</div>
|
|
|
|
<div id="imageGrid" class="image-grid"></div>
|
|
</div>
|
|
|
|
<script>
|
|
let processedFiles = {};
|
|
|
|
document.getElementById('fileUpload').addEventListener('change', function(event) {
|
|
const files = Array.from(event.target.files);
|
|
files.forEach(file => {
|
|
createImageBox(file);
|
|
uploadFile(file);
|
|
});
|
|
|
|
if (files.length > 0) {
|
|
document.getElementById('bulkDownloadContainer').style.display = 'block';
|
|
}
|
|
});
|
|
|
|
function createImageBox(file) {
|
|
const box = document.createElement('div');
|
|
box.className = 'image-box';
|
|
box.id = `box-${file.name}`;
|
|
|
|
// Create the preview wrapper first with the checkerboard background
|
|
const previewWrapper = document.createElement('div');
|
|
previewWrapper.className = 'preview-wrapper';
|
|
previewWrapper.style.cssText = `
|
|
background-image: linear-gradient(45deg, #808080 25%, transparent 25%),
|
|
linear-gradient(-45deg, #808080 25%, transparent 25%),
|
|
linear-gradient(45deg, transparent 75%, #808080 75%),
|
|
linear-gradient(-45deg, transparent 75%, #808080 75%);
|
|
background-size: 20px 20px;
|
|
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
|
background-color: #ffffff;
|
|
width: 100%;
|
|
height: 200px;
|
|
position: relative;
|
|
`;
|
|
|
|
// Create and set up the image with proper constraints from the start
|
|
const img = document.createElement('img');
|
|
img.className = 'image-preview';
|
|
img.style.cssText = `
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
`;
|
|
|
|
// Read and set the initial image
|
|
const reader = new FileReader();
|
|
reader.onload = e => img.src = e.target.result;
|
|
reader.readAsDataURL(file);
|
|
|
|
// Add image to wrapper
|
|
previewWrapper.appendChild(img);
|
|
|
|
// Create status element
|
|
const status = document.createElement('div');
|
|
status.className = 'status processing';
|
|
status.textContent = 'Processing...';
|
|
status.id = `status-${file.name}`;
|
|
|
|
// Create download buttons container
|
|
const downloadButtons = document.createElement('div');
|
|
downloadButtons.className = 'download-buttons';
|
|
downloadButtons.id = `downloads-${file.name}`;
|
|
downloadButtons.style.display = 'none';
|
|
|
|
// Assemble the box
|
|
box.appendChild(previewWrapper);
|
|
box.appendChild(status);
|
|
box.appendChild(downloadButtons);
|
|
|
|
document.getElementById('imageGrid').appendChild(box);
|
|
}
|
|
|
|
async function uploadFile(file) {
|
|
const formData = new FormData();
|
|
formData.append('files[]', file);
|
|
|
|
try {
|
|
const response = await fetch('', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
console.log('Full API Response:', result);
|
|
|
|
// Get the file result using original filename
|
|
const fileResult = result.response[file.name];
|
|
console.log('Processing file:', file.name, 'Result:', fileResult);
|
|
|
|
// Check if we have a successful response
|
|
if (fileResult && fileResult.success === true && fileResult.results) {
|
|
console.log('File processed successfully:', fileResult);
|
|
|
|
// Store processed files info
|
|
processedFiles[file.name] = {
|
|
uniqueId: fileResult.uniqueId,
|
|
png: fileResult.results.png,
|
|
tiff: fileResult.results.tiff,
|
|
originalName: file.name
|
|
};
|
|
|
|
// Update status
|
|
const status = document.getElementById(`status-${file.name}`);
|
|
status.className = 'status complete';
|
|
status.textContent = 'Processing complete';
|
|
|
|
// Update download buttons
|
|
const downloadButtons = document.getElementById(`downloads-${file.name}`);
|
|
downloadButtons.style.display = 'flex';
|
|
downloadButtons.innerHTML = `
|
|
<button class="download-button" onclick="downloadFile('${fileResult.results.png}', '${file.name.replace(/\.[^/.]+$/, '')}_result.png')">
|
|
Download PNG
|
|
</button>
|
|
<button class="download-button" onclick="downloadFile('${fileResult.results.tiff}', '${file.name.replace(/\.[^/.]+$/, '')}_clipping_path.tiff')">
|
|
Download TIFF
|
|
</button>
|
|
`;
|
|
|
|
// Create a wrapper for the image preview if it doesn't exist
|
|
const box = document.getElementById(`box-${file.name}`);
|
|
let previewWrapper = box.querySelector('.preview-wrapper');
|
|
if (!previewWrapper) {
|
|
previewWrapper = document.createElement('div');
|
|
previewWrapper.className = 'preview-wrapper';
|
|
previewWrapper.style.cssText = `
|
|
background-image: linear-gradient(45deg, #808080 25%, transparent 25%),
|
|
linear-gradient(-45deg, #808080 25%, transparent 25%),
|
|
linear-gradient(45deg, transparent 75%, #808080 75%),
|
|
linear-gradient(-45deg, transparent 75%, #808080 75%);
|
|
background-size: 20px 20px;
|
|
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
|
background-color: #ffffff;
|
|
width: 100%;
|
|
height: 200px;
|
|
position: relative;
|
|
`;
|
|
|
|
// Move the existing image into the wrapper
|
|
const existingImg = box.querySelector('.image-preview');
|
|
if (existingImg) {
|
|
existingImg.style.position = 'absolute';
|
|
existingImg.style.top = '0';
|
|
existingImg.style.left = '0';
|
|
existingImg.style.width = '100%';
|
|
existingImg.style.height = '100%';
|
|
existingImg.style.objectFit = 'contain';
|
|
previewWrapper.appendChild(existingImg);
|
|
}
|
|
|
|
// Insert wrapper where the image was
|
|
box.insertBefore(previewWrapper, box.firstChild);
|
|
}
|
|
|
|
// Update the image
|
|
const img = box.querySelector('.image-preview');
|
|
setTimeout(() => {
|
|
console.log('Updating image preview for:', file.name);
|
|
console.log('New image URL:', `processed/${fileResult.results.png}`);
|
|
img.src = `processed/${fileResult.results.png}?t=${new Date().getTime()}`;
|
|
}, 1500);
|
|
|
|
} else {
|
|
console.error('Invalid or failed file result:', fileResult);
|
|
showError(file.name, fileResult?.error || 'Failed to process image');
|
|
}
|
|
} catch (error) {
|
|
console.error('Upload error:', error);
|
|
showError(file.name, 'Failed to process image');
|
|
}
|
|
}
|
|
|
|
async function checkFileExists(url) {
|
|
try {
|
|
const response = await fetch(url, { method: 'HEAD' });
|
|
return response.ok;
|
|
} catch (error) {
|
|
console.error('File check error:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function showError(fileName, error) {
|
|
const status = document.getElementById(`status-${fileName}`);
|
|
status.className = 'status error';
|
|
status.textContent = error;
|
|
}
|
|
|
|
function downloadFile(processedFilename, desiredFilename) {
|
|
const link = document.createElement('a');
|
|
link.href = `processed/${processedFilename}`;
|
|
link.download = desiredFilename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
|
|
function downloadAll(type) {
|
|
Object.values(processedFiles).forEach(fileInfo => {
|
|
const processedFilename = fileInfo[type];
|
|
const originalName = fileInfo.originalName;
|
|
const extension = type === 'png' ? '_result.png' : '_clipping_path.tiff';
|
|
downloadFile(processedFilename, originalName.replace(/\.[^/.]+$/, "") + extension);
|
|
});
|
|
}
|
|
</script>
|
|
|
|
|
|
<!-- auth 4 of 4 NOTE: ensure values for clientID, authority (URL with tenant ID) and redirectUri are correct below -->
|
|
<script>
|
|
const msalConfig = {
|
|
auth: {
|
|
clientId: "9079054c-9620-4757-a256-23413042f1ef",
|
|
authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385",
|
|
redirectUri: "https://ai-sandbox.oliver.solutions/format"
|
|
},
|
|
cache: {
|
|
cacheLocation: "sessionStorage",
|
|
storeAuthStateInCookie: true,
|
|
}
|
|
};
|
|
|
|
const loginRequest = {
|
|
scopes: ["user.read"]
|
|
};
|
|
|
|
const myMSALObj = new msal.PublicClientApplication(msalConfig);
|
|
|
|
signIn();
|
|
|
|
function signIn() {
|
|
myMSALObj.loginPopup(loginRequest)
|
|
.then(loginResponse => {
|
|
console.log("User logged in:", loginResponse.account.username);
|
|
thisUser = loginResponse.account.username;
|
|
sessionStorage.setItem('accessToken', loginResponse.accessToken);
|
|
showProtectedContent(); // Show protected content after successful login
|
|
onAuthenticated(); // Special for this app
|
|
}).catch(error => {
|
|
console.error("Error during login:", error);
|
|
});
|
|
}
|
|
|
|
function signOut() {
|
|
// Clear the session storage and (does not) sign out from Microsoft Identity
|
|
sessionStorage.removeItem('accessToken');
|
|
//myMSALObj.logoutPopup();
|
|
console.log("User logged out.");
|
|
document.getElementById('protected-content').style.display = 'none'; // Hide protected content
|
|
document.getElementById('logout-button').style.display = 'none'; // Hide logout button
|
|
document.getElementById('login-button').style.display = 'flex'; // Show login button
|
|
}
|
|
|
|
|
|
function showProtectedContent() {
|
|
// Verify that the access token exists before showing protected content
|
|
const accessToken = sessionStorage.getItem('accessToken');
|
|
if (accessToken) {
|
|
document.getElementById('protected-content').style.display = 'flex';
|
|
document.getElementById('logout-button').style.display = 'flex'; // Show logout button
|
|
document.getElementById('login-button').style.display = 'none'; // Hide login button
|
|
}
|
|
}
|
|
|
|
// Check if the user is already logged in when the page loads
|
|
window.addEventListener('load', showProtectedContent);
|
|
</script>
|
|
<!-- end auth block -->
|
|
|
|
|
|
</body>
|
|
</html>
|