#include // for random(3) #include #include #include #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";