ErrorDocument 503 /pages/error.html?code=503
ErrorDocument 504 /pages/error.html?code=504
-# Security Headers for slayer.unlishema.org
+# Prevent Directory Listing
+Options -Indexes
+
+# Deny access to .htaccess and sensitive files
+<FilesMatch "^\.|\.bak|\.config|\.sql|\.fla|\.psd|\.ini|\.log|\.sh|\.inc|\.swp|\.dist|\.old|\.orig$">
+ Require all denied
+</FilesMatch>
+
+# Security and Performance Headers
<IfModule mod_headers.c>
+ # Unset conflicting headers first
+ Header unset ETag
+ Header unset Cache-Control
+
+ # Security Headers for slayer.unlishema.org
Header set X-Content-Type-Options "nosniff"
Header set X-XSS-Protection "1; mode=block"
Header set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header set Referrer-Policy "strict-origin-when-cross-origin"
- Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; frame-ancestors http://unlishema.local https://unlishema.org https://*.unlishema.org"
+ Header set Content-Security-Policy "default-src 'self' https://oldschool.runescape.wiki; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://oldschool.runescape.wiki; font-src 'self'; frame-ancestors http://unlishema.local https://unlishema.org https://*.unlishema.org"
Header set Permissions-Policy "geolocation=(), microphone=(), camera=()"
# Set the Host header to ensure requests target slayer.unlishema.org
Header set Host "slayer.unlishema.org"
-</IfModule>
-
-# Prevent Directory Listing
-Options -Indexes
-# Deny access to .htaccess and sensitive files
-<FilesMatch "^\.|\.bak|\.config|\.sql|\.fla|\.psd|\.ini|\.log|\.sh|\.inc|\.swp|\.dist|\.old|\.orig$">
- Require all denied
-</FilesMatch>
+ # Custom Cache-Control for specific files
+ <Files "images/icon.png">
+ Header set Cache-Control "no-cache, no-store, must-revalidate"
+ </Files>
-# Disable ETag for performance
-<IfModule mod_headers.c>
- Header unset ETag
+ # Leverage Browser Caching for static resources (overridden by the above rule for icon.png)
+ Header set Cache-Control "public, max-age=31536000"
</IfModule>
+
+# Disable ETag for performance (FileETag None is outside the mod_headers block)
FileETag None
-# Leverage Browser Caching for static resources
+# Leverage Browser Caching for static resources (using mod_expires)
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 3 months"
ExpiresByType application/javascript "access plus 6 hours"
ExpiresByType image/x-icon "access plus 1 year"
ExpiresDefault "access plus 1 day"
-</IfModule>
-
-# Custom Cache-Control for favicon (icon file located at images/icon.png)
-<Files "images/icon.png">
- Header set Cache-Control "no-cache, no-store, must-revalidate"
-</Files>
-
-# Custom Cache-Control for static files
-<IfModule mod_headers.c>
- Header set Cache-Control "public, max-age=31536000"
-</IfModule>
+</IfModule>
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>OSRS Wiki CORS Test</title>
+ <style>
+ body {
+ font-family: Arial, sans-serif;
+ margin: 20px;
+ background-color: #f4f4f4;
+ color: #333;
+ }
+
+ h1,
+ h2,
+ h3 {
+ color: #2c3e50;
+ }
+
+ .container {
+ max-width: 800px;
+ margin: auto;
+ }
+
+ .input-section {
+ padding: 20px;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ background-color: #fff;
+ }
+
+ .data-section {
+ padding: 20px;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ background-color: #fff;
+ }
+
+ .raw-data-section {
+ padding: 20px;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ background-color: #fff;
+ }
+
+ #querySearch {
+ width: 70%;
+ padding: 8px;
+ margin-right: 10px;
+ }
+
+ #querySelect {
+ width: 33%;
+ padding: 8px;
+ margin-top: 10px;
+ }
+
+ #itemSelect {
+ width: 70%;
+ padding: 8px;
+ margin-top: 10px;
+ }
+
+ img {
+ max-width: 100px;
+ height: auto;
+ display: block;
+ margin-top: 10px;
+ }
+
+ pre {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ background-color: #f8f8f8;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ }
+ </style>
+</head>
+
+<body>
+ <div class="container">
+ <h1>OSRS Wiki CORS Test</h1>
+ <p>This page tests CORS with the OSRS wiki and demonstrates fetching data.</p>
+
+ <div class="input-section">
+ <h3>Search a Bucket</h3>
+
+ <label for="querySearch">Enter a bucket API query:</label><br>
+ <input type="text" id="querySearch"
+ value="bucket('infobox_item').select('item_name','item_id','image','examine').run()">
+ <button onclick="fetchAndDisplayData()">Search</button><br>
+ <label for="querySelect">Choose an default bucket:</label><br>
+ <select id="querySelect"></select><br>
+
+ <h3>Select an Item</h3>
+ <label for="itemSelect">Choose an item from query:</label><br>
+ <select id="itemSelect"></select>
+ </div>
+
+ <div class="data-section">
+ <h3>Extracted Item Data</h3>
+ <div id="extractedData">
+ <p>Select an item to display its data here.</p>
+ </div>
+ </div>
+
+ <div class="raw-data-section">
+ <h3>Raw Data</h3>
+ <div id="rawData">
+ <p>Raw JSON data will appear here.</p>
+ </div>
+ </div>
+ </div>
+
+ <script>
+ document.addEventListener("DOMContentLoaded", async () => {
+ // Populate the combobox with some default items
+ const defaultItems = [
+ { name: "Items", query: "bucket('infobox_item').select('item_name','item_id','image','examine').run()" },
+ { name: "Monsters", query: "bucket('infobox_monster').select('name', 'examine').run()" },
+ { name: "Construction", query: "bucket('infobox_construction').select('page_name', 'image').run()" },
+ { name: "Locations", query: "bucket('infobox_location').select('page_name', 'is_members_only').run()" },
+ ];
+
+ const queryBox = document.getElementById("querySelect");
+ const selectBox = document.getElementById("itemSelect");
+
+ // Add default items to the queryBox
+ defaultItems.forEach(item => {
+ const option = document.createElement("option");
+ option.value = item.query;
+ option.textContent = item.name;
+ queryBox.appendChild(option);
+ });
+
+ // Add event listener to the queryBox using an arrow function
+ queryBox.addEventListener("change", (event) => {
+ handleQueryChange(event, true);
+ });
+
+ // Add event listener to the selectBox using an arrow function
+ selectBox.addEventListener("change", (event) => {
+ handleQueryChange(event, false);
+ });
+
+ fetchAndDisplayData(); // Initial fetch with default value
+ });
+
+ // Function to handle changes in the search input
+ function handleQueryChange(event, updateItemList) {
+ const newValue = event.target.value;
+ document.getElementById("querySearch").value = newValue;
+ fetchAndDisplayData(newValue, updateItemList);
+ }
+
+ // Function to extract the bucket name from the query
+ function extractBucketName(query) {
+ // Regex to find content inside bucket(...)
+ const regex = /bucket\('(.+?)'\)/;
+ const match = query.match(regex);
+
+ if (match && match[1]) {
+ return match[1]; // match[1] contains the captured group
+ }
+
+ return ''; // Return an empty string if no match is found
+ }
+
+ // Function to extract keys from the select(...) part of the query
+ function extractSelectKeys(query) {
+ // Regex to find content inside select(...)
+ const regex = /\.select\((.*?)\)/;
+ const match = query.match(regex);
+
+ if (match && match[1]) {
+ // Split the captured string by commas, trim whitespace, and remove quotes
+ return match[1].split(',')
+ .map(key => key.trim().replace(/^'|'$/g, ''));
+ }
+
+ return []; // Return an empty array if no match is found
+ }
+
+ // Main function to fetch and display data
+ async function fetchAndDisplayData(query, updateItemList = true) {
+ if (!query)
+ query = document.getElementById("querySearch").value;
+ const bucketName = extractBucketName(query);
+ const keys = extractSelectKeys(query);
+
+ const extractedDataElement = document.getElementById("extractedData");
+ const rawDataElement = document.getElementById("rawData");
+
+ extractedDataElement.innerHTML = "<p>Loading data...</p>";
+ rawDataElement.innerHTML = "<p>Loading raw data...</p>";
+
+ const itemData = await requestBucket(query);
+
+ if (itemData) {
+ // Clear previous content
+ extractedDataElement.innerHTML = "";
+
+ for (let i = 0; i < itemData.bucket.length; i++) {
+ const item = itemData.bucket[i];
+
+ if (updateItemList && itemData.bucket.length > 1) {
+ if (i == 0) document.getElementById("itemSelect").innerHTML = "";
+
+ // Add each item to the combobox
+ const option = document.createElement("option");
+
+ // Build the select query dynamically based on keys
+ option.value = `bucket('${bucketName}').select(`;
+ for (const key of keys)
+ option.value += `'${key}',`;
+ option.value = option.value.slice(0, -1); // Remove trailing comma
+ option.value += `).where('${keys[0]}', '${item[keys[0]]}').run()`;
+
+ option.textContent = keys ? item[keys[0]] : "No KEYS";
+ document.getElementById("itemSelect").appendChild(option);
+ }
+ if (i > 0) continue; // Only display the first item by default
+
+ for (const key of keys)
+ extractedDataElement.innerHTML += `<p><strong>${key}:</strong> ${item[key] || 'N/A'}</p>`;
+ }
+
+ rawDataElement.innerHTML = `<pre>${JSON.stringify(itemData, null, 2)}</pre>`;
+ } else {
+ extractedDataElement.textContent = "Failed to fetch data from the API.";
+ rawDataElement.textContent = "No raw data available.";
+ }
+ }
+
+ // Function to make a request to the runescape wiki api
+ async function requestBucket(query) {
+ const url = new URL('https://oldschool.runescape.wiki/api.php');
+ const params = {
+ action: 'bucket',
+ format: 'json',
+ query: query,
+ origin: '*' // This parameter is key for CORS
+ };
+ url.search = new URLSearchParams(params).toString();
+
+ try {
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ // The Bucket API sometimes returns an error object on failure
+ if (data.error) {
+ alert(`Query Error: ${query}`)
+ throw new Error(`API Error: ${data.error.info}`);
+ }
+
+ return data;
+ } catch (error) {
+ console.error('Error fetching data:', error);
+ return null;
+ }
+ }
+ </script>
+</body>
+
+</html>
\ No newline at end of file
{
- "version": "0.0.31"
+ "version": "0.0.32"
}
\ No newline at end of file
ErrorDocument 503 /pages/error.html?code=503
ErrorDocument 504 /pages/error.html?code=504
-# Security Headers for slayer.unlishema.org
+# Prevent Directory Listing
+Options -Indexes
+
+# Deny access to .htaccess and sensitive files
+<FilesMatch "^\.|\.bak|\.config|\.sql|\.fla|\.psd|\.ini|\.log|\.sh|\.inc|\.swp|\.dist|\.old|\.orig$">
+ Require all denied
+</FilesMatch>
+
+# Security and Performance Headers
<IfModule mod_headers.c>
+ # Unset conflicting headers first
+ Header unset ETag
+ Header unset Cache-Control
+
+ # Security Headers for slayer.unlishema.org
Header set X-Content-Type-Options "nosniff"
Header set X-XSS-Protection "1; mode=block"
Header set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header set Referrer-Policy "strict-origin-when-cross-origin"
- Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; frame-ancestors http://unlishema.local https://unlishema.org https://*.unlishema.org"
+ Header set Content-Security-Policy "default-src 'self' https://oldschool.runescape.wiki; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://oldschool.runescape.wiki; font-src 'self'; frame-ancestors http://unlishema.local https://unlishema.org https://*.unlishema.org"
Header set Permissions-Policy "geolocation=(), microphone=(), camera=()"
# Set the Host header to ensure requests target slayer.unlishema.org
Header set Host "slayer.unlishema.org"
-</IfModule>
-
-# Prevent Directory Listing
-Options -Indexes
-# Deny access to .htaccess and sensitive files
-<FilesMatch "^\.|\.bak|\.config|\.sql|\.fla|\.psd|\.ini|\.log|\.sh|\.inc|\.swp|\.dist|\.old|\.orig$">
- Require all denied
-</FilesMatch>
+ # Custom Cache-Control for specific files
+ <Files "images/icon.png">
+ Header set Cache-Control "no-cache, no-store, must-revalidate"
+ </Files>
-# Disable ETag for performance
-<IfModule mod_headers.c>
- Header unset ETag
+ # Leverage Browser Caching for static resources (overridden by the above rule for icon.png)
+ Header set Cache-Control "public, max-age=31536000"
</IfModule>
+
+# Disable ETag for performance (FileETag None is outside the mod_headers block)
FileETag None
-# Leverage Browser Caching for static resources
+# Leverage Browser Caching for static resources (using mod_expires)
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 3 months"
ExpiresByType application/javascript "access plus 6 hours"
ExpiresByType image/x-icon "access plus 1 year"
ExpiresDefault "access plus 1 day"
-</IfModule>
-
-# Custom Cache-Control for favicon (icon file located at images/icon.png)
-<Files "images/icon.png">
- Header set Cache-Control "no-cache, no-store, must-revalidate"
-</Files>
-
-# Custom Cache-Control for static files
-<IfModule mod_headers.c>
- Header set Cache-Control "public, max-age=31536000"
-</IfModule>
+</IfModule>
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>OSRS Wiki CORS Test</title>
+ <style>
+ body {
+ font-family: Arial, sans-serif;
+ margin: 20px;
+ background-color: #f4f4f4;
+ color: #333;
+ }
+
+ h1,
+ h2,
+ h3 {
+ color: #2c3e50;
+ }
+
+ .container {
+ max-width: 800px;
+ margin: auto;
+ }
+
+ .input-section {
+ padding: 20px;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ background-color: #fff;
+ }
+
+ .data-section {
+ padding: 20px;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ background-color: #fff;
+ }
+
+ .raw-data-section {
+ padding: 20px;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ background-color: #fff;
+ }
+
+ #querySearch {
+ width: 70%;
+ padding: 8px;
+ margin-right: 10px;
+ }
+
+ #querySelect {
+ width: 33%;
+ padding: 8px;
+ margin-top: 10px;
+ }
+
+ #itemSelect {
+ width: 70%;
+ padding: 8px;
+ margin-top: 10px;
+ }
+
+ img {
+ max-width: 100px;
+ height: auto;
+ display: block;
+ margin-top: 10px;
+ }
+
+ pre {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ background-color: #f8f8f8;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ }
+ </style>
+</head>
+
+<body>
+ <div class="container">
+ <h1>OSRS Wiki CORS Test</h1>
+ <p>This page tests CORS with the OSRS wiki and demonstrates fetching data.</p>
+
+ <div class="input-section">
+ <h3>Search a Bucket</h3>
+
+ <label for="querySearch">Enter a bucket API query:</label><br>
+ <input type="text" id="querySearch"
+ value="bucket('infobox_item').select('item_name','item_id','image','examine').run()">
+ <button onclick="fetchAndDisplayData()">Search</button><br>
+ <label for="querySelect">Choose an default bucket:</label><br>
+ <select id="querySelect"></select><br>
+
+ <h3>Select an Item</h3>
+ <label for="itemSelect">Choose an item from query:</label><br>
+ <select id="itemSelect"></select>
+ </div>
+
+ <div class="data-section">
+ <h3>Extracted Item Data</h3>
+ <div id="extractedData">
+ <p>Select an item to display its data here.</p>
+ </div>
+ </div>
+
+ <div class="raw-data-section">
+ <h3>Raw Data</h3>
+ <div id="rawData">
+ <p>Raw JSON data will appear here.</p>
+ </div>
+ </div>
+ </div>
+
+ <script>
+ document.addEventListener("DOMContentLoaded", async () => {
+ // Populate the combobox with some default items
+ const defaultItems = [
+ { name: "Items", query: "bucket('infobox_item').select('item_name','item_id','image','examine').run()" },
+ { name: "Monsters", query: "bucket('infobox_monster').select('name', 'examine').run()" },
+ { name: "Construction", query: "bucket('infobox_construction').select('page_name', 'image').run()" },
+ { name: "Locations", query: "bucket('infobox_location').select('page_name', 'is_members_only').run()" },
+ ];
+
+ const queryBox = document.getElementById("querySelect");
+ const selectBox = document.getElementById("itemSelect");
+
+ // Add default items to the queryBox
+ defaultItems.forEach(item => {
+ const option = document.createElement("option");
+ option.value = item.query;
+ option.textContent = item.name;
+ queryBox.appendChild(option);
+ });
+
+ // Add event listener to the queryBox using an arrow function
+ queryBox.addEventListener("change", (event) => {
+ handleQueryChange(event, true);
+ });
+
+ // Add event listener to the selectBox using an arrow function
+ selectBox.addEventListener("change", (event) => {
+ handleQueryChange(event, false);
+ });
+
+ fetchAndDisplayData(); // Initial fetch with default value
+ });
+
+ // Function to handle changes in the search input
+ function handleQueryChange(event, updateItemList) {
+ const newValue = event.target.value;
+ document.getElementById("querySearch").value = newValue;
+ fetchAndDisplayData(newValue, updateItemList);
+ }
+
+ // Function to extract the bucket name from the query
+ function extractBucketName(query) {
+ // Regex to find content inside bucket(...)
+ const regex = /bucket\('(.+?)'\)/;
+ const match = query.match(regex);
+
+ if (match && match[1]) {
+ return match[1]; // match[1] contains the captured group
+ }
+
+ return ''; // Return an empty string if no match is found
+ }
+
+ // Function to extract keys from the select(...) part of the query
+ function extractSelectKeys(query) {
+ // Regex to find content inside select(...)
+ const regex = /\.select\((.*?)\)/;
+ const match = query.match(regex);
+
+ if (match && match[1]) {
+ // Split the captured string by commas, trim whitespace, and remove quotes
+ return match[1].split(',')
+ .map(key => key.trim().replace(/^'|'$/g, ''));
+ }
+
+ return []; // Return an empty array if no match is found
+ }
+
+ // Main function to fetch and display data
+ async function fetchAndDisplayData(query, updateItemList = true) {
+ if (!query)
+ query = document.getElementById("querySearch").value;
+ const bucketName = extractBucketName(query);
+ const keys = extractSelectKeys(query);
+
+ const extractedDataElement = document.getElementById("extractedData");
+ const rawDataElement = document.getElementById("rawData");
+
+ extractedDataElement.innerHTML = "<p>Loading data...</p>";
+ rawDataElement.innerHTML = "<p>Loading raw data...</p>";
+
+ const itemData = await requestBucket(query);
+
+ if (itemData) {
+ // Clear previous content
+ extractedDataElement.innerHTML = "";
+
+ for (let i = 0; i < itemData.bucket.length; i++) {
+ const item = itemData.bucket[i];
+
+ if (updateItemList && itemData.bucket.length > 1) {
+ if (i == 0) document.getElementById("itemSelect").innerHTML = "";
+
+ // Add each item to the combobox
+ const option = document.createElement("option");
+
+ // Build the select query dynamically based on keys
+ option.value = `bucket('${bucketName}').select(`;
+ for (const key of keys)
+ option.value += `'${key}',`;
+ option.value = option.value.slice(0, -1); // Remove trailing comma
+ option.value += `).where('${keys[0]}', '${item[keys[0]]}').run()`;
+
+ option.textContent = keys ? item[keys[0]] : "No KEYS";
+ document.getElementById("itemSelect").appendChild(option);
+ }
+ if (i > 0) continue; // Only display the first item by default
+
+ for (const key of keys)
+ extractedDataElement.innerHTML += `<p><strong>${key}:</strong> ${item[key] || 'N/A'}</p>`;
+ }
+
+ rawDataElement.innerHTML = `<pre>${JSON.stringify(itemData, null, 2)}</pre>`;
+ } else {
+ extractedDataElement.textContent = "Failed to fetch data from the API.";
+ rawDataElement.textContent = "No raw data available.";
+ }
+ }
+
+ // Function to make a request to the runescape wiki api
+ async function requestBucket(query) {
+ const url = new URL('https://oldschool.runescape.wiki/api.php');
+ const params = {
+ action: 'bucket',
+ format: 'json',
+ query: query,
+ origin: '*' // This parameter is key for CORS
+ };
+ url.search = new URLSearchParams(params).toString();
+
+ try {
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ // The Bucket API sometimes returns an error object on failure
+ if (data.error) {
+ alert(`Query Error: ${query}`)
+ throw new Error(`API Error: ${data.error.info}`);
+ }
+
+ return data;
+ } catch (error) {
+ console.error('Error fetching data:', error);
+ return null;
+ }
+ }
+ </script>
+</body>
+
+</html>
\ No newline at end of file
{
- "version": "0.0.31"
+ "version": "0.0.32"
}
\ No newline at end of file
{ from: path.resolve(__dirname, 'src/.htaccess'), to: path.resolve(__dirname, 'dist/.htaccess'), toType: 'file' },
// Main app files
{ from: 'index.html', to: 'index.html' },
+ { from: 'dev.html', to: 'dev.html' },
{ from: 'dev-appconfig.json', to: 'appconfig.json' },
// Folders we need
{ from: 'images', to: 'images', globOptions: { ignore: ['**/data/**'] } },