377 lines
11 KiB
C++
377 lines
11 KiB
C++
#include <stdlib.h> // for random(3)
|
|
|
|
#include <scriptpp/scrvar.hpp>
|
|
#include <scriptpp/scrvect.hpp>
|
|
#include <scriptpp/scrmacro.hpp>
|
|
|
|
#include "basesubs.hpp"
|
|
#include "dullcgi.hpp"
|
|
|
|
#ifndef YTID_COOKIE_TTL
|
|
#define YTID_COOKIE_TTL (2000*24*3600)
|
|
#endif
|
|
|
|
|
|
static int strchr1(const char *p, int c)
|
|
{
|
|
for(; *p; p++)
|
|
if(*p == c)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int check_fname_safe(const char *uname)
|
|
{
|
|
static const char banned[] = ",/<>=?[\\]^`";
|
|
/* !"#$%&'()* are all before '+'; {|}~ are all after 'z'
|
|
(see below the check inside for)
|
|
if you think ascii isn't a fundamental world constant,
|
|
feel free to rewrite this, but we won't accept the patch
|
|
*/
|
|
const char *p;
|
|
if(!uname || !*uname || *uname == '.' || *uname == '-')
|
|
return 0;
|
|
for(p = uname; *p; p++)
|
|
if(*p < '+' || *p > 'z' || strchr1(banned, *p))
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
enum { youtube_id_length = 11 };
|
|
|
|
static int check_yt_id(const char *id)
|
|
{
|
|
const char *p;
|
|
if(!id || !*id)
|
|
return 0;
|
|
for(p = id; *p && p - id <= youtube_id_length+1; p++)
|
|
if((*p < 'A' || *p > 'Z') &&
|
|
(*p < 'a' || *p > 'z') &&
|
|
(*p < '0' || *p > '9') &&
|
|
*p != '_' && *p != '-')
|
|
{
|
|
return 0;
|
|
}
|
|
return (p - id == youtube_id_length);
|
|
}
|
|
|
|
class CheckFirstArg : public ScriptMacroprocessorMacro {
|
|
int (*check)(const char *);
|
|
public:
|
|
CheckFirstArg(const char *name, int (*chk)(const char *))
|
|
: ScriptMacroprocessorMacro(name), check(chk) {}
|
|
ScriptVariable Expand(const ScriptVector ¶ms) const;
|
|
};
|
|
|
|
ScriptVariable CheckFirstArg::Expand(const ScriptVector ¶ms) const
|
|
{
|
|
if(params.Length() < 2)
|
|
return ScriptVariableInv();
|
|
ScriptVariable p0 = params[0];
|
|
p0.Trim();
|
|
ScriptVariable res = check(p0.c_str()) ? params[1] : params[2];
|
|
if(res.IsInvalid())
|
|
res = "";
|
|
return res;
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////
|
|
// YoutubeCgi-specific stuff
|
|
//
|
|
|
|
struct YtIdData {
|
|
ScriptVector site_ids;
|
|
ScriptVariable random_site;
|
|
};
|
|
|
|
static bool is_site_enabled(const DullCgi *cgi, const ScriptVariable &site)
|
|
{
|
|
ScriptVariable enable =
|
|
cgi->GetConfigValue("site", site, "enable", 0, true);
|
|
enable.Trim();
|
|
enable.Tolower();
|
|
return (enable == "yes");
|
|
}
|
|
|
|
static void fill_data(const DullCgi *cgi, YtIdData *data)
|
|
{
|
|
ScriptVector v;
|
|
cgi->GetSectionNames("site", v);
|
|
|
|
int i;
|
|
for(i = 0; i < v.Length(); i++)
|
|
if(is_site_enabled(cgi, v[i]))
|
|
data->site_ids.AddItem(v[i]);
|
|
|
|
int len = data->site_ids.Length();
|
|
if(len > 0) {
|
|
// please note seeding of the generator is done with the
|
|
// custom randomize() function, called from main()
|
|
long r = random();
|
|
r %= len;
|
|
data->random_site = data->site_ids[r];
|
|
} else {
|
|
data->random_site = "";
|
|
}
|
|
}
|
|
|
|
static void analyse_path(DullCgi *cgi, bool &empty, ScriptVariable &ytid)
|
|
{
|
|
ScriptVariable path = cgi->GetPath();
|
|
if(path.IsInvalid())
|
|
path = "";
|
|
path.Trim("/ \t\r\n");
|
|
empty = (path == "");
|
|
if(!empty) {
|
|
ScriptVector pv(path, "/");
|
|
ytid = pv[0];
|
|
cgi->SetPositionals(pv);
|
|
}
|
|
}
|
|
|
|
// returns invalid for no parameter, "" for unable to guess
|
|
/* analyses the value of the CGI parameter "v", if it is there;
|
|
the ``guess'' succeedes if:
|
|
- either the parameter's value validates as a YT id as a whole;
|
|
- or there's a "watch?v=" substring and the 11 chars right after it
|
|
validate;
|
|
- or the last 11 chars of the string do, and there's '/' or '='
|
|
right before it;
|
|
- or the first ``/'' in the parameter, not taking into the account
|
|
these http://, https:// (or anyting_else://), is followed by 11
|
|
chars that validate as a YT id, and after them either the string
|
|
ends, or there's ``/'', or ``&'', or ``?''.
|
|
*/
|
|
static ScriptVariable guess_ytid_from_param(const DullCgi *cgi)
|
|
{
|
|
ScriptVariable vp = cgi->GetParam("v");
|
|
vp.Trim();
|
|
if(vp.IsInvalid() || vp == "")
|
|
return ScriptVariableInv();
|
|
if(vp.Length() == youtube_id_length)
|
|
return check_yt_id(vp.c_str()) ? vp : ScriptVariable("");
|
|
ScriptVariable::Substring wpos = vp.Strstr("watch?v=");
|
|
if(wpos.IsValid()) {
|
|
wpos = wpos.After();
|
|
wpos.SetLength(youtube_id_length);
|
|
ScriptVariable s = wpos.Get();
|
|
return check_yt_id(s.c_str()) ? s : ScriptVariable("");
|
|
}
|
|
ScriptVariable::Substring suff(vp, -youtube_id_length, youtube_id_length);
|
|
ScriptVariable res = suff.Get();
|
|
char prev = vp[suff.Index()-1];
|
|
if((prev == '/' || prev == '=') && check_yt_id(res.c_str()))
|
|
return res;
|
|
|
|
ScriptVariable::Substring scmpos = vp.Strstr("://");
|
|
ScriptVariable noscm = scmpos.IsValid() ? scmpos.After().Get() : vp;
|
|
ScriptVariable::Substring uri = noscm.Strstr("/");
|
|
if(uri.IsInvalid())
|
|
return "";
|
|
uri = uri.After();
|
|
uri.SetLength(youtube_id_length);
|
|
ScriptVariable::Substring urirest = uri.After();
|
|
if(urirest.Length() != 0) {
|
|
char afc = urirest[0];
|
|
if(afc != '/' && afc != '&' && afc != '?')
|
|
return "";
|
|
}
|
|
res = uri.Get();
|
|
if(check_yt_id(res.c_str()))
|
|
return res;
|
|
|
|
return "";
|
|
}
|
|
|
|
static ScriptVariable get_cookie_name(const DullCgi *cgi)
|
|
{
|
|
return cgi->GetConfigValue("general", 0, "cookie_name",
|
|
"ytid_site_choice", true);
|
|
}
|
|
|
|
static ScriptVariable pick_site_choice(const DullCgi *cgi)
|
|
{
|
|
ScriptVariable site_choice = cgi->GetParam("site");
|
|
if(site_choice.IsInvalid() || site_choice == "") {
|
|
ScriptVariable cn = get_cookie_name(cgi);
|
|
site_choice = cgi->GetCookie(cn);
|
|
}
|
|
if(site_choice.IsInvalid())
|
|
site_choice = "";
|
|
if(site_choice != "" && !is_site_enabled(cgi, site_choice))
|
|
site_choice = "";
|
|
return site_choice;
|
|
}
|
|
|
|
static void set_the_cookie(DullCgi *cgi, ScriptVariable &val)
|
|
{
|
|
ScriptVariable cn = get_cookie_name(cgi);
|
|
cgi->SetCookie(cn, val, YTID_COOKIE_TTL, true, false);
|
|
}
|
|
|
|
static void discard_the_cookie(DullCgi *cgi)
|
|
{
|
|
ScriptVariable cn = get_cookie_name(cgi);
|
|
cgi->DiscardCookie(cn);
|
|
}
|
|
|
|
static ScriptVariable compute_watch_url(DullCgi *cgi,
|
|
const ScriptVariable &ytid, const ScriptVariable &site_id)
|
|
{
|
|
ScriptMacroprocessor *sub_sub = cgi->MakeMacroprocessorClone();
|
|
|
|
sub_sub->AddMacro(new ScriptMacroConst("video_id", ytid));
|
|
|
|
ScriptVariable url =
|
|
cgi->GetConfigValue("site", site_id, "watch_url", 0, false);
|
|
if(url.IsInvalid() || url == "") {
|
|
url = cgi->GetConfigValue("general", 0, "default_watch_url", 0, false);
|
|
sub_sub->AddMacro(new ScriptMacroConst("site_id", site_id));
|
|
}
|
|
if(url.IsValid())
|
|
url = sub_sub->Process(url);
|
|
cgi->DisposeMacroprocessorClone(sub_sub);
|
|
return url;
|
|
}
|
|
|
|
static void send_redirect_response(DullCgi *cgi,
|
|
const ScriptVariable &ytid, const ScriptVariable &site_choice)
|
|
{
|
|
ScriptVariable url = compute_watch_url(cgi, ytid, site_choice);
|
|
if(url.IsInvalid() || url == "") {
|
|
cgi->SendErrorPage(500, "URL to redirect not configured");
|
|
return;
|
|
}
|
|
// cgi->AddMacro(new ScriptMacroConst("url", url));
|
|
// no need for this, it is done by BuildRedirectPage
|
|
cgi->SendRedirect(url);
|
|
}
|
|
|
|
|
|
class SiteUrl : public ScriptMacroprocessorMacro {
|
|
DullCgi *the_cgi;
|
|
public:
|
|
SiteUrl(DullCgi *acgi)
|
|
: ScriptMacroprocessorMacro("site_url"), the_cgi(acgi) {}
|
|
ScriptVariable Expand(const ScriptVector ¶ms) const;
|
|
};
|
|
|
|
ScriptVariable SiteUrl::Expand(const ScriptVector ¶ms) const
|
|
{
|
|
if(params.Length() != 1)
|
|
return ScriptVariableInv();
|
|
ScriptVariable p0 = params[0];
|
|
p0.Trim();
|
|
ScriptVariable url = the_cgi->GetConfigValue("site", p0, "url", 0, true);
|
|
url.Trim();
|
|
if(url.IsValid() && url != "")
|
|
return url;
|
|
|
|
ScriptVariable d_url =
|
|
the_cgi->GetConfigValue("general", 0, "default_url", 0, false);
|
|
d_url.Trim();
|
|
if(d_url.IsInvalid() || d_url == "")
|
|
return ScriptVariableInv();
|
|
|
|
ScriptMacroprocessor *sub_sub = the_cgi->MakeMacroprocessorClone();
|
|
sub_sub->AddMacro(new ScriptMacroConst("site_id", p0));
|
|
url = sub_sub->Process(d_url);
|
|
the_cgi->DisposeMacroprocessorClone(sub_sub);
|
|
return url;
|
|
}
|
|
|
|
|
|
class ViewUrl : public ScriptMacroprocessorMacro {
|
|
DullCgi *the_cgi;
|
|
public:
|
|
ViewUrl(DullCgi *acgi)
|
|
: ScriptMacroprocessorMacro("watch_url"), the_cgi(acgi) {}
|
|
ScriptVariable Expand(const ScriptVector ¶ms) const;
|
|
};
|
|
|
|
ScriptVariable ViewUrl::Expand(const ScriptVector ¶ms) const
|
|
{
|
|
if(params.Length() != 2)
|
|
return ScriptVariableInv();
|
|
ScriptVariable p0 = params[0];
|
|
p0.Trim();
|
|
ScriptVariable p1 = params[1];
|
|
p1.Trim();
|
|
|
|
ScriptVariable url = compute_watch_url(the_cgi, p1, p0);
|
|
if(url.IsInvalid())
|
|
url = "";
|
|
return url;
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////
|
|
// exported objects
|
|
//
|
|
|
|
int dullcgi_main(DullCgi *cgi)
|
|
{
|
|
cgi->AddMacro(new SiteUrl(cgi));
|
|
cgi->AddMacro(new ViewUrl(cgi));
|
|
|
|
bool path_empty;
|
|
ScriptVariable ytid(0); // invalid by default
|
|
|
|
analyse_path(cgi, path_empty, ytid);
|
|
|
|
if(path_empty)
|
|
ytid = guess_ytid_from_param(cgi);
|
|
// invalid for no parameter, "" for unable to guess
|
|
|
|
if(ytid.IsValid() && (ytid == "" || !check_yt_id(ytid.c_str()))) {
|
|
cgi->SendErrorPage(422, "Unprocessable Entity");
|
|
return 0;
|
|
}
|
|
|
|
ScriptVariable ytid2 = ytid.IsValid() ? ytid : ScriptVariable("");
|
|
cgi->AddMacro(new ScriptMacroConst("ytid", ytid2));
|
|
|
|
cgi->AddMacro(new CheckFirstArg("iffilenamesafe", check_fname_safe));
|
|
|
|
YtIdData data;
|
|
fill_data(cgi, &data);
|
|
|
|
cgi->AddMacro(new ScriptMacroConst("sites", data.site_ids.Join(" ")));
|
|
cgi->AddMacro(new ScriptMacroConst("random_site", data.random_site));
|
|
|
|
ScriptVariable mode = cgi->GetParam("mode");
|
|
|
|
ScriptVariable site_choice;
|
|
|
|
if(mode == "forget") {
|
|
discard_the_cookie(cgi);
|
|
} else {
|
|
site_choice = pick_site_choice(cgi);
|
|
}
|
|
|
|
cgi->AddMacro(new ScriptMacroConst("site_choice", site_choice));
|
|
IfCond *ifchoice = new IfCond("ifchoice");
|
|
ifchoice->SetCond(site_choice.IsValid() && site_choice != "");
|
|
cgi->AddMacro(ifchoice);
|
|
|
|
// may be we just need to redirect?
|
|
// this can only be if we have both ytid and site_choice, and the mode
|
|
// is either "watch" or "setwatch"
|
|
if(ytid2 != "" && site_choice != "") {
|
|
if(mode == "setwatch" || mode == "choose")
|
|
set_the_cookie(cgi, site_choice);
|
|
if(mode == "watch" || mode == "setwatch") {
|
|
send_redirect_response(cgi, ytid, site_choice);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
cgi->SendPage(ytid.IsValid() ? "choice" : "form");
|
|
|
|
return 0;
|
|
}
|
|
|
|
const char *dullcgi_config_path = "ytid.ini";
|
|
const char *dullcgi_program_name = "YTID";
|