2020-10-30 Fred Gleason <fredg@paravelsystems.com>

* Fixed a regression in 'RDFormPost' that broke file control
	processing.
	* Added a 'put' section to the WebGet interface.

Signed-off-by: Fred Gleason <fredg@paravelsystems.com>
This commit is contained in:
Fred Gleason 2020-10-30 20:13:27 -04:00
parent 169e0e9baa
commit 0904c2cbe3
5 changed files with 294 additions and 56 deletions

View File

@ -20514,3 +20514,7 @@
* Removed the runuser(1) dependency. * Removed the runuser(1) dependency.
2020-10-29 Fred Gleason <fredg@paravelsystems.com> 2020-10-29 Fred Gleason <fredg@paravelsystems.com>
* Added an 'RDFormPost::authenticate()' method. * Added an 'RDFormPost::authenticate()' method.
2020-10-30 Fred Gleason <fredg@paravelsystems.com>
* Fixed a regression in 'RDFormPost' that broke file control
processing.
* Added a 'put' section to the WebGet interface.

View File

@ -315,7 +315,7 @@ bool RDFormPost::getValue(const QString &name,bool *state,bool *ok)
bool RDFormPost::isFile(const QString &name) bool RDFormPost::isFile(const QString &name)
{ {
return post_filenames[name]; return post_filenames.value(name);
} }

View File

@ -1,6 +1,6 @@
// webget.cpp // webget.cpp
// //
// Rivendell download utility // Rivendell upload/download utility
// //
// (C) Copyright 2018-2020 Fred Gleason <fredg@paravelsystems.com> // (C) Copyright 2018-2020 Fred Gleason <fredg@paravelsystems.com>
// //
@ -28,7 +28,8 @@
#include <stdio.h> #include <stdio.h>
#include <openssl/sha.h> #include <openssl/sha.h>
#include <qapplication.h> #include <QApplication>
#include <QProcess>
#include <rdapplication.h> #include <rdapplication.h>
#include <rdescape_string.h> #include <rdescape_string.h>
@ -77,13 +78,18 @@ MainObject::MainObject(QObject *parent)
// Determine Connection Type // Determine Connection Type
// //
if(getenv("REQUEST_METHOD")==NULL) { if(getenv("REQUEST_METHOD")==NULL) {
rda->syslog(LOG_WARNING,"missing request method");
rda->logAuthenticationFailure(webget_post->clientAddress());
XmlExit("missing REQUEST_METHOD",500,"webget.cpp",LINE_NUMBER); XmlExit("missing REQUEST_METHOD",500,"webget.cpp",LINE_NUMBER);
} }
if(QString(getenv("REQUEST_METHOD")).lower()=="get") { if(QString(getenv("REQUEST_METHOD")).lower()=="get") {
ServeForm(); ServeLogin();
Exit(0); Exit(0);
} }
if(QString(getenv("REQUEST_METHOD")).lower()!="post") { if(QString(getenv("REQUEST_METHOD")).lower()!="post") {
rda->syslog(LOG_WARNING,"unsupported web method \"%s\"",
getenv("REQUEST_METHOD"));
rda->logAuthenticationFailure(webget_post->clientAddress());
XmlExit("invalid web method",400,"webget.cpp",LINE_NUMBER); XmlExit("invalid web method",400,"webget.cpp",LINE_NUMBER);
} }
if(getenv("REMOTE_ADDR")!=NULL) { if(getenv("REMOTE_ADDR")!=NULL) {
@ -101,6 +107,10 @@ MainObject::MainObject(QObject *parent)
// //
webget_post=new RDFormPost(RDFormPost::AutoEncoded,false); webget_post=new RDFormPost(RDFormPost::AutoEncoded,false);
if(webget_post->error()!=RDFormPost::ErrorOk) { if(webget_post->error()!=RDFormPost::ErrorOk) {
rda->syslog(LOG_WARNING,"post parsing error [%s]",
webget_post->errorString(webget_post->error()).
toUtf8().constData());
rda->logAuthenticationFailure(webget_post->clientAddress());
XmlExit(webget_post->errorString(webget_post->error()),400,"webget.cpp", XmlExit(webget_post->errorString(webget_post->error()),400,"webget.cpp",
LINE_NUMBER); LINE_NUMBER);
Exit(0); Exit(0);
@ -110,10 +120,7 @@ MainObject::MainObject(QObject *parent)
// Authenticate Connection // Authenticate Connection
// //
if(!Authenticate()) { if(!Authenticate()) {
printf("Content-type: text/html\n"); ServeLogin();
printf("Status: 401\n");
printf("\n");
printf("Invalid User name or Password!\n");
Exit(0); Exit(0);
} }
@ -129,23 +136,59 @@ MainObject::MainObject(QObject *parent)
void MainObject::ripcConnectedData(bool state) void MainObject::ripcConnectedData(bool state)
{ {
bool ok=false;
if(!state) { if(!state) {
XmlExit("unable to connect to ripc service",500,"webget.cpp",LINE_NUMBER); XmlExit("unable to connect to ripc service",500,"webget.cpp",LINE_NUMBER);
Exit(0); Exit(0);
} }
QString direction;
if(!webget_post->getValue("direction",&direction)) {
ServeForm();
Exit(0);
}
rda->syslog(LOG_NOTICE,"direction: %s",direction.toUtf8().constData());
if(direction.toLower()=="get") {
rda->syslog(LOG_NOTICE,"GETTING...");
GetAudio();
Exit(0);
}
if(direction.toLower()=="put") {
rda->syslog(LOG_NOTICE,"PUTTING...");
PutAudio();
Exit(0);
}
rda->syslog(LOG_WARNING,"invalid webget direction \"%s\" from %s",
direction.toUtf8().constData(),
webget_post->clientAddress().toString().toUtf8().constData());
rda->logAuthenticationFailure(webget_post->clientAddress()); // So Fail2Ban can notice this
ServeLogin();
}
void MainObject::GetAudio()
{
bool ok=false;
QString title; QString title;
if(!webget_post->getValue("title",&title)) { if(!webget_post->getValue("title",&title)) {
rda->syslog(LOG_WARNING,"missing \"title\"");
rda->logAuthenticationFailure(webget_post->clientAddress());
XmlExit("missing \"title\"",400,"webget.cpp",LINE_NUMBER); XmlExit("missing \"title\"",400,"webget.cpp",LINE_NUMBER);
Exit(0);
} }
unsigned preset; unsigned preset;
if(!webget_post->getValue("preset",&preset,&ok)) { if(!webget_post->getValue("preset",&preset,&ok)) {
rda->syslog(LOG_WARNING,"missing \"preset\"");
rda->logAuthenticationFailure(webget_post->clientAddress());
XmlExit("missing \"preset\"",400,"webget.cpp",LINE_NUMBER); XmlExit("missing \"preset\"",400,"webget.cpp",LINE_NUMBER);
} }
if(!ok) { if(!ok) {
rda->syslog(LOG_WARNING,"invalid \"preset\" value");
rda->logAuthenticationFailure(webget_post->clientAddress());
XmlExit("invalid \"preset\" value",400,"webget.cpp",LINE_NUMBER); XmlExit("invalid \"preset\" value",400,"webget.cpp",LINE_NUMBER);
} }
@ -180,6 +223,8 @@ void MainObject::ripcConnectedData(bool state)
printf("Status: 400\n"); printf("Status: 400\n");
printf("\n"); printf("\n");
printf("no such preset!\n"); printf("no such preset!\n");
rda->syslog(LOG_WARNING,"no such preset %u",preset);
rda->logAuthenticationFailure(webget_post->clientAddress());
Exit(0); Exit(0);
} }
@ -210,6 +255,9 @@ void MainObject::ripcConnectedData(bool state)
QString err_msg; QString err_msg;
RDTempDirectory *tempdir=new RDTempDirectory("rdxport-export"); RDTempDirectory *tempdir=new RDTempDirectory("rdxport-export");
if(!tempdir->create(&err_msg)) { if(!tempdir->create(&err_msg)) {
rda->syslog(LOG_WARNING,"unable to create temporary directory [%s]",
err_msg.toUtf8().constData());
rda->logAuthenticationFailure(webget_post->clientAddress());
XmlExit("unable to create temporary directory ["+err_msg+"]",500); XmlExit("unable to create temporary directory ["+err_msg+"]",500);
} }
QString tmpfile=tempdir->path()+"/exported_audio"; QString tmpfile=tempdir->path()+"/exported_audio";
@ -290,12 +338,71 @@ void MainObject::ripcConnectedData(bool state)
Exit(200); Exit(200);
} }
else { else {
rda->syslog(LOG_WARNING,"%s",
RDAudioConvert::errorText(conv_err).toUtf8().constData());
rda->logAuthenticationFailure(webget_post->clientAddress());
XmlExit(RDAudioConvert::errorText(conv_err),resp_code,"webget.cpp", XmlExit(RDAudioConvert::errorText(conv_err),resp_code,"webget.cpp",
LINE_NUMBER,conv_err); LINE_NUMBER,conv_err);
} }
} }
void MainObject::PutAudio()
{
QString group_name;
if(!webget_post->getValue("group",&group_name)) {
rda->syslog(LOG_WARNING,"missing \"group\" in put submission");
rda->logAuthenticationFailure(webget_post->clientAddress());
XmlExit("missing \"group\"",400,"webget.cpp",LINE_NUMBER);
Exit(0);
}
QString filename;
if(!webget_post->getValue("filename",&filename)) {
rda->syslog(LOG_WARNING,"missing \"filename\" in put submission");
rda->logAuthenticationFailure(webget_post->clientAddress());
XmlExit("missing \"missing\"",400,"webget.cpp",LINE_NUMBER);
Exit(0);
}
if(!webget_post->isFile("filename")) {
rda->syslog(LOG_WARNING,"\"filename\" is not a file in put submission");
rda->logAuthenticationFailure(webget_post->clientAddress());
XmlExit("invalid \"filename\"",400,"webget.cpp",LINE_NUMBER);
Exit(0);
}
QStringList args;
args.push_back("--verbose");
args.push_back(group_name);
args.push_back(filename);
QProcess *proc=new QProcess(this);
proc->start("rdimport",args);
proc->waitForFinished();
if(proc->exitStatus()==QProcess::CrashExit) {
delete proc;
rda->syslog(LOG_WARNING,"importer process crashed [cmdline: %s]",
("rdimport "+args.join(" ")).toUtf8().constData());
XmlExit("Importer process crashed!",500,"webget",LINE_NUMBER);
}
if(proc->exitCode()!=0) {
rda->syslog(LOG_WARNING,
"importer process returned exit code %d [cmdline: %s]",
proc->exitCode(),
("rdimport "+args.join(" ")).toUtf8().constData());
delete proc;
XmlExit("Importer process error!",500,"webget",LINE_NUMBER);
}
printf("Content-type: text/html\n");
printf("Status: 200\n");
printf("\n");
printf("%s\n",proc->readAllStandardOutput().constData());
delete proc;
Exit(0);
}
void MainObject::ServeForm() void MainObject::ServeForm()
{ {
QString sql; QString sql;
@ -306,9 +413,9 @@ void MainObject::ServeForm()
printf("<html>\n"); printf("<html>\n");
printf(" <head>\n"); printf(" <head>\n");
printf(" <title>Rivendell Webget</title>\n"); printf(" <title>Rivendell Webget [User: %s]</title>\n",
rda->user()->name().toUtf8().constData());
printf(" <script src=\"webget.js?%lu\" type=\"application/javascript\"></script>\n",t); printf(" <script src=\"webget.js?%lu\" type=\"application/javascript\"></script>\n",t);
printf(" <script src=\"utils.js?%lu\" type=\"application/javascript\"></script>\n",t);
printf(" <script type=\"application/javascript\">\n"); printf(" <script type=\"application/javascript\">\n");
printf(" var preset_ids=new Array();\n"); printf(" var preset_ids=new Array();\n");
printf(" var preset_exts=new Array();\n"); printf(" var preset_exts=new Array();\n");
@ -327,7 +434,21 @@ void MainObject::ServeForm()
delete q; delete q;
printf(" </script>\n"); printf(" </script>\n");
printf(" </head>\n"); printf(" </head>\n");
printf(" <body>\n"); printf(" <body>\n");
//
// Credentials
//
printf(" <input type=\"hidden\" name=\"LOGIN_NAME\" id=\"LOGIN_NAME\" value=\"%s\">\n",
rda->user()->name().toUtf8().constData());
printf(" <input type=\"hidden\" name=\"PASSWORD\" id=\"PASSWORD\" value=\"%s\">\n",
rda->user()->password().toUtf8().constData());
//
// Get Audio
//
printf(" <table style=\"margin: auto;padding: 10px 0\" cellpadding=\"0\" cellspacing=\"5\" border=\"0\">\n"); printf(" <table style=\"margin: auto;padding: 10px 0\" cellpadding=\"0\" cellspacing=\"5\" border=\"0\">\n");
printf(" <tr>\n"); printf(" <tr>\n");
printf(" <td colspan=\"2\"><img src=\"logos/webget_logo.png\" border=\"0\"></td>\n"); printf(" <td colspan=\"2\"><img src=\"logos/webget_logo.png\" border=\"0\"></td>\n");
@ -337,12 +458,12 @@ void MainObject::ServeForm()
printf(" </tr>\n"); printf(" </tr>\n");
printf(" <tr><td colspan=\"2\"><hr></td></tr>\n"); printf(" <tr><td colspan=\"2\"><hr></td></tr>\n");
printf(" <tr>\n"); printf(" <tr>\n");
printf(" <td style=\"text-align: right\">Cart Title:</td>\n"); printf(" <td style=\"text-align: right\">From Cart Title:</td>\n");
printf(" <td><input type=\"text\" id=\"title\" size=\"40\" maxlength=\"255\"></td>\n"); printf(" <td><input type=\"text\" id=\"title\" size=\"40\" oninput=\"TitleChanged()\"></td>\n");
printf(" </tr>\n"); printf(" </tr>\n");
printf(" <tr>\n"); printf(" <tr>\n");
printf(" <td style=\"text-align: right\">Format:</td>\n"); printf(" <td style=\"text-align: right\">Using Format:</td>\n");
printf(" <td>\n"); printf(" <td>\n");
printf(" <select id=\"preset\">\n"); printf(" <select id=\"preset\">\n");
sql=QString("select ")+ sql=QString("select ")+
@ -358,19 +479,105 @@ void MainObject::ServeForm()
printf(" </select>\n"); printf(" </select>\n");
printf(" </td>\n"); printf(" </td>\n");
printf(" </tr>\n"); printf(" </tr>\n");
printf(" <tr><td cellspan=\"2\">&nbsp</td></tr>\n"); printf(" <tr>\n");
printf(" <td>&nbsp;</td>\n");
printf(" <td><input type=\"button\" value=\"OK\" id=\"get_button\" onclick=\"ProcessGet()\" disabled></td>\n");
printf(" </tr>\n");
printf(" <tr><td>&nbsp;</td></tr>\n");
//
// Put Audio
//
printf(" <tr>\n");
printf(" <td colspan=\"2\"><strong>Put audio into Rivendell</strong></td>\n");
printf(" </tr>\n");
printf(" <tr><td colspan=\"2\"><hr></td></tr>\n");
printf(" <tr>\n");
printf(" <td style=\"text-align: right\">From File:</td>\n");
printf(" <td><input type=\"file\" id=\"filename\" size=\"40\" accept=\"audio/*\" onchange=\"FilenameChanged()\"></td>\n");
printf(" </tr>\n");
printf(" <tr>\n");
printf(" <td style=\"text-align: right\">To Group:</td>\n");
printf(" <td>\n");
printf(" <select id=\"group\">\n");
sql=QString("select ")+
"GROUPS.NAME "+ // 00
"from GROUPS left join USER_PERMS "+
"on GROUPS.NAME=USER_PERMS.GROUP_NAME where "+
"USER_PERMS.USER_NAME=\""+RDEscapeString(rda->user()->name())+"\" && "+
QString().sprintf("GROUPS.DEFAULT_CART_TYPE=%u && ",RDCart::Audio)+
"GROUPS.DEFAULT_LOW_CART>0 && "+
"GROUPS.DEFAULT_HIGH_CART>0 "+
"order by GROUPS.NAME";
q=new RDSqlQuery(sql);
while(q->next()) {
printf(" <option value=\"%s\">%s</option>\n",
q->value(0).toString().toUtf8().constData(),
q->value(0).toString().toUtf8().constData());
}
printf(" </select>\n");
printf(" </td>\n");
printf(" </tr>\n");
printf(" <tr>\n");
printf(" <td>&nbsp;</td>\n");
printf(" <td><input type=\"button\" value=\"OK\" id=\"put_button\" onclick=\"ProcessPut()\" disabled></td>\n");
printf(" </tr>\n");
//
// Footer
//
printf(" </table>\n");
printf(" </body>\n");
printf("</html>\n");
}
void MainObject::ServeLogin()
{
printf("Content-type: text/html\n\n");
//
// Head
//
printf("<html>\n");
printf(" <head>\n");
printf(" <title>Rivendell Webget</title>\n");
printf(" </head>\n");
//
// Body
//
printf(" <body>\n");
printf(" <form action=\"/rd-bin/webget.cgi\" method=\"post\" enctype=\"multipart/form-data\">\n");
printf(" <input type=\"hidden\" name=\"LOGIN_NAME\" value=\"%s\">\n",
rda->user()->name().toUtf8().constData());
printf(" <input type=\"hidden\" name=\"PASSWORD\" value=\"%s\">\n",
rda->user()->password().toUtf8().constData());
printf(" <table style=\"margin: auto;padding: 10px 0\" cellpadding=\"0\" cellspacing=\"5\" border=\"0\">\n");
printf(" <tr>\n");
printf(" <td colspan=\"2\"><img src=\"logos/webget_logo.png\" border=\"0\"></td>\n");
printf(" </tr>\n");
printf(" <tr>\n");
printf(" <td colspan=\"2\"><strong>Log in to Rivendell</strong></td>\n");
printf(" </tr>\n");
printf(" <tr><td colspan=\"2\"><hr></td></tr>\n");
printf(" <td style=\"text-align: right\">User Name:</td>\n"); printf(" <td style=\"text-align: right\">User Name:</td>\n");
printf(" <td><input type=\"text\" size=\"32\" maxsize=\"255\" id=\"LOGIN_NAME\"></td>\n"); printf(" <td><input type=\"text\" size=\"32\" name=\"LOGIN_NAME\" autofocus></td>\n");
printf(" </tr>\n"); printf(" </tr>\n");
printf(" <tr>\n"); printf(" <tr>\n");
printf(" <td style=\"text-align: right\">Password:</td>\n"); printf(" <td style=\"text-align: right\">Password:</td>\n");
printf(" <td><input type=\"password\" size=\"32\" maxsize=\"32\" id=\"PASSWORD\"></td>\n"); printf(" <td><input type=\"password\" size=\"32\" maxsize=\"32\" name=\"PASSWORD\"></td>\n");
printf(" <tr><td cellspan=\"2\">&nbsp</td></tr>\n"); printf(" <tr><td cellspan=\"2\">&nbsp</td></tr>\n");
printf(" <tr>\n"); printf(" <tr>\n");
printf(" <td>&nbsp;</td>\n"); printf(" <td>&nbsp;</td>\n");
printf(" <td><input type=\"button\" value=\"OK\" onclick=\"ProcessOkButton()\"></td>\n"); printf(" <td><input type=\"submit\" value=\"OK\"></td>\n");
printf(" </tr>\n"); printf(" </tr>\n");
printf(" </table>\n"); printf(" </table>\n");
printf(" </form>\n");
printf(" </body>\n"); printf(" </body>\n");
printf("</html>\n"); printf("</html>\n");
} }
@ -382,17 +589,19 @@ bool MainObject::Authenticate()
QString passwd; QString passwd;
if(!webget_post->getValue("LOGIN_NAME",&name)) { if(!webget_post->getValue("LOGIN_NAME",&name)) {
rda->syslog(LOG_WARNING,"missing LOGIN_NAME");
rda->logAuthenticationFailure(webget_post->clientAddress()); rda->logAuthenticationFailure(webget_post->clientAddress());
return false; return false;
} }
if(!webget_post->getValue("PASSWORD",&passwd)) { if(!webget_post->getValue("PASSWORD",&passwd)) {
rda->syslog(LOG_WARNING,"missing PASSWORD");
rda->logAuthenticationFailure(webget_post->clientAddress(),name); rda->logAuthenticationFailure(webget_post->clientAddress(),name);
return false; return false;
} }
RDUser *user=new RDUser(name); rda->user()->setName(name);
if((!user->exists())|| if((!rda->user()->exists())||
(!user->checkPassword(passwd,false))|| (!rda->user()->checkPassword(passwd,false))||
(!user->webgetLogin())) { (!rda->user()->webgetLogin())) {
rda->logAuthenticationFailure(webget_post->clientAddress(),name); rda->logAuthenticationFailure(webget_post->clientAddress(),name);
return false; return false;
} }

View File

@ -1,8 +1,8 @@
// webget.h // webget.h
// //
// Rivendell audio download utility // Rivendell audio upload/download utility
// //
// (C) Copyright 2018 Fred Gleason <fredg@paravelsystems.com> // (C) Copyright 2018-2020 Fred Gleason <fredg@paravelsystems.com>
// //
// This program is free software; you can redistribute it and/or modify // This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as // it under the terms of the GNU General Public License version 2 as
@ -41,7 +41,10 @@ class MainObject : public QObject
void ripcConnectedData(bool state); void ripcConnectedData(bool state);
private: private:
void GetAudio();
void PutAudio();
void ServeForm(); void ServeForm();
void ServeLogin();
bool Authenticate(); bool Authenticate();
void Exit(int code); void Exit(int code);
void XmlExit(const QString &msg,int code, void XmlExit(const QString &msg,int code,

View File

@ -22,29 +22,37 @@ function Id(id)
} }
function MakePost() function ProcessGet()
{ {
var sep=RD_MakeMimeSeparator(); form=new FormData();
form=sep+"\r\n";
form+=RD_AddMimePart('title',Id('title').value,sep,false); form.append('LOGIN_NAME',Id('LOGIN_NAME').value);
form+=RD_AddMimePart('preset',Id('preset').value,sep,false); form.append('PASSWORD',Id('PASSWORD').value);
form+=RD_AddMimePart('LOGIN_NAME',Id('LOGIN_NAME').value,sep,false); form.append('direction','get');
form+=RD_AddMimePart('PASSWORD',Id('PASSWORD').value,sep,true); form.append('title',Id('title').value);
form.append('preset',Id('preset').value);
return form; SendForm(form,"webget.cgi");
} }
function ProcessOkButton() function ProcessPut()
{ {
SendForm(MakePost(),"webget.cgi"); form=new FormData();
form.append('LOGIN_NAME',Id('LOGIN_NAME').value);
form.append('PASSWORD',Id('PASSWORD').value);
form.append('direction','put');
form.append('group',Id('group').value);
form.append('filename',Id('filename').files[0]);
SendForm(form,"webget.cgi");
} }
function SendForm(form,url) function SendForm(form,url)
{ {
var http=RD_GetXMLHttpRequest(); var http=new XMLHttpRequest();
if(http==null) { if(http==null) {
return; return;
} }
@ -61,30 +69,33 @@ function SendForm(form,url)
// Process the response // Process the response
// //
http.onload=function(e) { http.onload=function(e) {
if(this.status==200) {
var blob=new Blob([this.response], var blob=new Blob([this.response],
{type: http.getResponseHeader('content-type')}); {type: http.getResponseHeader('content-type')});
let a=document.createElement('a'); var f0=blob.type.split(';');
a.style='display: none'; if(f0[0]=='text/html') {
document.body.appendChild(a); reader=new FileReader();
let url=window.URL.createObjectURL(blob); reader.addEventListener('loadend',()=> {
a.href=url; alert(reader.result);
a.download=Id('title').value+'.'+FileExtension(Id('preset').value); });
a.click(); reader.readAsText(blob);
window.URL.revokeObjectURL(url);
} }
else { else {
if(this.status==401) { if((f0[0]=='audio/x-mpeg')||
alert('Invalid User Name or Password!'); (f0[0]=='audio/x-wav')||
(f0[0]=='audio/ogg')||
(f0[0]=='audio/flac')) {
let a=document.createElement('a');
a.style='display: none';
document.body.appendChild(a);
let url=window.URL.createObjectURL(blob);
a.href=url;
a.download=Id('title').value+'.'+FileExtension(Id('preset').value);
a.click();
window.URL.revokeObjectURL(url);
} }
else { else {
if(this.status=404) { alert('Unknown mimetype: '+f0[0]);
alert('No cart with that name found!');
}
else {
alert('Unable to access WebGet [response code: '+
http.status+']!');
}
} }
} }
} }
@ -102,3 +113,14 @@ function FileExtension(prof_id)
return 'dat'; return 'dat';
} }
function TitleChanged()
{
Id('get_button').disabled=Id('title').value.length==0;
}
function FilenameChanged()
{
Id('put_button').disabled=Id('filename').value.length==0;
}