Initial commit. Implemented OAuth, Linodes, volumes, and images
This commit is contained in:
49
linodes/backup_details/backup_details.css
Normal file
49
linodes/backup_details/backup_details.css
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* This file is part of Linode Manager Classic.
|
||||
*
|
||||
* Linode Manager Classic is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Linode Manager Classic is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import url('/global.css');
|
||||
|
||||
#backup_details {
|
||||
padding: 0px 15px 15px;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#restore-table td:nth-of-type(5) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table:first-of-type tbody tr td:first-of-type {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
tbody tr:last-of-type {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: inline;
|
||||
list-style-position: inside;
|
||||
padding-left: 0;
|
||||
}
|
453
linodes/backup_details/backup_details.js
Normal file
453
linodes/backup_details/backup_details.js
Normal file
@ -0,0 +1,453 @@
|
||||
/*
|
||||
* This file is part of Linode Manager Classic.
|
||||
*
|
||||
* Linode Manager Classic is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Linode Manager Classic is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { settings, elements, apiGet, apiPost, parseParams, regionNames, setupHeader, timeString } from "/global.js";
|
||||
|
||||
(function()
|
||||
{
|
||||
// Element names specific to this page
|
||||
elements.backupAge = "backup-age";
|
||||
elements.backupConfigs = "backup-configs";
|
||||
elements.backupDate = "backup-date";
|
||||
elements.backupDisks = "backup-disks";
|
||||
elements.backupLabel = "backup-label";
|
||||
elements.backupLocation = "backup-location";
|
||||
elements.backupSize = "backup-size";
|
||||
elements.backupType = "backup-type";
|
||||
elements.destLabel = "dest-label";
|
||||
elements.destLinodes = "dest-linodes";
|
||||
elements.destLocation = "dest-location";
|
||||
elements.destPlan = "dest-plan";
|
||||
elements.destSpace = "dest-space";
|
||||
elements.linodeLabel = "linode-label";
|
||||
elements.linodeTag = "linode-tag";
|
||||
elements.linodeTagLink = "linode-tag-link";
|
||||
elements.loading = "loading-linodes";
|
||||
elements.lmcRow = "lmc-tr3";
|
||||
elements.newLinode = "new-linode";
|
||||
elements.restorePrefix = "restore-";
|
||||
elements.selectPrefix = "select-";
|
||||
elements.subnav = "subnav-link";
|
||||
elements.subnavActive = "subnav-link-active";
|
||||
elements.unallocatedPrefix = "unallocated-";
|
||||
|
||||
// Data recieved from API calls
|
||||
var data = {};
|
||||
data.backup = {};
|
||||
data.linode = {};
|
||||
data.linodes = [];
|
||||
data.types = [];
|
||||
|
||||
// Static references to UI elements
|
||||
var ui = {};
|
||||
ui.backupAge = {};
|
||||
ui.backupConfigs = {};
|
||||
ui.backupDate = {};
|
||||
ui.backupDisks = {};
|
||||
ui.backupLabel = {};
|
||||
ui.backupLocation = {};
|
||||
ui.backupSize = {};
|
||||
ui.backupType = {};
|
||||
ui.destLabel = {};
|
||||
ui.destLinodes = {};
|
||||
ui.destLocation = {};
|
||||
ui.destPlan = {};
|
||||
ui.destSpace = {};
|
||||
ui.linodeLabel = {};
|
||||
ui.linodeTag = {};
|
||||
ui.linodeTagLink = {};
|
||||
ui.loading = {};
|
||||
ui.newLinode = {};
|
||||
|
||||
// Temporary state
|
||||
var state = {};
|
||||
state.haveTypes = false;
|
||||
|
||||
// Generate a linode table row
|
||||
var createLinodeRow = function(linode)
|
||||
{
|
||||
var row = document.createElement("tr");
|
||||
row.className = elements.lmcRow;
|
||||
|
||||
var label = document.createElement("td");
|
||||
label.innerHTML = linode.label;
|
||||
row.appendChild(label);
|
||||
|
||||
var plan = document.createElement("td");
|
||||
if (linode.type) {
|
||||
for (var i = 0; i < data.types.length; i++) {
|
||||
if (data.types[i].id == linode.type) {
|
||||
plan.innerHTML = data.types[i].label;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
plan.innerHTML = "Unknown";
|
||||
}
|
||||
if (plan.innerHTML == "")
|
||||
translatePlan(linode.type, plan);
|
||||
row.appendChild(plan);
|
||||
|
||||
var location = document.createElement("td");
|
||||
if (regionNames[linode.region])
|
||||
location.innerHTML = regionNames[linode.region];
|
||||
else
|
||||
location.innerHTML = linode.region;
|
||||
row.appendChild(location);
|
||||
|
||||
var freeSpace = document.createElement("td");
|
||||
freeSpace.id = elements.unallocatedPrefix + linode.id;
|
||||
row.appendChild(freeSpace);
|
||||
|
||||
var select = document.createElement("td");
|
||||
select.id = elements.selectPrefix + linode.id;
|
||||
row.appendChild(select);
|
||||
|
||||
return row;
|
||||
};
|
||||
|
||||
// Callback for backup details API call
|
||||
var displayBackup = function(response)
|
||||
{
|
||||
data.backup = response;
|
||||
|
||||
// Populate info table
|
||||
if (data.backup.label)
|
||||
ui.backupLabel.innerHTML = data.backup.label;
|
||||
|
||||
var now = new Date();
|
||||
var backupStart = new Date(data.backup.created + "Z");
|
||||
ui.backupDate.innerHTML = backupStart.toLocaleString();
|
||||
ui.backupAge.innerHTML = timeString(now - backupStart, true);
|
||||
|
||||
ui.backupType.innerHTML = data.backup.type;
|
||||
|
||||
for (var i = 0; i < data.backup.configs.length; i++) {
|
||||
var li = document.createElement("li");
|
||||
li.innerHTML = data.backup.configs[i];
|
||||
ui.backupConfigs.appendChild(li);
|
||||
}
|
||||
|
||||
data.backup.totalSize = 0;
|
||||
for (var i = 0; i < data.backup.disks.length; i++) {
|
||||
data.backup.totalSize += data.backup.disks[i].size;
|
||||
var li = document.createElement("li");
|
||||
li.innerHTML = data.backup.disks[i].label + " (" + data.backup.disks[i].filesystem + ") – " + data.backup.disks[i].size + "MB";
|
||||
ui.backupDisks.appendChild(li);
|
||||
}
|
||||
|
||||
ui.backupSize.innerHTML = data.backup.totalSize + " MB";
|
||||
|
||||
if (state.haveTypes && data.linode.id)
|
||||
insertTypes();
|
||||
};
|
||||
|
||||
// Callback for linode details API call
|
||||
var displayDetails = function(response)
|
||||
{
|
||||
data.linode = response;
|
||||
|
||||
// Set page title and header stuff
|
||||
document.title += " // " + data.linode.label;
|
||||
ui.linodeLabel.innerHTML = data.linode.label;
|
||||
if (data.linode.tags.length == 1) {
|
||||
ui.linodeTagLink.href = "/linodes?tag=" + data.linode.tags[0];
|
||||
ui.linodeTagLink.innerHTML = "(" + data.linode.tags[0] + ")";
|
||||
ui.linodeTag.style.display = "inline";
|
||||
}
|
||||
|
||||
// Display location
|
||||
if (regionNames[data.linode.region]) {
|
||||
ui.backupLocation.innerHTML = regionNames[data.linode.region];
|
||||
ui.destLocation.innerHTML = regionNames[data.linode.region];
|
||||
} else {
|
||||
ui.backupLocation.innerHTML = data.linode.region;
|
||||
ui.destLocation.innerHTML = data.linode.region;
|
||||
}
|
||||
|
||||
if (state.haveTypes && data.backup.id)
|
||||
insertTypes();
|
||||
};
|
||||
|
||||
// Callback for linode disks API call
|
||||
var displayDisks = function(response)
|
||||
{
|
||||
// Find the linode this response is for
|
||||
var lid = parseInt(response['_endpoint'].split("/")[3]);
|
||||
var index = -1;
|
||||
for (var i = 0; i < data.linodes.length; i++) {
|
||||
if (data.linodes[i].id == lid) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
// Add disks to array
|
||||
data.linodes[index].disks = data.linodes[index].disks.concat(response.data);
|
||||
|
||||
// Request the next page if there are more pages
|
||||
if (response.page != response.pages) {
|
||||
apiGet("/linode/instances/" + data.linodes[index].id + "/disks?page=" + (response.page + 1), displayDisks, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate Linode's free space
|
||||
var free = data.linodes[index].specs.disk;
|
||||
for (var i = 0; i < data.linodes[index].disks.length; i++)
|
||||
free -= data.linodes[index].disks[i].size;
|
||||
|
||||
// Update table
|
||||
var freeCell = document.getElementById(elements.unallocatedPrefix + data.linodes[index].id);
|
||||
var selectCell = document.getElementById(elements.selectPrefix + data.linodes[index].id);
|
||||
freeCell.innerHTML = free + " MB";
|
||||
if (free >= data.backup.totalSize) {
|
||||
var restoreLink = document.createElement("a");
|
||||
restoreLink.id = elements.restorePrefix + data.linodes[index].id;
|
||||
restoreLink.href = "#";
|
||||
restoreLink.innerHTML = "Restore to this Linode";
|
||||
restoreLink.addEventListener("click", handleRestore);
|
||||
selectCell.appendChild(restoreLink);
|
||||
} else {
|
||||
selectCell.innerHTML = "--- not enough free space ---";
|
||||
}
|
||||
};
|
||||
|
||||
// Callback for linodes API call
|
||||
var displayLinodes = function(response)
|
||||
{
|
||||
// Add linodes to array
|
||||
data.linodes = data.linodes.concat(response.data);
|
||||
|
||||
// Request the next page if there are more pages
|
||||
if (response.page != response.pages) {
|
||||
var filter = {
|
||||
"region": data.linode.region
|
||||
};
|
||||
apiGet("/linode/instances?page=" + (response.page + 1), displayLinodes, filter);
|
||||
return;
|
||||
}
|
||||
|
||||
ui.loading.remove();
|
||||
for (var i = 0; i < data.linodes.length; i++) {
|
||||
// Create row in the table
|
||||
ui.destLinodes.appendChild(createLinodeRow(data.linodes[i]));
|
||||
// Get the linode's disks
|
||||
data.linodes[i].disks = [];
|
||||
apiGet("/linode/instances/" + data.linodes[i].id + "/disks", displayDisks, null);
|
||||
}
|
||||
};
|
||||
|
||||
// Callback for linode types API call
|
||||
var displayTypes = function(response)
|
||||
{
|
||||
// Add types to array
|
||||
data.types = data.types.concat(response.data);
|
||||
|
||||
// Request the next page if there are more pages
|
||||
if (response.page != response.pages) {
|
||||
apiGet("/linode/types?page=" + (response.page + 1), displayTypes, null);
|
||||
return;
|
||||
}
|
||||
|
||||
state.haveTypes = true;
|
||||
if (data.backup.id && data.linode.id)
|
||||
insertTypes();
|
||||
};
|
||||
|
||||
// Handler for create linode button
|
||||
var handleCreate = function(event)
|
||||
{
|
||||
if (event.currentTarget.disabled)
|
||||
return;
|
||||
|
||||
// Find the selected type
|
||||
var index = -1;
|
||||
for (var i = 0; i < data.types.length; i++) {
|
||||
if (data.types[i].id == ui.destPlan.value) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
if (!confirm("Create a new " + data.types[i].label + " from this backup? This new instance will cost $" + data.types[i].price.monthly.toFixed(2) + " per month and backups will be enabled per your global account setting."))
|
||||
return;
|
||||
|
||||
var req = {
|
||||
"label": ui.destLabel.value,
|
||||
"type": ui.destPlan.value,
|
||||
"region": data.linode.region,
|
||||
"backup_id": data.backup.id
|
||||
};
|
||||
var callback = function(response)
|
||||
{
|
||||
location.href = "/linodes/dashboard?lid=" + response.id;
|
||||
};
|
||||
apiPost("/linode/instances", req, callback);
|
||||
};
|
||||
|
||||
// Handler for backup restore links
|
||||
var handleRestore = function(event)
|
||||
{
|
||||
// Find the linode associated with this link
|
||||
var lid = parseInt(event.currentTarget.id.replace(elements.restorePrefix, ""));
|
||||
var index = -1;
|
||||
for (var i = 0; i < data.linodes.length; i++) {
|
||||
if (data.linodes[i].id == lid) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index == -1)
|
||||
return;
|
||||
if (!confirm("Restore to Linode " + data.linodes[index].label + "?"))
|
||||
return;
|
||||
|
||||
var req = {
|
||||
"linode_id": lid,
|
||||
"overwrite": false
|
||||
};
|
||||
|
||||
var callback = function(response)
|
||||
{
|
||||
location.href = "/linodes/dashboard?lid=" + lid;
|
||||
};
|
||||
apiPost("/linode/instances/" + data.params.lid + "/backups/" + data.backup.id + "/restore", req, callback);
|
||||
};
|
||||
|
||||
// Insert linode types into selector
|
||||
var insertTypes = function()
|
||||
{
|
||||
for (var i = 0; i < data.types.length; i++) {
|
||||
if (data.types[i].disk < data.backup.totalSize)
|
||||
continue;
|
||||
|
||||
var option = document.createElement("option");
|
||||
option.value = data.types[i].id;
|
||||
option.innerHTML = data.types[i].label;
|
||||
ui.destPlan.appendChild(option);
|
||||
}
|
||||
|
||||
updatePrice(null);
|
||||
ui.newLinode.disabled = false;
|
||||
|
||||
var filter = {
|
||||
"region": data.linode.region
|
||||
};
|
||||
apiGet("/linode/instances", displayLinodes, filter);
|
||||
};
|
||||
|
||||
// Initial setup
|
||||
var setup = function()
|
||||
{
|
||||
// Parse URL parameters
|
||||
data.params = parseParams();
|
||||
|
||||
// We need a Linode ID, so die if we don't have it
|
||||
if (!data.params.lid) {
|
||||
alert("No Linode ID supplied!");
|
||||
return;
|
||||
}
|
||||
|
||||
// We also need a backup ID
|
||||
if (!data.params.bid) {
|
||||
alert("No Backup ID supplied!");
|
||||
return;
|
||||
}
|
||||
|
||||
setupHeader();
|
||||
|
||||
// Update links on page to include proper Linode ID
|
||||
var anchors = document.getElementsByTagName("a");
|
||||
for (var i = 0; i < anchors.length; i++)
|
||||
anchors[i].href = anchors[i].href.replace("lid=0", "lid=" + data.params.lid);
|
||||
|
||||
// Highlight the backups subnav link
|
||||
var subnavLinks = document.getElementsByClassName(elements.subnav);
|
||||
for (var i = 0; i < subnavLinks.length; i++) {
|
||||
if (subnavLinks[i].pathname == "/linodes/backups")
|
||||
subnavLinks[i].className += " " + elements.subnavActive;
|
||||
}
|
||||
|
||||
// Get element references
|
||||
ui.backupAge = document.getElementById(elements.backupAge);
|
||||
ui.backupConfigs = document.getElementById(elements.backupConfigs);
|
||||
ui.backupDate = document.getElementById(elements.backupDate);
|
||||
ui.backupDisks = document.getElementById(elements.backupDisks);
|
||||
ui.backupLabel = document.getElementById(elements.backupLabel);
|
||||
ui.backupLocation = document.getElementById(elements.backupLocation);
|
||||
ui.backupSize = document.getElementById(elements.backupSize);
|
||||
ui.backupType = document.getElementById(elements.backupType);
|
||||
ui.destLabel = document.getElementById(elements.destLabel);
|
||||
ui.destLinodes = document.getElementById(elements.destLinodes);
|
||||
ui.destLocation = document.getElementById(elements.destLocation);
|
||||
ui.destPlan = document.getElementById(elements.destPlan);
|
||||
ui.destSpace = document.getElementById(elements.destSpace);
|
||||
ui.linodeLabel = document.getElementById(elements.linodeLabel);
|
||||
ui.linodeTag = document.getElementById(elements.linodeTag);
|
||||
ui.linodeTagLink = document.getElementById(elements.linodeTagLink);
|
||||
ui.loading = document.getElementById(elements.loading);
|
||||
ui.newLinode = document.getElementById(elements.newLinode);
|
||||
|
||||
// Register event handlers
|
||||
ui.destPlan.addEventListener("input", updatePrice);
|
||||
ui.newLinode.addEventListener("click", handleCreate);
|
||||
|
||||
// Get data from API
|
||||
apiGet("/linode/instances/" + data.params.lid, displayDetails, null);
|
||||
apiGet("/linode/instances/" + data.params.lid + "/backups/" + data.params.bid, displayBackup, null);
|
||||
apiGet("/linode/types", displayTypes, null);
|
||||
};
|
||||
|
||||
// Update the given element with the given type's label
|
||||
var translatePlan = function(type, el)
|
||||
{
|
||||
var callback = function(response)
|
||||
{
|
||||
el.innerHTML = response.label;
|
||||
};
|
||||
|
||||
apiGet("/linode/types/" + type, callback, null);
|
||||
};
|
||||
|
||||
// Update the price display
|
||||
var updatePrice = function(event)
|
||||
{
|
||||
// Find the selected type
|
||||
var type = null;
|
||||
for (var i = 0; i < data.types.length; i++) {
|
||||
if (data.types[i].id == ui.destPlan.value) {
|
||||
type = data.types[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!type)
|
||||
return;
|
||||
|
||||
ui.destSpace.innerHTML = type.disk;
|
||||
};
|
||||
|
||||
// Attach onload handler
|
||||
window.addEventListener("load", setup);
|
||||
})();
|
100
linodes/backup_details/index.shtml
Normal file
100
linodes/backup_details/index.shtml
Normal file
@ -0,0 +1,100 @@
|
||||
<!--
|
||||
This file is part of Linode Manager Classic.
|
||||
|
||||
Linode Manager Classic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Linode Manager Classic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>LMC - Backups</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" type="text/css" href="backup_details.css" />
|
||||
<script src="backup_details.js" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!--#include virtual="/include/header.html"-->
|
||||
<!--#include virtual="/include/linode_subnav.html"-->
|
||||
<div id="main-content" class="wrapper">
|
||||
<div id="top-links"><a href="/linodes">Linodes</a> » <span id="linode-tag"><a id="linode-tag-link" href=""></a> » </span><a id="linode-label" href="/linodes/dashboard?lid=0"></a> » <a href="/linodes/backups?lid=0">Backups</a> » <span class="top-links-title">Details</span></div>
|
||||
<div id="backup_details">
|
||||
<table class="lmc-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colspan="2">Backup Details</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="lmc-tr3">
|
||||
<td>Label</td>
|
||||
<td id="backup-label"></td>
|
||||
</tr>
|
||||
<tr class="lmc-tr3">
|
||||
<td>Backup Date</td>
|
||||
<td><span id="backup-date"></span> (<span id="backup-age"></span>)</td>
|
||||
</tr>
|
||||
<tr class="lmc-tr3">
|
||||
<td>Type</td>
|
||||
<td id="backup-type"></td>
|
||||
</tr>
|
||||
<tr class="lmc-tr3">
|
||||
<td>Location</td>
|
||||
<td id="backup-location"></td>
|
||||
</tr>
|
||||
<tr class="lmc-tr3">
|
||||
<td>Configuration Profiles</td>
|
||||
<td>
|
||||
<ul id="backup-configs"></ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="lmc-tr3">
|
||||
<td>Disks</td>
|
||||
<td>
|
||||
<ul id="backup-disks"></ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="lmc-tr3">
|
||||
<td>Total size required</td>
|
||||
<td id="backup-size"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Select a Linode to restore this backup to:</p>
|
||||
<table id="restore-table" class="lmc-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Linode</td>
|
||||
<td>Plan</td>
|
||||
<td>Location</td>
|
||||
<td>Unallocated/Free Space</td>
|
||||
<td>Select</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dest-linodes">
|
||||
<tr class="lmc-tr3">
|
||||
<td><input id="dest-label" type="text" /></td>
|
||||
<td><select id="dest-plan"></select></td>
|
||||
<td id="dest-location"></td>
|
||||
<td><span id="dest-space"></span> MB</td>
|
||||
<td><button disabled id="new-linode" type="button">Create Linode from Backup</button></td>
|
||||
</tr>
|
||||
<tr id="loading-linodes" class="lmc-tr3">
|
||||
<td colspan="5">Loading Linodes...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user