Sindbad~EG File Manager
<?php
// Set HTTP header to prevent caching
header("Cache-Control: no-cache, must-revalidate");
header("Access-Control-Allow-Origin: https://www.mathxpert.org");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Origin, Content-Type, Accept");
header("Access-Control-Allow-Credentials: true");
// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
http_response_code(200);
exit();
}
// Start or resume the session
session_start();
$sessionId = session_id(); // guaranteed not to contain a pipe character
ini_set('display_errors', 1);
error_reporting(E_ALL);
if ($_SERVER['SERVER_NAME'] == 'localhost') {
$nextpagegraph = "https://localhost:8443/GraphDoc.php";
$nextpagesymbol = "https://localhost:8443/SymbolicDoc.php";
$serverAddress = 'localhost';
} else {
$nextpagegraph = "https://mathxpert.org/GraphDoc.php";
$nextpagesymbol = "https://mathxpert.org/SymbolicDoc.php";
$serverAddress = 'mathxpert.org';
}
$serverPort = 12349; // Adjust the server port. Ending in 9 for the Engine; in 7 for Polygon
$timeout = 3600; // Connection timeout in seconds. If the server does not respond by then, close the socket.
$startupDelay = 5; // Delay for server startup in seconds if the server is not already running
if (!(isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1) || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) {
$redirect = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
header('HTTP/1.1 301 Moved Permanently');
header('Location: ' . $redirect);
exit();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, maximum-scale=1.0, user-scalable=0">
<title>Select a Problem</title>
<style>
/* Set a global font-family rule for all text elements. Only Times New Roman actually works well. */
text {
font-family: 'Times New Roman', 'STIX Two Math', 'Cambria Math', 'Latin Modern Math', 'TeX Gyre Termes Math';
}
svg text {
font-family: 'Times New Roman';
}
button {
background-color: lightgreen; /* Light green background */
border: 1px solid #4CAF50; /* Slightly darker green border */
color: black; /* Text color */
padding: 2px 8px; /* Spacing inside button */
font-size: 16px; /* Text size */
cursor: pointer; /* Pointer cursor on hover */
border-radius: 5px; /* Rounded corners */
}
/* Slightly darker green on hover */
button:hover {
background-color: #90EE90; /* Slightly darker light green */
}
</style>
<script src="spinner_picker.js"></script>
<style>
/* Some basic style for this demo
* To prevent overscrolling on mobile use this css:
* body { overscroll-behavior: contain; }
*/
body { overscroll-behavior: contain; margin: 0; }
h1 { font-size: 24px; margin: 0 0 5px 0; font-weight: bold; }
h2 { font-size: 16px; margin: 0 0 5px 0; font-weight: normal; }
.demo-sample { float: left; text-align: center; width: 200px; margin: 10px 0 0 10px; padding: 5px 7px 2px 5px; border: 1px solid #333333; box-shadow: 1.5px 1.5px 2.5px 3px #ccc; }
.demo-sample canvas { width: 100%; height: 200px; border: solid black 1px; }
table, td, tr { border: none; border-collapse: collapse; }
</style>
</head>
<?php
if (!(isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' ||
$_SERVER['HTTPS'] == 1) ||
isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'))
{
$redirect = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
header('HTTP/1.1 301 Moved Permanently');
header('Location: ' . $redirect);
exit();
}
?>
<html>
<?php
include 'languagenumber.php';
if (isset($_GET['language']))
{ $languageName = $_GET["language"];
$languageNumber = getLanguageNumber($languageName);
}
else
{ $languageNumber = ENGLISH;
$languageName = "english";
}
echo("<script> var language = \"$languageName\"; </script>");
ob_start();
include 'toolbar.php';
$toolbarContent = ob_get_clean();
require("SendMessage.php");
$clientSocket = createClientSocket($serverAddress, $serverPort, $timeout);
// Encode the socket details for transmission
$clientSocketDetails = base64_encode(json_encode(['address' => $serverAddress, 'port' => $serverPort]));
?>
<style>
.hidden {
visibility: hidden;
position: absolute;
}
.demo-sample {
width: 100%; /* Ensure table takes full width of its container */
max-width: 800px; /* Prevent it from getting too wide on large screens */
border-collapse: collapse; /* Remove any spacing between cells */
margin: 0; /* Remove any default margins */
table-layout: fixed; /* Ensures that column widths are respected */
}
.demo-sample th, .demo-sample td {
text-align: left;
padding: 5px;
}
/* Adjust column widths using viewport width percentages */
#subjectHeader, #subject {
width: 30vw; /* 30% of viewport width */
}
#topicHeader, #topic {
width: 51vw; /* 51% of viewport width */
}
#problemNumberHeader, #problemnumber {
width: 15vw; /* 15% of viewport width */
}
/* Ensure the table scales properly on smaller screens */
@media (max-width: 600px) {
#subjectHeader, #subject {
width: 30vw;
}
#topicHeader, #topic {
width: 50vw;
}
#problemNumberHeader, #problemnumber {
width: 20vw;
}
}
#mainContainer {
display: flex;
flex-direction: column;
align-items: center; /* Centers everything */
width: 100%;
}
#svgContainer {
position: relative; /* This makes the loading spinner position correctly */
width: 100%;
max-width: 1000px; /* Adjust as needed */
}
#mySVG {
width: 100%; /* Ensures it takes up the full width */
height: auto;
}
#loadingIndicator {
position: absolute;
top: 10px; /* Adjusted to be at the top of mySVG */
left: 50%;
transform: translateX(-50%); /* Center horizontally */
width: 50px;
height: 50px;
z-index: 1000;
display: none; /* Initially hidden */
}
.spinner {
animation: rotate 1s linear infinite;
width: 50px;
height: 50px;
}
.path {
stroke: #3498db; /* Blue color */
stroke-linecap: round;
animation: dash 1.5s ease-in-out infinite;
}
@keyframes rotate {
100% { transform: rotate(360deg); }
}
@keyframes dash {
0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
}
</style>
<body onload="init()">
<select id="typesize">
<option value="1" class="selector-item" id="changeTypeSize">Type Size</option>
<option value="2" class="selector-item" id="normalSize">Normal</option>
<option value="3" class="selector-item" id="largerSize">Larger</option>
<option value="4" class="selector-item" id="smallerSize">Smaller</option>
</select>
<button id="acceptButton" onclick="Accept()">Accept Selected Problem</button>
<button id="editButton" onclick="Edit()">Edit Selected Problem</button>
<!-- <ul style="list-style-type: none;">
<li style="font-weight: bold;">Choose a problem from the MathXpert Problem Library:</li>
<li>1. Hover and scroll with the mouse wheel or with the mousepad</li>
<li>2. Hover and use the arrow-up- or w- and arrow-down- or s-key</li>
<li>3. Click on the upper or lower half</li>
<li>4. On mobile use the scroll gesture</li>
</ul>
-->
<div id="mainContainer">
<table class="demo-sample">
<tr>
<th width="30%" id="subjectHeader">Subject</th>
<th width="60%" id="topicHeader">Topic</th>
<th width="10%" id="problemNumberHeader">Problem Number</th>
</tr>
<tr>
<td><canvas id="subject"></canvas></td>
<td><canvas id="topic"></canvas></td>
<td><canvas id="problemnumber"></canvas></td>
</tr>
</table>
<div id="svgContainer">
<!-- Loading spinner inside the container -->
<div id="loadingIndicator">
<svg class="spinner" viewBox="0 0 50 50">
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
</svg>
</div>
<svg id="mySVG" viewBox="0 0 500 2000" xmlns="http://www3.org/2000/svg"></svg>
</div>
</div>
<!-- height purposely larger than the window -->
<div id="hiddenToolbar" class="hidden">
<?php echo $toolbarContent; ?>
</div>
<style>
body, html {
overflow: hidden; /* No scroll bars ever, they make it hard to use the selectors */
margin: 0;
padding: 0;
}
#emptySVG {
display: none; /* Hide the empty SVG element */
}
#mySVG {
width: 100%;
background-color: lightblue;
}
</style>
<svg id="emptySVG" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<text x="10" y="20"></text>
</svg>
<?php
// we are going to download the "topic strings" needed to set up the problem-picker control
// using the correct natural language
$response = sendMessage($clientSocket,"askTopicStrings", $languageName);
$response2 = sendMessage($clientSocket,"askSubjectStrings", $languageName);
$topicStrings = explode("\n", " \n" . $response); // now it's an array, with " " as 0-th element
$subjectStrings = explode("\n", $response2); // an array of subject strings, but no " " tacked on the front
// Before downloading all the problems, find out how many there are for each topic.
$problemNumbersAsString = sendMessage($clientSocket,"asknProblems","dummy");
// param is not used for this message but an empty parameter is an error in processMessage; hence "dummy" is sent.
$problemNumbers = explode( "\n",$problemNumbersAsString);
?>
<script>
// convert those arrays to Javascript
var topics = <?php echo json_encode($topicStrings,JSON_UNESCAPED_UNICODE); ?>;
var subjects = <?php echo json_encode($subjectStrings,JSON_UNESCAPED_UNICODE); ?>;
var jProblemNumbers = <?php echo json_encode($problemNumbers,JSON_UNESCAPED_UNICODE); ?>;
// We will fetch from the server the entire contents of the MathXpert Problem Library in SVG form
// This takes fourteen minutes if done synchronously, so we do it asynchronously,
// but here is the array in which we will load these problems when they arrive. Each entry will
// be an array of SVG problems for that topic.
const maxtopics = 180;
const AllProblems = new Array(maxtopics).fill(null);
</script>
<script>
// getProblem is synchronous, but it accesses the AllProblems
// array, which is being asynchronously filled. If it returns null,
// try again later
function getProblem(topic, problemnumber) {
console.log(`getProblem called with topic: ${topic}, problemnumber: ${problemnumber}`);
console.log(`AllProblems[${topic}]`, AllProblems[topic]);
if (AllProblems[topic] === null) {
console.log(`Data for topic ${topic} not yet available.`);
return document.getElementById("emptySVG"); // Return a placeholder element
}
if (AllProblems[topic] && AllProblems[topic][problemnumber - 1]) {
console.log(`Returning problem for topic ${topic}, problem number ${problemnumber}`);
let theProblem = AllProblems[topic][problemnumber - 1];
// theProblem is an array of <text> elements, without surrounding <svg> tags
return theProblem;
}
else {
console.error("Invalid topic or problem number. Topic: " + topic + ", Problem Number: " + problemnumber);
return "";
}
}
// console.log("About to load ProblemPicker");
</script>
<script src="ProblemPicker.js"></script>
<script src="ToolbarWidth.js"></script>
<script src="EntryPage.js"></script>
<script src="FetchMessage.js"></script>
<script>
function fetchProblems(topicNumber, clientSocketDetails) {
console.log(`Entering fetchProblems, ${topicNumber}`);
fetch(`FetchProblems.php?topic=${topicNumber}&clientSocketDetails=${clientSocketDetails}`, {
method: 'GET',
headers: {
'Content-Type': 'text/plain',
credentials: 'include' // 🔥 This ensures cookies (session) are sent! Added 2.21.25
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(problemsAsString => {
const problems = problemsAsString.split('\n\n');
AllProblems[topicNumber] = problems;
// Get currently selected subject, topic, and problem number
let subject = subjectPicker.getIndex();
let topic = topics33[subject][topicPicker.getIndex()];
let problemNumber = problemnumberPicker.getIndex() + 1; // Convert zero-based to one-based
console.log(`Checking availability of Problem ${problemNumber} for Topic ${topic}`);
// Only wait for the selected problem instead of all problems
forceProblemUpdate(topic, problemNumber);
})
.catch(error => {
console.error('There was a problem with fetch', error);
});
}
let updateTimeout = null; // Store timeout ID
function forceProblemUpdate(topic, problemNumber) {
console.log(`🔄 Checking Problem ${problemNumber} for Topic ${topic}`);
// Show loading indicator
document.getElementById("loadingIndicator").style.display = "block";
if (AllProblems[topic] && AllProblems[topic][problemNumber - 1]) {
let problemSVG = getProblem(topic, problemNumber);
if (problemSVG && problemSVG !== "") {
document.getElementById("mySVG").innerHTML = problemSVG;
console.log(`✅ Problem ${problemNumber} displayed successfully.`);
// Hide loading indicator
document.getElementById("loadingIndicator").style.display = "none";
return; // Stop retrying
}
}
console.log(`⏳ Problem ${problemNumber} not available yet, retrying...`);
// Clear any existing timeout before setting a new one
if (updateTimeout) clearTimeout(updateTimeout);
updateTimeout = setTimeout(() => forceProblemUpdate(topic, problemNumber), 500);
}
const clientSocketDetails = "<?php echo htmlspecialchars($clientSocketDetails, ENT_QUOTES, 'UTF-8'); ?>";
for (let topicNumber = 1; topicNumber <= 179; topicNumber++)
{
fetchProblems(topicNumber,clientSocketDetails);
}
</script>
<script>
document.getElementById('typesize').addEventListener('change', function() {
var selectedValue = this.value;
// Immediately invoke the appropriate function based on the selected value
if(selectedValue === "3") {
LargerType();
} else if(selectedValue === "4") {
SmallerType();
} else if(selectedValue === "2") {
NormalType();
}
// Use a timeout to reset the select box, allowing the UI to update
// This permits the user to choose "Larger" twice in a row.
setTimeout(() => {
this.selectedIndex = 0; // Resets to a non-selected state
}, 100); // Short delay to ensure the UI has time to react
});
</script>
<?php
/* This is the discarded synchronous code
$AllProblems = array(200); // enough to index the problems by topic
$maxtopic = 179;
foreach(range(1,$maxtopic) as $topic)
{ if ( 40 <= $topic && $topic <= 43)
continue;
$problemsAsString = sendMessage($clientSocket,"askProblemsSVG", strval($topic));
// Each problem is a single string with newlines; they are separated by double newlines.
if($problemsAsString===false)
{ echo "\nGoodbye, cruel world! My server left me just standing here!\n";
die();
}
$problems = explode("\n\n", $problemsAsString);
// now $problems is an array of the problems for $topic; each entry is a string giving SVG for one problem.
$AllProblems[$topic] = $problems; // we use key-value form, since some topics are not used
}
// The next <br> serves to put the <svg> element below the spinner controls.
// Without <br> it is not visible
*/
?>
<br>
<div style="clear: both;"></div>
<div id="chosenValues"></div>
<script>
// Initialize the default scale factor
var scaleFactor = 1;
function LargerType() {
// Check if it's already at the maximum size
if (scaleFactor > 0.5) {
// Decrease the scale factor by 20%
scaleFactor -= 0.2;
// Get the SVG element
var svg = document.getElementById('mySVG');
// Dynamically change the viewBox based on the scale factor
var newWidth = 500 * scaleFactor;
var newHeight = 80 * scaleFactor;
svg.setAttribute('viewBox', '0 0 ' + newWidth + ' ' + newHeight);
}
}
function SmallerType() {
// Check if it's already at the minimum size
if (scaleFactor < 2) {
// Increase the scale factor by 20%
scaleFactor += 0.2;
// Get the SVG element
var svg = document.getElementById('mySVG');
// Dynamically change the viewBox based on the scale factor
var newWidth = 500 * scaleFactor;
var newHeight = 80 * scaleFactor;
svg.setAttribute('viewBox', '0 0 ' + newWidth + ' ' + newHeight);
}
}
function NormalType() {
scaleFactor = 1.0;
// Get the SVG element
var svg = document.getElementById('mySVG');
// Dynamically change the viewBox based on the scale factor
var newWidth = 500 * scaleFactor;
var newHeight = 80 * scaleFactor;
svg.setAttribute('viewBox', '0 0 ' + newWidth + ' ' + newHeight);
}
function Accept() {
subject = subjectPicker.getIndex();
let topicIndex = topicPicker.getIndex();
topicnumber = topics33[subject][topicIndex];
//var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
// var height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
var windowwidth = document.body.clientWidth;
// var height = document.body.clientHeight; // wrong as it includes the favorites bar and the url field
var height = window.innerHeight; // technically this is the "viewport height"
const hiddenToolbar = document.getElementById('toolbar');
// console.log(hiddenToolbar);
const toolbarwidth = getToolbarWidth();
var graphwidth = windowwidth-toolbarwidth;
var nextpage;
var language = <?php echo ("\"$languageName\"")?>;
// is it a graph topic or a symbolic topic? We can't use C macros here so I just hard-coded it:
if(topicnumber < 44)
nextpage = <?php echo ("\"$nextpagegraph\"")?>;
else
nextpage = <?php echo ("\"$nextpagesymbol\"")?>;
var myform = document.createElement('form');
myform.setAttribute('method', 'post');
myform.setAttribute('action', nextpage);
myform.style.display = 'hidden';
var topicinput = document.createElement("input");
topicinput.setAttribute("type", "hidden");
topicinput.setAttribute("name", "topicField");
topicinput.setAttribute("value", topicnumber);
myform.appendChild(topicinput);
var widthinput = document.createElement("input");
widthinput.setAttribute("type", "hidden");
widthinput.setAttribute("name", "widthField");
if(topicnumber < 44)
widthinput.setAttribute("value", graphwidth);
else
widthinput.setAttribute("value", windowwidth);
myform.appendChild(widthinput);
var toolbarwidthinput = document.createElement("input");
toolbarwidthinput.setAttribute("type", "hidden");
toolbarwidthinput.setAttribute("name", "toolbarwidthField");
toolbarwidthinput.setAttribute("value", toolbarwidth);
myform.appendChild(toolbarwidthinput);
var heightinput = document.createElement("input");
heightinput.setAttribute("type", "hidden");
heightinput.setAttribute("name", "heightField");
heightinput.setAttribute("value", height);
myform.appendChild(heightinput);
var problemnumberinput = document.createElement("input");
problemnumberinput.setAttribute("type", "hidden");
problemnumberinput.setAttribute("name", "problemnumberField");
let problemIndex = problemnumberPicker.getIndex();
problemnumberinput.setAttribute("value", problemnumberPicker.getIndex()+1);
myform.appendChild(problemnumberinput);
var languageinput = document.createElement("input");
languageinput.setAttribute("type", "hidden");
languageinput.setAttribute("name", "language");
languageinput.setAttribute("value", language);
myform.appendChild(languageinput);
document.body.appendChild(myform);
// store the subject, topic, and problem in local storage so it will come up there next time
localStorage.setItem('subjectIndex', subject);
localStorage.setItem('topicIndex', topicIndex);
localStorage.setItem('problemIndex', problemIndex);
localStorage.setItem('lastProblemSVG', document.getElementById("mySVG").innerHTML);
myform.submit();
}
async function Edit() {
subject = subjectPicker.getIndex();
let topicIndex = topicPicker.getIndex();
topicnumber = topics33[subject][topicIndex];
// var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
// var height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
var windowwidth = document.body.clientWidth;
// var height = document.body.clientHeight; // wrong as it includes the favorites bar and the url field
var height = window.innerHeight; // technically this is the "viewport height"
const hiddenToolbar = document.getElementById('toolbar');
// console.log(hiddenToolbar);
const toolbarwidth = getToolbarWidth();
var graphwidth = windowwidth - toolbarwidth;
var nextpage;
var language = "<?php echo $languageName ?>";
// which Entry page will we use to edit this problem?
var target = entryPage(topicnumber);
target = target + "?language=" + language + "&windowWidth=" + windowwidth + "&windowHeight=" + height + "&toolbarWidth=" + toolbarwidth;
var myform = document.createElement('form');
myform.setAttribute('method', 'post');
myform.setAttribute('action', target);
myform.style.display = 'none';
var topicinput = document.createElement("input");
topicinput.setAttribute("type", "hidden");
topicinput.setAttribute("name", "topicField");
topicinput.setAttribute("value", topicnumber);
myform.appendChild(topicinput);
var widthinput = document.createElement("input");
widthinput.setAttribute("type", "hidden");
widthinput.setAttribute("name", "widthField");
widthinput.setAttribute("value", windowwidth);
myform.appendChild(widthinput);
var toolbarwidthinput = document.createElement("input");
toolbarwidthinput.setAttribute("type", "hidden");
toolbarwidthinput.setAttribute("name", "toolbarwidthField");
toolbarwidthinput.setAttribute("value", toolbarwidth);
myform.appendChild(toolbarwidthinput);
var heightinput = document.createElement("input");
heightinput.setAttribute("type", "hidden");
heightinput.setAttribute("name", "heightField");
heightinput.setAttribute("value", height);
myform.appendChild(heightinput);
var problemnumberinput = document.createElement("input");
problemnumberinput.setAttribute("type", "hidden");
problemnumberinput.setAttribute("name", "problemnumberField");
problemnumberinput.setAttribute("value", problemNumber); // Already computed earlier
myform.appendChild(problemnumberinput);
var problemtextinput = document.createElement("input");
problemtextinput.setAttribute("type", "hidden");
problemtextinput.setAttribute("name", "problemTextField");
var problemNumber = problemnumberPicker.getIndex() + 1;
var problemSVG = getProblem(topicnumber, problemNumber);
var languageinput = document.createElement("input");
languageinput.setAttribute("type", "hidden");
languageinput.setAttribute("name", "language");
languageinput.setAttribute("value", language);
// store the subject, topic, and problem in local storage so it will come up there next time
localStorage.setItem('subjectIndex', subject);
localStorage.setItem('topicIndex', topicIndex);
localStorage.setItem('problemIndex', problemNumber-1);
localStorage.setItem('lastProblemSVG', document.getElementById("mySVG").innerHTML);
myform.appendChild(languageinput);
// console.log("Hello from Edit()");
try {
const response = await sendMessageToEngine("askProblemsText", topicnumber.toString());
const problemsArray = response.split('\n');
const problemText = problemsArray[problemNumber-1];
problemtextinput.setAttribute("value", problemText);
console.log("the retrieved problem is ", problemText);
myform.appendChild(problemtextinput);
document.body.appendChild(myform);
// console.log(myform);
// submit it to the target page where this problem could be entered.
myform.submit();
}
catch (error) {
console.error('Error while getting problems:', error);
return;
}
}
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
if(typeof language === "undefined")
language = "english"; // ENGLISH
console.log("language ", language);
// if it is already defined, then languageName has been set in code supplied by the Engine with sendDocument
setLanguage(language); // translate button text etc. Engine is told what the language is by setSelectedLanguage. That's done when it changes, not here.
});
document.addEventListener('DOMContentLoaded', function () {
if (typeof language === "undefined")
language = "english"; // Default to English
console.log("Language:", language);
setLanguage(language); // Translate UI elements
// Show last problem immediately
const savedProblemSVG = localStorage.getItem('lastProblemSVG');
if (savedProblemSVG) {
requestAnimationFrame(() => {
document.getElementById("mySVG").innerHTML = savedProblemSVG;
console.log("Loaded saved problem instantly.");
});
}
});
</script>
<script src="MoveSvgElement.js"></script>
<script src="TranslateEnterPages.js"></script>
</body>
</html>
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists