Commit 9c7a15d8 by kigrig

functions upload

parent b6da529e
Showing with 655 additions and 0 deletions
#include <sys/socket.h> // For socket functions
#include <netinet/in.h> // For sockaddr_in
#include <cstdlib> // For exit() and EXIT_FAILURE
#include <iostream> // For cout
#include <fstream>
#include <unistd.h> // For read
#include <string>
#include <cmath>
#include <pgsql/libpq-fe.h>
using namespace std;
struct requestDetails{
string method; // HTTP paringu meetod
string path; // aadress
string HTTP_version; // HTTP versioon
string acceptation; // Mida brauser võtab vastuseks
};
struct requestDetails splitRequest( char* );
void sendResponse(int, struct requestDetails);
string createSoloCharts(string, string, string);
string createChartsOfOneYear(string, int&, string);
string createChartsThroughYears(string, int&, string);
PGresult* kohadJoel(string);
int countWordsInLine(string, char);
void loadJSScripts();
string addChart(string, string, int&);
void replaceInString(string&, string, string);
void replaceInString(string&, string, int);
void replaceSymbols(string&);
struct requestDetails splitRequest( char *startBuf ){
/*
* Jagab suur HTTP paringu vaikeste osadeks
* ehk taidab requestDetails struktuuri
* ja tuhistab bufferi.
*/
struct requestDetails details;
string buf(startBuf); // String class char[] asemel
details.method = buf.substr(0, buf.find(' ')); // Salvestame meie meetodi
buf.erase(0, buf.find(' ')+1); // Emaldame meetodi kogu paringust ehk salvestame 'GET'
details.path = buf.substr(0, buf.find(' ')); // Salvestame aadressi, mida kasutaja tahab saada
buf.erase(0, buf.find(' ')+1); // Eemaldame aadressi kogu paringust
details.HTTP_version = buf.substr(0, buf.find('\r')); // Salvestame HTTP versioon (1.1)
buf.erase(0, buf.find('\n')+1); // Emaldame HTTP versioon
buf.erase(0, buf.find("Accept:")); // Emaldame koik 'Accept:' sonani
buf.erase(0, buf.find(' ')+1); // Emaldame 'Accept:' sona
details.acceptation = buf.substr(0, buf.find('\r')); // Salvestame, mida kasutaja votab vastu
buf.erase(0, buf.find('\n')+1); // Emaldame, mida kasutaja votab vastu
buf.clear(); // Tuhistame bufferi
return details;
}
void sendResponse(int connection, struct requestDetails details){
/*
* Votab vastu paringu detailid ja vastavalt sellele koostab paringu.
*/
string response;
string aastad;
if(details.path != "/favicon.ico"){
response = "HTTP/1.1 200 OK\r\n\
Server: KIRILL\r\n\
Keep-Alive: timeout=5, max=999\r\n\
"; // Koostab vastus, et fail on leitud ning ta saadab teda
if(details.acceptation.find("text/html") != string::npos)response += "Content-type: text/html\r\n\r\n";
else if(details.acceptation.find("text/css") != string::npos)response += "Content-type: text/css\r\n\r\n";
else{
if(details.path.find(".js") != string::npos) response += "Content-type: text/javascript\r\n\r\n";
}
} else {
response = "HTTP/1.1 404 NOT FOUND\r\n\
Content-type: text/html\r\n\r\n"; // Saadakse, kui fail pole leitud
}
if(details.path == "/" || details.path == "index.html"){ // Kui kasutaja tuleb main lehele
ifstream file;
file.open("html/index.html", fstream::in); // Avame faili index.html
if(!file.is_open()) cout << "No index.html", exit(1); // Akki index.html puudub
string abi;
while(!file.eof()){ // <- Loeme iga submol index.html lopuni
char c = file.get(); // <-
if(!file.eof()) response += c; // Lisame vastuseks
}
}
else if(details.path.find("/response?", 0) != string::npos){ // Kui kasutaja taidas vormi
// Vaatame punkti, mida ta valis. Edasi salvestame.
string answer = details.path.substr(details.path.find("answer=", 0)+7);
aastad = details.path.substr(details.path.find("aastad=", 0)+7, details.path.find("&answer=", 0)-details.path.find("aastad=", 0)-7);
// Valmistame koik jogede nimed
string rivers = details.path.substr(details.path.find("nimed=", 0)+6); // Salvestame nimed stringi rivers
rivers.erase(rivers.find('&'), string::npos); // Eemaldame &aastad...
while(rivers.find('+') != string::npos) // '+' asemel paneme tuhiku
rivers.replace(rivers.find('+'), 1, " ");
replaceSymbols(rivers); // Eesti sumbolite jaoks
response += createSoloCharts(rivers, aastad, answer); // Valmistame graafikud ja lisame vastuseks
} else {
// Siin me uurime leida faili, mida tahab kasutaja.
// Loeme faili ning lisame vastuseks
// Kui fail puudub, siis kirjutame sellest
string fileName = details.path.substr(1, string::npos);
ifstream r(fileName);
if(r.is_open())
while(!r.eof()){
if(r.peek() == -1) break;
response += r.get();
}
else response += "No such file";
};
response += "\r\n\r\n"; // Naitame, et HTTP vastus lopeb ning enam midagi ei tule.
send(connection, response.c_str(), response.size(), 0); // Saadame kasutajale
}
string createSoloCharts(string rivers, string aastadeVahemik, string valik){
cout << rivers << " - " << aastadeVahemik << endl;
// Funktsioon koostab graafikud antud valiku alusel
// Votab vastu jogede nimed formaadis "Tallinn, Võhandu, Pärnu" ehk ', ' nimede vahel
// Votab vastu uks aasta ehk Nt: "1956" voi aastade vahemikku Nt: "1956-2000" (1. arv vaiksem 2.)
int wordsCount = countWordsInLine(rivers, ' '); // Loeme mitu jogede nimed meil on
loadJSScripts(); // Lisame "response.js" faili vajalikud pluginid
string response;
string words[wordsCount];
// eemaldame ','
for(int i = 0; i < wordsCount; i++){
words[i].assign(rivers, 0, rivers.find(','));
rivers.erase(0, rivers.find(',')+2);
}
/////////////////////
int chartCounter = 0; // Loeme kui palju graafikud on uldse
// Lisame uldise html osa alguse
response = "<!DOCTYPE html>\n\
<html>\n\
<head>\n\
<title>Projekt</title>\n\
<meta charset=\"utf-8\">\n\
<link rel=\"stylesheet\" type=\"text/css\" href=\"css/chartist.css\">\n\
<link rel=\"stylesheet\" type=\"text/css\" href=\"css/CSS.css\">\n\
</head>\n\
<body>\n";
// Kasutaja valiku alusel teeme graafikud ja lisame vastuseks
for(int i = 0; i < wordsCount; i++){
if(valik == "a1") response.append(createChartsThroughYears(words[i], chartCounter, aastadeVahemik));
if(valik == "a2") response.append(createChartsOfOneYear(words[i], chartCounter, aastadeVahemik));
}
// Lisame uldise html osa loppu
response += "<script type=\"text/javascript\" src=\"../scripts/chartist.js\"></script>\n\
<script type=\"text/javascript\" src=\"../scripts/chartist-plugin-pointlabels.js\"></script>\n\
<script type=\"text/javascript\" src=\"../scripts/response.js\"></script>\n\
</body>\n\
</html>";
return response;
}
string createChartsOfOneYear(string name, int &pos, string aastadeVahemik){
// Loob chartist.js graafiku, mis vastab mingi joe nimele ja valitud aastale
// ja mida ta kirjutab/lisab "response.js",
// ning tagastab html koodi selle graafiku jaoks (selleks oligi vaja chartCounter)
PGconn *conn;
PGresult *res;
ofstream file("scripts/response.js", ofstream::app); // Avame faili, kus salvestame graafikud
// Uhendame DB'ga
conn = PQconnectdb("dbname=ewis host=ekleer.pld.ttu.ee user=read_ewis password=RO-A11ik45-2022");
if (PQstatus(conn) == CONNECTION_BAD) { // Kui ei onnestunud uhendada
printf("We were unable to connect to the database\n");
exit(1);
}
res = kohadJoel(name); // Leiame koik kohad, kus oli tehtud mooteid
int riversPlaceCount = PQntuples(res); // Loeme mitu neid oli kokku
string riversPlaceNames[riversPlaceCount]; // Loome massiivi kohtade (valglade) nimedele
string valgla[riversPlaceCount]; // Loome massiivi valglade pindalate jaoks
for(int i = 0; i < riversPlaceCount; i++){ // Taidame molema massiivi
riversPlaceNames[i] = PQgetvalue(res, i, 0);
valgla[i] = PQgetvalue(res, i, 1);
}
string HTMLcode;
// Tsukel on selle jaoks, et teha iga valgla kohta graafiku
for(int totalCounter = 0; totalCounter < riversPlaceCount; totalCounter++){
HTMLcode.append(addChart(riversPlaceNames[totalCounter], valgla[totalCounter], pos));
//SQL paring
string command = "SELECT DISTINCT seire_jogi_hydrol.hkuu, round(AVG(seire_jogi_hydrol.vaartus))\
FROM seire_jogi_hydrol\
INNER JOIN seire_jogi_hydrol_jaamad USING (id_jaam)\
INNER JOIN joe_andmed\
ON joe_andmed.id_jogi/10 = seire_jogi_hydrol_jaamad.id_jogi\
AND joe_andmed.id_peajogi = joe_andmed.id_jogi\
AND seire_jogi_hydrol_jaamad.lavendi_nimi = '{lavendi_nimi}'\
AND seire_jogi_hydrol.haasta = {aasta}\
GROUP BY seire_jogi_hydrol.hkuu\
ORDER BY seire_jogi_hydrol.hkuu;";
replaceInString(command, "{lavendi_nimi}", riversPlaceNames[totalCounter]);
replaceInString(command, "{aasta}", aastadeVahemik.substr(0, aastadeVahemik.find('-')));
res = PQexec(conn, command.c_str());
// Maaksimum vooluhulk on sellejaoks, et naidat graafikul
int maxValue = 0;
for(int i = 0; i < PQntuples(res); i++){
if(maxValue < stoi(PQgetvalue(res, i, 1), nullptr)){
maxValue = stoi(PQgetvalue(res, i, 1), nullptr);
}
}
if (PQresultStatus(res) != PGRES_TUPLES_OK) { // Kui paring onnestus
cout << "We did not get any data!\n";
PQfinish(conn);
return 0;
} else {
// Edasi koostakse JS script graafiku jaoks
int rec_count = PQntuples(res);
string toWrite;
toWrite = "\nChartist.Line('#chart{pos}', {\n\tlabels: [";
replaceInString(toWrite, "{pos}", pos);
pos++;
if(rec_count > 0){
toWrite.append(PQgetvalue(res, 0, 0));
for(int i = 1; i < rec_count; i++){
toWrite += "+1, ";
toWrite.append(PQgetvalue(res, i, 0));
}
toWrite += "+1],\n\t\n\tseries: [\n\t\t{ name: 'Vooluhulk (m^3/s)', data: [";
toWrite.append(PQgetvalue(res, 0, 1));
for(int i = 1; i < rec_count; i++){
toWrite += ", ";
toWrite.append(PQgetvalue(res, i, 1));
}
} else {
toWrite += "null],\n\n\tseries: [\n\t\t{ name: 'Vooluhulk (m^3/s)', data: [";
toWrite += "null";
}
double sum = 0;
for(int i = 0; i < rec_count; i++)
sum += strtod(PQgetvalue(res, i, 1), NULL);
sum = sum /(rec_count*sqrt(2));
if(rec_count > 0){
toWrite += "]},\n\t\n\t\t{ name: 'Maksimum-5%', data: [";
toWrite += " {max-5%}";
replaceInString(toWrite, "{max-5%}", to_string(0.001+(long)maxValue*0.95));
for(int i = 1; i < rec_count; i++){
toWrite += ", {max-5%}";
replaceInString(toWrite, "{max-5%}", to_string((long)maxValue*0.95));
}
toWrite += "]},\n\t\n\t\t{ name: 'Maksimum+5%', data: [";
toWrite += " {max+5%}";
replaceInString(toWrite, "{max+5%}", to_string(0.001+(long)maxValue*1.05));
for(int i = 1; i < rec_count; i++){
toWrite += ", {max+5%}";
replaceInString(toWrite, "{max+5%}", to_string((long)maxValue*1.05));
}
} else {
toWrite += "]},\n\t\n\t\t{ name: 'Maksimum-5%', data: [";
toWrite += "null";
toWrite += "]},\n\t\n\t\t{ name: 'Maksimum+5%', data: [";
toWrite += "null";
}
toWrite += "]}\n\t]\n}, {\n\tseries:{'Maksimum-5%': { showPoint: true, showLine: true, showArea: false},\n\t\
'Maksimum+5%': { showPoint: true, showLine: true, showArea: false}},\n\t\
axisX: {showLabel: false},\n\twidth: 800,\n\theight: 600,\n";
toWrite += "\tplugins: [\n\t\tChartist.plugins.ctAccessibility({\n\t\t\tcaption:' Jõe voolu hulk ";
if(rec_count > 0) toWrite += to_string(stoi(PQgetvalue(res, 0, 0), nullptr)+1);
toWrite += "-";
if(rec_count > 0) toWrite += to_string(stoi(PQgetvalue(res, rec_count-1, 0), nullptr)+1);
toWrite += "',\n\t\t\tseriesHeader: 'Kuud',\n";
toWrite += "\t\t\tsummary: 'Voolu hulgad, mis olid DB\\'s, teie valitud aasta eest.',\n";
toWrite += "\t\tvisuallyHiddenStyles: 'position: relative; text-align: center; top: 100%; width: 80%; left: 10%; font-size: 24px; overflow-x: auto;'\n\t\t}),\n";
toWrite += "\nChartist.plugins.ctThreshold({ threshold:";
if(rec_count > 0) toWrite += to_string(sum);
else toWrite += to_string(0);
toWrite += "}),";
toWrite += "\t\tChartist.plugins.ctAxisTitle({\n\
\n\t\t\taxisX: { axisTitle: 'Kuud', axisClass: 'ct-axis-title',\
\n\t\t\t\toffset: { x: 0, y: 20 }, textAnchor: 'middle'},\n\
\n\t\t\taxisY: { axisTitle: 'Vooluhulk (m^3/s)', axisClass: 'ct-axis-title',\
\n\t\t\t\toffset: { x: 0, y: 12 }, textAnchor: 'middle', flipTitle: true }\n\t\t})\n\t]\n});\n";
file.write(toWrite.c_str(), toWrite.length());
}
}
file.close();
PQclear(res);
PQfinish(conn);
return HTMLcode;
}
string createChartsThroughYears(string name, int &pos, string aastadeVahemik){
// Loob chartist.js graafikuid, mis vastab mingi joe nimele ja valitud aastade vahemikule
// ja mida ta kirjutab/lisab "response.js",
// ning tagastab html koodi selle graafiku jaoks (selleks oligi vaja chartCounter)
PGconn *conn;
PGresult *res;
ofstream file("scripts/response.js", ofstream::app);
conn = PQconnectdb("dbname=ewis host=ekleer.pld.ttu.ee user=read_ewis password=RO-A11ik45-2022");
if (PQstatus(conn) == CONNECTION_BAD) {
printf("We were unable to connect to the database\n");
exit(1);
}
res = kohadJoel(name);
int riversPlaceCount = PQntuples(res);
string riversPlaceNames[riversPlaceCount];
string valgla[riversPlaceCount];
for(int i = 0; i < riversPlaceCount; i++){
riversPlaceNames[i] = PQgetvalue(res, i, 0);
valgla[i] = PQgetvalue(res, i, 1);
}
string HTMLcode;
for(int totalCounter = 0; totalCounter < riversPlaceCount; totalCounter++){
HTMLcode.append(addChart(riversPlaceNames[totalCounter], valgla[totalCounter], pos));
string command = "SELECT DISTINCT seire_jogi_hydrol.haasta, AVG(seire_jogi_hydrol.vaartus)\
FROM seire_jogi_hydrol\
INNER JOIN seire_jogi_hydrol_jaamad USING (id_jaam)\
INNER JOIN joe_andmed\
ON joe_andmed.id_jogi/10 = seire_jogi_hydrol_jaamad.id_jogi\
AND joe_andmed.id_peajogi = joe_andmed.id_jogi\
AND seire_jogi_hydrol_jaamad.lavendi_nimi = '{lavendi_nimi}'\
AND seire_jogi_hydrol.haasta >= {min_aasta}\
AND seire_jogi_hydrol.haasta <= {max_aasta}\
GROUP BY seire_jogi_hydrol.haasta\
ORDER BY seire_jogi_hydrol.haasta;";
replaceInString(command, "{lavendi_nimi}", riversPlaceNames[totalCounter]);
replaceInString(command, "{min_aasta}", aastadeVahemik.substr(0, aastadeVahemik.find('-')));
replaceInString(command, "{max_aasta}", aastadeVahemik.substr(aastadeVahemik.find('-')+1, string::npos));
res = PQexec(conn, command.c_str());
if (PQresultStatus(res) != PGRES_TUPLES_OK) {
cout << "We did not get any data!\n";
PQfinish(conn);
return 0;
} else {
int rec_count = PQntuples(res);
string toWrite;
toWrite = "\nChartist.Line('#chart{pos}', {\n\tlabels: [";
replaceInString(toWrite, "{pos}", pos);
pos++;
if(rec_count > 0){
toWrite.append(PQgetvalue(res, 0, 0));
for(int i = 1; i < rec_count; i++){
toWrite += ", ";
toWrite.append(PQgetvalue(res, i, 0));
}
toWrite += "],\n\t\n\tseries: [\n\t\t{ name: 'Vooluhulk (m^3/s)', data: [";
toWrite.append(PQgetvalue(res, 0, 1));
for(int i = 1; i < rec_count; i++){
toWrite += ", ";
toWrite.append(PQgetvalue(res, i, 1));
}
} else {
toWrite += "null],\n\n\tseries: [\n\t\t{ name: 'Vooluhulk (m^3/s)', data: [";
toWrite += "null";
}
double sum = 0;
for(int i = 0; i < rec_count; i++)
sum += strtod(PQgetvalue(res, i, 1), NULL);
sum = sum /(rec_count*sqrt(2));
if(rec_count > 0){
toWrite += "]},\n\t\n\t\t{ name: 'Ohtlik punkt all', data: [";
toWrite.append(to_string(sum));
for(int i = 1; i < rec_count; i++){
toWrite += ", ";
toWrite.append(to_string(sum));
}
} else {
toWrite += "]},\n\t\n\t\t{ name: 'Ohtlik punkt all', data: [";
toWrite += "null";
}
double ule = sum*2;
if(rec_count > 0){
toWrite += "]},\n\t\n\t\t{ name: 'Ohtlik punkt üleval', data: [";
toWrite.append(to_string(ule+0.001));
for(int i = 1; i < rec_count; i++){
toWrite += ", ";
toWrite.append(to_string(ule));
}
} else {
toWrite += "]},\n\t\n\t\t{ name: 'Ohtlik punkt üleval', data: [";
toWrite += "null";
}
toWrite += "]}\n\t]\n}, {\n\tseries:{'Ohtlik punkt all': { showPoint: false, showLine: true, showArea: true},\
'Ohtlik punkt üleval': { showPoint: false, showLine: true, showArea: false},\
'Vooluhulk (m^3/s)': { showPoint: false, showLine: true, showArea: false}},\n\t\
axisX: {showLabel: false},\n\twidth: 800,\n\theight: 600,\n";
toWrite += "\tplugins: [\n\t\tChartist.plugins.ctAccessibility({\n\t\t\tcaption:' Jõe voolu hulk ";
if(rec_count > 0) toWrite += PQgetvalue(res, 0, 0);
toWrite += "-";
if(rec_count > 0) toWrite += PQgetvalue(res, PQntuples(res)-1, 0);
toWrite += "',\n\t\t\tseriesHeader: 'Aastad',\n";
toWrite += "\t\t\tsummary: 'Voolu hulgad, mis olid DB\\'s, ning nende vastavad aasta',\n";
toWrite += "\t\tvisuallyHiddenStyles: 'position: relative; text-align: center; top: 100%; width: 80%; left: 10%; font-size: 24px; overflow-x: auto;'\n\t\t}),\n";
toWrite += "\nChartist.plugins.ctThreshold({ threshold:";
if(rec_count > 0) toWrite += to_string(sum);
else toWrite += to_string(0);
toWrite += "}),";
toWrite += "\t\tChartist.plugins.ctAxisTitle({\n\
\n\t\t\taxisX: { axisTitle: 'Aastad', axisClass: 'ct-axis-title',\
\n\t\t\t\toffset: { x: 0, y: 20 }, textAnchor: 'middle'},\n\
\n\t\t\taxisY: { axisTitle: 'Vooluhulk (m^3/s)', axisClass: 'ct-axis-title',\
\n\t\t\t\toffset: { x: 0, y: 12 }, textAnchor: 'middle', flipTitle: true }\n\t\t})\n\t]\n});\n";
file.write(toWrite.c_str(), toWrite.length());
}
}
file.close();
PQclear(res);
PQfinish(conn);
return HTMLcode;
}
PGresult* kohadJoel(string name){
// Leiab valglad, kust oli tehtud moodetus
PGconn *conn;
PGresult *res;
conn = PQconnectdb("dbname=ewis host=ekleer.pld.ttu.ee user=read_ewis password=RO-A11ik45-2022");
if (PQstatus(conn) == CONNECTION_BAD) {
printf("We were unable to connect to the database\n");
exit(1);
}
string command = "SELECT DISTINCT seire_jogi_hydrol_jaamad.lavendi_nimi, round(seire_jogi_hydrol_jaamad.valgla_km2, 2)\
FROM seire_jogi_hydrol\
INNER JOIN seire_jogi_hydrol_jaamad USING (id_jaam)\
INNER JOIN joe_andmed\
ON joe_andmed.id_jogi/10 = seire_jogi_hydrol_jaamad.id_jogi\
AND joe_andmed.id_peajogi = joe_andmed.id_jogi\
AND joe_andmed.joenimi = '{name}'\
AND seire_jogi_hydrol_jaamad.lavendi_nimi IS NOT NULL\
;";
command.replace(command.find("{name}"), 6, name);
res = PQexec(conn, command.c_str());
return res;
}
int countWordsInLine(string rivers, char symbol){
// Loeb mitu sona string classis on, mille vahel on symbol
int wordsCount = 1;
size_t pos = 0;
while(pos < string::npos){
if(rivers.find(symbol, pos) == string::npos) break;
wordsCount++;
pos = rivers.find(symbol, pos) + 1;
}
return wordsCount;
}
void loadJSScripts(){
// Lisab "response.js" faili vajalikud teised JS scriptid ja pluginid
ofstream w("scripts/response.js");
ifstream r("scripts/begining.js");
string abi;
while(!r.eof()){
if(r.peek() == -1) break;
abi += r.get();
}
w << abi;
w.close();
}
string addChart(string name, string pindala, int &pos){
// Teeb HTML <div> konteineri graafiku jaoks
string response;
response += "<h1>{name}({pindala} km^2)</h1>\n";
replaceInString(response, "{name}", name);
replaceInString(response, "{pindala}", pindala);
response += "<div class=\"ct-chart\" id=\"chart{pos}\"></div>\n";
replaceInString(response, "{pos}", pos);
return response;
}
void replaceInString(string &whereFind, string whatFind, string replaceWith){
// Ootsib mingis stringis mingi teine string ja vahetab teise stringiga
whereFind.replace(whereFind.find(whatFind), whatFind.length(), replaceWith);
}
void replaceInString(string &whereFind, string whatFind, int replaceWith){
// Ootsib mingis stringis mingi teine string ja vahetab teise numbriga
whereFind.replace(whereFind.find(whatFind), whatFind.length(), to_string(replaceWith));
}
void replaceSymbols( string &rivers ){
// Tehtud eestide sumbolite jaoks
// Vahetab kahebaitilise ASCII koodi tavalise sumboliga
while(rivers.find('%') != string::npos){
size_t pos = rivers.find('%');
if(rivers[pos + 3] == '%'){
string sub = rivers.substr(pos, 6);
rivers.erase(pos, 6);
if(sub == "%C3%A4")
rivers.insert(pos, "ä");
else if(sub == "%C3%B5")
rivers.insert(pos, "õ");
else if(sub == "%C3%BC")
rivers.insert(pos, "ü");
else if(sub == "%C3%B6")
rivers.insert(pos, "ö");
else if(sub == "%C5%A1")
rivers.insert(pos, "š");
else if(sub == "%C5%BE")
rivers.insert(pos, "ž");
} else {
int c = stoi(rivers.substr(pos+1, 2), nullptr, 16);
rivers[pos] = (char) c;
rivers.erase(pos+1, 2);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment