thalassa/examples/ytid/ytid.cpp
2026-03-19 06:23:52 +05:00

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 &params) const;
};
ScriptVariable CheckFirstArg::Expand(const ScriptVector &params) 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 &params) const;
};
ScriptVariable SiteUrl::Expand(const ScriptVector &params) 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 &params) const;
};
ScriptVariable ViewUrl::Expand(const ScriptVector &params) 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";